Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect the verification status of the phone number to the contact information settings #1717

15 changes: 2 additions & 13 deletions js/src/components/contact-information/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -127,7 +116,7 @@ const ContactInformation = ( { onPhoneNumberVerified } ) => {
<PhoneNumberCard
view="setup-mc"
phoneNumber={ phone }
initEditing={ initEditing }
initEditing={ null }
Copy link
Contributor

@puntope puntope Oct 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 We can just omit this prop initEditing. Since the default value is null

Copy link
Member Author

@eason9487 eason9487 Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in e3f4d79.

onPhoneNumberVerified={ onPhoneNumberVerified }
/>
<StoreAddressCard />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -35,7 +36,21 @@ export function PhoneNumberCardPreview( { editHref, learnMore } ) {

if ( loaded ) {
if ( data.isValid ) {
content = data.display;
const verificationStatus = data.isVerified ? (
<div className="gla-phone-number-card-preview__verified-status">
{ __( 'Verified', 'google-listings-and-ads' ) }
</div>
) : (
<div className="gla-phone-number-card-preview__unverified-status">
{ __( 'Unverified', 'google-listings-and-ads' ) }
</div>
);
content = (
<>
{ data.display }
{ verificationStatus }
</>
);
} else {
warning = __(
'Please add your phone number',
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Comment on lines 102 to 103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I don't understand well this concept of initialize with the editing UI and initialize with the viewing UI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased in 0ae56ea.

* `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
*/
Expand All @@ -117,14 +117,27 @@ const PhoneNumberCard = ( {
} ) => {
const { loaded, data } = phoneNumber;
const [ isEditing, setEditing ] = useState( initEditing );
const onPhoneNumberVerifiedRef = useRef();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ What is the advantage of useRef vs useCallback?

(I take as granted this is for avoiding error as onPhoneNumberVerified needs to be a dependency in useEffect

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the advantage of useRef vs useCallback?

I think it's probably not a choice about advantages, but rather what the concern is to be addressed here. useCallback is used when needing a callback with stable reference based on deps array. useRef has plenty of use scenarios and one of them is the need of ignoring the reference changes such as the use case here.

I take as granted this is for avoiding error as onPhoneNumberVerified needs to be a dependency in useEffect

Yes, it avoids the possibility of infinity re-rendering loop.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ don't understand this line tbh. What does it mean "The isEditing state is on hold"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased in 0ae56ea.

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.
Comment on lines +133 to +135
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ This was a bit confusing for me.

Suggestion:

When initEditing prop is not true we call onPhoneNumberVerified when the loaded phone number has already been verified.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intended to illustrate how onPhoneNumberVerified calls are handled under different conditions. I'd like to keep the explicit mention of "If initEditing is false or null" to clarify why the conditional below is a shorter one initEditing ! == true rather than a longer one ( initEditing === false || initEditing === null ), and to avoid the appearance that it could be changed to initEditing === false.

useEffect( () => {
if ( loaded && initEditing !== true && isVerified ) {
onPhoneNumberVerifiedRef.current();
}
}, [ loaded, data.isValid, isEditing ] );
}, [ loaded, isVerified, initEditing ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Could be possible to have isVerified but not loaded phoneNumber?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 290da41.


// Return a simple loading AccountCard since the initial edit state is unknown before loaded.
if ( isEditing === null ) {
Expand Down
37 changes: 36 additions & 1 deletion js/src/data/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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 );
Expand Down
6 changes: 5 additions & 1 deletion js/src/hooks/useGoogleMCPhoneNumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const emptyData = {
countryCallingCode: '',
nationalNumber: '',
isValid: false,
isVerified: false,
display: '',
};

Expand All @@ -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 ) => {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I guess this is an enum 'verified'|'unverified' ?? Or it could be more things?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in bba9fa3.

'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' ],
Expand Down Expand Up @@ -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();
Expand All @@ -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() );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 Maybe we can have 'VERIFIED' as well in the frontend and get rid of this strtolower function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the status fields returned by the other GLA APIs are almost always lowercase, the idea here was to keep them consistent.

} catch ( InvalidValue $exception ) {
// log and fail silently
do_action( 'woocommerce_gla_exception', $exception, __METHOD__ );
Expand All @@ -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
);
Expand Down
10 changes: 6 additions & 4 deletions src/MerchantCenter/MerchantCenterService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 phone_verification_status seems not accurate naming for me. It sounds like we expect a status (like 'verified'. However we just want a booean.

So I would suggest 'phone_number_verified' or 'is_verified' or something like that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 32df87f.


/** @var Settings $settings */
$settings = $this->container->get( Settings::class );
Expand All @@ -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'];
}

/**
Expand Down
1 change: 1 addition & 0 deletions tests/Tools/HelperTrait/MerchantTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down