woocommerce-paypal-payments/modules/ppcp-applepay/resources/js/ApplepayButton.js
Philipp Stracker 4a0e410f52
🔧 Add missing config to eslintrc
- env.browser: Linter recognizes browser elements, like `HTMLElement`
- globals.jQuery: Library is present on all pages
2024-07-24 18:55:22 +02:00

1177 lines
31 KiB
JavaScript

/* global ApplePaySession */
/* global PayPalCommerceGateway */
import ContextHandlerFactory from './Context/ContextHandlerFactory';
import { createAppleErrors } from './Helper/applePayError';
import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
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 { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
/**
* 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.
*/
/**
* List of valid context values that the button can have.
*
* @type {Object}
*/
const CONTEXT = {
Product: 'product',
Cart: 'cart',
Checkout: 'checkout',
PayNow: 'pay-now',
MiniCart: 'mini-cart',
BlockCart: 'cart-block',
BlockCheckout: 'checkout-block',
Preview: 'preview',
Blocks: [ 'cart-block', 'checkout-block' ],
};
/**
* 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 {
/**
* Whether the payment button is initialized.
*
* @type {boolean}
*/
#isInitialized = false;
#wrapperId = '';
#ppcpButtonWrapperId = '';
/**
* Context describes the button's location on the website and what details it submits.
*
* @type {''|'product'|'cart'|'checkout'|'pay-now'|'mini-cart'|'cart-block'|'checkout-block'|'preview'}
*/
context = '';
externalHandler = null;
buttonConfig = null;
ppcpConfig = null;
paymentsClient = null;
formData = null;
contextHandler = null;
updatedContactInfo = [];
selectedShippingMethod = [];
/**
* Stores initialization data sent to the button.
*/
initialPaymentRequest = null;
constructor( context, externalHandler, buttonConfig, ppcpConfig ) {
apmButtonsInit( ppcpConfig );
this.context = context;
this.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.contextHandler = ContextHandlerFactory.create(
this.context,
this.buttonConfig,
this.ppcpConfig
);
this.refreshContextData();
// Debug helpers
document.ppcpApplepayButtons = document.ppcpApplepayButtons || {};
document.ppcpApplepayButtons[ this.context ] = this;
this.log = function () {
if ( ! this.buttonConfig.is_debug ) {
return;
}
console.log( `[ApplePayButton | ${ this.context }]`, ...arguments );
};
if ( this.buttonConfig.is_debug ) {
jQuery( document ).on( 'ppcp-applepay-debug', () => {
this.log( this );
} );
}
}
/**
* 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;
}
/**
* Whether the current page qualifies to use the Apple Pay button.
*
* In admin, the button is always eligible, to display an accurate preview.
* On front-end, PayPal's response decides if customers can use Apple Pay.
*
* @return {boolean} True, if the button can be displayed.
*/
get isEligible() {
if ( ! this.#isInitialized ) {
return true;
}
if ( this.buttonConfig.is_admin ) {
return true;
}
return !! ( this.applePayConfig.isEligible && window.ApplePaySession );
}
/**
* Returns the wrapper ID for the current button context.
* The ID varies for the MiniCart context.
*
* @return {string} The wrapper-element's ID (without the `#` prefix).
*/
get wrapperId() {
if ( ! this.#wrapperId ) {
let id;
if ( CONTEXT.MiniCart === this.context ) {
id = this.buttonConfig.button.mini_cart_wrapper;
} else {
id = this.buttonConfig.button.wrapper;
}
this.#wrapperId = id.replace( /^#/, '' );
}
return this.#wrapperId;
}
/**
* Returns the wrapper ID for the ppcpButton
*
* @return {string} The wrapper-element's ID (without the `#` prefix).
*/
get ppcpButtonWrapperId() {
if ( ! this.#ppcpButtonWrapperId ) {
let id;
if ( CONTEXT.MiniCart === this.context ) {
id = this.ppcpConfig.button.mini_cart_wrapper;
} else if ( CONTEXT.Blocks.includes( this.context ) ) {
id = '#express-payment-method-ppcp-gateway-paypal';
} else {
id = this.ppcpConfig.button.wrapper;
}
this.#ppcpButtonWrapperId = id.replace( /^#/, '' );
}
return this.#ppcpButtonWrapperId;
}
/**
* Returns the context-relevant PPCP style object.
* The style for the MiniCart context can be different.
*
* The PPCP style are custom style options, that are provided by this plugin.
*
* @return {PPCPStyle} The style object.
*/
get ppcpStyle() {
if ( CONTEXT.MiniCart === this.context ) {
return this.ppcpConfig.button.mini_cart_style;
}
return this.ppcpConfig.button.style;
}
/**
* Returns default style options that are propagated to and rendered by the Apple Pay button.
*
* These styles are the official style options provided by the Apple Pay SDK.
*
* @return {ApplePayStyle} The style object.
*/
get buttonStyle() {
return {
type: this.buttonConfig.button.type,
lang: this.buttonConfig.button.lang,
color: this.buttonConfig.button.color,
};
}
/**
* Returns the HTML element that wraps the current button
*
* @return {HTMLElement|null} The wrapper element, or null.
*/
get wrapperElement() {
return document.getElementById( this.wrapperId );
}
/**
* Returns an array of HTMLElements that belong to the payment button.
*
* @return {HTMLElement[]} List of payment button wrapper elements.
*/
get allElements() {
const selectors = [];
// Payment button (Pay now, smart button block)
selectors.push( `#${ this.wrapperId }` );
// Block Checkout: Express checkout button.
if ( CONTEXT.Blocks.includes( this.context ) ) {
selectors.push( '#express-payment-method-ppcp-applepay' );
}
// Classic Checkout: Apple Pay gateway.
if ( CONTEXT.Checkout === this.context ) {
selectors.push( '.wc_payment_method.payment_method_ppcp-applepay' );
}
this.log( 'Wrapper Elements:', selectors );
return /** @type {HTMLElement[]} */ selectors.flatMap( ( selector ) =>
Array.from( document.querySelectorAll( selector ) )
);
}
/**
* Checks whether the main button-wrapper is present in the current DOM.
*
* @return {boolean} True, if the button context (wrapper element) is found.
*/
get isPresent() {
return this.wrapperElement instanceof HTMLElement;
}
init( config ) {
if ( this.#isInitialized ) {
return;
}
if ( ! this.contextHandler.validateContext() ) {
return;
}
this.log( 'Init' );
this.initEventHandlers();
this.#isInitialized = true;
this.applePayConfig = config;
if ( ! this.isEligible ) {
this.hide();
} else {
this.show();
this.fetchTransactionInfo().then( () => {
const button = this.addButton();
if ( ! button ) {
return;
}
button.addEventListener( 'click', ( evt ) => {
evt.preventDefault();
this.onButtonClick();
} );
} );
}
}
reinit() {
if ( ! this.applePayConfig ) {
return;
}
this.#isInitialized = false;
this.init( this.applePayConfig );
}
/**
* Hides all wrappers that belong to this ApplePayButton instance.
*/
hide() {
this.allElements.forEach( ( element ) => {
element.style.display = 'none';
} );
}
/**
* Ensures all wrapper elements of this ApplePayButton instance are visible.
*/
show() {
if ( ! this.isPresent ) {
this.log( 'Cannot show button, wrapper is not present' );
return;
}
// Classic Checkout: Make the Apple Pay gateway visible after page load.
if ( CONTEXT.Checkout === this.context ) {
document
.querySelectorAll( 'style#ppcp-hide-apple-pay' )
.forEach( ( el ) => el.remove() );
}
this.allElements.forEach( ( element ) => {
element.style.display = '';
} );
}
async fetchTransactionInfo() {
this.transactionInfo = await this.contextHandler.transactionInfo();
}
initEventHandlers() {
const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
const wrapperId = `#${ this.wrapperId }`;
if ( wrapperId === ppcpButtonWrapper ) {
throw new Error(
`[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapperId }"`
);
}
const syncButtonVisibility = () => {
if ( ! this.isEligible ) {
return;
}
const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
setVisible( wrapperId, $ppcpButtonWrapper.is( ':visible' ) );
setEnabled(
wrapperId,
! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
);
};
jQuery( document ).on(
'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
( ev, data ) => {
if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
syncButtonVisibility();
}
}
);
syncButtonVisibility();
}
/**
* Starts an Apple Pay session.
*
* @param {Object} paymentRequest The payment request object.
*/
applePaySession( paymentRequest ) {
this.log( 'applePaySession', paymentRequest );
const session = new ApplePaySession( 4, paymentRequest );
session.begin();
if ( this.shouldRequireShippingInButton() ) {
session.onshippingmethodselected =
this.onShippingMethodSelected( session );
session.onshippingcontactselected =
this.onShippingContactSelected( session );
}
session.onvalidatemerchant = this.onValidateMerchant( session );
session.onpaymentauthorized = this.onPaymentAuthorized( session );
return session;
}
/**
* Adds an Apple Pay purchase button.
*
* @return {HTMLElement|null} The newly created `<apple-pay-button>` element. Null on failure.
*/
addButton() {
this.log( 'addButton' );
const appleContainer = document.getElementById( this.wrapperId );
const style = this.buttonStyle;
const id = 'apple-' + this.wrapperId;
if ( ! appleContainer ) {
return null;
}
const ppcpStyle = this.ppcpStyle;
appleContainer.innerHTML = `<apple-pay-button id='${ id }' buttonstyle='${ style.color }' type='${ style.type }' locale='${ style.lang }' />`;
appleContainer.classList.add( 'ppcp-button-' + ppcpStyle.shape );
if ( ppcpStyle.height ) {
appleContainer.style.setProperty(
'--apple-pay-button-height',
`${ ppcpStyle.height }px`
);
appleContainer.style.height = `${ ppcpStyle.height }px`;
}
return appleContainer.querySelector( 'apple-pay-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 ( CONTEXT.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 show the shipping fields.
*
* @return {boolean} True, if shipping fields should be captured by ApplePay.
*/
shouldRequireShippingInButton() {
return (
this.contextHandler.shippingAllowed() &&
this.buttonConfig.product.needShipping &&
( CONTEXT.Checkout !== this.context ||
this.shouldUpdateButtonWithFormData() )
);
}
/**
* If the button should be updated with the form addresses.
*
* @return {boolean} True, when Apple Pay data should be submitted to WooCommerce.
*/
shouldUpdateButtonWithFormData() {
if ( CONTEXT.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.
*/
shouldCompletePaymentWithContextHandler() {
// Data already handled, ex: PayNow
if ( ! this.contextHandler.shippingAllowed() ) {
return true;
}
// Use WC form data mode in Checkout.
return (
CONTEXT.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
);
// Add custom data.
// "applicationData" is originating a "PayPalApplePayError: An internal server error has
// occurred" on paypal.Applepay().confirmOrder(). paymentRequest.applicationData =
// this.fillApplicationData(this.formData);
if ( ! this.shouldRequireShippingInButton() ) {
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',
],
requiredBillingContactFields: [ 'postalAddress' ], // ApplePay does not implement billing
// email and phone fields.
};
if ( ! this.shouldRequireShippingInButton() ) {
if ( this.shouldCompletePaymentWithContextHandler() ) {
// Data needs handled externally.
baseRequest.requiredShippingContactFields = [];
} else {
// Minimum data required for order creation.
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;
}
refreshContextData() {
if ( CONTEXT.Product === this.context ) {
// Refresh product data that makes the price change.
this.productQuantity = document.querySelector( 'input.qty' )?.value;
this.products = this.contextHandler.products();
this.log( 'Products updated', this.products );
}
}
//------------------------
// 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,
textStatus,
jqXHR
) => {
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, b ) => {
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,
textStatus,
jqXHR
) => {
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.refreshContextData();
switch ( this.context ) {
case CONTEXT.Product:
return {
action: 'ppcp_update_shipping_contact',
product_id: productId,
products: JSON.stringify( this.products ),
caller_page: 'productDetail',
product_quantity: this.productQuantity,
simplified_contact: event.shippingContact,
need_shipping: this.shouldRequireShippingInButton(),
'woocommerce-process-checkout-nonce': this.nonce,
};
case CONTEXT.Cart:
case CONTEXT.Checkout:
case CONTEXT.BlockCart:
case CONTEXT.BlockCheckout:
case CONTEXT.MiniCart:
return {
action: 'ppcp_update_shipping_contact',
simplified_contact: event.shippingContact,
caller_page: 'cart',
need_shipping: this.shouldRequireShippingInButton(),
'woocommerce-process-checkout-nonce': this.nonce,
};
}
}
getShippingMethodData( event ) {
const productId = this.buttonConfig.product.id;
this.refreshContextData();
switch ( this.context ) {
case CONTEXT.Product:
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
simplified_contact:
this.updatedContactInfo ||
this.initialPaymentRequest.shippingContact ||
this.initialPaymentRequest.billingContact,
product_id: productId,
products: JSON.stringify( this.products ),
caller_page: 'productDetail',
product_quantity: this.productQuantity,
'woocommerce-process-checkout-nonce': this.nonce,
};
case CONTEXT.Cart:
case CONTEXT.Checkout:
case CONTEXT.BlockCart:
case CONTEXT.BlockCheckout:
case CONTEXT.MiniCart:
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
simplified_contact:
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.products ),
product_quantity: this.productQuantity ?? null,
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: ( jqXHR, textStatus ) => {
this.log( 'onpaymentauthorized complete' );
},
success: (
authorizationResult,
textStatus,
jqXHR
) => {
this.log( 'onpaymentauthorized ok' );
resolve( authorizationResult );
},
error: ( jqXHR, textStatus, errorThrown ) => {
this.log(
'onpaymentauthorized error',
textStatus
);
reject( new Error( errorThrown ) );
},
} );
} catch ( error ) {
this.log( 'onpaymentauthorized catch', error );
console.log( error ); // handle 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, reject ) => {
approveFailed = true;
resolve();
}
),
order: {
get: () =>
new Promise(
( resolve, reject ) => {
resolve( null );
}
),
},
}
);
if ( ! approveFailed ) {
this.log(
'onpaymentauthorized approveOrder OK'
);
session.completePayment(
ApplePaySession.STATUS_SUCCESS
);
} else {
this.log(
'onpaymentauthorized approveOrder FAIL'
);
session.completePayment(
ApplePaySession.STATUS_FAILURE
);
session.abort();
console.error( error );
}
} 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();
}
};
}
fillBillingContact( data ) {
return {
givenName: data.billing_first_name ?? '',
familyName: data.billing_last_name ?? '',
emailAddress: data.billing_email ?? '',
phoneNumber: data.billing_phone ?? '',
addressLines: [ data.billing_address_1, data.billing_address_2 ],
locality: data.billing_city ?? '',
postalCode: data.billing_postcode ?? '',
countryCode: data.billing_country ?? '',
administrativeArea: data.billing_state ?? '',
};
}
fillShippingContact( data ) {
if ( data.shipping_first_name === '' ) {
return this.fillBillingContact( data );
}
return {
givenName:
data?.shipping_first_name && data.shipping_first_name !== ''
? data.shipping_first_name
: data?.billing_first_name,
familyName:
data?.shipping_last_name && data.shipping_last_name !== ''
? data.shipping_last_name
: data?.billing_last_name,
emailAddress:
data?.shipping_email && data.shipping_email !== ''
? data.shipping_email
: data?.billing_email,
phoneNumber:
data?.shipping_phone && data.shipping_phone !== ''
? data.shipping_phone
: data?.billing_phone,
addressLines: [
data.shipping_address_1 ?? '',
data.shipping_address_2 ?? '',
],
locality:
data?.shipping_city && data.shipping_city !== ''
? data.shipping_city
: data?.billing_city,
postalCode:
data?.shipping_postcode && data.shipping_postcode !== ''
? data.shipping_postcode
: data?.billing_postcode,
countryCode:
data?.shipping_country && data.shipping_country !== ''
? data.shipping_country
: data?.billing_country,
administrativeArea:
data?.shipping_state && data.shipping_state !== ''
? data.shipping_state
: data?.billing_state,
};
}
fillApplicationData( data ) {
const jsonString = JSON.stringify( data );
const utf8Str = encodeURIComponent( jsonString ).replace(
/%([0-9A-F]{2})/g,
( match, p1 ) => {
return String.fromCharCode( '0x' + p1 );
}
);
return btoa( utf8Str );
}
}
export default ApplePayButton;