Merge pull request #2665 from woocommerce/PCP-3541-sync-woo-with-axo-shipping

Fastlane update shipping options & taxes when changing address (3541)
This commit is contained in:
Emili Castells 2024-10-07 10:39:05 +02:00 committed by GitHub
commit 3be4456bb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 227 additions and 38 deletions

View file

@ -801,8 +801,6 @@ class AxoManager {
} }
async onChangeEmail() { async onChangeEmail() {
this.clearData();
if ( ! this.status.active ) { if ( ! this.status.active ) {
log( 'Email checking skipped, AXO not active.' ); log( 'Email checking skipped, AXO not active.' );
return; return;
@ -813,11 +811,17 @@ class AxoManager {
return; return;
} }
if ( this.data.email === this.emailInput.value ) {
log( 'Email has not changed since last validation.' );
return;
}
log( log(
`Email changed: ${ `Email changed: ${
this.emailInput ? this.emailInput.value : '<empty>' this.emailInput ? this.emailInput.value : '<empty>'
}` }`
); );
this.clearData();
this.emailInput.value = this.stripSpaces( this.emailInput.value ); this.emailInput.value = this.stripSpaces( this.emailInput.value );

View file

@ -1,30 +1,35 @@
class FormFieldGroup { class FormFieldGroup {
#stored;
#data = {};
#active = false;
#baseSelector;
#contentSelector;
#fields = {};
#template;
constructor( config ) { constructor( config ) {
this.data = {}; this.#baseSelector = config.baseSelector;
this.#contentSelector = config.contentSelector;
this.baseSelector = config.baseSelector; this.#fields = config.fields || {};
this.contentSelector = config.contentSelector; this.#template = config.template;
this.fields = config.fields || {}; this.#stored = new Map();
this.template = config.template;
this.active = false;
} }
setData( data ) { setData( data ) {
this.data = data; this.#data = data;
this.refresh(); this.refresh();
} }
dataValue( fieldKey ) { dataValue( fieldKey ) {
if ( ! fieldKey || ! this.fields[ fieldKey ] ) { if ( ! fieldKey || ! this.#fields[ fieldKey ] ) {
return ''; return '';
} }
if ( typeof this.fields[ fieldKey ].valueCallback === 'function' ) { if ( typeof this.#fields[ fieldKey ].valueCallback === 'function' ) {
return this.fields[ fieldKey ].valueCallback( this.data ); return this.#fields[ fieldKey ].valueCallback( this.#data );
} }
const path = this.fields[ fieldKey ].valuePath; const path = this.#fields[ fieldKey ].valuePath;
if ( ! path ) { if ( ! path ) {
return ''; return '';
@ -35,27 +40,84 @@ class FormFieldGroup {
.reduce( .reduce(
( acc, key ) => ( acc, key ) =>
acc && acc[ key ] !== undefined ? acc[ key ] : undefined, acc && acc[ key ] !== undefined ? acc[ key ] : undefined,
this.data this.#data
); );
return value ? value : ''; return value ? value : '';
} }
/**
* Changes the value of the input field.
*
* @param {Element|null} field
* @param {string|boolean} value
* @return {boolean} True indicates that the previous value was different from the new value.
*/
#setFieldValue( field, value ) {
let oldVal;
const isValidOption = () => {
for ( let i = 0; i < field.options.length; i++ ) {
if ( field.options[ i ].value === value ) {
return true;
}
}
return false;
};
if ( ! field ) {
return false;
}
// If an invalid option is provided, do nothing.
if ( 'SELECT' === field.tagName && ! isValidOption() ) {
return false;
}
if ( 'checkbox' === field.type || 'radio' === field.type ) {
value = !! value;
oldVal = field.checked;
field.checked = value;
} else {
oldVal = field.value;
field.value = value;
}
return oldVal !== value;
}
/**
* Activate form group: Render a custom Fastlane UI to replace the WooCommerce form.
*
* Indicates: Ryan flow.
*/
activate() { activate() {
this.active = true; this.#active = true;
this.storeFormData();
this.refresh(); this.refresh();
} }
/**
* Deactivate form group: Remove the custom Fastlane UI - either display the default
* WooCommerce checkout form or no form at all (when no email was provided yet).
*
* Indicates: Gary flow / no email provided / not using Fastlane.
*/
deactivate() { deactivate() {
this.active = false; this.#active = false;
this.restoreFormData();
this.refresh(); this.refresh();
} }
toggle() { toggle() {
this.active ? this.deactivate() : this.activate(); if ( this.#active ) {
this.deactivate();
} else {
this.activate();
}
} }
refresh() { refresh() {
const content = document.querySelector( this.contentSelector ); const content = document.querySelector( this.#contentSelector );
if ( ! content ) { if ( ! content ) {
return; return;
@ -63,44 +125,145 @@ class FormFieldGroup {
content.innerHTML = ''; content.innerHTML = '';
if ( ! this.active ) { if ( ! this.#active ) {
this.hideField( this.contentSelector ); this.hideField( this.#contentSelector );
} else { } else {
this.showField( this.contentSelector ); this.showField( this.#contentSelector );
} }
Object.keys( this.fields ).forEach( ( key ) => { this.loopFields( ( { selector } ) => {
const field = this.fields[ key ]; if ( this.#active /* && ! field.showInput */ ) {
this.hideField( selector );
if ( this.active && ! field.showInput ) {
this.hideField( field.selector );
} else { } else {
this.showField( field.selector ); this.showField( selector );
} }
} ); } );
if ( typeof this.template === 'function' ) { if ( typeof this.#template === 'function' ) {
content.innerHTML = this.template( { content.innerHTML = this.#template( {
value: ( fieldKey ) => { value: ( fieldKey ) => {
return this.dataValue( fieldKey ); return this.dataValue( fieldKey );
}, },
isEmpty: () => { isEmpty: () => {
let isEmpty = true; let isEmpty = true;
Object.keys( this.fields ).forEach( ( fieldKey ) => {
this.loopFields( ( field, fieldKey ) => {
if ( this.dataValue( fieldKey ) ) { if ( this.dataValue( fieldKey ) ) {
isEmpty = false; isEmpty = false;
return false; return false;
} }
} ); } );
return isEmpty; return isEmpty;
}, },
} ); } );
} }
} }
/**
* Invoke a callback on every field in the current group.
*
* @param {(field: object, key: string) => void} callback
*/
loopFields( callback ) {
for ( const [ key, field ] of Object.entries( this.#fields ) ) {
const { selector, inputName } = field;
const inputSelector = `${ selector } [name="${ inputName }"]`;
const fieldInfo = {
inputSelector: inputName ? inputSelector : '',
...field,
};
callback( fieldInfo, key );
}
}
/**
* Stores the current form data in an internal storage.
* This allows the original form to be restored later.
*/
storeFormData() {
const storeValue = ( field, name ) => {
if ( 'checkbox' === field.type || 'radio' === field.type ) {
this.#stored.set( name, field.checked );
this.#setFieldValue( field, this.dataValue( name ) );
} else {
this.#stored.set( name, field.value );
this.#setFieldValue( field, '' );
}
};
this.loopFields( ( { inputSelector }, fieldKey ) => {
if ( inputSelector && ! this.#stored.has( fieldKey ) ) {
const elInput = document.querySelector( inputSelector );
if ( elInput ) {
storeValue( elInput, fieldKey );
}
}
} );
}
/**
* Restores the form data to its initial state before the form group was activated.
* This function iterates through the stored form fields and resets their values or states.
*/
restoreFormData() {
let formHasChanged = false;
// Reset form fields to their initial state.
this.loopFields( ( { inputSelector }, fieldKey ) => {
if ( ! this.#stored.has( fieldKey ) ) {
return;
}
const elInput = inputSelector
? document.querySelector( inputSelector )
: null;
const oldValue = this.#stored.get( fieldKey );
this.#stored.delete( fieldKey );
if ( this.#setFieldValue( elInput, oldValue ) ) {
formHasChanged = true;
}
} );
if ( formHasChanged ) {
document.body.dispatchEvent( new Event( 'update_checkout' ) );
}
}
/**
* Syncs the internal field-data with the hidden checkout form fields.
*/
syncDataToForm() {
if ( ! this.#active ) {
return;
}
let formHasChanged = false;
// Push data to the (hidden) checkout form.
this.loopFields( ( { inputSelector }, fieldKey ) => {
const elInput = inputSelector
? document.querySelector( inputSelector )
: null;
if ( this.#setFieldValue( elInput, this.dataValue( fieldKey ) ) ) {
formHasChanged = true;
}
} );
// Tell WooCommerce about the changes.
if ( formHasChanged ) {
document.body.dispatchEvent( new Event( 'update_checkout' ) );
}
}
showField( selector ) { showField( selector ) {
const field = document.querySelector( const field = document.querySelector(
this.baseSelector + ' ' + selector this.#baseSelector + ' ' + selector
); );
if ( field ) { if ( field ) {
field.classList.remove( 'ppcp-axo-field-hidden' ); field.classList.remove( 'ppcp-axo-field-hidden' );
@ -109,7 +272,7 @@ class FormFieldGroup {
hideField( selector ) { hideField( selector ) {
const field = document.querySelector( const field = document.querySelector(
this.baseSelector + ' ' + selector this.#baseSelector + ' ' + selector
); );
if ( field ) { if ( field ) {
field.classList.add( 'ppcp-axo-field-hidden' ); field.classList.add( 'ppcp-axo-field-hidden' );
@ -117,7 +280,7 @@ class FormFieldGroup {
} }
inputElement( name ) { inputElement( name ) {
const baseSelector = this.fields[ name ].selector; const baseSelector = this.#fields[ name ].selector;
const select = document.querySelector( baseSelector + ' select' ); const select = document.querySelector( baseSelector + ' select' );
if ( select ) { if ( select ) {
@ -138,9 +301,7 @@ class FormFieldGroup {
} }
toSubmitData( data ) { toSubmitData( data ) {
Object.keys( this.fields ).forEach( ( fieldKey ) => { this.loopFields( ( field, fieldKey ) => {
const field = this.fields[ fieldKey ];
if ( ! field.valuePath || ! field.selector ) { if ( ! field.valuePath || ! field.selector ) {
return true; return true;
} }

View file

@ -45,42 +45,52 @@ class BillingView {
firstName: { firstName: {
selector: '#billing_first_name_field', selector: '#billing_first_name_field',
valuePath: null, valuePath: null,
inputName: 'billing_first_name',
}, },
lastName: { lastName: {
selector: '#billing_last_name_field', selector: '#billing_last_name_field',
valuePath: null, valuePath: null,
inputName: 'billing_last_name',
}, },
street1: { street1: {
selector: '#billing_address_1_field', selector: '#billing_address_1_field',
valuePath: 'billing.address.addressLine1', valuePath: 'billing.address.addressLine1',
inputName: 'billing_address_1',
}, },
street2: { street2: {
selector: '#billing_address_2_field', selector: '#billing_address_2_field',
valuePath: null, valuePath: null,
inputName: 'billing_address_2',
}, },
postCode: { postCode: {
selector: '#billing_postcode_field', selector: '#billing_postcode_field',
valuePath: 'billing.address.postalCode', valuePath: 'billing.address.postalCode',
inputName: 'billing_postcode',
}, },
city: { city: {
selector: '#billing_city_field', selector: '#billing_city_field',
valuePath: 'billing.address.adminArea2', valuePath: 'billing.address.adminArea2',
inputName: 'billing_city',
}, },
stateCode: { stateCode: {
selector: '#billing_state_field', selector: '#billing_state_field',
valuePath: 'billing.address.adminArea1', valuePath: 'billing.address.adminArea1',
inputName: 'billing_state',
}, },
countryCode: { countryCode: {
selector: '#billing_country_field', selector: '#billing_country_field',
valuePath: 'billing.address.countryCode', valuePath: 'billing.address.countryCode',
inputName: 'billing_country',
}, },
company: { company: {
selector: '#billing_company_field', selector: '#billing_company_field',
valuePath: null, valuePath: null,
inputName: 'billing_company',
}, },
phone: { phone: {
selector: '#billing_phone_field', selector: '#billing_phone_field',
valuePath: 'billing.phoneNumber', valuePath: 'billing.phoneNumber',
inputName: 'billing_phone',
}, },
}, },
} ); } );

View file

@ -90,42 +90,54 @@ class ShippingView {
key: 'firstName', key: 'firstName',
selector: '#shipping_first_name_field', selector: '#shipping_first_name_field',
valuePath: 'shipping.name.firstName', valuePath: 'shipping.name.firstName',
inputName: 'shipping_first_name',
}, },
lastName: { lastName: {
selector: '#shipping_last_name_field', selector: '#shipping_last_name_field',
valuePath: 'shipping.name.lastName', valuePath: 'shipping.name.lastName',
inputName: 'shipping_last_name',
}, },
street1: { street1: {
selector: '#shipping_address_1_field', selector: '#shipping_address_1_field',
valuePath: 'shipping.address.addressLine1', valuePath: 'shipping.address.addressLine1',
inputName: 'shipping_address_1',
}, },
street2: { street2: {
selector: '#shipping_address_2_field', selector: '#shipping_address_2_field',
valuePath: null, valuePath: null,
inputName: 'shipping_address_2',
}, },
postCode: { postCode: {
selector: '#shipping_postcode_field', selector: '#shipping_postcode_field',
valuePath: 'shipping.address.postalCode', valuePath: 'shipping.address.postalCode',
inputName: 'shipping_postcode',
}, },
city: { city: {
selector: '#shipping_city_field', selector: '#shipping_city_field',
valuePath: 'shipping.address.adminArea2', valuePath: 'shipping.address.adminArea2',
inputName: 'shipping_city',
}, },
stateCode: { stateCode: {
selector: '#shipping_state_field', selector: '#shipping_state_field',
valuePath: 'shipping.address.adminArea1', valuePath: 'shipping.address.adminArea1',
inputName: 'shipping_state',
}, },
countryCode: { countryCode: {
selector: '#shipping_country_field', selector: '#shipping_country_field',
valuePath: 'shipping.address.countryCode', valuePath: 'shipping.address.countryCode',
inputName: 'shipping_country',
}, },
company: { company: {
selector: '#shipping_company_field', selector: '#shipping_company_field',
valuePath: null, valuePath: null,
inputName: 'shipping_company',
}, },
shipDifferentAddress: { shipDifferentAddress: {
selector: '#ship-to-different-address', selector: '#ship-to-different-address',
valuePath: null, valuePath: null,
inputName: 'ship_to_different_address',
// Used by Woo to ensure correct location for taxes & shipping cost.
valueCallback: () => true,
}, },
phone: { phone: {
//'selector': '#billing_phone_field', // There is no shipping phone field. //'selector': '#billing_phone_field', // There is no shipping phone field.
@ -163,6 +175,7 @@ class ShippingView {
activate() { activate() {
this.group.activate(); this.group.activate();
this.group.syncDataToForm();
} }
deactivate() { deactivate() {
@ -175,6 +188,7 @@ class ShippingView {
setData( data ) { setData( data ) {
this.group.setData( data ); this.group.setData( data );
this.group.syncDataToForm();
} }
toSubmitData( data ) { toSubmitData( data ) {