woocommerce-paypal-payments/modules/ppcp-axo/resources/js/AxoManager.js

1489 lines
38 KiB
JavaScript
Raw Normal View History

2024-07-12 12:58:34 +02:00
import Fastlane from './Connection/Fastlane';
import { log } from './Helper/Debug';
import DomElementCollection from './Components/DomElementCollection';
import ShippingView from './Views/ShippingView';
import BillingView from './Views/BillingView';
import CardView from './Views/CardView';
import PayPalInsights from './Insights/PayPalInsights';
import {
disable,
enable,
} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import { getCurrentPaymentMethod } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
2024-02-09 17:49:45 +00:00
/**
* Internal customer details.
*
* @typedef {Object} CustomerDetails
* @property {null|string} email - Customer email.
* @property {null|string} phone - Fastlane phone number.
* @property {null|Object} billing - Billing details object.
* @property {null|Object} shipping - Shipping details object.
* @property {null|Object} card - Payment details object.
*/
2024-02-09 17:49:45 +00:00
class AxoManager {
axoConfig = null;
ppcpConfig = null;
$ = null;
2024-09-04 15:06:50 +02:00
fastlane = null;
/**
* @type {FastlaneCardComponent}
*/
cardComponent = null;
2024-09-04 15:06:50 +02:00
initialized = false;
hideGatewaySelection = false;
phoneNumber = null;
/**
* @type {CustomerDetails}
*/
data = {};
status = {};
styles = {};
locale = 'en_us';
el = null;
emailInput = null;
phoneInput = null;
shippingView = null;
billingView = null;
cardView = null;
constructor( namespace, axoConfig, ppcpConfig ) {
this.namespace = namespace;
2024-07-12 12:58:34 +02:00
this.axoConfig = axoConfig;
this.ppcpConfig = ppcpConfig;
this.fastlane = new Fastlane( namespace );
2024-07-12 12:58:34 +02:00
this.$ = jQuery;
this.hasProcessedSessionRestore = false;
2024-07-12 12:58:34 +02:00
this.status = {
active: false,
validEmail: false,
hasProfile: false,
useEmailWidget: this.useEmailWidget(),
hasCard: false,
};
this.clearData();
2024-07-12 12:58:34 +02:00
// TODO - Do we need a public `states` property for this?
2024-07-12 12:58:34 +02:00
this.states = this.axoConfig.woocommerce.states;
this.el = new DomElementCollection();
this.emailInput = document.querySelector(
this.el.fieldBillingEmail.selector + ' input'
);
this.styles = {
root: {
backgroundColorPrimary: '#ffffff',
},
};
this.cardOptions = this.getCardOptions();
this.enabledShippingLocations =
this.axoConfig.enabled_shipping_locations;
2024-07-12 12:58:34 +02:00
this.registerEventHandlers();
this.shippingView = new ShippingView(
this.el.shippingAddressContainer.selector,
this.el,
this.states
);
this.billingView = new BillingView(
this.el.billingAddressContainer.selector,
this.el
);
this.cardView = new CardView(
this.el.paymentContainer.selector + '-details',
this.el,
this
);
document.axoDebugSetStatus = ( key, value ) => {
this.setStatus( key, value );
};
document.axoDebugObject = () => {
return this;
};
if (
this.axoConfig?.insights?.enabled &&
this.axoConfig?.insights?.client_id &&
this.axoConfig?.insights?.session_id
) {
PayPalInsights.config( this.axoConfig?.insights?.client_id, {
debug: axoConfig?.wp_debug === '1',
2024-07-12 12:58:34 +02:00
} );
PayPalInsights.setSessionId( this.axoConfig?.insights?.session_id );
PayPalInsights.trackJsLoad();
if ( document.querySelector( '.woocommerce-checkout' ) ) {
PayPalInsights.trackBeginCheckout( {
amount: this.axoConfig?.insights?.amount,
page_type: 'checkout',
user_data: {
country: 'US',
is_store_member: false,
},
} );
}
}
this.onChangePhone = this.onChangePhone.bind( this );
this.initPhoneSyncWooToFastlane();
2024-07-12 12:58:34 +02:00
this.triggerGatewayChange();
}
/**
* Checks if the current flow is the "Ryan flow": Ryan is a known customer who created a
* Fastlane profile before. Ryan can leverage all benefits of the accelerated 1-click checkout.
*
* @return {boolean} True means, Fastlane could link the customer's email to an existing account.
*/
get isRyanFlow() {
return !! this.data.card;
}
/**
* CSS selector to target the Fastlane Card Component wrapper.
*
* @return {string} CSS selector.
*/
get cardFormSelector() {
return this.el.paymentContainer.selector + '-form';
}
2024-07-12 12:58:34 +02:00
registerEventHandlers() {
// Payment method change tracking with duplicate prevention
let lastSelectedPaymentMethod = document.querySelector(
'input[name=payment_method]:checked'
)?.value;
2024-07-12 12:58:34 +02:00
this.$( document ).on(
'change',
'input[name=payment_method]',
( ev ) => {
if ( lastSelectedPaymentMethod !== ev.target.value ) {
PayPalInsights.trackSelectPaymentMethod( {
payment_method_selected:
this.axoConfig?.insights
?.payment_method_selected_map[
ev.target.value
] || 'other',
page_type: 'checkout',
} );
lastSelectedPaymentMethod = ev.target.value;
}
2024-07-12 12:58:34 +02:00
}
);
// Listen to Gateway Radio button changes.
this.el.gatewayRadioButton.on( 'change', ( ev ) => {
if ( ev.target.checked ) {
this.activateAxo();
} else {
this.deactivateAxo();
}
} );
this.$( document ).on(
'updated_checkout payment_method_selected',
() => {
this.triggerGatewayChange();
}
);
// On checkout form submitted.
this.el.submitButton.on( 'click', () => {
this.onClickSubmitButton();
return false;
} );
// Click change shipping address link.
this.el.changeShippingAddressLink.on( 'click', async () => {
if ( this.status.hasProfile ) {
const { selectionChanged, selectedAddress } =
await this.fastlane.profile.showShippingAddressSelector();
if ( selectionChanged ) {
this.setShipping( selectedAddress );
this.shippingView.refresh();
}
}
} );
// Click change billing address link.
this.el.changeBillingAddressLink.on( 'click', async () => {
if ( this.status.hasProfile ) {
this.el.changeCardLink.trigger( 'click' );
}
} );
// Click change card link.
this.el.changeCardLink.on( 'click', async () => {
const response = await this.fastlane.profile.showCardSelector();
if ( response.selectionChanged ) {
this.setCard( response.selectedCard );
this.setBilling( {
address:
response.selectedCard.paymentSource.card.billingAddress,
} );
}
} );
// Cancel "continuation" mode.
this.el.showGatewaySelectionLink.on( 'click', async () => {
this.hideGatewaySelection = false;
this.$( '.wc_payment_methods label' ).show();
this.$( '.wc_payment_methods input' ).show();
this.cardView.refresh();
} );
// Prevents sending checkout form when pressing Enter key on input field
// and triggers customer lookup
this.$( 'form.woocommerce-checkout input' ).on(
'keydown',
async ( ev ) => {
if (
ev.key === 'Enter' &&
getCurrentPaymentMethod() === 'ppcp-axo-gateway'
) {
ev.preventDefault();
log(
`Enter key attempt - emailInput: ${ this.emailInput.value }`
);
log(
`this.lastEmailCheckedIdentity: ${ this.lastEmailCheckedIdentity }`
);
this.validateEmail( this.el.fieldBillingEmail.selector );
if (
this.emailInput &&
this.lastEmailCheckedIdentity !== this.emailInput.value
) {
await this.onChangeEmail();
}
}
}
);
this.reEnableEmailInput();
// Clear last email checked identity when email field is focused.
this.$( '#billing_email_field input' ).on( 'focus', ( ev ) => {
log(
`Clear the last email checked: ${ this.lastEmailCheckedIdentity }`
);
this.lastEmailCheckedIdentity = '';
} );
// Listening to status update event
document.addEventListener( 'axo_status_updated', ( ev ) => {
const termsField = document.querySelector( "[name='terms-field']" );
if ( termsField ) {
const status = ev.detail;
const shouldHide =
status.active &&
status.validEmail === false &&
status.hasProfile === false;
termsField.parentElement.style.display = shouldHide
? 'none'
: 'block';
}
} );
}
rerender() {
/**
* active | 0 1 1 1
* validEmail | * 0 1 1
* hasProfile | * * 0 1
* --------------------------------
* defaultSubmitButton | 1 0 0 0
* defaultEmailField | 1 0 0 0
* defaultFormFields | 1 0 1 0
* extraFormFields | 0 0 0 1
* axoEmailField | 0 1 0 0
* axoProfileViews | 0 0 0 1
* axoPaymentContainer | 0 0 1 1
* axoSubmitButton | 0 0 1 1
*/
const scenario = this.identifyScenario(
this.status.active,
this.status.validEmail,
this.status.hasProfile
);
log( `Scenario: ${ JSON.stringify( scenario ) }` );
// Reset some elements to a default status.
this.el.watermarkContainer.hide();
if ( scenario.defaultSubmitButton ) {
this.el.defaultSubmitButton.show();
this.el.billingEmailSubmitButton.hide();
} else {
this.el.defaultSubmitButton.hide();
this.el.billingEmailSubmitButton.show();
}
if ( scenario.defaultEmailField ) {
this.el.fieldBillingEmail.show();
} else {
this.el.fieldBillingEmail.hide();
}
if ( scenario.defaultFormFields ) {
this.el.customerDetails.show();
this.toggleLoaderAndOverlay(
this.el.customerDetails,
'loader',
'ppcp-axo-overlay'
);
} else {
this.el.customerDetails.hide();
}
if ( scenario.extraFormFields ) {
this.el.customerDetails.show();
// Hiding of unwanted will be handled by the axoProfileViews handler.
}
if ( scenario.axoEmailField ) {
this.showAxoEmailField();
this.el.watermarkContainer.show();
// Move watermark to after email.
document
.querySelector(
'#billing_email_field .woocommerce-input-wrapper'
)
.append(
document.querySelector(
this.el.watermarkContainer.selector
)
);
} else {
this.el.emailWidgetContainer.hide();
if ( ! scenario.defaultEmailField ) {
this.el.fieldBillingEmail.hide();
}
}
if ( scenario.axoProfileViews ) {
this.shippingView.activate();
this.cardView.activate();
if ( this.status.hasCard ) {
this.billingView.activate();
}
// Move watermark to after shipping.
this.$( this.el.shippingAddressContainer.selector ).after(
this.$( this.el.watermarkContainer.selector )
);
this.el.watermarkContainer.show();
// Add class to customer details container.
this.$( this.el.axoCustomerDetails.selector ).addClass( 'col-1' );
} else {
this.shippingView.deactivate();
this.billingView.deactivate();
this.cardView.deactivate();
this.$( this.el.axoCustomerDetails.selector ).removeClass(
'col-1'
);
}
if ( scenario.axoPaymentContainer ) {
this.el.paymentContainer.show();
this.el.gatewayDescription.hide();
document
.querySelector( this.el.billingEmailSubmitButton.selector )
.setAttribute( 'disabled', 'disabled' );
} else {
this.el.paymentContainer.hide();
}
if ( scenario.axoSubmitButton ) {
this.el.submitButtonContainer.show();
} else {
this.el.submitButtonContainer.hide();
}
this.ensureBillingFieldsConsistency();
this.ensureShippingFieldsConsistency();
}
identifyScenario( active, validEmail, hasProfile ) {
const response = {
defaultSubmitButton: false,
defaultEmailField: false,
defaultFormFields: false,
extraFormFields: false,
axoEmailField: false,
axoProfileViews: false,
axoPaymentContainer: false,
axoSubmitButton: false,
};
if ( active && validEmail && hasProfile ) {
response.extraFormFields = true;
response.axoProfileViews = true;
response.axoPaymentContainer = true;
response.axoSubmitButton = true;
return response;
}
if ( active && validEmail && ! hasProfile ) {
response.defaultFormFields = true;
response.axoEmailField = true;
response.axoPaymentContainer = true;
response.axoSubmitButton = true;
return response;
}
if ( active && ! validEmail ) {
response.axoEmailField = true;
return response;
}
if ( ! active ) {
response.defaultSubmitButton = true;
response.defaultEmailField = true;
response.defaultFormFields = true;
return response;
}
throw new Error( 'Invalid scenario.' );
}
ensureBillingFieldsConsistency() {
const $billingFields = this.$(
'.woocommerce-billing-fields .form-row:visible'
);
const $billingHeaders = this.$( '.woocommerce-billing-fields h3' );
if ( this.billingView.isActive() ) {
if ( $billingFields.length ) {
$billingHeaders.show();
} else {
$billingHeaders.hide();
}
} else {
$billingHeaders.show();
}
}
ensureShippingFieldsConsistency() {
const $shippingFields = this.$(
'.woocommerce-shipping-fields .form-row:visible'
);
const $shippingHeaders = this.$( '.woocommerce-shipping-fields h3' );
if ( this.shippingView.isActive() ) {
if ( $shippingFields.length ) {
$shippingHeaders.show();
} else {
$shippingHeaders.hide();
}
} else {
$shippingHeaders.show();
}
}
showAxoEmailField() {
if ( this.status.useEmailWidget ) {
this.el.emailWidgetContainer.show();
this.el.fieldBillingEmail.hide();
} else {
this.el.emailWidgetContainer.hide();
this.el.fieldBillingEmail.show();
}
}
setStatus( key, value ) {
this.status[ key ] = value;
log( `Status updated: ${ JSON.stringify( this.status ) }` );
document.dispatchEvent(
new CustomEvent( 'axo_status_updated', { detail: this.status } )
);
this.rerender();
}
activateAxo() {
this.initPlacements();
this.initFastlane();
this.setStatus( 'active', true );
2024-09-04 15:06:50 +02:00
this.readPhoneFromWoo();
2024-07-12 12:58:34 +02:00
log( `Attempt on activation - emailInput: ${ this.emailInput.value }` );
log(
`this.lastEmailCheckedIdentity: ${ this.lastEmailCheckedIdentity }`
);
const urlParams = new URLSearchParams( window.location.search );
const hasErrorParam = urlParams.get( 'ppcp_fastlane_error' ) === '1';
if ( hasErrorParam ) {
log(
'Payment failure detected, session restoration will be attempted'
);
} else if (
2024-07-12 12:58:34 +02:00
this.emailInput &&
this.lastEmailCheckedIdentity !== this.emailInput.value
) {
this.onChangeEmail();
2024-09-04 15:11:46 +02:00
} else {
this.refreshFastlanePrefills();
2024-07-12 12:58:34 +02:00
}
}
deactivateAxo() {
this.setStatus( 'active', false );
}
initPlacements() {
const wrapper = this.el.axoCustomerDetails;
// Customer details container.
if ( ! document.querySelector( wrapper.selector ) ) {
document.querySelector( wrapper.anchorSelector ).insertAdjacentHTML(
'afterbegin',
`
<div id="${ wrapper.id }" class="${ wrapper.className }"></div>
`
);
}
const wrapperElement = document.querySelector( wrapper.selector );
// Billing view container.
const bc = this.el.billingAddressContainer;
if ( ! document.querySelector( bc.selector ) ) {
wrapperElement.insertAdjacentHTML(
'beforeend',
`
<div id="${ bc.id }" class="${ bc.className }"></div>
`
);
}
// Shipping view container.
const sc = this.el.shippingAddressContainer;
if ( ! document.querySelector( sc.selector ) ) {
wrapperElement.insertAdjacentHTML(
'beforeend',
`
<div id="${ sc.id }" class="${ sc.className }"></div>
`
);
}
// billingEmailFieldWrapper
const befw = this.el.billingEmailFieldWrapper;
if ( ! document.querySelector( befw.selector ) ) {
document
.querySelector(
'#billing_email_field .woocommerce-input-wrapper'
)
.insertAdjacentHTML(
'afterend',
`
<div id="${ befw.id }"></div>
`
);
}
// Watermark container
const wc = this.el.watermarkContainer;
if ( ! document.querySelector( wc.selector ) ) {
document.querySelector( befw.selector ).insertAdjacentHTML(
'beforeend',
`
<div class="${ wc.className }" id="${ wc.id }"></div>
`
);
}
// Payment container
const pc = this.el.paymentContainer;
if ( ! document.querySelector( pc.selector ) ) {
const gatewayPaymentContainer = document.querySelector(
'.payment_method_ppcp-axo-gateway'
);
gatewayPaymentContainer.insertAdjacentHTML(
'beforeend',
`
<div id="${ pc.id }" class="${ pc.className } axo-hidden">
<div id="${ pc.id }-form" class="${ pc.className }-form"></div>
<div id="${ pc.id }-details" class="${ pc.className }-details"></div>
2024-04-10 18:27:21 +01:00
</div>
2024-07-12 12:58:34 +02:00
`
);
}
if ( this.useEmailWidget() ) {
// Display email widget.
const ec = this.el.emailWidgetContainer;
if ( ! document.querySelector( ec.selector ) ) {
wrapperElement.insertAdjacentHTML(
'afterbegin',
`
<div id="${ ec.id }" class="${ ec.className }">
--- EMAIL WIDGET PLACEHOLDER ---
</div>
2024-07-12 12:58:34 +02:00
`
);
}
} else {
// Move email to the AXO container.
const emailRow = document.querySelector(
this.el.fieldBillingEmail.selector
);
wrapperElement.prepend( emailRow );
document
.querySelector( this.el.billingEmailFieldWrapper.selector )
.prepend(
document.querySelector(
'#billing_email_field .woocommerce-input-wrapper'
)
);
}
}
async initFastlane() {
if ( this.initialized ) {
return;
}
this.initialized = true;
await this.connect();
await this.renderWatermark();
this.renderEmailSubmitButton();
this.watchEmail();
await this.restoreSessionAfterFailure();
2024-07-12 12:58:34 +02:00
}
async connect() {
if ( this.axoConfig.environment.is_sandbox ) {
window.localStorage.setItem( 'axoEnv', 'sandbox' );
}
await this.fastlane.connect( {
locale: this.locale,
styles: this.styles,
cardOptions: {
allowedBrands: this.cardOptions,
},
shippingAddressOptions: {
allowedLocations: this.enabledShippingLocations,
},
2024-07-12 12:58:34 +02:00
} );
this.fastlane.setLocale( 'en_us' );
}
triggerGatewayChange() {
this.el.gatewayRadioButton.trigger( 'change' );
}
async renderWatermark( includeAdditionalInfo = true ) {
(
await this.fastlane.FastlaneWatermarkComponent( {
includeAdditionalInfo,
} )
).render( this.el.watermarkContainer.selector );
this.toggleWatermarkLoading(
this.el.watermarkContainer,
'ppcp-axo-watermark-loading',
'loader'
);
}
renderEmailSubmitButton() {
const billingEmailSubmitButton = this.el.billingEmailSubmitButton;
const billingEmailSubmitButtonSpinner =
this.el.billingEmailSubmitButtonSpinner;
if ( ! document.querySelector( billingEmailSubmitButton.selector ) ) {
2024-09-30 17:26:23 +02:00
const wrapper = document.querySelector(
'#billing_email_field .woocommerce-input-wrapper'
);
const watermarkContainer = document.querySelector(
'#ppcp-axo-watermark-container'
);
wrapper.insertAdjacentHTML(
'beforeend',
`
<button type="button" id="${ billingEmailSubmitButton.id }" class="${ billingEmailSubmitButton.className }">
${ this.axoConfig.billing_email_button_text }
<span id="${ billingEmailSubmitButtonSpinner.id }"></span>
</button>
2024-07-12 12:58:34 +02:00
`
2024-09-30 17:26:23 +02:00
);
2024-07-12 12:58:34 +02:00
2024-09-30 17:26:23 +02:00
const buttonElement = document.querySelector(
billingEmailSubmitButton.selector
);
// Reorder button to ensure it's before the watermark container
wrapper.insertBefore( buttonElement, watermarkContainer );
buttonElement.offsetHeight;
buttonElement.classList.remove(
'ppcp-axo-billing-email-submit-button-hidden'
);
buttonElement.classList.add(
'ppcp-axo-billing-email-submit-button-loaded'
);
2024-07-12 12:58:34 +02:00
}
}
watchEmail() {
if ( this.useEmailWidget() ) {
// TODO
} else {
this.emailInput.addEventListener( 'change', async () => {
log(
`Change event attempt - emailInput: ${ this.emailInput.value }`
);
log(
`this.lastEmailCheckedIdentity: ${ this.lastEmailCheckedIdentity }`
);
if (
this.emailInput &&
this.lastEmailCheckedIdentity !== this.emailInput.value
) {
this.validateEmail( this.el.fieldBillingEmail.selector );
this.onChangeEmail();
}
} );
log(
`Last, this.emailInput.value attempt - emailInput: ${ this.emailInput.value }`
);
log(
`this.lastEmailCheckedIdentity: ${ this.lastEmailCheckedIdentity }`
);
if ( this.emailInput.value ) {
this.onChangeEmail();
}
}
}
/**
* Locates the WooCommerce checkout "billing phone" field and adds event listeners to it.
*/
initPhoneSyncWooToFastlane() {
this.phoneInput = document.querySelector( '#billing_phone' );
this.phoneInput?.addEventListener( 'change', this.onChangePhone );
}
/**
* Strips the country prefix and non-numeric characters from the phone number. If the remaining
* phone number is valid, it's returned. Otherwise, the function returns null.
*
* @param {string} number - Phone number to sanitize.
* @return {string|null} A valid US phone number, or null if the number is invalid.
*/
sanitizePhoneNumber( number ) {
const localNumber = number.replace( /^\+1/, '' );
const cleanNumber = localNumber.replace( /\D/g, '' );
// All valid US mobile numbers have exactly 10 digits.
return cleanNumber.length === 10 ? cleanNumber : null;
}
/**
* Reads the phone number from the WooCommerce checkout form, sanitizes it, and (if valid)
* stores it in the internal customer details object.
*
* @return {boolean} True, if the internal phone number was updated.
*/
2024-09-04 15:06:50 +02:00
readPhoneFromWoo() {
if ( ! this.phoneInput ) {
return false;
}
const phoneNumber = this.phoneInput.value;
const validPhoneNumber = this.sanitizePhoneNumber( phoneNumber );
if ( ! validPhoneNumber ) {
return false;
}
this.data.phone = validPhoneNumber;
return true;
}
2024-07-12 12:58:34 +02:00
async onChangeEmail() {
if ( ! this.status.active ) {
log( 'Email checking skipped, AXO not active.' );
return;
}
if ( ! this.emailInput ) {
log( 'Email field not initialized.' );
return;
}
if ( this.data.email === this.emailInput.value ) {
log( 'Email has not changed since last validation.' );
return;
}
2024-07-12 12:58:34 +02:00
log(
`Email changed: ${
this.emailInput ? this.emailInput.value : '<empty>'
}`
);
this.clearData();
2024-07-12 12:58:34 +02:00
this.emailInput.value = this.stripSpaces( this.emailInput.value );
this.$( this.el.paymentContainer.selector + '-details' ).html( '' );
this.removeFastlaneComponent();
2024-07-12 12:58:34 +02:00
this.setStatus( 'validEmail', false );
this.setStatus( 'hasProfile', false );
this.hideGatewaySelection = false;
this.lastEmailCheckedIdentity = this.emailInput.value;
if (
! this.emailInput.value ||
! this.emailInput.checkValidity() ||
! this.validateEmailFormat( this.emailInput.value )
) {
log( 'The email address is not valid.' );
return;
}
this.data.email = this.emailInput.value;
this.billingView.setData( this.data );
2024-09-04 15:06:50 +02:00
this.readPhoneFromWoo();
2024-07-12 12:58:34 +02:00
if ( ! this.fastlane.identity ) {
log( 'Not initialized.' );
return;
}
PayPalInsights.trackSubmitCheckoutEmail( {
page_type: 'checkout',
} );
this.disableGatewaySelection();
this.spinnerToggleLoaderAndOverlay(
this.el.billingEmailSubmitButtonSpinner,
'loader',
'ppcp-axo-overlay'
);
await this.lookupCustomerByEmail();
this.spinnerToggleLoaderAndOverlay(
this.el.billingEmailSubmitButtonSpinner,
'loader',
'ppcp-axo-overlay'
);
this.enableGatewaySelection();
}
/**
* Event handler that fires when the customer changed the phone number in the WooCommerce
* checkout form. If Fastlane is active, the component is refreshed.
*
* @return {Promise<void>}
*/
async onChangePhone() {
2024-09-04 15:06:50 +02:00
const hasChanged = this.readPhoneFromWoo();
if ( hasChanged && this.status.active ) {
2024-09-04 15:11:46 +02:00
await this.refreshFastlanePrefills();
}
return Promise.resolve();
}
2024-07-12 12:58:34 +02:00
async lookupCustomerByEmail() {
const lookupResponse =
await this.fastlane.identity.lookupCustomerByEmail(
this.emailInput.value
);
log( `lookupCustomerByEmail: ${ JSON.stringify( lookupResponse ) }` );
if ( lookupResponse.customerContextId ) {
// Email is associated with a Connect profile or a PayPal member.
// Authenticate the customer to get access to their profile.
log(
'Email is associated with a Connect profile or a PayPal member'
);
const authResponse =
await this.fastlane.identity.triggerAuthenticationFlow(
lookupResponse.customerContextId
);
log(
`AuthResponse - triggerAuthenticationFlow: ${ JSON.stringify(
authResponse
) }`
);
if ( authResponse.authenticationState === 'succeeded' ) {
const shippingData = authResponse.profileData.shippingAddress;
if ( shippingData ) {
this.setShipping( shippingData );
}
if ( authResponse.profileData.card ) {
this.setStatus( 'hasCard', true );
} else {
await this.initializeFastlaneComponent();
2024-07-12 12:58:34 +02:00
}
const cardBillingAddress =
authResponse.profileData?.card?.paymentSource?.card
?.billingAddress;
if ( cardBillingAddress ) {
this.setCard( authResponse.profileData.card );
const billingData = {
address: cardBillingAddress,
};
const phoneNumber =
authResponse.profileData?.shippingAddress?.phoneNumber
?.nationalNumber ?? '';
if ( phoneNumber ) {
billingData.phoneNumber = phoneNumber;
}
this.setBilling( billingData );
}
this.setStatus( 'validEmail', true );
this.setStatus( 'hasProfile', true );
this.hideGatewaySelection = true;
this.$( '.wc_payment_methods label' ).hide();
this.$( '.wc_payment_methods input' ).hide();
await this.renderWatermark( false );
this.rerender();
} else {
// authentication failed or canceled by the customer
// set status as guest customer
log( 'Authentication Failed' );
this.setStatus( 'validEmail', true );
this.setStatus( 'hasProfile', false );
await this.renderWatermark( true );
await this.initializeFastlaneComponent();
2024-07-12 12:58:34 +02:00
}
} else {
// No profile found with this email address.
// This is a guest customer.
log( 'No profile found with this email address.' );
this.setStatus( 'validEmail', true );
this.setStatus( 'hasProfile', false );
await this.renderWatermark( true );
await this.initializeFastlaneComponent();
2024-07-12 12:58:34 +02:00
}
}
disableGatewaySelection() {
this.$( '.wc_payment_methods input' ).prop( 'disabled', true );
}
enableGatewaySelection() {
this.$( '.wc_payment_methods input' ).prop( 'disabled', false );
}
clearData() {
this.data = {
email: null,
phone: null,
2024-07-12 12:58:34 +02:00
billing: null,
shipping: null,
card: null,
};
}
setShipping( shipping ) {
this.data.shipping = shipping;
this.shippingView.setData( this.data );
}
setBilling( billing ) {
this.data.billing = billing;
this.billingView.setData( this.data );
}
setCard( card ) {
this.data.card = card;
this.cardView.setData( this.data );
}
onClickSubmitButton() {
// TODO: validate data.
if ( this.isRyanFlow ) {
2024-07-12 12:58:34 +02:00
// Ryan flow
log( 'Starting Ryan flow.' );
this.$( '#ship-to-different-address-checkbox' ).prop(
'checked',
'checked'
);
const data = {};
this.billingView.toSubmitData( data );
this.shippingView.toSubmitData( data );
this.cardView.toSubmitData( data );
this.ensureBillingPhoneNumber( data );
log( `Ryan flow - submitted nonce: ${ this.data.card.id }` );
this.submit( this.data.card.id, data );
} else {
// Gary flow
log( 'Starting Gary flow.' );
try {
this.cardComponent
.getPaymentToken( this.tokenizeData() )
.then( ( response ) => {
log( `Gary flow - submitted nonce: ${ response.id }` );
this.submit( response.id );
} );
} catch ( e ) {
alert( 'Error tokenizing data.' );
log( `Error tokenizing data. ${ e.message }`, 'error' );
}
}
}
cardComponentData() {
const config = {
2024-07-12 12:58:34 +02:00
fields: {
cardholderName: {
enabled: this.axoConfig.name_on_card === '1',
},
},
styles: this.deleteKeysWithEmptyString(
this.axoConfig.style_options
),
};
// Ryan is a known customer, we do not need his phone number.
if ( this.data.phone && ! this.isRyanFlow ) {
config.fields.phoneNumber = {
prefill: this.data.phone,
};
}
return config;
}
/**
* Initializes the Fastlane UI component, using configuration provided by the
* `cardComponentData()` method. If the UI component was already initialized, nothing happens.
*
* @return {Promise<*>} Resolves when the component was rendered.
*/
async initializeFastlaneComponent() {
2024-09-04 15:11:46 +02:00
if ( ! this.status.active || this.cardComponent ) {
return Promise.resolve();
}
const elem = this.cardFormSelector;
const config = this.cardComponentData();
this.cardComponent =
await this.fastlane.FastlaneCardComponent( config );
return this.cardComponent.render( elem );
2024-07-12 12:58:34 +02:00
}
/**
* Reverts the changes made by `initializeFastlaneComponent()`.
*
* Calling this method will lose any input that the user made inside the
* Fastlane Card Component.
*/
removeFastlaneComponent() {
document.querySelector( this.cardFormSelector ).innerHTML = '';
this.cardComponent = null;
}
2024-09-04 15:11:46 +02:00
/**
* Updates the prefill-values in the UI component. This method only updates empty fields.
*
* @return {Promise<*>} Resolves when the component was refreshed.
*/
async refreshFastlanePrefills() {
if ( ! this.cardComponent ) {
return Promise.resolve();
}
const { fields } = this.cardComponentData();
const prefills = Object.keys( fields ).reduce( ( result, key ) => {
if ( fields[ key ].hasOwnProperty( 'prefill' ) ) {
result[ key ] = fields[ key ].prefill;
}
return result;
}, {} );
return this.cardComponent.updatePrefills( prefills );
}
2024-07-12 12:58:34 +02:00
tokenizeData() {
return {
cardholderName: {
fullName: this.billingView.fullName(),
},
billingAddress: {
addressLine1: this.billingView.inputValue( 'street1' ),
addressLine2: this.billingView.inputValue( 'street2' ),
adminArea1: this.billingView.inputValue( 'stateCode' ),
adminArea2: this.billingView.inputValue( 'city' ),
postalCode: this.billingView.inputValue( 'postCode' ),
countryCode: this.billingView.inputValue( 'countryCode' ),
},
};
}
submit( nonce, data ) {
// Send the nonce and previously captured device data to server to complete checkout
if ( ! this.el.axoNonceInput.get() ) {
this.$( 'form.woocommerce-checkout' ).append(
`<input type="hidden" id="${ this.el.axoNonceInput.id }" name="axo_nonce" value="" />`
);
}
this.el.axoNonceInput.get().value = nonce;
if ( data ) {
// Ryan flow.
const form = document.querySelector( 'form.woocommerce-checkout' );
const formData = new FormData( form );
this.showLoading();
// Fill in form data.
Object.keys( data ).forEach( ( key ) => {
formData.set( key, data[ key ] );
} );
// Set type of user (Ryan) to be received on WC gateway process payment request.
formData.set( 'fastlane_member', true );
fetch( wc_checkout_params.checkout_url, {
// TODO: maybe create a new endpoint to process_payment.
method: 'POST',
body: formData,
} )
.then( ( response ) => response.json() )
.then( ( responseData ) => {
if ( responseData.result === 'failure' ) {
if ( responseData.messages ) {
const $notices = this.$(
'.woocommerce-notices-wrapper'
).eq( 0 );
$notices.html( responseData.messages );
this.$( 'html, body' ).animate(
{
scrollTop: $notices.offset().top,
},
500
);
}
log(
`Error sending checkout form. ${ responseData }`,
'error'
);
this.hideLoading();
return;
}
if ( responseData.redirect ) {
window.location.href = responseData.redirect;
}
} )
.catch( ( error ) => {
log(
`Error sending checkout form. ${ error.message }`,
'error'
);
this.hideLoading();
} );
} else {
// Gary flow.
this.el.defaultSubmitButton.click();
}
}
showLoading() {
const submitContainerSelector = '.woocommerce-checkout-payment';
jQuery( 'form.woocommerce-checkout' ).append(
'<div class="blockUI blockOverlay" style="z-index: 1000; border: medium; margin: 0px; padding: 0px; width: 100%; height: 100%; top: 0px; left: 0px; background: rgb(255, 255, 255); opacity: 0.6; cursor: default; position: absolute;"></div>'
);
disable( submitContainerSelector );
}
hideLoading() {
const submitContainerSelector = '.woocommerce-checkout-payment';
jQuery( 'form.woocommerce-checkout .blockOverlay' ).remove();
enable( submitContainerSelector );
}
useEmailWidget() {
return this.axoConfig?.widgets?.email === 'use_widget';
}
getCardOptions() {
const DEFAULT_ALLOWED_CARDS = [
'VISA',
'MASTERCARD',
'AMEX',
'DISCOVER',
];
const merchantCountry = this.axoConfig.merchant_country || 'US';
const allowedCards = new Set(
this.axoConfig.allowed_cards?.[ merchantCountry ] ||
DEFAULT_ALLOWED_CARDS
);
const disabledCards = new Set(
( this.axoConfig.disable_cards || [] ).map( ( card ) =>
card.toUpperCase()
)
);
return [ ...allowedCards ].filter(
( card ) => ! disabledCards.has( card )
);
}
2024-07-12 12:58:34 +02:00
deleteKeysWithEmptyString = ( obj ) => {
for ( const key of Object.keys( obj ) ) {
if ( obj[ key ] === '' ) {
delete obj[ key ];
} else if ( typeof obj[ key ] === 'object' ) {
obj[ key ] = this.deleteKeysWithEmptyString( obj[ key ] );
if ( Object.keys( obj[ key ] ).length === 0 ) {
delete obj[ key ];
}
}
}
return Array.isArray( obj ) ? obj.filter( ( val ) => val ) : obj;
};
ensureBillingPhoneNumber( data ) {
if ( data.billing_phone === '' ) {
let phone = '';
const cc = this.data.shipping?.phoneNumber?.countryCode;
const number = this.data.shipping?.phoneNumber?.nationalNumber;
2024-07-12 12:58:34 +02:00
if ( cc ) {
phone = `+${ cc } `;
}
phone += number;
data.billing_phone = phone;
}
}
toggleLoaderAndOverlay( element, loaderClass, overlayClass ) {
const loader = document.querySelector(
`${ element.selector } .${ loaderClass }`
);
const overlay = document.querySelector(
`${ element.selector } .${ overlayClass }`
);
if ( loader ) {
loader.classList.toggle( loaderClass );
}
if ( overlay ) {
overlay.classList.toggle( overlayClass );
}
}
spinnerToggleLoaderAndOverlay( element, loaderClass, overlayClass ) {
const spinner = document.querySelector( `${ element.selector }` );
if ( spinner ) {
spinner.classList.toggle( loaderClass );
spinner.classList.toggle( overlayClass );
}
}
toggleWatermarkLoading( container, loadingClass, loaderClass ) {
const watermarkLoading = document.querySelector(
`${ container.selector }.${ loadingClass }`
);
const watermarkLoader = document.querySelector(
`${ container.selector }.${ loaderClass }`
);
if ( watermarkLoading ) {
watermarkLoading.classList.toggle( loadingClass );
}
if ( watermarkLoader ) {
watermarkLoader.classList.toggle( loaderClass );
}
}
validateEmailFormat( value ) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test( value );
}
stripSpaces( str ) {
return str.replace( /\s+/g, '' );
}
2024-07-12 12:58:34 +02:00
validateEmail( billingEmail ) {
const billingEmailSelector = document.querySelector( billingEmail );
const value = document.querySelector( billingEmail + ' input' ).value;
if ( this.validateEmailFormat( value ) ) {
billingEmailSelector.classList.remove( 'woocommerce-invalid' );
billingEmailSelector.classList.add( 'woocommerce-validated' );
this.setStatus( 'validEmail', true );
} else {
billingEmailSelector.classList.remove( 'woocommerce-validated' );
billingEmailSelector.classList.add( 'woocommerce-invalid' );
this.setStatus( 'validEmail', false );
}
}
reEnableEmailInput() {
const reEnableInput = ( ev ) => {
const submitButton = document.querySelector(
this.el.billingEmailSubmitButton.selector
);
if ( submitButton.hasAttribute( 'disabled' ) ) {
submitButton.removeAttribute( 'disabled' );
}
};
this.$( '#billing_email_field input' ).on( 'focus', reEnableInput );
this.$( '#billing_email_field input' ).on( 'input', reEnableInput );
this.$( '#billing_email_field input' ).on( 'click', reEnableInput );
}
async restoreSessionAfterFailure() {
if ( ! this.fastlane || this.hasProcessedSessionRestore ) {
return;
}
const urlParams = new URLSearchParams( window.location.search );
const hasErrorParam = urlParams.get( 'ppcp_fastlane_error' ) === '1';
if ( ! hasErrorParam ) {
return;
}
urlParams.delete( 'ppcp_fastlane_error' );
const newUrl = new URL( window.location );
newUrl.search = urlParams.toString();
window.history.replaceState( {}, '', newUrl );
this.hasProcessedSessionRestore = true;
try {
if ( this.emailInput?.value ) {
log(
`Restoring Fastlane session for email: ${ this.emailInput.value }`
);
const lookupResult =
await this.fastlane.identity.lookupCustomerByEmail(
this.emailInput.value
);
if ( lookupResult?.customerContextId ) {
const authenticatedCustomerResult =
await this.fastlane.identity.triggerAuthenticationFlow(
lookupResult.customerContextId
);
if (
authenticatedCustomerResult?.authenticationState ===
'succeeded'
) {
const { profileData } = authenticatedCustomerResult;
if ( profileData?.shippingAddress ) {
this.setShipping( profileData.shippingAddress );
}
if ( profileData?.card ) {
this.setCard( profileData.card );
this.setStatus( 'hasCard', true );
const cardBillingAddress =
profileData.card?.paymentSource?.card
?.billingAddress;
if ( cardBillingAddress ) {
const billingData = {
address: cardBillingAddress,
};
const phoneNumber =
profileData.shippingAddress?.phoneNumber
?.nationalNumber;
if ( phoneNumber ) {
billingData.phoneNumber = phoneNumber;
}
this.setBilling( billingData );
}
}
this.setStatus( 'validEmail', true );
this.setStatus( 'hasProfile', true );
this.hideGatewaySelection = true;
this.$( '.wc_payment_methods label' ).hide();
this.$( '.wc_payment_methods input' ).hide();
await this.renderWatermark( false );
log( 'Fastlane session successfully restored' );
}
}
}
} catch ( error ) {
log( 'Failed to restore Fastlane session', 'warn' );
console.warn( 'Fastlane session restoration error:', error );
}
}
2024-02-09 17:49:45 +00:00
}
export default AxoManager;