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

[Checkout UI extensions] Add Localized Fields API to Unstable #2547

Merged
merged 5 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fluffy-rules-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/ui-extensions-react': minor
'@shopify/ui-extensions': minor
---

Adds the Localized Fields API and `useLocalizedFields` hook.
1 change: 1 addition & 0 deletions packages/ui-extensions-react/src/surfaces/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,4 @@ export {useDeliverySelectionGroups} from './checkout/hooks/delivery-selection-gr
export {useCheckoutToken} from './checkout/hooks/checkout-token';
export {useCustomerPrivacy} from './checkout/hooks/customer-privacy';
export {useInstructions} from './checkout/hooks/instructions';
export {useLocalizedFields} from './checkout/hooks/localized-fields';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {
LocalizedField,
LocalizedFieldKey,
RenderExtensionTarget,
} from '@shopify/ui-extensions/checkout';

import {ScopeNotGrantedError} from '../errors';

import {useApi} from './api';
import {useSubscription} from './subscription';

/**
* Returns the current localized fields and
* re-renders your component if the values change.
*/
export function useLocalizedFields<
Target extends RenderExtensionTarget = RenderExtensionTarget,
>(keys: LocalizedFieldKey[]): LocalizedField[] | undefined {
const {localizedFields} = useApi<Target>();

if (!localizedFields) {
throw new ScopeNotGrantedError(
'Using localized fields requires having personal customer data permissions granted to your app.',
);
}

return useSubscription(localizedFields)?.filter(({key}) =>
keys.includes(key),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type {LocalizedField} from '@shopify/ui-extensions/checkout';

import {useLocalizedFields} from '../localized-fields';

import {mount, createMockStatefulRemoteSubscribable} from './mount';

describe('useLocalizedFields', () => {
it('throws an error if localized fields are not available', () => {
const extensionApi = {
localizedFields: undefined,
};

expect(() => {
mount.hook(() => useLocalizedFields(['TAX_CREDENTIAL_BR']), {
extensionApi,
});
}).toThrow(
'Using localized fields requires having personal customer data permissions granted to your app.',
);
});

it('returns empty if no localized fields match the passed keys', () => {
const localizedFields: LocalizedField[] = [
{
key: 'TAX_CREDENTIAL_BR',
title: 'CPF/CNPJ',
value: 'test-value',
},
{
key: 'SHIPPING_CREDENTIAL_BR',
title: 'CPF/CNPJ',
value: 'test-value',
},
];

const extensionApi = {
localizedFields: createMockStatefulRemoteSubscribable(localizedFields),
};

const {value} = mount.hook(
() => useLocalizedFields(['TAX_CREDENTIAL_ES']),
{extensionApi},
);

expect(value).toStrictEqual([]);
});

it('returns an array of localized fields that match the passsed keys', () => {
const localizedFields: LocalizedField[] = [
{
key: 'TAX_CREDENTIAL_MX',
title: 'Tax credential MX',
value: 'test-value',
},
{
key: 'SHIPPING_CREDENTIAL_MX',
title: 'Shipping credential MX',
value: 'test-value',
},
{
key: 'TAX_CREDENTIAL_USE_MX',
title: 'Tax credential use MX',
value: 'test-value',
},
];

const extensionApi = {
localizedFields: createMockStatefulRemoteSubscribable(localizedFields),
};

const {value} = mount.hook(
() => useLocalizedFields(['TAX_CREDENTIAL_MX', 'TAX_CREDENTIAL_USE_MX']),
{extensionApi},
);

expect(value).toMatchObject([localizedFields[0], localizedFields[2]]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';

import {
getExample,
getLinksByTag,
STANDARD_API_PROPERTIES_DESCRIPTION,
REQUIRES_PROTECTED_CUSTOMER_DATA,
} from '../helper.docs';

const data: ReferenceEntityTemplateSchema = {
name: 'Localized Fields',
description: 'The API for interacting with localized fields.',
isVisualComponent: false,
requires: REQUIRES_PROTECTED_CUSTOMER_DATA,
category: 'APIs',
type: 'API',
definitions: [
{
title: 'StandardApi',
description: STANDARD_API_PROPERTIES_DESCRIPTION,
type: 'Docs_Standard_LocalizedFieldsApi',
},
{
title: 'useLocalizedFields',
description:
'Returns the current localized fields and re-renders your component if the values change.',
type: 'UseLocalizedFieldsGeneratedType',
},
],
defaultExample: getExample('localized-fields/default', ['jsx', 'js']),
related: getLinksByTag('apis'),
};

export default data;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {extension} from '@shopify/ui-extensions/checkout';

// 1. Choose an extension target
export default extension(
'purchase.checkout.block.render',
(root, {buyerJourney, localizedFields}) => {
let taxIdField;

localizedFields?.subscribe(
(localizedFields) => {
// 2. Access localized field values
taxIdField = localizedFields?.find(
({key}) =>
key.startsWith('TAX_CREDENTIAL'),
);
},
);

// 3. Validate localized field values
buyerJourney.intercept(
({canBlockProgress}) => {
return canBlockProgress &&
taxIdField &&
(!taxIdField.value ||
taxIdField.value.length > 10)
? {
behavior: 'block',
reason: 'Invalid tax ID',
errors: [
{
message: `${taxIdField.title} is required and
cannot exceed 10 characters in length`,
// Show an error under the localized field or
// exclude target to show at the top of the page
target: `$.cart.localizedField.${taxIdField.key}`,
},
],
}
: {
behavior: 'allow',
};
},
);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
reactExtension,
useBuyerJourneyIntercept,
useLocalizedFields,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
'purchase.checkout.block.render',
() => <Extension />,
);

function Extension() {
// 1. Access localized field values
const localizedFields = useLocalizedFields([
'TAX_CREDENTIAL_BR',
]);

// 2. Access localized field values
const taxIdField = localizedFields?.[0];

// 3. Validate localized field values
useBuyerJourneyIntercept(
({canBlockProgress}) => {
return canBlockProgress &&
taxIdField &&
(!taxIdField.value ||
taxIdField.value.length > 10)
? {
behavior: 'block',
reason: 'Invalid tax ID',
errors: [
{
message: `${taxIdField.title} is required and
cannot exceed 10 characters in length`,
// Show an error under the localized field or
// exclude target to show at the top of the page
target: `$.cart.localizedField.${taxIdField.key}`,
},
],
}
: {
behavior: 'allow',
};
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@ You can apply changes to customer consent by using the \`applyTrackingConsentCha
tabs: getExtensionCodeTabs('subscription'),
},
},
'localized-fields/default': {
description: '',
codeblock: {
title: 'Read localized fields',
tabs: getExtensionCodeTabs('localized-fields/default'),
},
},
'session-token': {
description: `
You can request a session token from Shopify to use on your application server. The contents of the token claims are signed using your shared app secret so you can trust the claims came from Shopify unaltered.
Expand Down
43 changes: 43 additions & 0 deletions packages/ui-extensions/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,49 @@ export type CountryCode =
| 'ZW'
| 'ZZ';

// For instructions on how to update this list see: https://github.com/Shopify/checkout-web/pull/39534
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Separating this comment prevents inclusion of the PR link in the docs and IDE.

Screenshot 2025-01-02 at 12 26 56 PM

/**
* A union of keys for the localized fields that are required by certain countries.
*/
export type LocalizedFieldKey =
| 'SHIPPING_CREDENTIAL_BR'
| 'SHIPPING_CREDENTIAL_CL'
| 'SHIPPING_CREDENTIAL_CN'
| 'SHIPPING_CREDENTIAL_CO'
| 'SHIPPING_CREDENTIAL_CR'
| 'SHIPPING_CREDENTIAL_EC'
| 'SHIPPING_CREDENTIAL_ES'
| 'SHIPPING_CREDENTIAL_GT'
| 'SHIPPING_CREDENTIAL_ID'
| 'SHIPPING_CREDENTIAL_KR'
| 'SHIPPING_CREDENTIAL_MY'
| 'SHIPPING_CREDENTIAL_MX'
| 'SHIPPING_CREDENTIAL_PE'
| 'SHIPPING_CREDENTIAL_PT'
| 'SHIPPING_CREDENTIAL_PY'
| 'SHIPPING_CREDENTIAL_TR'
| 'SHIPPING_CREDENTIAL_TW'
| 'SHIPPING_CREDENTIAL_TYPE_CO'
| 'TAX_CREDENTIAL_BR'
| 'TAX_CREDENTIAL_CL'
| 'TAX_CREDENTIAL_CO'
| 'TAX_CREDENTIAL_CR'
| 'TAX_CREDENTIAL_EC'
| 'TAX_CREDENTIAL_ES'
| 'TAX_CREDENTIAL_GT'
| 'TAX_CREDENTIAL_ID'
| 'TAX_CREDENTIAL_IT'
| 'TAX_CREDENTIAL_MX'
| 'TAX_CREDENTIAL_MY'
| 'TAX_CREDENTIAL_PE'
| 'TAX_CREDENTIAL_PT'
| 'TAX_CREDENTIAL_PY'
| 'TAX_CREDENTIAL_TR'
| 'TAX_CREDENTIAL_TYPE_CO'
| 'TAX_CREDENTIAL_TYPE_MX'
| 'TAX_CREDENTIAL_USE_MX'
| 'TAX_EMAIL_IT';

/**
* Union of supported storefront API versions
*/
Expand Down
1 change: 1 addition & 0 deletions packages/ui-extensions/src/surfaces/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type {
Market,
Language,
Localization,
LocalizedField,
DeliveryGroup,
DeliveryGroupType,
DeliveryGroupDetails,
Expand Down
3 changes: 3 additions & 0 deletions packages/ui-extensions/src/surfaces/checkout/api/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export interface Docs_Standard_CostApi extends Pick<StandardApi, 'cost'> {}
export interface Docs_Standard_LocalizationApi
extends Pick<StandardApi, 'i18n' | 'localization'> {}

export interface Docs_Standard_LocalizedFieldsApi
extends Pick<StandardApi, 'localizedFields'> {}

export interface Docs_Standard_MetafieldsApi
extends Pick<StandardApi, 'appMetafields' | 'metafields'> {}
export interface Docs_Checkout_MetafieldsApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
Timezone,
GraphQLError,
StorefrontApiVersion,
LocalizedFieldKey,
} from '../../../../shared';

export type {ApiVersion, Capability} from '../../../../shared';
Expand Down Expand Up @@ -396,6 +397,12 @@ export interface Localization {
market: StatefulRemoteSubscribable<Market | undefined>;
}

export interface LocalizedField {
key: LocalizedFieldKey;
title: string;
value: string;
}

/**
* Provides details on the buyer's progression through the checkout.
*/
Expand Down Expand Up @@ -734,6 +741,12 @@ export interface StandardApi<Target extends ExtensionTarget = ExtensionTarget> {
* {% include /apps/checkout/privacy-icon.md %} Requires access to [protected customer data](/docs/apps/store/data-protection/protected-customer-data).
*/
applyTrackingConsentChange: ApplyTrackingConsentChangeType;

/**
* The API for reading additional fields that are required in checkout under certain circumstances.
* For example, some countries require additional fields for customs information or tax identification numbers.
*/
localizedFields?: StatefulRemoteSubscribable<LocalizedField[] | undefined>;
}

export interface Ui {
Expand Down
1 change: 1 addition & 0 deletions packages/ui-extensions/src/surfaces/checkout/targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,4 +899,5 @@ export interface CustomerAccountStandardApi<
| 'version'
| 'customerPrivacy'
| 'applyTrackingConsentChange'
| 'localizedFields'
> {}
Loading