Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 90 additions & 50 deletions assets/js/applepay/applepay.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,66 +125,94 @@ export default class ApplePay {
return;
}

const cart_items = this.getItems();
const shipping_methods = this.isOnCheckout ? [] : this.woocommerce.getShippingMethods(this.country_code);
const first_shipping_item = this.getFirstShippingItem(shipping_methods);
this.setupPayment();

const all_items = first_shipping_item !== null ? [].concat(cart_items, first_shipping_item) : cart_items;
if (this.renderButton) {
this.injectApplePayButton();
}

const total_to_pay = this.sumTotalAmount(all_items);
if (this.onReady) {
this.onReady(true);
}
});
}

const total_item = {
label: 'Totaal',
amount: total_to_pay,
type: 'final',
};
/**
* Build (or rebuild) the Buckaroo ApplePayPayment session from the current
* cart state. Extracted from init() so the standard checkout method can
* refresh the totals right before opening the sheet.
*/
setupPayment() {
const cart_items = this.getItems();
let shipping_methods = [];
let all_items = cart_items;
let total_to_pay;

if (this.isOnCheckout) {
const cart_total = this.woocommerce.getCartTotal();

if (cart_total && cart_total.shipping > 0) {
all_items = [].concat(cart_items, {
type: 'final',
label: convert.maxCharacters(cart_total.shipping_label || 'Shipping', 25),
amount: convert.toDecimal(cart_total.shipping),
qty: 1,
});
}

total_to_pay =
cart_total && typeof cart_total.total === 'number' && cart_total.total > 0
? convert.toDecimal(cart_total.total)
: this.sumTotalAmount(all_items);
} else {
shipping_methods = this.woocommerce.getShippingMethods(this.country_code);
const first_shipping_item = this.getFirstShippingItem(shipping_methods);

all_items = first_shipping_item !== null ? [].concat(cart_items, first_shipping_item) : cart_items;
total_to_pay = this.sumTotalAmount(all_items);

if (shipping_methods.length > 0) {
this.selected_shipping_method = shipping_methods[0].identifier;
this.selected_shipping_amount = shipping_methods[0].amount;
}
this.total_price = total_to_pay;

// Express gathers full contact data from the Apple sheet; the
// checkout method only authorises and uses the WooCommerce form.
const requiredContactFields = this.isOnCheckout ? [] : ['name', 'email', 'postalAddress', 'phone'];
const shippingMethodsCallback = this.isOnCheckout ? null : this.processShippingMethodsCallback.bind(this);
const changeContactCallback = this.isOnCheckout ? null : this.processChangeContactInfoCallback.bind(this);

const applepay_options = new BuckarooSdk.ApplePay.ApplePayOptions(
this.store_info.store_name,
this.store_info.country_code,
this.store_info.currency_code,
this.store_info.culture_code,
this.store_info.merchant_id,
all_items,
total_item,
'shipping',
shipping_methods,
this.processApplepayCallback.bind(this),
shippingMethodsCallback,
changeContactCallback,
requiredContactFields,
requiredContactFields
);

// The SDK needs a button selector; for the standard method (no
// button) we pass the container itself — beginPayment() does not
// require the element, it is triggered programmatically.
const buttonSelector = this.renderButton
? `${this.containerSelector} apple-pay-button`
: this.containerSelector;
}

this.payment = new BuckarooSdk.ApplePay.ApplePayPayment(buttonSelector, applepay_options);
this.total_price = total_to_pay;

if (this.renderButton) {
this.injectApplePayButton();
}
const total_item = {
label: 'Totaal',
amount: total_to_pay,
type: 'final',
};

if (this.onReady) {
this.onReady(true);
}
});
// Express gathers full contact data from the Apple sheet; the
// checkout method only authorises and uses the WooCommerce form.
const requiredContactFields = this.isOnCheckout ? [] : ['name', 'email', 'postalAddress', 'phone'];
const shippingMethodsCallback = this.isOnCheckout ? null : this.processShippingMethodsCallback.bind(this);
const changeContactCallback = this.isOnCheckout ? null : this.processChangeContactInfoCallback.bind(this);

const applepay_options = new BuckarooSdk.ApplePay.ApplePayOptions(
this.store_info.store_name,
this.store_info.country_code,
this.store_info.currency_code,
this.store_info.culture_code,
this.store_info.merchant_id,
all_items,
total_item,
'shipping',
shipping_methods,
this.processApplepayCallback.bind(this),
shippingMethodsCallback,
changeContactCallback,
requiredContactFields,
requiredContactFields
);

const buttonSelector = this.renderButton
? `${this.containerSelector} apple-pay-button`
: this.containerSelector;

this.payment = new BuckarooSdk.ApplePay.ApplePayPayment(buttonSelector, applepay_options);
}

/**
Expand All @@ -196,7 +224,19 @@ export default class ApplePay {
* @returns {boolean} whether a session could be started
*/
triggerPayment(event) {
if (this.payment && typeof this.payment.beginPayment === 'function') {
if (!this.payment) {
return false;
}

if (this.isOnCheckout) {
try {
this.setupPayment();
} catch (e) {
// keep the existing session if the refresh fails
}
}

if (typeof this.payment.beginPayment === 'function') {
this.payment.beginPayment(event || new Event('click'));
return true;
}
Expand Down
26 changes: 26 additions & 0 deletions assets/js/applepay/woocommerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,32 @@ export default class Woocommerce {
return methods;
}

/**
* Grand total of the current cart (incl. chosen shipping, payment fee,
* coupons and taxes). Used by the standard checkout method so the amount
* authorised in the Apple Pay sheet always equals the amountDebit that is
* sent to Buckaroo (Buckaroo rejects the transaction on a mismatch).
*
* @returns {{total: number, shipping: number, shipping_label: string}|null}
*/
getCartTotal() {
let totals = null;
jQuery
.ajax({
url: this.url,
data: {
'wc-api': `${this.api_namespace}-get-cart-total`,
},
async: false,
dataType: 'json',
})
.done(response => {
totals = response;
});

return totals;
}

getStoreInformation() {
let information = [];
jQuery
Expand Down
115 changes: 75 additions & 40 deletions assets/js/blocks/gateways/buckaroo_applepay.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,81 +8,116 @@ import { __ } from '@wordpress/i18n';
* The customer selects Apple Pay and clicks the normal "Place Order" button;
* that click opens the Apple Pay sheet (authorise only). Billing and shipping
* come from the WooCommerce checkout form, not from Apple Pay.
*
* Flow:
* 1. On mount, build a checkout-mode Apple Pay instance (no button).
* 2. Intercept the Place Order button click (capture phase = still a user
* gesture, required by Safari) and open the Apple Pay sheet.
* 3. On authorisation, keep the token and let Place Order proceed.
* 4. onPaymentSetup hands the token to the server, which charges it against
* the order built from the checkout-form addresses.
*/
function BuckarooApplepayCheckout({ gateway, eventRegistration, emitResponse, setErrorMessage }) {

const PLACE_ORDER_SELECTOR =
'.wc-block-components-checkout-place-order-button, ' +
'.wc-block-checkout__actions button[type="submit"], ' +
'.wc-block-checkout__actions_row button[type="submit"]';

function BuckarooApplepayCheckout({ gateway, eventRegistration, emitResponse, setErrorMessage, onStateChange }) {
const tokenRef = useRef(null);
const applepayRef = useRef(null);
const onStateChangeRef = useRef(onStateChange);
onStateChangeRef.current = onStateChange;
const setErrorMessageRef = useRef(setErrorMessage);
setErrorMessageRef.current = setErrorMessage;

const getPlaceOrderButton = () =>
document.querySelector('.wc-block-components-checkout-place-order-button') ||
document.querySelector('button.wc-block-components-button[type="submit"]');
const getPlaceOrderButton = () => document.querySelector(PLACE_ORDER_SELECTOR);

// Build the (button-less) Apple Pay instance for this method.
useEffect(() => {
const showError = message => {
if (typeof setErrorMessageRef.current === 'function') {
setErrorMessageRef.current(message);
}
};

const ensureInstance = () => {
if (applepayRef.current) {
return applepayRef.current;
}
if (!window.BuckarooApplePay || typeof window.BuckarooApplePay.create !== 'function') {
return undefined;
return null;
}

try {
applepayRef.current = window.BuckarooApplePay.create({
const instance = window.BuckarooApplePay.create({
isOnCheckout: true,
renderButton: false,
containerSelector: '.applepay-blocks-checkout-method',
onAuthorized: payment => {
// Authorised: keep the token and let Place Order proceed.
tokenRef.current = JSON.stringify(payment);
const button = getPlaceOrderButton();
if (button) {
button.click();
showError('');

if (typeof onStateChangeRef.current === 'function') {
onStateChangeRef.current({ paymentData: tokenRef.current });
}

setTimeout(() => {
const button = getPlaceOrderButton();
if (button) {
button.click();
}
}, 0);
},
});
applepayRef.current.rebuild();
applepayRef.current.init();
instance.rebuild();
instance.init();
applepayRef.current = instance;
return instance;
} catch (e) {
// Apple Pay unavailable in this context; method stays inert.
return null;
}
};

useEffect(() => {
if (ensureInstance()) {
return undefined;
}

const timer = setInterval(() => {
if (ensureInstance()) {
clearInterval(timer);
}
}, 250);
const stop = setTimeout(() => clearInterval(timer), 10000);

return () => {
clearInterval(timer);
clearTimeout(stop);
applepayRef.current = null;
};
}, []);

// Intercept Place Order: open the Apple Pay sheet within the click gesture.
// While this method's content is mounted it is the active payment method, so
// the listener is only live for Apple Pay.
useEffect(() => {
const button = getPlaceOrderButton();
if (!button) {
return undefined;
}

const handler = event => {
// Already authorised -> let WooCommerce place the order.
if (tokenRef.current) {
const target = event.target;
const button = target && typeof target.closest === 'function' ? target.closest(PLACE_ORDER_SELECTOR) : null;
if (!button) {
return;
}
if (!applepayRef.current) {

if (tokenRef.current) {
return;
}

event.preventDefault();
event.stopImmediatePropagation();
applepayRef.current.triggerPayment(event);
event.stopPropagation();

const instance = ensureInstance();
if (!instance || instance.triggerPayment(event) !== true) {
showError(
__(
'Apple Pay is not available in this browser or context. Please choose another payment method.',
'wc-buckaroo-bpe-gateway'
)
);
}
};

button.addEventListener('click', handler, true);
return () => button.removeEventListener('click', handler, true);
document.addEventListener('click', handler, true);
return () => document.removeEventListener('click', handler, true);
}, []);

// Provide the authorised token to the server during order placement.
useEffect(() => {
if (!eventRegistration || !eventRegistration.onPaymentSetup) {
return undefined;
Expand All @@ -104,7 +139,7 @@ function BuckarooApplepayCheckout({ gateway, eventRegistration, emitResponse, se
},
},
};
});
}, 5);

return () => unsubscribe();
}, [eventRegistration, emitResponse]);
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dist/applepay.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => 'b9b25c86b4ff713fe6b4');
<?php return array('dependencies' => array(), 'version' => '0864461881cf1a5aea73');
Loading
Loading