-
Notifications
You must be signed in to change notification settings - Fork 40
/
Copy pathrender.tsx
139 lines (127 loc) · 5.29 KB
/
render.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import type {ReactElement, PropsWithChildren} from 'react';
import {Component} from 'react';
import {render as remoteRender} from '@remote-ui/react';
import {extension} from '@shopify/ui-extensions/checkout';
import type {
ExtensionTargets,
RenderExtensionTarget,
ApiForExtension,
} from '@shopify/ui-extensions/checkout';
import {ExtensionApiContext} from './context';
/**
* Registers your React-based UI Extension to run for the selected extension target.
* Additionally, this function will automatically provide the extension API as React
* context, which you can access anywhere in your extension by using the `useApi()`
* hook.
*
* @param target The extension target you are registering for. You can see a full list
* of the available targets in our [developer documentation](https://shopify.dev/docs/api/checkout-ui-extensions/extension-targets-overview#supported-locations).
*
* @param render The function that will be called when Checkout begins rendering
* your extension. This function is called with the API checkout provided to your
* extension, and must return a React element that will be rendered into the extension
* target you specified. Alternatively, it can return a promise for a React element,
* which allows you to perform initial asynchronous work like fetching data from your
* own backend.
*/
export function reactExtension<Target extends RenderExtensionTarget>(
target: Target,
render: (
api: ApiForExtension<Target>,
) => ReactElement<any> | Promise<ReactElement<any>>,
): ExtensionTargets[Target] {
// 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
// purchase.checkout.block.render extension, since all render extensions have the same general
// shape (`RenderExtension`).
return extension<'purchase.checkout.block.render'>(
target as any,
async (root, api) => {
const element = await render(api as ApiForExtension<Target>);
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;
}
/**
* Registers your React-based UI Extension to run for the selected extension target.
* Additionally, this function will automatically provide the extension API as React
* context, which you can access anywhere in your extension by using the `useApi()`
* hook.
*
* @param target The extension target you are registering for. You can see a full list
* of the available targets in our [developer documentation](https://shopify.dev/docs/api/checkout-ui-extensions/extension-targets-overview#supported-locations).
*
* @param render The function that will be called when Checkout begins rendering
* your extension. This function is called with the API checkout provided to your
* extension, and must return a React element that will be rendered into the extension
* target you specified. Alternatively, it can return a promise for a React element,
* which allows you to perform initial asynchronous work like fetching data from your
* own backend.
*
* @deprecated This is deprecated. Use `reactExtension` instead.
*/
export function render<Target extends RenderExtensionTarget>(
target: Target,
render: (api: ApiForExtension<Target>) => ReactElement<any>,
): ExtensionTargets[Target] {
return reactExtension(target, render);
}
interface ErrorState {
hasError: boolean;
}
// Using ErrorBoundary allows us to relay the errors coming from React reconcilation
// to the global object using reportError.
// eslint-disable-next-line @typescript-eslint/ban-types
class ErrorBoundary extends Component<PropsWithChildren<{}>, ErrorState> {
static getDerivedStateFromError() {
// Update state so the next render will show the fallback UI.
return {hasError: true};
}
state: ErrorState = {hasError: false};
componentDidCatch(error: Error, errorInfo: {componentStack: string}) {
// in development, these errors are logged by React itself so we don’t need to re-log them
// eslint-disable-next-line no-process-env
if (process.env.NODE_ENV !== 'development') {
// eslint-disable-next-line no-console
console.error(
`The above error occurred in the <${extractComponentName(
errorInfo.componentStack,
)}> component:\n${errorInfo.componentStack}`,
);
}
reportError(error);
}
render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
// This is an example of component stack:
//
// at Hello (webpack:///./src/index.tsx_+_220_modules?:1082:9)
// at Banner
// at Extension (webpack:///./src/index.tsx_+_220_modules?:1075:7)
// at render_esnext_ErrorBoundary (webpack:///./src/index.tsx_+_220_modules?:1052:124)
export function extractComponentName(componentStack: string) {
const match = componentStack.match(/^\s+at\s(\w+)\s/);
return (match && match[1]) ?? 'Unknown';
}