import type {ReactElement, PropsWithChildren} from 'react'; import {Component} from 'react'; import {render as remoteRender} from '@remote-ui/react'; import {extension} from '@shopify/checkout-ui-extensions'; import type { ExtensionPoints, RenderExtensionPoint, ApiForRenderExtension, } from '@shopify/checkout-ui-extensions'; import {ExtensionApiContext} from './context'; /** * Registers your React-based UI Extension to run for the selected extension point. * 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 point you are registering for. This extension * point must be a `RenderExtensionPoint`; if you are trying to register for a non- * rendering extension point, like `Checkout::PostPurchase::ShouldRender`, use the * `extension()` function provided by this library instead. * * @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 * point you specified. */ export function reactExtension( target: ExtensionPoint, render: ( api: ApiForRenderExtension, ) => ReactElement | Promise>, ): 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, async (root, api) => { const element = await render( api as ApiForRenderExtension, ); await new Promise((resolve, reject) => { try { remoteRender( {element} , 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 point. * 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 point you are registering for. This extension * point must be a `RenderExtensionPoint`; if you are trying to register for a non- * rendering extension point, like `Checkout::PostPurchase::ShouldRender`, use the * `extension()` function provided by this library instead. * * @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 * point you specified. */ export function render( target: ExtensionPoint, render: (api: ApiForRenderExtension) => ReactElement, ): ExtensionPoints[ExtensionPoint] { 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, 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'; }