mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
1165 lines
30 KiB
JavaScript
1165 lines
30 KiB
JavaScript
/* global ApplePaySession */
|
|
/* global PayPalCommerceGateway */
|
|
|
|
import { createAppleErrors } from './Helper/applePayError';
|
|
import FormValidator from '../../../ppcp-button/resources/js/modules/Helper/FormValidator';
|
|
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
|
|
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
|
|
import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
|
|
import {
|
|
PaymentContext,
|
|
PaymentMethods,
|
|
} from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
|
|
import {
|
|
combineStyles,
|
|
combineWrapperIds,
|
|
} from '../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
|
|
|
|
/**
|
|
* Plugin-specific styling.
|
|
*
|
|
* Note that most properties of this object do not apply to the Apple Pay button.
|
|
*
|
|
* @typedef {Object} PPCPStyle
|
|
* @property {string} shape - Outline shape.
|
|
* @property {?number} height - Button height in pixel.
|
|
*/
|
|
|
|
/**
|
|
* Style options that are defined by the Apple Pay SDK and are required to render the button.
|
|
*
|
|
* @typedef {Object} ApplePayStyle
|
|
* @property {string} type - Defines the button label.
|
|
* @property {string} color - Button color
|
|
* @property {string} lang - The locale; an empty string will apply the user-agent's language.
|
|
*/
|
|
|
|
/**
|
|
* This object describes the transaction details.
|
|
*
|
|
* @typedef {Object} TransactionInfo
|
|
* @property {string} countryCode - The ISO country code
|
|
* @property {string} currencyCode - The ISO currency code
|
|
* @property {string} totalPriceStatus - Usually 'FINAL', can also be 'DRAFT'
|
|
* @property {string} totalPrice - Total monetary value of the transaction.
|
|
* @property {Array} chosenShippingMethods - Selected shipping method.
|
|
* @property {string} shippingPackages - A list of available shipping methods, defined by WooCommerce.
|
|
*/
|
|
|
|
/**
|
|
* A payment button for Apple Pay.
|
|
*
|
|
* On a single page, multiple Apple Pay buttons can be displayed, which also means multiple
|
|
* ApplePayButton instances exist. A typical case is on the product page, where one Apple Pay button
|
|
* is located inside the minicart-popup, and another pay-now button is in the product context.
|
|
*/
|
|
class ApplePayButton extends PaymentButton {
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
static methodId = PaymentMethods.APPLEPAY;
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
static cssClass = 'ppcp-button-applepay';
|
|
|
|
#formData = null;
|
|
#updatedContactInfo = [];
|
|
#selectedShippingMethod = [];
|
|
|
|
/**
|
|
* Initialization data sent to the button.
|
|
*/
|
|
#initialPaymentRequest = null;
|
|
|
|
/**
|
|
* Details about the processed transaction, provided to the Apple SDK.
|
|
*
|
|
* @type {?TransactionInfo}
|
|
*/
|
|
#transactionInfo = null;
|
|
|
|
/**
|
|
* Apple Pay specific API configuration.
|
|
*/
|
|
#applePayConfig = null;
|
|
|
|
/**
|
|
* Details about the product (relevant on product page)
|
|
*
|
|
* @type {{quantity: ?number, items: []}}
|
|
*/
|
|
#product = {};
|
|
|
|
/**
|
|
* The start time of the configuration process.
|
|
* @type {number}
|
|
*/
|
|
#configureStartTime = 0;
|
|
|
|
/**
|
|
* The maximum time to wait for buttonAttributes before proceeding with initialization.
|
|
* @type {number}
|
|
*/
|
|
#maxWaitTime = 1000;
|
|
|
|
/**
|
|
* The stored button attributes.
|
|
* @type {Object|null}
|
|
*/
|
|
#storedButtonAttributes = null;
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
static getWrappers( buttonConfig, ppcpConfig ) {
|
|
return combineWrapperIds(
|
|
buttonConfig?.button?.wrapper || '',
|
|
buttonConfig?.button?.mini_cart_wrapper || '',
|
|
ppcpConfig?.button?.wrapper || '',
|
|
'ppc-button-applepay-container',
|
|
'ppc-button-ppcp-applepay'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
static getStyles( buttonConfig, ppcpConfig ) {
|
|
const { color, lang, type } = buttonConfig?.button || {};
|
|
const buttonStyle = { color, lang, type };
|
|
|
|
const buttonStyles = {
|
|
style: buttonStyle,
|
|
mini_cart_style: buttonStyle,
|
|
};
|
|
|
|
return combineStyles( ppcpConfig?.button || {}, buttonStyles );
|
|
}
|
|
|
|
constructor(
|
|
context,
|
|
externalHandler,
|
|
buttonConfig,
|
|
ppcpConfig,
|
|
contextHandler,
|
|
buttonAttributes
|
|
) {
|
|
// Disable debug output in the browser console:
|
|
// buttonConfig.is_debug = false;
|
|
|
|
super(
|
|
context,
|
|
externalHandler,
|
|
buttonConfig,
|
|
ppcpConfig,
|
|
contextHandler,
|
|
buttonAttributes
|
|
);
|
|
|
|
this.init = this.init.bind( this );
|
|
this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this );
|
|
this.onButtonClick = this.onButtonClick.bind( this );
|
|
|
|
this.#product = {
|
|
quantity: null,
|
|
items: [],
|
|
};
|
|
|
|
this.log( 'Create instance' );
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
get requiresShipping() {
|
|
if ( ! super.requiresShipping ) {
|
|
return false;
|
|
}
|
|
|
|
if ( ! this.buttonConfig.product?.needShipping ) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
PaymentContext.Checkout !== this.context ||
|
|
this.shouldUpdateButtonWithFormData
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Details about the processed transaction.
|
|
*
|
|
* This object defines the price that is charged, and text that is displayed inside the
|
|
* payment sheet.
|
|
*
|
|
* @return {?TransactionInfo} The TransactionInfo object.
|
|
*/
|
|
get transactionInfo() {
|
|
return this.#transactionInfo;
|
|
}
|
|
|
|
/**
|
|
* Assign the new transaction details to the payment button.
|
|
*
|
|
* @param {TransactionInfo} newTransactionInfo - Transaction details.
|
|
*/
|
|
set transactionInfo( newTransactionInfo ) {
|
|
this.#transactionInfo = newTransactionInfo;
|
|
|
|
this.refresh();
|
|
}
|
|
|
|
/**
|
|
* The nonce for ajax requests.
|
|
*
|
|
* @return {string} The nonce value
|
|
*/
|
|
get nonce() {
|
|
const input = document.getElementById(
|
|
'woocommerce-process-checkout-nonce'
|
|
);
|
|
|
|
return input?.value || this.buttonConfig.nonce;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
registerValidationRules( invalidIf, validIf ) {
|
|
validIf( () => this.isPreview );
|
|
|
|
invalidIf(
|
|
() => ! this.#applePayConfig,
|
|
'No API configuration - missing configure() call?'
|
|
);
|
|
|
|
invalidIf(
|
|
() => ! this.#transactionInfo,
|
|
'No transactionInfo - missing configure() call?'
|
|
);
|
|
|
|
invalidIf(
|
|
() =>
|
|
this.buttonAttributes?.height &&
|
|
isNaN( parseInt( this.buttonAttributes.height ) ),
|
|
'Invalid height in buttonAttributes'
|
|
);
|
|
|
|
invalidIf(
|
|
() =>
|
|
this.buttonAttributes?.borderRadius &&
|
|
isNaN( parseInt( this.buttonAttributes.borderRadius ) ),
|
|
'Invalid borderRadius in buttonAttributes'
|
|
);
|
|
|
|
invalidIf(
|
|
() => ! this.contextHandler?.validateContext(),
|
|
`Invalid context handler.`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Configures the button instance. Must be called before the initial `init()`.
|
|
*
|
|
* @param {Object} apiConfig - API configuration.
|
|
* @param {TransactionInfo} transactionInfo - Transaction details.
|
|
* @param {Object} buttonAttributes - Button attributes.
|
|
*/
|
|
configure( apiConfig, transactionInfo, buttonAttributes = {} ) {
|
|
// Start timing on first configure call
|
|
if ( ! this.#configureStartTime ) {
|
|
this.#configureStartTime = Date.now();
|
|
}
|
|
|
|
// If valid buttonAttributes, store them
|
|
if ( buttonAttributes?.height && buttonAttributes?.borderRadius ) {
|
|
this.#storedButtonAttributes = { ...buttonAttributes };
|
|
}
|
|
|
|
// Use stored attributes if current ones are missing
|
|
const attributes = buttonAttributes?.height
|
|
? buttonAttributes
|
|
: this.#storedButtonAttributes;
|
|
|
|
// Check if we've exceeded wait time
|
|
const timeWaited = Date.now() - this.#configureStartTime;
|
|
if ( timeWaited > this.#maxWaitTime ) {
|
|
this.log(
|
|
'ApplePay: Timeout waiting for buttonAttributes - proceeding with initialization'
|
|
);
|
|
this.#applePayConfig = apiConfig;
|
|
this.#transactionInfo = transactionInfo;
|
|
this.buttonAttributes = attributes || buttonAttributes;
|
|
this.init();
|
|
return;
|
|
}
|
|
|
|
// Block any initialization until we have valid buttonAttributes
|
|
if ( ! attributes?.height || ! attributes?.borderRadius ) {
|
|
setTimeout(
|
|
() =>
|
|
this.configure(
|
|
apiConfig,
|
|
transactionInfo,
|
|
buttonAttributes
|
|
),
|
|
100
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Reset timer for future configure calls
|
|
this.#configureStartTime = 0;
|
|
|
|
this.#applePayConfig = apiConfig;
|
|
this.#transactionInfo = transactionInfo;
|
|
this.buttonAttributes = attributes;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Use `reinit()` to force a full refresh of an initialized button.
|
|
if ( this.isInitialized ) {
|
|
return;
|
|
}
|
|
|
|
// Stop, if configuration is invalid.
|
|
if ( ! this.validateConfiguration() ) {
|
|
return;
|
|
}
|
|
|
|
super.init();
|
|
this.checkEligibility();
|
|
}
|
|
|
|
async reinit() {
|
|
// Missing (invalid) configuration indicates, that the first `init()` call did not happen yet.
|
|
if ( ! this.validateConfiguration( true ) ) {
|
|
return;
|
|
}
|
|
|
|
// Ensures transaction info is updated when cart or checkout update events are triggered.
|
|
await this.contextHandler
|
|
.transactionInfo()
|
|
.then( ( transactionInfo ) => {
|
|
this.transactionInfo = transactionInfo;
|
|
} )
|
|
.catch( ( error ) => {
|
|
console.error( 'Failed to get transaction info:', error );
|
|
} );
|
|
|
|
super.reinit();
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Re-check if the current session is eligible for Apple Pay.
|
|
*/
|
|
checkEligibility() {
|
|
if ( this.isPreview ) {
|
|
this.isEligible = true;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if ( ! window.ApplePaySession?.canMakePayments() ) {
|
|
this.isEligible = false;
|
|
return;
|
|
}
|
|
|
|
this.isEligible = !! this.#applePayConfig.isEligible;
|
|
} catch ( error ) {
|
|
this.isEligible = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts an Apple Pay session, which means that the user interacted with the Apple Pay button.
|
|
*
|
|
* @param {Object} paymentRequest The payment request object.
|
|
*/
|
|
applePaySession( paymentRequest ) {
|
|
this.log( 'applePaySession', paymentRequest );
|
|
const session = new ApplePaySession( 4, paymentRequest );
|
|
|
|
if ( this.requiresShipping ) {
|
|
session.onshippingmethodselected =
|
|
this.onShippingMethodSelected( session );
|
|
session.onshippingcontactselected =
|
|
this.onShippingContactSelected( session );
|
|
}
|
|
|
|
session.onvalidatemerchant = this.onValidateMerchant( session );
|
|
session.onpaymentauthorized = this.onPaymentAuthorized( session );
|
|
|
|
/**
|
|
* This starts the merchant validation process and displays the payment sheet
|
|
* {@see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778001-begin}
|
|
*
|
|
* After calling the `begin` method, the browser invokes your `onvalidatemerchant` handler
|
|
* {@see https://applepaydemo.apple.com/apple-pay-js-api}
|
|
*/
|
|
session.begin();
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Applies CSS classes and inline styling to the payment button wrapper.
|
|
*/
|
|
applyWrapperStyles() {
|
|
super.applyWrapperStyles();
|
|
|
|
const wrapper = this.wrapperElement;
|
|
if ( ! wrapper ) {
|
|
return;
|
|
}
|
|
|
|
// Try stored attributes if current ones are missing
|
|
const attributes =
|
|
this.buttonAttributes?.height || this.buttonAttributes?.borderRadius
|
|
? this.buttonAttributes
|
|
: this.#storedButtonAttributes;
|
|
|
|
const defaultHeight = 48;
|
|
const defaultBorderRadius = 4;
|
|
|
|
const height = attributes?.height
|
|
? parseInt( attributes.height, 10 )
|
|
: defaultHeight;
|
|
|
|
if ( ! isNaN( height ) ) {
|
|
wrapper.style.setProperty(
|
|
'--apple-pay-button-height',
|
|
`${ height }px`
|
|
);
|
|
wrapper.style.height = `${ height }px`;
|
|
} else {
|
|
wrapper.style.setProperty(
|
|
'--apple-pay-button-height',
|
|
`${ defaultHeight }px`
|
|
);
|
|
wrapper.style.height = `${ defaultHeight }px`;
|
|
}
|
|
|
|
const borderRadius = attributes?.borderRadius
|
|
? parseInt( attributes.borderRadius, 10 )
|
|
: defaultBorderRadius;
|
|
if ( ! isNaN( borderRadius ) ) {
|
|
wrapper.style.borderRadius = `${ borderRadius }px`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the payment button and calls `this.insertButton()` to make the button visible in the
|
|
* correct wrapper.
|
|
*/
|
|
addButton() {
|
|
const { color, type, language } = this.style;
|
|
|
|
// If current buttonAttributes are missing, try to use stored ones
|
|
if (
|
|
! this.buttonAttributes?.height &&
|
|
this.#storedButtonAttributes?.height
|
|
) {
|
|
this.buttonAttributes = { ...this.#storedButtonAttributes };
|
|
}
|
|
|
|
const button = document.createElement( 'apple-pay-button' );
|
|
button.id = 'apple-' + this.wrapperId;
|
|
|
|
button.setAttribute( 'buttonstyle', color );
|
|
button.setAttribute( 'type', type );
|
|
button.setAttribute( 'locale', language );
|
|
|
|
button.style.display = 'block';
|
|
|
|
button.addEventListener( 'click', ( evt ) => {
|
|
evt.preventDefault();
|
|
this.onButtonClick();
|
|
} );
|
|
|
|
this.insertButton( button );
|
|
}
|
|
|
|
//------------------------
|
|
// Button click
|
|
//------------------------
|
|
|
|
/**
|
|
* Show Apple Pay payment sheet when Apple Pay payment button is clicked
|
|
*/
|
|
async onButtonClick() {
|
|
this.log( 'onButtonClick' );
|
|
|
|
const paymentRequest = this.paymentRequest();
|
|
|
|
// Do this on another place like on create order endpoint handler.
|
|
window.ppcpFundingSource = 'apple_pay';
|
|
|
|
// Trigger woocommerce validation if we are in the checkout page.
|
|
if ( PaymentContext.Checkout === this.context ) {
|
|
const checkoutFormSelector = 'form.woocommerce-checkout';
|
|
const errorHandler = new ErrorHandler(
|
|
PayPalCommerceGateway.labels.error.generic,
|
|
document.querySelector( '.woocommerce-notices-wrapper' )
|
|
);
|
|
|
|
try {
|
|
const formData = new FormData(
|
|
document.querySelector( checkoutFormSelector )
|
|
);
|
|
this.#formData = Object.fromEntries( formData.entries() );
|
|
|
|
this.updateRequestDataWithForm( paymentRequest );
|
|
} catch ( error ) {
|
|
console.error( error );
|
|
}
|
|
|
|
this.log( '=== paymentRequest', paymentRequest );
|
|
|
|
const session = this.applePaySession( paymentRequest );
|
|
const formValidator =
|
|
PayPalCommerceGateway.early_checkout_validation_enabled
|
|
? new FormValidator(
|
|
PayPalCommerceGateway.ajax.validate_checkout.endpoint,
|
|
PayPalCommerceGateway.ajax.validate_checkout.nonce
|
|
)
|
|
: null;
|
|
|
|
if ( formValidator ) {
|
|
try {
|
|
const errors = await formValidator.validate(
|
|
document.querySelector( checkoutFormSelector )
|
|
);
|
|
if ( errors.length > 0 ) {
|
|
errorHandler.messages( errors );
|
|
jQuery( document.body ).trigger( 'checkout_error', [
|
|
errorHandler.currentHtml(),
|
|
] );
|
|
session.abort();
|
|
return;
|
|
}
|
|
} catch ( error ) {
|
|
console.error( error );
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Default session initialization.
|
|
this.applePaySession( paymentRequest );
|
|
}
|
|
|
|
/**
|
|
* If the button should be updated with the form addresses.
|
|
*
|
|
* @return {boolean} True, when Apple Pay data should be submitted to WooCommerce.
|
|
*/
|
|
get shouldUpdateButtonWithFormData() {
|
|
if ( PaymentContext.Checkout !== this.context ) {
|
|
return false;
|
|
}
|
|
return (
|
|
this.buttonConfig?.preferences?.checkout_data_mode ===
|
|
'use_applepay'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Indicates how payment completion should be handled if with the context handler default
|
|
* actions. Or with Apple Pay module specific completion.
|
|
*
|
|
* @return {boolean} True, when the Apple Pay data should be submitted to WooCommerce.
|
|
*/
|
|
get shouldCompletePaymentWithContextHandler() {
|
|
// Data already handled, ex: PayNow
|
|
if ( ! this.contextHandler.shippingAllowed() ) {
|
|
return true;
|
|
}
|
|
|
|
// Use WC form data mode in Checkout.
|
|
return (
|
|
PaymentContext.Checkout === this.context &&
|
|
! this.shouldUpdateButtonWithFormData
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates Apple Pay paymentRequest with form data.
|
|
*
|
|
* @param {Object} paymentRequest Object to extend with form data.
|
|
*/
|
|
updateRequestDataWithForm( paymentRequest ) {
|
|
if ( ! this.shouldUpdateButtonWithFormData ) {
|
|
return;
|
|
}
|
|
|
|
// Add billing address.
|
|
paymentRequest.billingContact = this.fillBillingContact(
|
|
this.#formData
|
|
);
|
|
|
|
if ( ! this.requiresShipping ) {
|
|
return;
|
|
}
|
|
|
|
// Add shipping address.
|
|
paymentRequest.shippingContact = this.fillShippingContact(
|
|
this.#formData
|
|
);
|
|
|
|
// Get shipping methods.
|
|
const rate = this.transactionInfo.chosenShippingMethods[ 0 ];
|
|
paymentRequest.shippingMethods = [];
|
|
|
|
// Add selected shipping method.
|
|
for ( const shippingPackage of this.transactionInfo.shippingPackages ) {
|
|
if ( rate === shippingPackage.id ) {
|
|
const shippingMethod = {
|
|
label: shippingPackage.label,
|
|
detail: '',
|
|
amount: shippingPackage.cost_str,
|
|
identifier: shippingPackage.id,
|
|
};
|
|
|
|
// Remember this shipping method as the selected one.
|
|
this.#selectedShippingMethod = shippingMethod;
|
|
|
|
paymentRequest.shippingMethods.push( shippingMethod );
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add other shipping methods.
|
|
for ( const shippingPackage of this.transactionInfo.shippingPackages ) {
|
|
if ( rate !== shippingPackage.id ) {
|
|
paymentRequest.shippingMethods.push( {
|
|
label: shippingPackage.label,
|
|
detail: '',
|
|
amount: shippingPackage.cost_str,
|
|
identifier: shippingPackage.id,
|
|
} );
|
|
}
|
|
}
|
|
|
|
// Store for reuse in case this data is not provided by ApplePay on authorization.
|
|
this.#initialPaymentRequest = paymentRequest;
|
|
|
|
this.log(
|
|
'=== paymentRequest.shippingMethods',
|
|
paymentRequest.shippingMethods
|
|
);
|
|
}
|
|
|
|
paymentRequest() {
|
|
const applepayConfig = this.#applePayConfig;
|
|
const buttonConfig = this.buttonConfig;
|
|
const baseRequest = {
|
|
countryCode: applepayConfig.countryCode,
|
|
merchantCapabilities: applepayConfig.merchantCapabilities,
|
|
supportedNetworks: applepayConfig.supportedNetworks,
|
|
requiredShippingContactFields: [
|
|
'postalAddress',
|
|
'email',
|
|
'phone',
|
|
],
|
|
// ApplePay does not implement billing email and phone fields.
|
|
requiredBillingContactFields: [ 'postalAddress' ],
|
|
};
|
|
|
|
if ( ! this.requiresShipping ) {
|
|
if ( this.shouldCompletePaymentWithContextHandler ) {
|
|
// Data is handled externally.
|
|
baseRequest.requiredShippingContactFields = [];
|
|
} else {
|
|
// Minimum data required to create order.
|
|
baseRequest.requiredShippingContactFields = [
|
|
'email',
|
|
'phone',
|
|
];
|
|
}
|
|
}
|
|
|
|
const paymentRequest = Object.assign( {}, baseRequest );
|
|
paymentRequest.currencyCode = buttonConfig.shop.currencyCode;
|
|
paymentRequest.total = {
|
|
label: buttonConfig.shop.totalLabel,
|
|
type: 'final',
|
|
amount: this.transactionInfo.totalPrice,
|
|
};
|
|
|
|
return paymentRequest;
|
|
}
|
|
|
|
refreshProductContextData() {
|
|
if ( PaymentContext.Product !== this.context ) {
|
|
return;
|
|
}
|
|
|
|
// Refresh product data that makes the price change.
|
|
this.#product.quantity = document.querySelector( 'input.qty' )?.value;
|
|
|
|
// Always an array; grouped products can return multiple items.
|
|
this.#product.items = this.contextHandler.products();
|
|
|
|
this.log( 'Products updated', this.#product );
|
|
}
|
|
|
|
//------------------------
|
|
// Payment process
|
|
//------------------------
|
|
|
|
/**
|
|
* Make ajax call to change the verification-status of the current domain.
|
|
*
|
|
* @param {boolean} isValid
|
|
*/
|
|
adminValidation( isValid ) {
|
|
// eslint-disable-next-line no-unused-vars
|
|
const ignored = fetch( this.buttonConfig.ajax_url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams( {
|
|
action: 'ppcp_validate',
|
|
'woocommerce-process-checkout-nonce': this.nonce,
|
|
validation: isValid,
|
|
} ).toString(),
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Returns an event handler that Apple Pay calls when displaying the payment sheet.
|
|
*
|
|
* @see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778021-onvalidatemerchant
|
|
*
|
|
* @param {Object} session The ApplePaySession object.
|
|
*
|
|
* @return {(function(*): void)|*} Callback that runs after the merchant validation
|
|
*/
|
|
onValidateMerchant( session ) {
|
|
return ( applePayValidateMerchantEvent ) => {
|
|
this.log( 'onvalidatemerchant call' );
|
|
|
|
widgetBuilder.paypal
|
|
.Applepay()
|
|
.validateMerchant( {
|
|
validationUrl: applePayValidateMerchantEvent.validationURL,
|
|
} )
|
|
.then( ( validateResult ) => {
|
|
session.completeMerchantValidation(
|
|
validateResult.merchantSession
|
|
);
|
|
|
|
this.adminValidation( true );
|
|
} )
|
|
.catch( ( validateError ) => {
|
|
console.error( validateError );
|
|
this.adminValidation( false );
|
|
this.log( 'onvalidatemerchant session abort' );
|
|
session.abort();
|
|
} );
|
|
};
|
|
}
|
|
|
|
onShippingMethodSelected( session ) {
|
|
this.log( 'onshippingmethodselected', this.buttonConfig.ajax_url );
|
|
const ajaxUrl = this.buttonConfig.ajax_url;
|
|
return ( event ) => {
|
|
this.log( 'onshippingmethodselected call' );
|
|
|
|
const data = this.getShippingMethodData( event );
|
|
|
|
jQuery.ajax( {
|
|
url: ajaxUrl,
|
|
method: 'POST',
|
|
data,
|
|
success: ( applePayShippingMethodUpdate ) => {
|
|
this.log( 'onshippingmethodselected ok' );
|
|
const response = applePayShippingMethodUpdate.data;
|
|
if ( applePayShippingMethodUpdate.success === false ) {
|
|
response.errors = createAppleErrors( response.errors );
|
|
}
|
|
this.#selectedShippingMethod = event.shippingMethod;
|
|
|
|
// Sort the response shipping methods, so that the selected shipping method is
|
|
// the first one.
|
|
response.newShippingMethods =
|
|
response.newShippingMethods.sort( ( a ) => {
|
|
if (
|
|
a.label === this.#selectedShippingMethod.label
|
|
) {
|
|
return -1;
|
|
}
|
|
return 1;
|
|
} );
|
|
|
|
if ( applePayShippingMethodUpdate.success === false ) {
|
|
response.errors = createAppleErrors( response.errors );
|
|
}
|
|
session.completeShippingMethodSelection( response );
|
|
},
|
|
error: ( jqXHR, textStatus, errorThrown ) => {
|
|
this.log( 'onshippingmethodselected error', textStatus );
|
|
console.warn( textStatus, errorThrown );
|
|
session.abort();
|
|
},
|
|
} );
|
|
};
|
|
}
|
|
|
|
onShippingContactSelected( session ) {
|
|
this.log( 'onshippingcontactselected', this.buttonConfig.ajax_url );
|
|
|
|
const ajaxUrl = this.buttonConfig.ajax_url;
|
|
|
|
return ( event ) => {
|
|
this.log( 'onshippingcontactselected call' );
|
|
|
|
const data = this.getShippingContactData( event );
|
|
|
|
jQuery.ajax( {
|
|
url: ajaxUrl,
|
|
method: 'POST',
|
|
data,
|
|
success: ( applePayShippingContactUpdate ) => {
|
|
this.log( 'onshippingcontactselected ok' );
|
|
const response = applePayShippingContactUpdate.data;
|
|
this.#updatedContactInfo = event.shippingContact;
|
|
if ( applePayShippingContactUpdate.success === false ) {
|
|
response.errors = createAppleErrors( response.errors );
|
|
}
|
|
if ( response.newShippingMethods ) {
|
|
this.#selectedShippingMethod =
|
|
response.newShippingMethods[ 0 ];
|
|
}
|
|
session.completeShippingContactSelection( response );
|
|
},
|
|
error: ( jqXHR, textStatus, errorThrown ) => {
|
|
this.log( 'onshippingcontactselected error', textStatus );
|
|
console.warn( textStatus, errorThrown );
|
|
session.abort();
|
|
},
|
|
} );
|
|
};
|
|
}
|
|
|
|
getShippingContactData( event ) {
|
|
const productId = this.buttonConfig.product.id;
|
|
|
|
this.refreshProductContextData();
|
|
|
|
switch ( this.context ) {
|
|
case PaymentContext.Product:
|
|
return {
|
|
action: 'ppcp_update_shipping_contact',
|
|
product_id: productId,
|
|
products: JSON.stringify( this.#product.items ),
|
|
caller_page: 'productDetail',
|
|
product_quantity: this.#product.quantity,
|
|
simplified_contact: event.shippingContact,
|
|
need_shipping: this.requiresShipping,
|
|
'woocommerce-process-checkout-nonce': this.nonce,
|
|
};
|
|
|
|
case PaymentContext.Cart:
|
|
case PaymentContext.Checkout:
|
|
case PaymentContext.BlockCart:
|
|
case PaymentContext.BlockCheckout:
|
|
case PaymentContext.MiniCart:
|
|
return {
|
|
action: 'ppcp_update_shipping_contact',
|
|
simplified_contact: event.shippingContact,
|
|
caller_page: 'cart',
|
|
need_shipping: this.requiresShipping,
|
|
'woocommerce-process-checkout-nonce': this.nonce,
|
|
};
|
|
}
|
|
}
|
|
|
|
getShippingMethodData( event ) {
|
|
const productId = this.buttonConfig.product.id;
|
|
|
|
this.refreshProductContextData();
|
|
|
|
switch ( this.context ) {
|
|
case PaymentContext.Product:
|
|
return {
|
|
action: 'ppcp_update_shipping_method',
|
|
shipping_method: event.shippingMethod,
|
|
simplified_contact: this.hasValidContactInfo(
|
|
this.#updatedContactInfo
|
|
)
|
|
? this.#updatedContactInfo
|
|
: this.#initialPaymentRequest?.shippingContact ??
|
|
this.#initialPaymentRequest?.billingContact,
|
|
product_id: productId,
|
|
products: JSON.stringify( this.#product.items ),
|
|
caller_page: 'productDetail',
|
|
product_quantity: this.#product.quantity,
|
|
'woocommerce-process-checkout-nonce': this.nonce,
|
|
};
|
|
|
|
case PaymentContext.Cart:
|
|
case PaymentContext.Checkout:
|
|
case PaymentContext.BlockCart:
|
|
case PaymentContext.BlockCheckout:
|
|
case PaymentContext.MiniCart:
|
|
return {
|
|
action: 'ppcp_update_shipping_method',
|
|
shipping_method: event.shippingMethod,
|
|
simplified_contact: this.hasValidContactInfo(
|
|
this.#updatedContactInfo
|
|
)
|
|
? this.#updatedContactInfo
|
|
: this.#initialPaymentRequest?.shippingContact ??
|
|
this.#initialPaymentRequest?.billingContact,
|
|
caller_page: 'cart',
|
|
'woocommerce-process-checkout-nonce': this.nonce,
|
|
};
|
|
}
|
|
}
|
|
|
|
onPaymentAuthorized( session ) {
|
|
this.log( 'onpaymentauthorized' );
|
|
return async ( event ) => {
|
|
this.log( 'onpaymentauthorized call' );
|
|
|
|
const processInWooAndCapture = async ( data ) => {
|
|
return new Promise( ( resolve, reject ) => {
|
|
try {
|
|
const billingContact =
|
|
data.billing_contact ||
|
|
this.#initialPaymentRequest.billingContact;
|
|
const shippingContact =
|
|
data.shipping_contact ||
|
|
this.#initialPaymentRequest.shippingContact;
|
|
const shippingMethod =
|
|
this.#selectedShippingMethod ||
|
|
( this.#initialPaymentRequest.shippingMethods ||
|
|
[] )[ 0 ];
|
|
|
|
const requestData = {
|
|
action: 'ppcp_create_order',
|
|
caller_page: this.context,
|
|
product_id: this.buttonConfig.product.id ?? null,
|
|
products: JSON.stringify( this.#product.items ),
|
|
product_quantity: this.#product.quantity,
|
|
shipping_contact: shippingContact,
|
|
billing_contact: billingContact,
|
|
token: event.payment.token,
|
|
shipping_method: shippingMethod,
|
|
'woocommerce-process-checkout-nonce': this.nonce,
|
|
funding_source: 'applepay',
|
|
_wp_http_referer: '/?wc-ajax=update_order_review',
|
|
paypal_order_id: data.paypal_order_id,
|
|
};
|
|
|
|
this.log(
|
|
'onpaymentauthorized request',
|
|
this.buttonConfig.ajax_url,
|
|
data
|
|
);
|
|
|
|
jQuery.ajax( {
|
|
url: this.buttonConfig.ajax_url,
|
|
method: 'POST',
|
|
data: requestData,
|
|
complete: () => {
|
|
this.log( 'onpaymentauthorized complete' );
|
|
},
|
|
success: ( authorizationResult ) => {
|
|
this.log( 'onpaymentauthorized ok' );
|
|
resolve( authorizationResult );
|
|
},
|
|
error: ( jqXHR, textStatus, errorThrown ) => {
|
|
this.log(
|
|
'onpaymentauthorized error',
|
|
textStatus
|
|
);
|
|
reject( new Error( errorThrown ) );
|
|
},
|
|
} );
|
|
} catch ( error ) {
|
|
this.error( 'onpaymentauthorized catch', error );
|
|
}
|
|
} );
|
|
};
|
|
|
|
const id = await this.contextHandler.createOrder();
|
|
|
|
this.log(
|
|
'onpaymentauthorized paypal order ID',
|
|
id,
|
|
event.payment.token,
|
|
event.payment.billingContact
|
|
);
|
|
|
|
try {
|
|
const confirmOrderResponse = await widgetBuilder.paypal
|
|
.Applepay()
|
|
.confirmOrder( {
|
|
orderId: id,
|
|
token: event.payment.token,
|
|
billingContact: event.payment.billingContact,
|
|
} );
|
|
|
|
this.log(
|
|
'onpaymentauthorized confirmOrderResponse',
|
|
confirmOrderResponse
|
|
);
|
|
|
|
if (
|
|
confirmOrderResponse &&
|
|
confirmOrderResponse.approveApplePayPayment
|
|
) {
|
|
if (
|
|
confirmOrderResponse.approveApplePayPayment.status ===
|
|
'APPROVED'
|
|
) {
|
|
try {
|
|
if (
|
|
this.shouldCompletePaymentWithContextHandler
|
|
) {
|
|
// No shipping, expect immediate capture, ex: PayNow, Checkout with
|
|
// form data.
|
|
|
|
let approveFailed = false;
|
|
await this.contextHandler.approveOrder(
|
|
{
|
|
orderID: id,
|
|
},
|
|
{
|
|
// actions mock object.
|
|
restart: () =>
|
|
new Promise( ( resolve ) => {
|
|
approveFailed = true;
|
|
resolve();
|
|
} ),
|
|
order: {
|
|
get: () =>
|
|
new Promise( ( resolve ) => {
|
|
resolve( null );
|
|
} ),
|
|
},
|
|
}
|
|
);
|
|
|
|
if ( ! approveFailed ) {
|
|
this.log(
|
|
'onpaymentauthorized approveOrder OK'
|
|
);
|
|
session.completePayment(
|
|
ApplePaySession.STATUS_SUCCESS
|
|
);
|
|
} else {
|
|
this.error(
|
|
'onpaymentauthorized approveOrder FAIL'
|
|
);
|
|
session.completePayment(
|
|
ApplePaySession.STATUS_FAILURE
|
|
);
|
|
session.abort();
|
|
}
|
|
} else {
|
|
// Default payment.
|
|
|
|
const data = {
|
|
billing_contact:
|
|
event.payment.billingContact,
|
|
shipping_contact:
|
|
event.payment.shippingContact,
|
|
paypal_order_id: id,
|
|
};
|
|
const authorizationResult =
|
|
await processInWooAndCapture( data );
|
|
if (
|
|
authorizationResult.result === 'success'
|
|
) {
|
|
session.completePayment(
|
|
ApplePaySession.STATUS_SUCCESS
|
|
);
|
|
window.location.href =
|
|
authorizationResult.redirect;
|
|
} else {
|
|
session.completePayment(
|
|
ApplePaySession.STATUS_FAILURE
|
|
);
|
|
}
|
|
}
|
|
} catch ( error ) {
|
|
session.completePayment(
|
|
ApplePaySession.STATUS_FAILURE
|
|
);
|
|
session.abort();
|
|
console.error( error );
|
|
}
|
|
} else {
|
|
console.error( 'Error status is not APPROVED' );
|
|
session.completePayment(
|
|
ApplePaySession.STATUS_FAILURE
|
|
);
|
|
}
|
|
} else {
|
|
console.error( 'Invalid confirmOrderResponse' );
|
|
session.completePayment( ApplePaySession.STATUS_FAILURE );
|
|
}
|
|
} catch ( error ) {
|
|
console.error(
|
|
'Error confirming order with applepay token',
|
|
error
|
|
);
|
|
session.completePayment( ApplePaySession.STATUS_FAILURE );
|
|
session.abort();
|
|
}
|
|
};
|
|
}
|
|
|
|
#extractContactInfo( data, primaryPrefix, fallbackPrefix ) {
|
|
if ( ! data || typeof data !== 'object' ) {
|
|
data = {};
|
|
}
|
|
|
|
const getValue = ( key ) =>
|
|
data[ `${ primaryPrefix }_${ key }` ] ||
|
|
data[ `${ fallbackPrefix }_${ key }` ] ||
|
|
'';
|
|
|
|
return {
|
|
givenName: getValue( 'first_name' ),
|
|
familyName: getValue( 'last_name' ),
|
|
emailAddress: getValue( 'email' ),
|
|
phoneNumber: getValue( 'phone' ),
|
|
addressLines: [ getValue( 'address_1' ), getValue( 'address_2' ) ],
|
|
locality: getValue( 'city' ),
|
|
postalCode: getValue( 'postcode' ),
|
|
countryCode: getValue( 'country' ),
|
|
administrativeArea: getValue( 'state' ),
|
|
};
|
|
}
|
|
|
|
fillBillingContact( data ) {
|
|
return this.#extractContactInfo( data, 'billing', '' );
|
|
}
|
|
|
|
fillShippingContact( data ) {
|
|
if ( ! data?.shipping_first_name ) {
|
|
return this.fillBillingContact( data );
|
|
}
|
|
|
|
return this.#extractContactInfo( data, 'shipping', 'billing' );
|
|
}
|
|
|
|
hasValidContactInfo( value ) {
|
|
return Array.isArray( value )
|
|
? value.length > 0
|
|
: Object.keys( value || {} ).length > 0;
|
|
}
|
|
}
|
|
|
|
export default ApplePayButton;
|