PIPRES-589 Enable Apple Pay in Chrome, Edge and Firefox via QR code flow - #1485
Open
TLabutis wants to merge 7 commits into
Open
PIPRES-589 Enable Apple Pay in Chrome, Edge and Firefox via QR code flow#1485TLabutis wants to merge 7 commits into
TLabutis wants to merge 7 commits into
Conversation
Merged
Apple Pay Direct and the checkout method were gated on window.ApplePaySession, which only Safari defines natively. Loading Apple's official Apple Pay JS SDK as a synchronous head script defines ApplePaySession in third-party browsers, where clicking the button shows Apple's iOS 18 QR code sheet; the existing DOM-ready gates and the isApplePayMethod cookie then work unchanged. hookActionFrontControllerSetMedia registers the SDK on product, cart and order pages only when the applepay method is enabled, so shops without Apple Pay never hit Apple's CDN. The Direct button is now created as the SDK's <apple-pay-button> custom element, since the old WebKit-CSS button renders blank outside Safari: createAppleButton creates it optimistically because the SDK registers the element asynchronously, and swaps in the legacy button after 3 seconds if registration never happens. Click handlers are bound directly on the element via bindAppleButtonClick because the SDK button swallows click propagation, so delegated handlers never fire; this also fixes the re-created button losing its click listener after updatedProduct and updatedCart refreshes. Merchant validation and payment creation are untouched: Mollie's /wallets/applepay/sessions endpoint receives the validationURL exactly as before. No validationUrl allowlist and no session version bump.
The SDK registration had no attribute, so the synchronous head script render-blocked product, cart and order pages on applepay.cdn-apple.com; an Apple CDN slowdown would stall first paint on the shop's three busiest page types. defer keeps every DOM-ready guarantee, since deferred scripts finish executing before DOMContentLoaded fires. The DOM-ready ApplePaySession checks are safe with today's SDK: the bootstrap assigns the polyfill synchronously at top level and only the apple-pay-button element arrives through its dynamic import. But 1.latest is a rolling URL with no version pin, so Apple can restructure the bootstrap into an async loader at any time. As insurance, whenApplePaySessionAvailable gates initApplePayDirect and setApplePayMethodCookie re-runs on customElements.whenDefined, so both the Direct button and the isApplePayMethod cookie recover if the polyfill ever lands after DOM ready. whenDefined resolves as a microtask after the defining module's full top-level evaluation, which is why a one-shot recheck suffices and no polling is needed. The 3s legacy-button fallback in createAppleButton stays: init runs at DOM ready before the element import lands, and in Safari behind a CSP that blocks Apple's CDN it is the only path to a working button.
TLabutis
force-pushed
the
PIPRES-589-apple-pay-qr-code-flow
branch
from
August 19, 2026 08:23
b684a97 to
c1b9fd8
Compare
Apple's QR relay for non-Safari browsers validates the payment request strictly where Safari's native bridge is lenient: a float amount with binary noise (47.890000000000001), a numeric paymentFee or a numeric shipping method identifier makes the sheet close right after the phone scan. Format every amount sent to the sheet with number_format/toFixed and cast the carrier identifier to string in OrderTotalCollector, ShippingMethodUtility, UpdateApplePayShippingContactHandler, UpdateApplePayShippingMethodHandler, the applePayDirectAjax cart total and applePayDirectProduct.js.
Apple Pay sheet addresses are soft-deleted by design, and Customer::customerHasAddress filters deleted rows, so Cart::getPackageShippingCost silently falls back to the shop default country's zone whenever an explicit carrier id is passed to getOrderTotal. Shipping gets priced off the wrong zone's ranges, the sheet authorizes that total, and the webhook, which prices the cart without a carrier argument and resolves the real zone, computes a different total and refuses the order with a wrong-amount error. This also affects the native Safari flow for any buyer whose delivery zone differs from the shop default. Set each candidate carrier as the cart's delivery option in memory and call getOrderTotal without a carrier id, restoring delivery_option afterwards, in OrderTotalCollector and ShippingMethodUtility; drop the explicit carrier argument in UpdateApplePayShippingMethodHandler and CreateApplePayOrderHandler, which persist the carrier on the cart already. UpdateApplePayShippingContactHandler now also persists the first listed shipping method, because the sheet preselects it without firing onshippingmethodselected, so the cart must already carry it or the authorized total diverges from the charged one.
Mollie's hosted Apple Pay checkout page does not support the QR flow: opened in a non-Safari browser it reports Apple Pay as unavailable, cancels the payment and sends the shopper back to the store. With the SDK registered on the order page, the polyfill set the isApplePayMethod cookie in Chrome and the checkout list advertised a method the shopper could not finish. Register the SDK only on the product and cart pages, where Apple Pay Direct renders its own button, and restore apple_payment.js to the plain native-session check so the checkout list stays gated to Safari. This also drops the whenDefined re-check added earlier, which without an SDK on the checkout page could only ever advertise the dead method.
The classic theme lays out the quantity spinner, the add-to-cart button and the Apple Pay Direct button in a single nowrap flex row. When the row runs out of space the quantity block is the only item allowed to shrink, so the spinner arrows drop under the input while the 200px button keeps its full width. Allow the row to wrap so the whole button moves to its own line instead. The squeeze predates the QR flow: the legacy Safari-only button has the same footprint, other browsers just never rendered a button in this row before.
Cart::setDeliveryOption() ends by running CartRule::autoRemoveFromCart() and CartRule::autoAddToCart(), which call removeCartRule() and $cart->update() on the context cart. During Apple Pay sheet quoting the context cart is the shopper's real cart, so looping every candidate carrier through setDeliveryOption() could permanently delete a carrier-restricted voucher and persist a mid-loop carrier while merely rendering the sheet. Assign the delivery_option property directly instead. Pricing is unaffected: getTotalShippingCost() resolves the carrier through getDeliveryOption() with use_cache=false, re-reading the in-memory property on every call, and the PS 8 Calculator's Fees never consults id_carrier when getOrderTotal() is called without an explicit carrier. The handlers keep setDeliveryOption() because there the shopper actually selected the carrier, matching core checkout semantics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Apple Pay Direct (product and cart buttons) now works in Chrome, Edge and Firefox on desktop via Apple's iOS 18 QR code flow. The regular checkout method list stays Safari-only on purpose (see the hosted-page finding below).
Why
Every Apple Pay gate in the module is keyed on
window.ApplePaySession, which only Safari defines natively. Since iOS 18, Apple supports third-party browsers through its official Apple Pay JS SDK: the shopper clicks the button, gets a QR code sheet, and scans it with an iPhone. Mollie's merchant validation endpoint and the payment flow stay exactly the same.How
mollie.php:hookActionFrontControllerSetMediaregistershttps://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.jsas a deferred head script on product and cart pages, only when the applepay method is enabled. Deferred scripts finish executing beforeDOMContentLoaded, and the SDK bootstrap assigns theApplePaySessionpolyfill synchronously at its top level, so all existing DOM-ready gates work unchanged, while an Apple CDN slowdown can no longer block first paint. Also exposesapplePayLocalefor the button element.applePayDirectProduct.js/applePayDirectCart.js: the Direct button is now the SDK's<apple-pay-button>custom element (the old WebKit-CSS<button>renders blank outside Safari).createAppleButtoncreates it optimistically because the SDK registers the element asynchronously through a dynamicimport(), and swaps in the legacy button after 3 seconds if registration never happens (SDK blocked/unavailable). Click handlers are bound directly on the element viabindAppleButtonClick, because the SDK button swallows click propagation so delegated handlers never fire. This also fixes a pre-existing bug: the button re-created onupdatedProduct/updatedCartlost its click listener.1.latestis unpinned, so if Apple ever moves theApplePaySessionassignment behind the SDK's dynamic import,whenApplePaySessionAvailablere-runs the Direct init oncecustomElements.whenDefined('apple-pay-button')resolves. With today's SDK it short-circuits at DOM ready - this is not fixing a reproducible bug.apple_pay_direct.css: sizing for the custom element via Apple's CSS custom properties; Hummingbird selectors extended to match it.Checkout method list: settled, stays Safari-only
Mollie's hosted Apple Pay wallet page does not support the QR flow. Opened in a non-Safari browser it shows "Apple Pay is unavailable", cancels the payment and bounces the shopper back to the store (verified empirically with a test payment in Chrome). An earlier revision registered the SDK on the order page, which made the polyfill set the
isApplePayMethodcookie in Chrome and advertised a method the shopper could not finish. The SDK is now product/cart only andapple_payment.jskeeps its plain native-session check, so the checkout list keeps today's Safari-only behavior.Fixes found while QA-ing the QR flow
paymentFeeor a numeric shipping method identifier close the sheet right after the phone scan. All amounts are now formatted strings.Customer::customerHasAddressfilters deleted rows, soCart::getPackageShippingCostsilently priced shipping against the shop default country's zone whenever an explicit carrier id was passed togetOrderTotal. The sheet then authorized a total the webhook (which resolves the real zone) refused with "Wrong cart amount". Carrier totals are now priced through the cart's delivery option, and the first listed shipping method is persisted because the sheet preselects it without firingonshippingmethodselected. The quoting loops assigndelivery_optiondirectly instead of callingCart::setDeliveryOption(), which would runCartRule::autoRemoveFromCart()against the shopper's real cart on every quoted carrier. Known limitation: multi-package carts (advanced stock split) use a multi-carrierdelivery_optionkey, so their quotes fall back to the cart's current best-price option; single-package carts, the only case Apple Pay Direct produces, are exact.Verification
ApplePaySession, cookieisApplePayMethod=0, method hidden - matching pre-PR behavior.ApplePaySession(no payment authorization attempted): session amount tracks quantity changes and variant switches; re-created button afterupdatedProductkeeps a working click handler; cart session amount includes voucher discounts.Left for manual QA (needs a LIVE API key - test keys do not expose Apple Pay; use real small amounts or refund after)
Merchant caveats to document
applepay.cdn-apple.cominscript-src(and QA should watch the console forconnect-src/frame-srcneeds around the QR sheet).