Skip to content

Commit

Permalink
Backport extension APIs to 2023-04 (#1135)
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade committed Jul 24, 2023
1 parent 948e711 commit b6df631
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-carpets-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions-react': minor
---

Rename `useExtensionData()` to `useExtension()`
5 changes: 5 additions & 0 deletions .changeset/little-sheep-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions-react': patch
---

Allow passing target name to `useApi()` for type inference
5 changes: 5 additions & 0 deletions .changeset/neat-vans-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions': minor
---

Backported `Extension.target` field for checkout UI extensions
5 changes: 5 additions & 0 deletions .changeset/odd-crabs-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions-react': minor
---

Make @shopify/ui-extensions a peer dependency for React library
5 changes: 5 additions & 0 deletions .changeset/pink-poets-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions': patch
---

Preserve generic type argument for `I18nTranslate` function
5 changes: 5 additions & 0 deletions .changeset/slimy-timers-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions-react': patch
---

Allow React extensions to return asynchronous results
5 changes: 4 additions & 1 deletion packages/ui-extensions-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@
"dependencies": {
"@remote-ui/async-subscription": "^2.1.12",
"@remote-ui/react": "4.5.x",
"@shopify/ui-extensions": "2023.4.1",
"@types/react": ">=17.0.0 <18.0.0"
},
"peerDependencies": {
"@shopify/ui-extensions": "2023.4.x",
"react": ">=17.0.0 <18.0.0"
},
"peerDependenciesMeta": {
"@shopify/ui-extensions": {
"optional": false
},
"react": {
"optional": false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {AdminUIExtensionError} from '../errors';
*/
export function useApi<
ID extends RenderExtensionTarget = RenderExtensionTarget,
>(): ApiForRenderExtension<ID> {
>(_id?: ID): ApiForRenderExtension<ID> {
const api = useContext(ExtensionApiContext);

if (api == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import {ExtensionApiContext} from '../context';
*
* For reference, see [ExtensionPoints](https://shopify.dev/docs/api/checkout-ui-extensions/apis/extensionpoints) to determine what API object will be returned by your extension point.
*/
export function useApi<
ID extends RenderExtensionPoint = RenderExtensionPoint,
>(): ApiForRenderExtension<ID> {
export function useApi<ID extends RenderExtensionPoint = RenderExtensionPoint>(
_id?: ID,
): ApiForRenderExtension<ID> {
const api = useContext(ExtensionApiContext);

if (api == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ import {useApi} from './api';
/**
* Returns the metadata about the extension.
*/
export function useExtensionData<
export function useExtension<
ID extends RenderExtensionPoint = RenderExtensionPoint,
>(): Extension {
return useApi<ID>().extension;
}

/**
* Returns the metadata about the extension.
*
* @deprecated Use `useExtension()` instead.
*/
export const useExtensionData = useExtension;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export {useTarget} from './target';
export {useAppMetafields} from './app-metafields';
export {useShop} from './shop';
export {useStorage} from './storage';
export {useExtensionData} from './extension-data';
export {useExtension, useExtensionData} from './extension';
export {useSubscription} from './subscription';
export {useCustomer, useEmail, usePhone} from './buyer-identity';
export {useTranslate} from './translate';
Expand Down
53 changes: 30 additions & 23 deletions packages/ui-extensions-react/src/surfaces/checkout/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,41 @@ import {ExtensionApiContext} from './context';
*/
export function reactExtension<ExtensionPoint extends RenderExtensionPoint>(
target: ExtensionPoint,
render: (api: ApiForRenderExtension<ExtensionPoint>) => ReactElement<any>,
render: (
api: ApiForRenderExtension<ExtensionPoint>,
) => ReactElement<any> | Promise<ReactElement<any>>,
): ExtensionPoints[ExtensionPoint] {
// TypeScript can’t infer the type of the callback because it’s a big union
// type. To get around it, we’ll just fake like we are rendering the
// Checkout::Dynamic::Render extension, since all render extensions have the same general
// shape (`RenderExtension`).
return extension<'Checkout::Dynamic::Render'>(target as any, (root, api) => {
return new Promise((resolve, reject) => {
try {
remoteRender(
<ExtensionApiContext.Provider value={api}>
<ErrorBoundary>
{render(api as ApiForRenderExtension<ExtensionPoint>)}
</ErrorBoundary>
</ExtensionApiContext.Provider>,
root,
() => {
resolve();
},
);
} catch (error) {
// Workaround for https://github.com/Shopify/ui-extensions/issues/325
// eslint-disable-next-line no-console
console.error(error);
reject(error);
}
});
}) as any;
return extension<'Checkout::Dynamic::Render'>(
target as any,
async (root, api) => {
const element = await render(
api as ApiForRenderExtension<ExtensionPoint>,
);

await new Promise<void>((resolve, reject) => {
try {
remoteRender(
<ExtensionApiContext.Provider value={api}>
<ErrorBoundary>{element}</ErrorBoundary>
</ExtensionApiContext.Provider>,
root,
() => {
resolve();
},
);
} catch (error) {
// Workaround for https://github.com/Shopify/ui-extensions/issues/325
// eslint-disable-next-line no-console
console.error(error);
reject(error);
}
});
},
) as any;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,17 @@ describe('render()', () => {
console.error = consoleError;
});

it('calls extension() with the extension point', () => {
reactExtension('Checkout::Dynamic::Render', () => <></>);
it('calls extension() with the extension point', async () => {
await reactExtension('Checkout::Dynamic::Render', () => <></>);

expect(extension).toHaveBeenCalledWith(
'Checkout::Dynamic::Render',
expect.any(Function),
);
});

it('reports errors thrown during reconcilation onto global this', () => {
reactExtension('Checkout::Dynamic::Render', () => {
it('reports errors thrown during reconcilation onto global this', async () => {
await reactExtension('Checkout::Dynamic::Render', () => {
return <Thrown />;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
SellingPlan,
Attribute,
} from '../shared';
import type {ExtensionPoint} from '../../extension-points';

/**
* A key-value storage object for extension points.
Expand Down Expand Up @@ -59,7 +60,18 @@ export type Capability = 'api_access' | 'network_access' | 'block_progress';
/**
* Meta information about an extension point.
*/
export interface Extension {
export interface Extension<Target extends ExtensionPoint = ExtensionPoint> {
/**
* The identifier that specifies where in Shopify’s UI your code is being
* injected. This will be one of the targets you have included in your
* extension’s configuration file.
*
* @example 'Checkout::Dynamic::Render'
* @see https://shopify.dev/docs/api/checkout-ui-extensions/unstable/extension-targets-overview
* @see https://shopify.dev/docs/apps/app-extensions/configuration#targets
*/
target: Target;

/**
* The published version of the running extension point.
*
Expand Down Expand Up @@ -213,12 +225,14 @@ export type Version = string;
*
* @example translate("banner.title")
*/
export type I18nTranslate<ReplacementType = string> = (
key: string,
options?: {[placeholderKey: string]: ReplacementType | string | number},
) => ReplacementType extends string | number
? string
: (string | ReplacementType)[];
export interface I18nTranslate {
<ReplacementType = string>(
key: string,
options?: {[placeholderKey: string]: ReplacementType | string | number},
): ReplacementType extends string | number
? string
: (string | ReplacementType)[];
}

export interface I18n {
/**
Expand Down Expand Up @@ -385,9 +399,7 @@ export interface BuyerJourney {
completed: StatefulRemoteSubscribable<boolean>;
}

export interface StandardApi<
ExtensionPoint extends import('../../extension-points').ExtensionPoint,
> {
export interface StandardApi<Target extends ExtensionPoint = ExtensionPoint> {
/**
* Methods for interacting with [Web Pixels](https://shopify.dev/docs/apps/marketing), such as emitting an event.
*/
Expand Down Expand Up @@ -456,11 +468,16 @@ export interface StandardApi<
/**
* Meta information about the extension.
*/
extension: Extension;
extension: Extension<Target>;

/**
* The identifier of the running extension point.
* @example 'Checkout::PostPurchase::Render'
* The identifier that specifies where in Shopify’s UI your code is being
* injected. This will be one of the targets you have included in your
* extension’s configuration file.
*
* @example 'Checkout::Dynamic::Render'
* @see https://shopify.dev/docs/api/checkout-ui-extensions/unstable/extension-targets-overview
* @see https://shopify.dev/docs/apps/app-extensions/configuration#targets
*/
extensionPoint: ExtensionPoint;

Expand Down

0 comments on commit b6df631

Please sign in to comment.