diff --git a/js/src/components/contact-information/index.js b/js/src/components/contact-information/index.js index 3d8665779e..a519723dfd 100644 --- a/js/src/components/contact-information/index.js +++ b/js/src/components/contact-information/index.js @@ -82,24 +82,13 @@ export function ContactInformationPreview() { * Renders a contact information section with specified initial state and texts. * * @param {Object} props React props. - * @param {Function} [props.onPhoneNumberVerified] Called when the phone number is verified. + * @param {Function} [props.onPhoneNumberVerified] Called when the phone number is verified or has been verified. * @fires gla_documentation_link_click with `{ context: 'setup-mc-contact-information', link_id: 'contact-information-read-more', href: 'https://docs.woocommerce.com/document/google-listings-and-ads/#contact-information' }` * @fires gla_documentation_link_click with `{ context: 'settings-no-phone-number-notice', link_id: 'contact-information-read-more', href: 'https://docs.woocommerce.com/document/google-listings-and-ads/#contact-information' }` * @fires gla_documentation_link_click with `{ context: 'settings-no-store-address-notice', link_id: 'contact-information-read-more', href: 'https://docs.woocommerce.com/document/google-listings-and-ads/#contact-information' }` */ const ContactInformation = ( { onPhoneNumberVerified } ) => { const phone = useGoogleMCPhoneNumber(); - - /** - * Since it is still lacking the phone verification state, - * all onboarding accounts are considered unverified phone numbers. - * - * TODO: replace the code at next line back to the original logic with - * `const initEditing = null;` - * after the phone verification state can be detected. - */ - const initEditing = true; - const title = mcTitle; const trackContext = 'setup-mc-contact-information'; @@ -127,7 +116,7 @@ const ContactInformation = ( { onPhoneNumberVerified } ) => { diff --git a/js/src/components/contact-information/phone-number-card/phone-number-card-preview.js b/js/src/components/contact-information/phone-number-card/phone-number-card-preview.js index f81a92de23..6b503780a0 100644 --- a/js/src/components/contact-information/phone-number-card/phone-number-card-preview.js +++ b/js/src/components/contact-information/phone-number-card/phone-number-card-preview.js @@ -9,6 +9,7 @@ import { __ } from '@wordpress/i18n'; import { APPEARANCE } from '.~/components/account-card'; import useGoogleMCPhoneNumber from '.~/hooks/useGoogleMCPhoneNumber'; import ContactInformationPreviewCard from '../contact-information-preview-card'; +import './phone-number-card-preview.scss'; /** * Triggered when phone number edit button is clicked. @@ -35,7 +36,21 @@ export function PhoneNumberCardPreview( { editHref, learnMore } ) { if ( loaded ) { if ( data.isValid ) { - content = data.display; + const verificationStatus = data.isVerified ? ( +
+ { __( 'Verified', 'google-listings-and-ads' ) } +
+ ) : ( +
+ { __( 'Unverified', 'google-listings-and-ads' ) } +
+ ); + content = ( + <> + { data.display } + { verificationStatus } + + ); } else { warning = __( 'Please add your phone number', diff --git a/js/src/components/contact-information/phone-number-card/phone-number-card-preview.scss b/js/src/components/contact-information/phone-number-card/phone-number-card-preview.scss new file mode 100644 index 0000000000..dd46bc1516 --- /dev/null +++ b/js/src/components/contact-information/phone-number-card/phone-number-card-preview.scss @@ -0,0 +1,15 @@ +.gla-contact-info-preview-card { + .gla-account-card__description { + gap: $grid-unit-05; + } +} + +.gla-phone-number-card-preview { + &__verified-status { + color: $alert-green; + } + + &__unverified-status { + color: $alert-red; + } +} diff --git a/js/src/components/contact-information/phone-number-card/phone-number-card.js b/js/src/components/contact-information/phone-number-card/phone-number-card.js index 4892ad8dd4..8068784af8 100644 --- a/js/src/components/contact-information/phone-number-card/phone-number-card.js +++ b/js/src/components/contact-information/phone-number-card/phone-number-card.js @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { useState, useEffect } from '@wordpress/element'; +import { useState, useEffect, useRef } from '@wordpress/element'; import { CardDivider } from '@wordpress/components'; import { Spinner } from '@woocommerce/components'; @@ -101,10 +101,10 @@ function EditPhoneNumberCard( { phoneNumber, onPhoneNumberVerified } ) { * @param {boolean|null} [props.initEditing=null] Specify the inital UI state. * `true`: initialize with the editing UI. * `false`: initialize with the viewing UI. - * `null`: determine the initial UI state according to the `data.isValid` after the `phoneNumber` loaded. + * `null`: determine the initial UI state according to the `data.isVerified` after the `phoneNumber` loaded. * @param {Function} [props.onEditClick] Called when clicking on "Edit" button. * If this callback is omitted, it will enter edit mode when clicking on "Edit" button. - * @param {Function} [props.onPhoneNumberVerified] Called when the phone number is verified in edit mode. + * @param {Function} [props.onPhoneNumberVerified] Called when the phone number is verified or has been verified. * * @fires gla_mc_phone_number_edit_button_click */ @@ -117,14 +117,27 @@ const PhoneNumberCard = ( { } ) => { const { loaded, data } = phoneNumber; const [ isEditing, setEditing ] = useState( initEditing ); + const onPhoneNumberVerifiedRef = useRef(); + onPhoneNumberVerifiedRef.current = onPhoneNumberVerified; + + const { isVerified } = data; // Handle the initial UI state of `initEditing = null`. // The `isEditing` state is on hold. Determine it after the `phoneNumber` loaded. useEffect( () => { if ( loaded && isEditing === null ) { - setEditing( ! data.isValid ); + setEditing( ! isVerified ); + } + }, [ loaded, isVerified, isEditing ] ); + + // If `initEditing` is true, EditPhoneNumberCard takes care of the call of `onPhoneNumberVerified` + // after the phone number is verified. If `initEditing` is false or null, this useEffect handles + // the call of `onPhoneNumberVerified` when the loaded phone number has already been verified. + useEffect( () => { + if ( loaded && initEditing !== true && isVerified ) { + onPhoneNumberVerifiedRef.current(); } - }, [ loaded, data.isValid, isEditing ] ); + }, [ loaded, isVerified, initEditing ] ); // Return a simple loading AccountCard since the initial edit state is unknown before loaded. if ( isEditing === null ) { diff --git a/js/src/data/selectors.js b/js/src/data/selectors.js index 92ff118343..c8648b04a1 100644 --- a/js/src/data/selectors.js +++ b/js/src/data/selectors.js @@ -10,6 +10,10 @@ import createSelector from 'rememo'; import { STORE_KEY } from './constants'; import { getReportQuery, getReportKey, getPerformanceQuery } from './utils'; +/** + * @typedef {import('.~/data/actions').CountryCode} CountryCode + */ + export const getShippingRates = ( state ) => { return state.mc.shipping.rates; }; @@ -74,11 +78,42 @@ export const getExistingGoogleAdsAccounts = ( state ) => { return state.mc.accounts.existing_ads; }; +/** + * @typedef {Object} Address + * @property {string|null} street_address Street-level part of the address. `null` when empty. + * @property {string|null} locality City, town or commune. `null` when empty. + * @property {string|null} region Top-level administrative subdivision of the country. `null` when empty. + * @property {string|null} postal_code Postal code or ZIP. `null` when empty. + * @property {CountryCode} country Two-letter country code in ISO 3166-1 alpha-2 format. Example: 'US'. + * + * @typedef {Object} ContactInformation + * @property {number} id The Google Merchant Center account ID. + * @property {string|null} phone_number The phone number in international format associated with the Google Merchant Center account. Example: '+12133734253'. `null` if the phone number is not yet set. + * @property {'verified'|'unverified'|null} phone_verification_status The verification status of the phone number associated with the Google Merchant Center account. `null` if the phone number is not yet set. + * @property {Address|null} mc_address The address associated with the Google Merchant Center account. `null` if the address is not yet set. + * @property {Address|null} wc_address The WooCommerce store address. `null` if the address is not yet set. + * @property {boolean} is_mc_address_different Whether the Google Merchant Center account address is different than the WooCommerce store address. + * @property {string[]} wc_address_errors The errors associated with the WooCommerce store address. + */ + +/** + * Select the state of contact information associated with the Google Merchant Center account. + * + * @param {Object} state The current store state will be injected by `wp.data`. + * @return {ContactInformation|null} The contact information associated with the Google Merchant Center account. It would return `null` before the data is fetched. + */ export const getGoogleMCContactInformation = ( state ) => { return state.mc.contact; }; -// Create another selector to separate the `hasFinishedResolution` state with `getGoogleMCContactInformation`. +/** + * Select the state of phone number associated with the Google Merchant Center account. + * + * Create another selector to separate the `hasFinishedResolution` state with `getGoogleMCContactInformation`. + * + * @param {Object} state The current store state will be injected by `wp.data`. + * @return {{ data: ContactInformation|null, loaded: boolean }} The payload of contact information associated with the Google Merchant Center account and its loaded state. + */ export const getGoogleMCPhoneNumber = createRegistrySelector( ( select ) => ( state ) => { const selector = select( STORE_KEY ); diff --git a/js/src/hooks/useGoogleMCPhoneNumber.js b/js/src/hooks/useGoogleMCPhoneNumber.js index 59072ec38e..1be1a8ce26 100644 --- a/js/src/hooks/useGoogleMCPhoneNumber.js +++ b/js/src/hooks/useGoogleMCPhoneNumber.js @@ -14,6 +14,7 @@ const emptyData = { countryCallingCode: '', nationalNumber: '', isValid: false, + isVerified: false, display: '', }; @@ -29,13 +30,14 @@ const emptyData = { * @property {string} countryCallingCode The country calling code. Example: '1'. * @property {string} nationalNumber The national (significant) number. Example: '2133734253'. * @property {boolean} isValid Whether the phone number is valid. + * @property {boolean} isVerified Whether the phone number is verified. * @property {string} display The phone number string in international format. Example: '+1 213 373 4253'. */ /** * A hook to load user's phone number data from Google Merchant Center. * - * @return {PhoneNumber} [description] + * @return {PhoneNumber} The payload of parsed phone number associated with the Google Merchant Center account and its loaded state. */ export default function useGoogleMCPhoneNumber() { return useSelect( ( select ) => { @@ -50,6 +52,8 @@ export default function useGoogleMCPhoneNumber() { data = { ...parsed, isValid: parsed.isValid(), + isVerified: + contact.phone_verification_status === 'verified', display: parsed.formatInternational(), }; delete data.metadata; diff --git a/js/src/setup-mc/setup-stepper/index.js b/js/src/setup-mc/setup-stepper/index.js index d5941f41b1..7a45bcbc08 100644 --- a/js/src/setup-mc/setup-stepper/index.js +++ b/js/src/setup-mc/setup-stepper/index.js @@ -13,11 +13,7 @@ import useMCSetup from '.~/hooks/useMCSetup'; import stepNameKeyMap from './stepNameKeyMap'; const SetupStepper = () => { - const { - hasFinishedResolution, - data: mcSetup, - invalidateResolution: mcSetupInvalidateResolution, - } = useMCSetup(); + const { hasFinishedResolution, data: mcSetup } = useMCSetup(); if ( ! hasFinishedResolution && ! mcSetup ) { return ; @@ -36,16 +32,7 @@ const SetupStepper = () => { return null; } - const handleRefetchSavedStep = () => { - mcSetupInvalidateResolution(); - }; - - return ( - - ); + return ; }; export default SetupStepper; diff --git a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js index eca206baf3..b9677f2800 100644 --- a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js +++ b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js @@ -28,10 +28,9 @@ import stepNameKeyMap from './stepNameKeyMap'; /** * @param {Object} props React props * @param {string} [props.savedStep] A saved step overriding the current step - * @param {Function} [props.onRefetchSavedStep] Callback when Saved Step is updated * @fires gla_setup_mc with `{ target: 'step1_continue' | 'step2_continue' | 'step3_continue', trigger: 'click' }`. */ -const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { +const SavedSetupStepper = ( { savedStep } ) => { const [ step, setStep ] = useState( savedStep ); const { settings } = useSettings(); @@ -83,7 +82,6 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { trigger: 'click', } ); setStep( stepNameKeyMap.product_listings ); - onRefetchSavedStep(); }; const handleSetupListingsContinue = () => { @@ -92,7 +90,6 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { trigger: 'click', } ); setStep( stepNameKeyMap.store_requirements ); - onRefetchSavedStep(); }; const handleStoreRequirementsContinue = () => { @@ -101,11 +98,11 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { trigger: 'click', } ); setStep( stepNameKeyMap.paid_ads ); - onRefetchSavedStep(); }; const handleStepClick = ( stepKey ) => { - if ( Number( stepKey ) <= Number( savedStep ) ) { + // Only allow going back to the previous steps. + if ( Number( stepKey ) < Number( step ) ) { setStep( stepKey ); } }; diff --git a/src/API/Site/Controllers/MerchantCenter/ContactInformationController.php b/src/API/Site/Controllers/MerchantCenter/ContactInformationController.php index a59a79d580..6481714d93 100644 --- a/src/API/Site/Controllers/MerchantCenter/ContactInformationController.php +++ b/src/API/Site/Controllers/MerchantCenter/ContactInformationController.php @@ -124,35 +124,40 @@ protected function get_contact_information_endpoint_edit_callback(): callable { */ protected function get_schema_properties(): array { return [ - 'id' => [ + 'id' => [ 'type' => 'integer', 'description' => __( 'The Merchant Center account ID.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', ], - 'phone_number' => [ + 'phone_number' => [ 'type' => 'string', 'description' => __( 'The phone number associated with the Merchant Center account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], - 'mc_address' => [ + 'phone_verification_status' => [ + 'type' => 'string', + 'description' => __( 'The verification status of the phone number associated with the Merchant Center account.', 'google-listings-and-ads' ), + 'context' => [ 'view' ], + ], + 'mc_address' => [ 'type' => 'object', 'description' => __( 'The address associated with the Merchant Center account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'properties' => $this->get_address_schema(), ], - 'wc_address' => [ + 'wc_address' => [ 'type' => 'object', 'description' => __( 'The WooCommerce store address.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'properties' => $this->get_address_schema(), ], - 'is_mc_address_different' => [ + 'is_mc_address_different' => [ 'type' => 'boolean', 'description' => __( 'Whether the Merchant Center account address is different than the WooCommerce store address.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], - 'wc_address_errors' => [ + 'wc_address_errors' => [ 'type' => 'array', 'description' => __( 'The errors associated with the WooCommerce address', 'google-listings-and-ads' ), 'context' => [ 'view' ], @@ -215,10 +220,11 @@ public function get_update_args(): array { * @return Response */ protected function get_contact_information_response( ?AccountBusinessInformation $contact_information, Request $request ): Response { - $phone_number = null; - $mc_address = null; - $wc_address = null; - $is_address_diff = false; + $phone_number = null; + $phone_verification_status = null; + $mc_address = null; + $wc_address = null; + $is_address_diff = false; if ( $this->settings->get_store_address() instanceof AccountAddress ) { $wc_address = $this->settings->get_store_address(); @@ -228,7 +234,8 @@ protected function get_contact_information_response( ?AccountBusinessInformation if ( $contact_information instanceof AccountBusinessInformation ) { if ( ! empty( $contact_information->getPhoneNumber() ) ) { try { - $phone_number = PhoneNumber::cast( $contact_information->getPhoneNumber() )->get(); + $phone_number = PhoneNumber::cast( $contact_information->getPhoneNumber() )->get(); + $phone_verification_status = strtolower( $contact_information->getPhoneVerificationStatus() ); } catch ( InvalidValue $exception ) { // log and fail silently do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); @@ -249,12 +256,13 @@ protected function get_contact_information_response( ?AccountBusinessInformation return $this->prepare_item_for_response( [ - 'id' => $this->options->get_merchant_id(), - 'phone_number' => $phone_number, - 'mc_address' => self::serialize_address( $mc_address ), - 'wc_address' => self::serialize_address( $wc_address ), - 'is_mc_address_different' => $is_address_diff, - 'wc_address_errors' => $wc_address_errors, + 'id' => $this->options->get_merchant_id(), + 'phone_number' => $phone_number, + 'phone_verification_status' => $phone_verification_status, + 'mc_address' => self::serialize_address( $mc_address ), + 'wc_address' => self::serialize_address( $wc_address ), + 'is_mc_address_different' => $is_address_diff, + 'wc_address_errors' => $wc_address_errors, ], $request ); diff --git a/src/MerchantCenter/MerchantCenterService.php b/src/MerchantCenter/MerchantCenterService.php index c4017fd1b3..6b957e2376 100644 --- a/src/MerchantCenter/MerchantCenterService.php +++ b/src/MerchantCenter/MerchantCenterService.php @@ -293,8 +293,9 @@ protected function maybe_add_contact_info_issue( array $issues, DateTime $cache_ */ protected function is_mc_contact_information_setup(): bool { $is_setup = [ - 'phone_number' => false, - 'address' => false, + 'phone_number' => false, + 'phone_verification_status' => false, + 'address' => false, ]; try { @@ -310,7 +311,8 @@ protected function is_mc_contact_information_setup(): bool { } if ( $contact_info instanceof AccountBusinessInformation ) { - $is_setup['phone_number'] = ! empty( $contact_info->getPhoneNumber() ); + $is_setup['phone_number'] = ! empty( $contact_info->getPhoneNumber() ); + $is_setup['phone_verification_status'] = 'VERIFIED' === $contact_info->getPhoneVerificationStatus(); /** @var Settings $settings */ $settings = $this->container->get( Settings::class ); @@ -323,7 +325,7 @@ protected function is_mc_contact_information_setup(): bool { } } - return $is_setup['phone_number'] && $is_setup['address']; + return $is_setup['phone_number'] && $is_setup['phone_verification_status'] && $is_setup['address']; } /** diff --git a/tests/Tools/HelperTrait/MerchantTrait.php b/tests/Tools/HelperTrait/MerchantTrait.php index 00fe186b78..a2d0055819 100644 --- a/tests/Tools/HelperTrait/MerchantTrait.php +++ b/tests/Tools/HelperTrait/MerchantTrait.php @@ -48,6 +48,7 @@ public function get_valid_account(): Account { public function get_valid_business_info(): AccountBusinessInformation { $business_info = new AccountBusinessInformation(); $business_info->setPhoneNumber( $this->valid_account_phone_number ); + $business_info->setPhoneVerificationStatus( 'VERIFIED' ); $business_info->setAddress( $this->get_sample_address() ); return $business_info;