Skip to content

Commit

Permalink
Merge pull request #2547 from Shopify/rc/unstable-release
Browse files Browse the repository at this point in the history
[Checkout UI extensions] Add Localized Fields API to `Unstable`
  • Loading branch information
rcaplanshopify authored Jan 2, 2025
2 parents d2bf2b0 + 2f6ef29 commit b1b3473
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 0 deletions.
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
/**
* 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'
> {}

0 comments on commit b1b3473

Please sign in to comment.