diff --git a/.changeset/flat-lions-yell.md b/.changeset/flat-lions-yell.md new file mode 100644 index 0000000000..b92a2d58f0 --- /dev/null +++ b/.changeset/flat-lions-yell.md @@ -0,0 +1,5 @@ +--- +'@shopify/hydrogen-react': patch +--- + +`ShopPayButton` component now can receive a `storeDomain`. The component now does not require `ShopifyProvider`. diff --git a/packages/hydrogen-react/docs/generated/generated_docs_data.json b/packages/hydrogen-react/docs/generated/generated_docs_data.json index f4fab16186..7d941e0200 100644 --- a/packages/hydrogen-react/docs/generated/generated_docs_data.json +++ b/packages/hydrogen-react/docs/generated/generated_docs_data.json @@ -4683,26 +4683,49 @@ "category": "components", "isVisualComponent": false, "related": [], - "description": "The `ShopPayButton` component renders a button that redirects to the Shop Pay checkout. It renders a [``](https://shopify.dev/custom-storefronts/tools/web-components) custom element, for which it will lazy-load the source code automatically. It relies on the `` context provider.", + "description": "The `ShopPayButton` component renders a button that redirects to the Shop Pay checkout. It renders a [``](https://shopify.dev/custom-storefronts/tools/web-components) custom element, for which it will lazy-load the source code automatically.", "type": "component", "defaultExample": { - "description": "I am the default example", + "description": " without ", "codeblock": { "tabs": [ { "title": "JavaScript", - "code": "import {ShopPayButton} from '@shopify/hydrogen-react';\n\nexport function AddVariantQuantity1({variantId}) {\n return ;\n}\n\nexport function AddVariantQuantityMultiple({variantId, quantity}) {\n return (\n \n );\n}\n", + "code": "import {ShopPayButton} from '@shopify/hydrogen-react';\n\nexport function AddVariantQuantity1({variantId, storeDomain}) {\n return ;\n}\n\nexport function AddVariantQuantityMultiple({variantId, quantity, storeDomain}) {\n return (\n \n );\n}\n", "language": "jsx" }, { "title": "TypeScript", - "code": "import {ShopPayButton} from '@shopify/hydrogen-react';\n\nexport function AddVariantQuantity1({variantId}: {variantId: string}) {\n return ;\n}\n\nexport function AddVariantQuantityMultiple({\n variantId,\n quantity,\n}: {\n variantId: string;\n quantity: number;\n}) {\n return (\n \n );\n}\n", + "code": "import {ShopPayButton} from '@shopify/hydrogen-react';\n\nexport function AddVariantQuantity1({\n variantId,\n storeDomain,\n}: {\n variantId: string;\n storeDomain: string;\n}) {\n return ;\n}\n\nexport function AddVariantQuantityMultiple({\n variantId,\n quantity,\n storeDomain,\n}: {\n variantId: string;\n quantity: number;\n storeDomain: string;\n}) {\n return (\n \n );\n}\n", "language": "tsx" } ], - "title": "Example code" + "title": " without " } }, + "examples": { + "description": "", + "examples": [ + { + "description": "If `` context provider is used in your app, you can use the `` without supplying a `storeDomain` prop", + "codeblock": { + "tabs": [ + { + "title": "JavaScript", + "code": "import {ShopifyProvider, ShopPayButton} from '@shopify/hydrogen-react';\n\nexport default function App() {\n return (\n \n \n \n );\n}\n\nexport function AddVariantQuantity1({variantId}) {\n return ;\n}\n", + "language": "jsx" + }, + { + "title": "TypeScript", + "code": "import {ShopifyProvider, ShopPayButton} from '@shopify/hydrogen-react';\n\nexport default function App() {\n return (\n \n \n \n );\n}\n\nexport function AddVariantQuantity1({variantId}: {variantId: string}) {\n return ;\n}\n", + "language": "tsx" + } + ], + "title": " with " + } + } + ] + }, "definitions": [ { "title": "Props", @@ -4713,7 +4736,7 @@ "filePath": "/ShopPayButton.tsx", "syntaxKind": "TypeAliasDeclaration", "name": "ShopPayButtonProps", - "value": "ShopPayButtonStyleProps & (ShopPayVariantIds | ShopPayVariantAndQuantities)", + "value": "ShopPayButtonStyleProps & ShopPayDomainProps & (ShopPayVariantIds | ShopPayVariantAndQuantities)", "description": "" }, "ShopPayButtonStyleProps": { @@ -4741,6 +4764,23 @@ } ] }, + "ShopPayDomainProps": { + "filePath": "/ShopPayButton.tsx", + "syntaxKind": "TypeAliasDeclaration", + "name": "ShopPayDomainProps", + "value": "{\n /** The domain of your Shopify storefront URL (eg: `your-store.myshopify.com`). */\n storeDomain?: string;\n}", + "description": "", + "members": [ + { + "filePath": "/ShopPayButton.tsx", + "syntaxKind": "PropertySignature", + "name": "storeDomain", + "value": "string", + "description": "The domain of your Shopify storefront URL (eg: `your-store.myshopify.com`).", + "isOptional": true + } + ] + }, "ShopPayVariantIds": { "filePath": "/ShopPayButton.tsx", "syntaxKind": "TypeAliasDeclaration", diff --git a/packages/hydrogen-react/src/ShopPayButton.doc.ts b/packages/hydrogen-react/src/ShopPayButton.doc.ts index dfb82290a9..f170dd6098 100644 --- a/packages/hydrogen-react/src/ShopPayButton.doc.ts +++ b/packages/hydrogen-react/src/ShopPayButton.doc.ts @@ -5,10 +5,10 @@ const data: ReferenceEntityTemplateSchema = { category: 'components', isVisualComponent: false, related: [], - description: `The \`ShopPayButton\` component renders a button that redirects to the Shop Pay checkout. It renders a [\`\`](https://shopify.dev/custom-storefronts/tools/web-components) custom element, for which it will lazy-load the source code automatically. It relies on the \`\` context provider.`, + description: `The \`ShopPayButton\` component renders a button that redirects to the Shop Pay checkout. It renders a [\`\`](https://shopify.dev/custom-storefronts/tools/web-components) custom element, for which it will lazy-load the source code automatically.`, type: 'component', defaultExample: { - description: 'I am the default example', + description: ' without ', codeblock: { tabs: [ { @@ -22,9 +22,33 @@ const data: ReferenceEntityTemplateSchema = { language: 'tsx', }, ], - title: 'Example code', + title: ' without ', }, }, + examples: { + description: '', + examples: [ + { + description: + 'If `` context provider is used in your app, you can use the `` without supplying a `storeDomain` prop', + codeblock: { + tabs: [ + { + title: 'JavaScript', + code: './ShopPayButton2.example.jsx', + language: 'jsx', + }, + { + title: 'TypeScript', + code: './ShopPayButton2.example.tsx', + language: 'tsx', + }, + ], + title: ' with ', + }, + }, + ], + }, definitions: [ { title: 'Props', diff --git a/packages/hydrogen-react/src/ShopPayButton.example.jsx b/packages/hydrogen-react/src/ShopPayButton.example.jsx index 35006f9439..732cd80106 100644 --- a/packages/hydrogen-react/src/ShopPayButton.example.jsx +++ b/packages/hydrogen-react/src/ShopPayButton.example.jsx @@ -1,11 +1,14 @@ import {ShopPayButton} from '@shopify/hydrogen-react'; -export function AddVariantQuantity1({variantId}) { - return ; +export function AddVariantQuantity1({variantId, storeDomain}) { + return ; } -export function AddVariantQuantityMultiple({variantId, quantity}) { +export function AddVariantQuantityMultiple({variantId, quantity, storeDomain}) { return ( - + ); } diff --git a/packages/hydrogen-react/src/ShopPayButton.example.tsx b/packages/hydrogen-react/src/ShopPayButton.example.tsx index c094b26d92..492bc96df1 100644 --- a/packages/hydrogen-react/src/ShopPayButton.example.tsx +++ b/packages/hydrogen-react/src/ShopPayButton.example.tsx @@ -1,17 +1,28 @@ import {ShopPayButton} from '@shopify/hydrogen-react'; -export function AddVariantQuantity1({variantId}: {variantId: string}) { - return ; +export function AddVariantQuantity1({ + variantId, + storeDomain, +}: { + variantId: string; + storeDomain: string; +}) { + return ; } export function AddVariantQuantityMultiple({ variantId, quantity, + storeDomain, }: { variantId: string; quantity: number; + storeDomain: string; }) { return ( - + ); } diff --git a/packages/hydrogen-react/src/ShopPayButton.stories.tsx b/packages/hydrogen-react/src/ShopPayButton.stories.tsx index c75eaa2ba9..672bea225d 100644 --- a/packages/hydrogen-react/src/ShopPayButton.stories.tsx +++ b/packages/hydrogen-react/src/ShopPayButton.stories.tsx @@ -8,14 +8,21 @@ const Template: Story = (props) => ; export const NoQuantity = Template.bind({}); NoQuantity.args = { - variantIds: ['123', '456'], + variantIds: [ + 'gid://shopify/ProductVariant/123', + 'gid://shopify/ProductVariant/456', + ], + storeDomain: 'https://notashop.myshopify.io', className: '', width: '', }; export const Quantities = Template.bind({}); Quantities.args = { - variantIdsAndQuantities: [{id: '123', quantity: 2}], + variantIdsAndQuantities: [ + {id: 'gid://shopify/ProductVariant/123', quantity: 2}, + ], + storeDomain: 'https://notashop.myshopify.io', className: '', width: '', }; diff --git a/packages/hydrogen-react/src/ShopPayButton.test.tsx b/packages/hydrogen-react/src/ShopPayButton.test.tsx index 85c348c86b..05df09be72 100644 --- a/packages/hydrogen-react/src/ShopPayButton.test.tsx +++ b/packages/hydrogen-react/src/ShopPayButton.test.tsx @@ -6,6 +6,7 @@ import { DoublePropsErrorMessage, MissingPropsErrorMessage, InvalidPropsErrorMessage, + MissingStoreDomainErrorMessage, } from './ShopPayButton.js'; import {getShopifyConfig} from './ShopifyProvider.test.js'; @@ -114,4 +115,53 @@ describe(``, () => { }), ).toThrow(InvalidPropsErrorMessage); }); + + it(`throws error if no 'storeDomain' is supplied`, () => { + const fakeId = 'gid://shopify/ProductVariant/123'; + expect(() => render()).toThrow( + MissingStoreDomainErrorMessage, + ); + }); + + it(`allows to use 'storeDomain' props without ShopifyProvider`, () => { + const fakeId = 'gid://shopify/ProductVariant/123'; + const {container} = render( + , + ); + const button = container.querySelector('shop-pay-button'); + + expect(button).toHaveAttribute( + 'store-url', + 'https://notashop.myshopify.com', + ); + }); + + it(`uses 'storeDomain' props over 'ShopifyProvider'`, () => { + const fakeId = 'gid://shopify/ProductVariant/123'; + const {container} = render( + , + { + wrapper: ({children}) => ( + + {children} + + ), + }, + ); + const button = container.querySelector('shop-pay-button'); + + expect(button).toHaveAttribute( + 'store-url', + 'https://notashop.myshopify.com', + ); + }); }); diff --git a/packages/hydrogen-react/src/ShopPayButton.tsx b/packages/hydrogen-react/src/ShopPayButton.tsx index 5ffa528056..da7142e9e2 100644 --- a/packages/hydrogen-react/src/ShopPayButton.tsx +++ b/packages/hydrogen-react/src/ShopPayButton.tsx @@ -1,9 +1,10 @@ -import {useShop} from './ShopifyProvider.js'; +import {defaultShopifyContext, useShop} from './ShopifyProvider.js'; import {useLoadScript} from './load-script.js'; import {parseGid} from './analytics-utils.js'; // By using 'never' in the "or" cases below, it makes these props "exclusive" and means that you cannot pass both of them; you must pass either one OR the other. type ShopPayButtonProps = ShopPayButtonStyleProps & + ShopPayDomainProps & (ShopPayVariantIds | ShopPayVariantAndQuantities); type ShopPayButtonStyleProps = { @@ -13,6 +14,11 @@ type ShopPayButtonStyleProps = { width?: string; }; +type ShopPayDomainProps = { + /** The domain of your Shopify storefront URL (eg: `your-store.myshopify.com`). */ + storeDomain?: string; +}; + type ShopPayVariantIds = { /** An array of IDs of the variants to purchase with Shop Pay. This will only ever have a quantity of 1 for each variant. If you want to use other quantities, then use `variantIdsAndQuantities`. */ variantIds: string[]; @@ -55,16 +61,26 @@ export function ShopPayButton({ className, variantIdsAndQuantities, width, + storeDomain: _storeDomain, }: ShopPayButtonProps): JSX.Element { - const {storeDomain} = useShop(); + const shop = useShop(); + const storeDomain = _storeDomain || shop?.storeDomain; const shopPayLoadedStatus = useLoadScript(SHOPJS_URL); let ids: string[] = []; + if (!storeDomain || storeDomain === defaultShopifyContext.storeDomain) { + throw new Error(MissingStoreDomainErrorMessage); + } + if (variantIds && variantIdsAndQuantities) { throw new Error(DoublePropsErrorMessage); } + if (!variantIds && !variantIdsAndQuantities) { + throw new Error(MissingPropsErrorMessage); + } + if (variantIds) { ids = variantIds.reduce((prev, curr) => { const bareId = parseGid(curr).id; @@ -104,6 +120,8 @@ export function ShopPayButton({ ); } +export const MissingStoreDomainErrorMessage = + 'You must pass a "storeDomain" prop to the "ShopPayButton" component, or wrap it in a "ShopifyProvider" component.'; export const InvalidPropsErrorMessage = `You must pass in "variantIds" in the form of ["gid://shopify/ProductVariant/1"]`; export const MissingPropsErrorMessage = `You must pass in either "variantIds" or "variantIdsAndQuantities" to ShopPayButton`; export const DoublePropsErrorMessage = `You must provide either a variantIds or variantIdsAndQuantities prop, but not both in the ShopPayButton component`; diff --git a/packages/hydrogen-react/src/ShopPayButton2.example.jsx b/packages/hydrogen-react/src/ShopPayButton2.example.jsx new file mode 100644 index 0000000000..2d057d068b --- /dev/null +++ b/packages/hydrogen-react/src/ShopPayButton2.example.jsx @@ -0,0 +1,19 @@ +import {ShopifyProvider, ShopPayButton} from '@shopify/hydrogen-react'; + +export default function App() { + return ( + + + + ); +} + +export function AddVariantQuantity1({variantId}) { + return ; +} diff --git a/packages/hydrogen-react/src/ShopPayButton2.example.tsx b/packages/hydrogen-react/src/ShopPayButton2.example.tsx new file mode 100644 index 0000000000..e993a63915 --- /dev/null +++ b/packages/hydrogen-react/src/ShopPayButton2.example.tsx @@ -0,0 +1,19 @@ +import {ShopifyProvider, ShopPayButton} from '@shopify/hydrogen-react'; + +export default function App() { + return ( + + + + ); +} + +export function AddVariantQuantity1({variantId}: {variantId: string}) { + return ; +} diff --git a/packages/hydrogen-react/src/ShopifyProvider.tsx b/packages/hydrogen-react/src/ShopifyProvider.tsx index 9856e094b3..8a9b2f2f21 100644 --- a/packages/hydrogen-react/src/ShopifyProvider.tsx +++ b/packages/hydrogen-react/src/ShopifyProvider.tsx @@ -3,7 +3,7 @@ import type {LanguageCode, CountryCode} from './storefront-api-types.js'; import {SFAPI_VERSION} from './storefront-api-constants.js'; import {getPublicTokenHeadersRaw} from './storefront-client.js'; -const ShopifyContext = createContext({ +export const defaultShopifyContext: ShopifyContextValue = { storeDomain: 'test', storefrontToken: 'abc123', storefrontApiVersion: SFAPI_VERSION, @@ -18,7 +18,11 @@ const ShopifyContext = createContext({ getShopifyDomain() { return ''; }, -}); +}; + +const ShopifyContext = createContext( + defaultShopifyContext, +); /** * The `` component enables use of the `useShop()` hook. The component should wrap your app. diff --git a/templates/demo-store/app/routes/($lang)/products/$productHandle.tsx b/templates/demo-store/app/routes/($lang)/products/$productHandle.tsx index 277d02d1df..e171e83d10 100644 --- a/templates/demo-store/app/routes/($lang)/products/$productHandle.tsx +++ b/templates/demo-store/app/routes/($lang)/products/$productHandle.tsx @@ -110,6 +110,7 @@ export async function loader({params, request, context}: LoaderArgs) { { product, shop, + storeDomain: context.storefront.getShopifyDomain(), recommended, seo, analytics: { @@ -192,7 +193,7 @@ export default function Product() { } export function ProductForm() { - const {product, analytics} = useLoaderData(); + const {product, analytics, storeDomain} = useLoaderData(); const [currentSearchParams] = useSearchParams(); const transition = useTransition(); @@ -296,7 +297,10 @@ export function ProductForm() { )} {!isOutOfStock && ( - + )} )}