You’d think that when a customer views an order and the order has a pending or failed status, they should be able to pay right from that view order page, right? Yeah, but nope. The view-order template just renders the order info.
The Order Listing page does, however, include a Payment button though.
Why This Feature is Important
Allowing customers to pay directly from the view order page enhances user experience and reduces the friction in completing a purchase. This is particularly useful for orders that have failed or are pending, as it provides a quick and easy way for customers to complete their payment without navigating through multiple pages.
The Challenge
The default WooCommerce view-order template does not include a pay button. You can add this functionality by either customizing the WooCommerce View Order Template or by using WooCommerce hooks. I recommend the latter because customizing template files can lead to issues when WooCommerce updates its templates, potentially breaking your customizations if they are not kept up-to-date.
Adding the Pay Order Button Using Hooks
To add the pay order button, you need to add custom code to your functions.php file or create a custom plugin. Below, I'll show you the code you need and where to place it.
The Solution
The solution should look like this.
Here's a straightforward solution using hooks to add the payment button to the view-order page. This method ensures your customizations remain intact even after WooCommerce updates.
Copy and the following custom php and then you can paste it into functions.php file (omit the <?php opening tag because functions.php has one already otherwise your site will start showing php content and you won't want that.) or use a WordPress Plugin Generator (recommended approach) and copy and paste the code there. If you have used the plugin generator you'll only need to activate the plugin.
<?php
/**
* This code hooks into woocommerce_view_order and woocommerce_after_order_details and adds a pay order button for pending or failed orders.
* @see https://orbisius.com/7685
* @author Svetoslav Marinov | https://orbisius.com
* @license Creative Commons Zero v1.0 Universal
*/
$orb_post_7685_obj = Orb_Post_7685_Add_Payment_Button_to_View_Order::getInstance();
add_action( 'woocommerce_view_order', [ $orb_post_7685_obj, 'renderPaymentButtonOnViewOrder' ] );
add_action( 'woocommerce_after_order_details', [ $orb_post_7685_obj, 'renderPaymentButtonOnViewOrder' ] );
class Orb_Post_7685_Add_Payment_Button_to_View_Order {
/**
* View order just shows the order details. If it's a order that needs payment it must have a pay now button.
* This shows up above the order details page title and there's no hook after the title in the template.
* This is also rendered after the details but before customer info because it may not exist
* @param int|WC_Order $order_id
* @return void
*/
public function renderPaymentButtonOnViewOrder( $order_id ) {
$order = wc_get_order( $order_id );
$payable_statuses = [
'failed',
'pending',
];
if ( ! in_array( $order->get_status(), $payable_statuses ) ) {
return;
}
$payment_url = $order->get_checkout_payment_url();
printf( '<div class="qs_site_app_billing_payment_button_wrapper">
<a class="woocommerce-button button button-primary pay" href="%s">%s</a></div>', $payment_url, __("Pay for this order", "woocommerce") );
}
/**
* Singleton pattern i.e. we have only one instance of this obj
*
* @staticvar static $instance
* @return static
*/
public static function getInstance() {
static $instance = null;
// This will make the calling class to be instantiated.
// no need each sub class to define this method.
if (is_null($instance)) {
$instance = new static();
}
return $instance;
}
}
View Order WooCommerce template [/plugins/woocommerce/templates/myaccount/view-order.php] looks like this
<?php
/**
* View Order
*
* Shows the details of a particular order on the account page.
*
* This template can be overridden by copying it to yourtheme/woocommerce/myaccount/view-order.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 3.0.0
*/
defined( 'ABSPATH' ) || exit;
$notes = $order->get_customer_order_notes();
?>
<p>
<?php
printf(
/* translators: 1: order number 2: order date 3: order status */
esc_html__( 'Order #%1$s was placed on %2$s and is currently %3$s.', 'woocommerce' ),
'<mark class="order-number">' . $order->get_order_number() . '</mark>', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'<mark class="order-date">' . wc_format_datetime( $order->get_date_created() ) . '</mark>', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'<mark class="order-status">' . wc_get_order_status_name( $order->get_status() ) . '</mark>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
?>
</p>
<?php if ( $notes ) : ?>
<h2><?php esc_html_e( 'Order updates', 'woocommerce' ); ?></h2>
<ol class="woocommerce-OrderUpdates commentlist notes">
<?php foreach ( $notes as $note ) : ?>
<li class="woocommerce-OrderUpdate comment note">
<div class="woocommerce-OrderUpdate-inner comment_container">
<div class="woocommerce-OrderUpdate-text comment-text">
<p class="woocommerce-OrderUpdate-meta meta"><?php echo date_i18n( esc_html__( 'l jS \o\f F Y, h:ia', 'woocommerce' ), strtotime( $note->comment_date ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
<div class="woocommerce-OrderUpdate-description description">
<?php echo wpautop( wptexturize( $note->comment_content ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<div class="clear"></div>
</div>
<div class="clear"></div>
</div>
</li>
<?php endforeach; ?>
</ol>
<?php endif; ?>
<?php do_action( 'woocommerce_view_order', $order_id ); ?>
Order Details WooCommerce template [/plugins/woocommerce/templates/order/order-details.php] looks like this
<?php
/**
* Order details
*
* This template can be overridden by copying it to yourtheme/woocommerce/order/order-details.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 8.5.0
*
* @var bool $show_downloads Controls whether the downloads table should be rendered.
*/
defined( 'ABSPATH' ) || exit;
$order = wc_get_order( $order_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
if ( ! $order ) {
return;
}
$order_items = $order->get_items( apply_filters( 'woocommerce_purchase_order_item_types', 'line_item' ) );
$show_purchase_note = $order->has_status( apply_filters( 'woocommerce_purchase_note_order_statuses', array( 'completed', 'processing' ) ) );
$show_customer_details = is_user_logged_in() && $order->get_user_id() === get_current_user_id();
$downloads = $order->get_downloadable_items();
if ( $show_downloads ) {
wc_get_template(
'order/order-downloads.php',
array(
'downloads' => $downloads,
'show_title' => true,
)
);
}
?>
<section class="woocommerce-order-details">
<?php do_action( 'woocommerce_order_details_before_order_table', $order ); ?>
<h2 class="woocommerce-order-details__title"><?php esc_html_e( 'Order details', 'woocommerce' ); ?></h2>
<table class="woocommerce-table woocommerce-table--order-details shop_table order_details">
<thead>
<tr>
<th class="woocommerce-table__product-name product-name"><?php esc_html_e( 'Product', 'woocommerce' ); ?></th>
<th class="woocommerce-table__product-table product-total"><?php esc_html_e( 'Total', 'woocommerce' ); ?></th>
</tr>
</thead>
<tbody>
<?php
do_action( 'woocommerce_order_details_before_order_table_items', $order );
foreach ( $order_items as $item_id => $item ) {
$product = $item->get_product();
wc_get_template(
'order/order-details-item.php',
array(
'order' => $order,
'item_id' => $item_id,
'item' => $item,
'show_purchase_note' => $show_purchase_note,
'purchase_note' => $product ? $product->get_purchase_note() : '',
'product' => $product,
)
);
}
do_action( 'woocommerce_order_details_after_order_table_items', $order );
?>
</tbody>
<tfoot>
<?php
foreach ( $order->get_order_item_totals() as $key => $total ) {
?>
<tr>
<th scope="row"><?php echo esc_html( $total['label'] ); ?></th>
<td><?php echo wp_kses_post( $total['value'] ); ?></td>
</tr>
<?php
}
?>
<?php if ( $order->get_customer_note() ) : ?>
<tr>
<th><?php esc_html_e( 'Note:', 'woocommerce' ); ?></th>
<td><?php echo wp_kses_post( nl2br( wptexturize( $order->get_customer_note() ) ) ); ?></td>
</tr>
<?php endif; ?>
</tfoot>
</table>
<?php do_action( 'woocommerce_order_details_after_order_table', $order ); ?>
</section>
<?php
/**
* Action hook fired after the order details.
*
* @since 4.4.0
* @param WC_Order $order Order data.
*/
do_action( 'woocommerce_after_order_details', $order );
if ( $show_customer_details ) {
wc_get_template( 'order/order-details-customer.php', array( 'order' => $order ) );
}