Skip to content

Commit

Permalink
Adds optional channel prop to ShopPayButton
Browse files Browse the repository at this point in the history
  • Loading branch information
QuintonC committed Oct 25, 2023
1 parent 7fc088e commit a371b3a
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 7 deletions.
69 changes: 63 additions & 6 deletions packages/hydrogen-react/docs/generated/generated_docs_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -4485,12 +4485,12 @@
"tabs": [
{
"title": "JavaScript",
"code": "import {ShopPayButton} from '@shopify/hydrogen-react';\n\nexport function AddVariantQuantity1({variantId, storeDomain}) {\n return <ShopPayButton variantIds={[variantId]} storeDomain={storeDomain} />;\n}\n\nexport function AddVariantQuantityMultiple({variantId, quantity, storeDomain}) {\n return (\n <ShopPayButton\n variantIdsAndQuantities={[{id: variantId, quantity}]}\n storeDomain={storeDomain}\n />\n );\n}\n",
"code": "import {ShopPayButton} from '@shopify/hydrogen-react';\n\nexport function AddVariantQuantity1({variantId, storeDomain}) {\n return <ShopPayButton variantIds={[variantId]} storeDomain={storeDomain} />;\n}\n\nexport function AddVariantQuantityMultiple({variantId, quantity, storeDomain}) {\n return (\n <ShopPayButton\n variantIdsAndQuantities={[{id: variantId, quantity}]}\n storeDomain={storeDomain}\n />\n );\n}\n\nexport function ChannelAttribution({channel, variantId, storeDomain}) {\n return (\n <ShopPayButton\n channel={channel}\n variantIds={[variantId]}\n storeDomain={storeDomain}\n />\n );\n}\n",
"language": "jsx"
},
{
"title": "TypeScript",
"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 <ShopPayButton variantIds={[variantId]} storeDomain={storeDomain} />;\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 <ShopPayButton\n variantIdsAndQuantities={[{id: variantId, quantity}]}\n storeDomain={storeDomain}\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 <ShopPayButton variantIds={[variantId]} storeDomain={storeDomain} />;\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 <ShopPayButton\n variantIdsAndQuantities={[{id: variantId, quantity}]}\n storeDomain={storeDomain}\n />\n );\n}\n\nexport function ChannelAttribution({\n channel,\n variantId,\n storeDomain,\n}: {\n channel: 'headless' | 'hydrogen';\n variantId: string;\n storeDomain: string;\n}) {\n return (\n <ShopPayButton\n channel={channel}\n variantIds={[variantId]}\n storeDomain={storeDomain}\n />\n );\n}\n",
"language": "tsx"
}
],
Expand Down Expand Up @@ -4530,7 +4530,7 @@
"filePath": "/ShopPayButton.tsx",
"syntaxKind": "TypeAliasDeclaration",
"name": "ShopPayButtonProps",
"value": "ShopPayButtonStyleProps & ShopPayDomainProps & (ShopPayVariantIds | ShopPayVariantAndQuantities)",
"value": "ShopPayButtonStyleProps & ShopPayDomainProps & ShopPayChannelAttribution & (ShopPayVariantIds | ShopPayVariantAndQuantities)",
"description": ""
},
"ShopPayButtonStyleProps": {
Expand Down Expand Up @@ -4575,6 +4575,30 @@
}
]
},
"ShopPayChannelAttribution": {
"filePath": "/ShopPayButton.tsx",
"syntaxKind": "TypeAliasDeclaration",
"name": "ShopPayChannelAttribution",
"value": "{\n /** A string that adds channel attribution to the order. Can be either `headless` or `hydrogen` */\n channel?: Channel;\n}",
"description": "",
"members": [
{
"filePath": "/ShopPayButton.tsx",
"syntaxKind": "PropertySignature",
"name": "channel",
"value": "Channel",
"description": "A string that adds channel attribution to the order. Can be either `headless` or `hydrogen`",
"isOptional": true
}
]
},
"Channel": {
"filePath": "/ShopPayButton.tsx",
"syntaxKind": "TypeAliasDeclaration",
"name": "Channel",
"value": "'headless' | 'hydrogen'",
"description": "The valid values for `channel` at checkout for custom storefront channel attribution can be one of either `custom-storefronts` or `headless-storefronts`.\n\nTo prevent from confusion, the values exposed on the `ShopPayButton` are simplified to match the channel display name rather than the registered channel identifier."
},
"ShopPayVariantIds": {
"filePath": "/ShopPayButton.tsx",
"syntaxKind": "TypeAliasDeclaration",
Expand Down Expand Up @@ -6007,14 +6031,14 @@
{
"filePath": "/flatten-connection.ts",
"syntaxKind": "MethodSignature",
"name": "__@iterator@441",
"name": "__@iterator@414",
"value": "() => IterableIterator<unknown>",
"description": "Iterator"
},
{
"filePath": "/flatten-connection.ts",
"syntaxKind": "MethodSignature",
"name": "__@unscopables@443",
"name": "__@unscopables@416",
"value": "() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }",
"description": "Returns an object whose properties have the value 'true'\r\nwhen they will be absent when used in a 'with' statement."
},
Expand Down Expand Up @@ -6321,8 +6345,41 @@
"filePath": "/load-script.tsx",
"syntaxKind": "TypeAliasDeclaration",
"name": "LoadScriptParams",
"value": "[src: string, options?: { module?: boolean; in?: \"body\" | \"head\"; }]",
"value": "[src: string, options?: LoadScriptOptions]",
"description": ""
},
"LoadScriptOptions": {
"filePath": "/load-script.tsx",
"syntaxKind": "TypeAliasDeclaration",
"name": "LoadScriptOptions",
"value": "{\n module?: boolean;\n in?: 'head' | 'body';\n attributes?: Record<string, string>;\n}",
"description": "",
"members": [
{
"filePath": "/load-script.tsx",
"syntaxKind": "PropertySignature",
"name": "module",
"value": "boolean",
"description": "",
"isOptional": true
},
{
"filePath": "/load-script.tsx",
"syntaxKind": "PropertySignature",
"name": "in",
"value": "\"body\" | \"head\"",
"description": "",
"isOptional": true
},
{
"filePath": "/load-script.tsx",
"syntaxKind": "PropertySignature",
"name": "attributes",
"value": "Record<string, string>",
"description": "",
"isOptional": true
}
]
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions packages/hydrogen-react/src/ShopPayButton.example.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,13 @@ export function AddVariantQuantityMultiple({variantId, quantity, storeDomain}) {
/>
);
}

export function ChannelAttribution({channel, variantId, storeDomain}) {
return (
<ShopPayButton
channel={channel}
variantIds={[variantId]}
storeDomain={storeDomain}
/>
);
}
18 changes: 18 additions & 0 deletions packages/hydrogen-react/src/ShopPayButton.example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,21 @@ export function AddVariantQuantityMultiple({
/>
);
}

export function ChannelAttribution({
channel,
variantId,
storeDomain,
}: {
channel: 'headless' | 'hydrogen';
variantId: string;
storeDomain: string;
}) {
return (
<ShopPayButton
channel={channel}
variantIds={[variantId]}
storeDomain={storeDomain}
/>
);
}
11 changes: 11 additions & 0 deletions packages/hydrogen-react/src/ShopPayButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ Quantities.args = {
className: '',
width: '',
};

export const ChannelAttribution = Template.bind({});
ChannelAttribution.args = {
channel: 'hydrogen',
variantIdsAndQuantities: [
{id: 'gid://shopify/ProductVariant/123', quantity: 2},
],
storeDomain: 'https://notashop.myshopify.io',
className: '',
width: '',
};
42 changes: 42 additions & 0 deletions packages/hydrogen-react/src/ShopPayButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DoublePropsErrorMessage,
MissingPropsErrorMessage,
InvalidPropsErrorMessage,
InvalidChannelErrorMessage,
MissingStoreDomainErrorMessage,
} from './ShopPayButton.js';
import {getShopifyConfig} from './ShopifyProvider.test.js';
Expand Down Expand Up @@ -165,4 +166,45 @@ describe(`<ShopPayButton />`, () => {
'https://notashop.myshopify.com',
);
});

it(`throws an error if you pass an invalid channel value`, () => {
expect(() =>
render(
<ShopPayButton
// @ts-expect-error Purposely passing in invalid channel
channel="test"
variantIdsAndQuantities={[]}
/>,
{
wrapper: ({children}) => (
<ShopifyProvider {...getShopifyConfig()}>
{children}
</ShopifyProvider>
),
},
),
).toThrow(InvalidChannelErrorMessage);
});

it(`creates the correct attribute when using 'channel'`, () => {
const {container} = render(
<ShopPayButton
channel="hydrogen"
variantIds={['gid://shopify/ProductVariant/123']}
/>,
{
wrapper: ({children}) => (
<ShopifyProvider {...getShopifyConfig()}>{children}</ShopifyProvider>
),
},
);

const button = container.querySelector('shop-pay-button');

expect(button).toHaveAttribute(
'store-url',
'https://notashop.myshopify.io',
);
expect(button).toHaveAttribute('channel', 'custom-storefronts');
});
});
42 changes: 41 additions & 1 deletion packages/hydrogen-react/src/ShopPayButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 &
ShopPayChannelAttribution &
(ShopPayVariantIds | ShopPayVariantAndQuantities);

type ShopPayButtonStyleProps = {
Expand Down Expand Up @@ -36,11 +37,26 @@ type ShopPayVariantAndQuantities = {
}>;
};

/**
* The valid values for `channel` at checkout for custom storefront channel attribution can be
* one of either `custom-storefronts` or `headless-storefronts`.
*
* To prevent from confusion, the values exposed on the `ShopPayButton` are simplified to match
* the channel display name rather than the registered channel identifier.
*/
type Channel = 'headless' | 'hydrogen';

type ShopPayChannelAttribution = {
/** A string that adds channel attribution to the order. Can be either `headless` or `hydrogen` */
channel?: Channel;
};

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicElements {
'shop-pay-button': {
channel?: string;
variants: string;
'store-url': string;
};
Expand All @@ -51,12 +67,22 @@ declare global {
const SHOPJS_URL =
'https://cdn.shopify.com/shopifycloud/shop-js/v1.0/client.js';

const CHANNEL_MAP: Record<Channel, string> = {
headless: 'headless-storefronts',
hydrogen: 'custom-storefronts',
};

function isChannel(channel: string): channel is Channel {
return Object.keys(CHANNEL_MAP).includes(channel);
}

/**
* The `ShopPayButton` component renders a button that redirects to the Shop Pay checkout.
* It renders a [`<shop-pay-button>`](https://shopify.dev/custom-storefronts/tools/web-components) custom element, for which it will lazy-load the source code automatically.
* It relies on the `<ShopProvider>` context provider.
*/
export function ShopPayButton({
channel,
variantIds,
className,
variantIdsAndQuantities,
Expand All @@ -68,6 +94,7 @@ export function ShopPayButton({
const shopPayLoadedStatus = useLoadScript(SHOPJS_URL);

let ids: string[] = [];
let channelAttribution: string | undefined;

if (!storeDomain || storeDomain === defaultShopifyContext.storeDomain) {
throw new Error(MissingStoreDomainErrorMessage);
Expand All @@ -81,6 +108,14 @@ export function ShopPayButton({
throw new Error(MissingPropsErrorMessage);
}

if (channel) {
if (isChannel(channel)) {
channelAttribution = CHANNEL_MAP[channel];
} else {
throw new Error(InvalidChannelErrorMessage);
}
}

if (variantIds) {
ids = variantIds.reduce<string[]>((prev, curr) => {
const bareId = parseGid(curr).id;
Expand Down Expand Up @@ -114,7 +149,11 @@ export function ShopPayButton({
return (
<div className={className} style={style}>
{shopPayLoadedStatus === 'done' && (
<shop-pay-button store-url={storeDomain} variants={ids.join(',')} />
<shop-pay-button
{...(channelAttribution ? {channel: channelAttribution} : {})}
store-url={storeDomain}
variants={ids.join(',')}
/>
)}
</div>
);
Expand All @@ -125,3 +164,4 @@ export const MissingStoreDomainErrorMessage =
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`;
export const InvalidChannelErrorMessage = `Invalid channel attribution value. Must be either "headless" or "hydrogen"`;

0 comments on commit a371b3a

Please sign in to comment.