| 1 |
# Checkout payment methods |
| 2 |
|
| 3 |
## Vipps (`vipps`) |
| 4 |
|
| 5 |
`Vipps.class.php` registers the Blocks integration and supplies `vipps_data`. |
| 6 |
`js/wc-payment-method-vipps.js` registers its label, description/event subscriber, |
| 7 |
and custom place-order button. The button and subscriber share a controller |
| 8 |
because WooCommerce renders them as separate components. |
| 9 |
|
| 10 |
### Order creation and widget handoff |
| 11 |
|
| 12 |
1. The button validates checkout, creates a pending payment-URL promise, and starts |
| 13 |
the SDK trigger. The trigger resolver waits for that promise. |
| 14 |
2. The button calls WooCommerce's `onSubmit()`. Do not await `trigger.open()` first: |
| 15 |
the trigger needs the URL from the order this submission will create. |
| 16 |
3. `onPaymentSetup` supplies the payment method and `vipps_checkout_widget` marker. |
| 17 |
Its success response means request data is ready; it does not mean the customer |
| 18 |
has paid. The PHP bridge currently selects requests by payment method, not by |
| 19 |
this marker. |
| 20 |
4. WooCommerce creates/processes the order through the Store API. The PHP bridge |
| 21 |
calls the gateway's normal `process_payment()` unless its pre-process filter |
| 22 |
supplies a result. It places the payment URL/reference in payment details and |
| 23 |
sets the result status to success. This status also tells WooCommerce's later |
| 24 |
legacy fallback that processing has already been handled. |
| 25 |
5. `onCheckoutSuccess` reads `processingResponse.paymentDetails`, with support for |
| 26 |
older event/response shapes. It prefers `vippsPaymentUrl` and accepts the normal |
| 27 |
`redirectUrl` as a fallback for a legacy gateway response. |
| 28 |
6. The controller resolves the SDK promise with the URL. The observer returns |
| 29 |
`{ type: SUCCESS, redirectUrl: '' }`, completing WooCommerce checkout while |
| 30 |
clearing its automatic redirect. This override applies only to an active |
| 31 |
widget attempt; unrelated success events return `true`. |
| 32 |
|
| 33 |
### Why the empty redirect is required |
| 34 |
|
| 35 |
WooCommerce and the SDK can both receive the same payment URL. Delivering it to |
| 36 |
the SDK does not consume or clear WooCommerce's stored redirect. Returning plain |
| 37 |
`SUCCESS` or `true` lets WooCommerce complete checkout and navigate to that URL, |
| 38 |
interrupting the widget flow. |
| 39 |
|
| 40 |
WooCommerce's success observer handler passes the response to `SET_COMPLETE`. |
| 41 |
The completion reducer accepts any string `redirectUrl`, including `''`, in place |
| 42 |
of the stored URL. The checkout processor navigates only when its redirect is |
| 43 |
nonempty. Preserve the explicit empty string; do not replace it with omission, |
| 44 |
`null`, or `true`. |
| 45 |
|
| 46 |
Source references (reviewed September 2026; upstream paths may change): |
| 47 |
|
| 48 |
- [](https://developer.woocommerce.com/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/checkout-flow-and-events/#oncheckoutsuccessCheckout event contract](https://developer.woocommerce.com/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/checkout-flow-and-events/#oncheckoutsuccess](https://developer.woocommerce.com/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/checkout-flow-and-events/#oncheckoutsuccess) |
| 49 |
- [](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/public-api/block-data/checkout/utils.tsSuccess observer handling](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/public-api/block-data/checkout/utils.ts](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/public-api/block-data/checkout/utils.ts) |
| 50 |
- [](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/public-api/block-data/checkout/reducers.tsSET_COMPLETE redirect override](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/public-api/block-data/checkout/reducers.ts](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/public-api/block-data/checkout/reducers.ts) |
| 51 |
- [](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.tsCheckout navigation](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.ts](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.ts) |
| 52 |
- [](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/Legacy.phpLegacy gateway adapter and status guard](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/Legacy.php](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/StoreApi/Legacy.php) |
| 53 |
|
| 54 |
### SDK lifecycle and recovery |
| 55 |
|
| 56 |
`vipps.js` exposes `ensureVippsWidgetHostStarted()` so express and standard checkout |
| 57 |
share one desktop host. The Blocks controller can also start the host if the |
| 58 |
helper is absent. Host initialization alone does not affect WooCommerce navigation. |
| 59 |
|
| 60 |
The SDK owns payment presentation: desktop normally uses a dialog; mobile/tablet |
| 61 |
and desktop fallback paths may navigate. SDK success/cancel handlers close the |
| 62 |
dialog and follow the merchant-return URL when supplied. These return URLs serve |
| 63 |
a different purpose from the initial payment-session URL. See the |
| 64 |
[](https://developer.vippsmobilepay.com/docs/knowledge-base/widget/Widget SDK documentation](https://developer.vippsmobilepay.com/docs/knowledge-base/widget/](https://developer.vippsmobilepay.com/docs/knowledge-base/widget/). |
| 65 |
|
| 66 |
The controller's attempt ends when the URL is delivered. It does not represent |
| 67 |
payment settlement. WooCommerce's processing/redirect flags also disable the |
| 68 |
button. During the attempt, `setVippsPaymentBusy()` in `vipps.js` shows the same |
| 69 |
spinner and overlay as express checkout, reusing the existing `.vippsoverlay` |
| 70 |
markup and `body.processing` styles. Each flow has its own owner key so clearing |
| 71 |
one cannot hide the other's spinner. URL delivery or failure releases the overlay |
| 72 |
for the SDK UI; unmounting the checkout button also releases its overlay owner. |
| 73 |
The button exposes `aria-busy` while its attempt is pending. |
| 74 |
Missing payment URLs return a retryable checkout error; checkout failures |
| 75 |
reject the waiting resolver. If the SDK is unavailable at handoff, the controller |
| 76 |
uses full-page navigation directly. |
| 77 |
|
| 78 |
A session-storage handoff marker refreshes stale checkout pages on history restore, |
| 79 |
focus, or visibility return. A per-path guard prevents repeated reloads. This is |
| 80 |
separate from payment confirmation; order metadata in the marker is optional. |
| 81 |
|
| 82 |
## Express Vipps (`vippsexpress`) |
| 83 |
|
| 84 |
`ExpressCheckoutButton` renders the server shortcode markup. `vipps.js` handles |
| 85 |
the express button, calls the express session endpoint, and supplies its URL |
| 86 |
directly to the SDK. Although registration maps `paymentMethodId` to `vipps`, |
| 87 |
this express flow does not use the standard button's pending Store API promise. |
| 88 |
Its confirmation dialog and handoff recovery remain in the shared script. |
| 89 |
|
| 90 |
## Classic shortcode checkout |
| 91 |
|
| 92 |
`vipps-classic-checkout.js` uses the existing WooCommerce form and the optional |
| 93 |
branded `#vipps-classic-checkout-submit` submit button. It handles |
| 94 |
`checkout_place_order_vipps` synchronously, suppresses core's default AJAX request, |
| 95 |
and submits the full checkout form to `wc_checkout_params.checkout_url` from the |
| 96 |
SDK resolver. WooCommerce still performs validation, creates/resumes the order, |
| 97 |
and invokes the gateway. This adapter owns the response, so core does not also |
| 98 |
redirect to the payment URL. It does not emit the classic success hook, whose |
| 99 |
normal consumer would redirect or enter its generic error branch. |
| 100 |
|
| 101 |
The script reuses the shared host and spinner. Form fields are serialized before |
| 102 |
being disabled; their prior disabled states are restored on a known checkout |
| 103 |
failure. The form stays locked after URL delivery until the dialog exits, so |
| 104 |
another payment cannot be submitted behind the dialog. A close/cancel without a |
| 105 |
return URL reloads checkout; a close during submission waits for the request to |
| 106 |
finish first. Restoring an old attempt from the back-forward cache also reloads. |
| 107 |
|
| 108 |
Server checkout errors remain visible through WooCommerce's error presenter when |
| 109 |
available, with a notice fallback for versions that do not pass the checkout |
| 110 |
controller into the hook. Refresh/reload responses are honored. An unconfirmed |
| 111 |
network result or unusable success response offers a reload link and blocks a |
| 112 |
second POST; it never automatically retries a possibly successful payment request. |
| 113 |
If the SDK fails after submission, a valid returned payment URL can be followed |
| 114 |
directly without creating another payment. |
| 115 |
|
| 116 |
The branded button is selected by the checked `payment_method` radio. For Vipps, |
| 117 |
the script removes its `hidden` class and hides `#place_order`; for every other |
| 118 |
gateway it restores the normal button. The switch is delegated so it survives |
| 119 |
WooCommerce replacing the payment section after `updated_checkout`. The native |
| 120 |
button remains the fallback when the branded button or JavaScript is unavailable. |
| 121 |
The button uses the same form-submit path, so it does not bypass WooCommerce |
| 122 |
validation or create a second payment request. |
| 123 |
|
| 124 |
On `woocommerce-order-pay` pages it uses the existing-order Store API route rather |
| 125 |
than cart checkout AJAX. It localizes `VippsOrderPayConfig` before this script runs: |
| 126 |
|
| 127 |
```js |
| 128 |
window.VippsOrderPayConfig = { |
| 129 |
orderId: 123, |
| 130 |
orderKey: 'wc_order_key_for_guest_links', |
| 131 |
billingEmail: '[email protected]', |
| 132 |
endpoint: '/wp-json/wc/store/v1/checkout/123', |
| 133 |
nonce: 'store-api-nonce', |
| 134 |
billingAddress: { first_name: '', last_name: '', address_1: '', city: '', state: '', postcode: '', country: '', email: '' }, |
| 135 |
shippingAddress: { first_name: '', last_name: '', address_1: '', city: '', state: '', postcode: '', country: '' } |
| 136 |
}; |
| 137 |
``` |
| 138 |
The adapter sends the selected Vipps method and form fields as |
| 139 |
`payment_data`, then reads the URL from `payment_result.payment_details` or its |
| 140 |
redirect fallback. It never sends the pay form to cart checkout AJAX. |
| 141 |
|
| 142 |
Before sending a Vipps request, it checks the same `terms-field` marker and |
| 143 |
`terms` checkbox used by WooCommerce's native pay-for-order handler. Missing |
| 144 |
terms are displayed using standard WooCommerce error-notice markup, and the page |
| 145 |
scrolls to the notice. Store API failures, missing payment URLs, and SDK startup |
| 146 |
failures use the same notice path. The notice container is refreshed and focused |
| 147 |
so the customer can correct the form and retry. |
| 148 |
|
| 149 |
The order-pay form remains native when this configuration or the SDK is missing. |
| 150 |
Its existing submit handler is prevented only for an active Vipps SDK attempt. |
| 151 |
Success hands the URL to the SDK; cancellation/close reloads the same authorized |
| 152 |
pay URL, and failures restore the form for retry. The adapter never automatically |
| 153 |
reposts an uncertain request. |
| 154 |
Blocks and non-Vipps methods retain their existing handlers. If the main plugin |
| 155 |
already attaches a Vipps-specific classic submit handler, consolidate that handler |
| 156 |
with this one so it cannot initiate a second payment independently. |
| 157 |
|
| 158 |
### Loading and dependencies |
| 159 |
|
| 160 |
Load `vipps-classic-checkout.js` only on the classic checkout and |
| 161 |
`woocommerce-order-pay` pages. Order-pay must remain enabled when the configured |
| 162 |
checkout page uses Blocks, because WooCommerce renders the pay form through the |
| 163 |
classic shortcode template in that case. The script depends on `jquery`, |
| 164 |
`wc-checkout`, and `vipps-gw`; the latter must load `vipps.js` and the SDK before |
| 165 |
the customer submits. Do not load this adapter on the Checkout Block itself. |
| 166 |
|