Skip to content

Commit

Permalink
Feat/cart provider (#12)
Browse files Browse the repository at this point in the history
* Copy CartProvider from hydrogen repo

* Add worktop lib to use the cookies package for CartProvider

* Modify CartProvider to fit hydrogen-ui

* Add CartProvider story

* add changeset

* Cart provider updates (#14)

* Change ts moduleresolution (#13)

* Change TS's module resolution to 'node' instead of nodenext

* Include stories in TS checking now, and fix issues

* Starting work on simplifying the cart code in hui

* Simply more cart-related files and remove a bunch of them

* All CI checks are passing, and types are improved.

* Better file names

* Fix mock import filename

* Remove any type

* Improve release notes, update type names, fix typo

* Correct alex config file and allow hooks

Co-authored-by: Anthony Frehner <frehner@users.noreply.github.com>
  • Loading branch information
lordofthecactus and frehner committed Oct 20, 2022
1 parent 7fb60a6 commit b9c3940
Show file tree
Hide file tree
Showing 17 changed files with 3,792 additions and 5 deletions.
3 changes: 0 additions & 3 deletions .alexrc

This file was deleted.

3 changes: 3 additions & 0 deletions .alexrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
allow: ['hook', 'hooks'],
};
25 changes: 25 additions & 0 deletions .changeset/seven-boxes-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@shopify/hydrogen-react': patch
---

Add `<CartProvider/>` and releated hooks & types.

Component:

- `<CartProvider/>`

Hooks:

- `useCart()`
- `useCartFetch()`
- `useInstantCheckout()`

Types:

- `CartState`
- `CartStatus`
- `Cart`
- `CartWithActions`
- `CartAction`

Also updated `flattenConnection()` to better handle a `null` or `undefined` argument.
5 changes: 4 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@
},
"dependencies": {
"@google/model-viewer": "^1.12.1",
"@xstate/fsm": "^2.0.0",
"@xstate/react": "^3.0.1",
"graphql": "^16.6.0",
"type-fest": "^3.1.0"
"type-fest": "^3.1.0",
"worktop": "^0.7.3"
},
"repository": {
"type": "git",
Expand Down
246 changes: 246 additions & 0 deletions packages/react/src/CartProvider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import {ComponentProps, useState} from 'react';
import type {Story} from '@ladle/react';
import {CartProvider, storageAvailable, useCart} from './CartProvider.js';
import {ShopifyContextValue, ShopifyProvider} from './ShopifyProvider.js';
import {CART_ID_STORAGE_KEY} from './cart-constants.js';

const merchandiseId = 'gid://shopify/ProductVariant/41007290482744';

function CartComponent() {
const {
status,
lines,
note,
buyerIdentity,
attributes,
discountCodes,
linesAdd,
cartCreate,
linesUpdate,
linesRemove,
noteUpdate,
buyerIdentityUpdate,
cartAttributesUpdate,
discountCodesUpdate,
} = useCart();

const localStorageId = storageAvailable('localStorage')
? localStorage.getItem(CART_ID_STORAGE_KEY)
: null;

const [lineToAdd, setLineToAdd] = useState(merchandiseId);
const [lineToRemove, setLineToRemove] = useState('');
const [lineToUpdate, setLineToUpdate] = useState('');
const [lineToUpdateQuantity, setLineToUpdateQuantity] = useState(1);
const [newNote, setNote] = useState('');
const [newBuyerIdentity, setBuyerIdentity] = useState(
`{"countryCode": "DE"}`
);
const [newCartAttributes, setCartAttributes] = useState(
'[{"key": "foo", "value": "bar"}]'
);
const [newDiscount, setDiscount] = useState('["H2O"]');

return (
<>
<div>
<h1>This is your current cart</h1>
<h3>Cart status</h3>
<p>{status}</p>
<h3>Fetched from local storage with this cart id</h3>
<p>{localStorageId}</p>
<h3>Cart lines</h3>
<p>{JSON.stringify(lines, null, 2)}</p>
<h3>Note</h3>
<p>{note}</p>
<h3>Buyer identity</h3>
<p>{JSON.stringify(buyerIdentity)}</p>
<h3>attributes</h3>
<p>{JSON.stringify(attributes)}</p>
<h3>discounts</h3>
<p>{JSON.stringify(discountCodes)}</p>
</div>
<div>
<h2>
These are the cart actions you can do using useCart and the
CartProvider
</h2>
<h3>Create a new cart</h3>
<button
onClick={() => {
cartCreate({
lines: [],
});
}}
>
Create cart
</button>
<div style={{display: 'grid', gap: 10}}>
<h3>Add to cart</h3>
<label htmlFor="lineToAdd">Merchandise ID</label>
<input
type="text"
value={lineToAdd}
onChange={(e) => setLineToAdd(e.target.value)}
/>
<button
onClick={() => {
linesAdd([
{
merchandiseId: lineToAdd,
quantity: 1,
},
]);
}}
>
Add to cart
</button>
</div>
<div style={{display: 'grid', gap: 10}}>
<h3>Remove cart line</h3>
<label htmlFor="lineToRemove">CartLine Variant ID</label>
<input
type="text"
value={lineToRemove}
onChange={(e) => setLineToRemove(e.target.value)}
/>
<button
onClick={() => {
linesRemove([lineToRemove]);
}}
>
Remove cart line
</button>
</div>
<div style={{display: 'grid', gap: 10}}>
<h3>Update cart line</h3>
<label htmlFor="lineToUpdate">CartLine Variant ID</label>
<input
type="text"
value={lineToUpdate}
onChange={(e) => setLineToUpdate(e.target.value)}
/>
<label htmlFor="lineToUpdateQuantity">Quantity</label>
<input
type="number"
value={lineToUpdateQuantity}
onChange={(e) => setLineToUpdateQuantity(Number(e.target.value))}
/>
<button
onClick={() => {
linesUpdate([
{
id: lineToUpdate,
quantity: lineToUpdateQuantity,
},
]);
}}
>
Update cart line
</button>
</div>
<div style={{display: 'grid', gap: 10}}>
<h3>Note update</h3>
<label htmlFor="noteUpdate">Note</label>
<input
type="text"
value={newNote}
onChange={(e) => setNote(e.target.value)}
/>
<button
onClick={() => {
noteUpdate(newNote);
}}
>
Update note
</button>
</div>
<div style={{display: 'grid', gap: 10}}>
<h3>Buyer identity update</h3>
<label htmlFor="buyerIdentityUpdate">Buyer Identity</label>
<input
type="text"
value={newBuyerIdentity}
onChange={(e) => setBuyerIdentity(e.target.value)}
/>
<button
onClick={() => {
buyerIdentityUpdate(JSON.parse(`${newBuyerIdentity}`));
}}
>
update Buyer Identity
</button>
</div>
<div style={{display: 'grid', gap: 10}}>
<h3>Update attributes</h3>
<label htmlFor="cartAttributesUpdate">attributes</label>
<input
type="text"
value={newCartAttributes}
onChange={(e) => setCartAttributes(e.target.value)}
/>
<button
onClick={() => {
cartAttributesUpdate(JSON.parse(`${newCartAttributes}`));
}}
>
update cart attributes
</button>
</div>
<div style={{display: 'grid', gap: 10}}>
<h3>Update discount</h3>
<label htmlFor="discountUpdate">discounts</label>
<input
type="text"
value={newDiscount}
onChange={(e) => setDiscount(e.target.value)}
/>
<button
onClick={() => {
discountCodesUpdate(JSON.parse(`${newDiscount}`));
}}
>
update discounts
</button>
</div>
</div>
</>
);
}

const config: ShopifyContextValue = {
storeDomain: 'hydrogen-preview.myshopify.com',
storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
storefrontApiVersion: '2022-07',
country: {
isoCode: 'CA',
},
language: {
isoCode: 'EN',
},
locale: 'en-CA',
};

const Template: Story<ComponentProps<typeof CartProvider>> = (props) => {
return (
<ShopifyProvider shopifyConfig={config}>
<CartProvider {...props}>
<CartComponent />
</CartProvider>
</ShopifyProvider>
);
};

export const Default = Template.bind({});
Default.args = {
/** Maximum number of cart lines to fetch. Defaults to 250 cart lines. */
numCartLines: 30,
/** A callback that is invoked when the process to create a cart begins, but before the cart is created in the Storefront API. */
data: undefined,
/** A fragment used to query the Storefront API's [Cart object](https://shopify.dev/api/storefront/latest/objects/cart) for all queries and mutations. A default value is used if no argument is provided. */
cartFragment: undefined,
/** A customer access token that's accessible on the server if there's a customer login. */
customerAccessToken: undefined,
/** The ISO country code for i18n. */
countryCode: 'DE',
};
83 changes: 83 additions & 0 deletions packages/react/src/CartProvider.test.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {flattenConnection} from './flatten-connection.js';
import {getPrice} from './Money.test.helpers.js';
import type {Cart, CartLine} from './storefront-api-types.js';
import type {PartialDeep} from 'type-fest';

export const CART_LINE: PartialDeep<CartLine, {recurseIntoArrays: true}> = {
attributes: [{key: 'color', value: 'red'}],
quantity: 1,
id: 'abc',
merchandise: {
id: 'def',
availableForSale: true,
priceV2: {
amount: '123',
currencyCode: 'USD',
},
product: {
handle: 'foo',
title: 'Product Name',
},
requiresShipping: true,
selectedOptions: [{name: 'size', value: 'large'}],
title: 'Product Name - Large',
},
cost: {
totalAmount: {
amount: '123',
currencyCode: 'USD',
},
compareAtAmountPerQuantity: {
amount: '125',
currencyCode: 'USD',
},
},
};

export const CART: PartialDeep<Cart, {recurseIntoArrays: true}> = {
id: 'abc',
checkoutUrl: 'https://shopify.com/checkout',
attributes: [],
buyerIdentity: {
countryCode: 'US',
email: '',
phone: '',
},
discountCodes: [],
totalQuantity: 0,
cost: {
subtotalAmount: getPrice(),
totalAmount: getPrice(),
totalTaxAmount: getPrice(),
totalDutyAmount: getPrice(),
},
lines: {edges: []},
note: '',
};

export function getCartMock(
options?: Partial<Cart>
): PartialDeep<Cart, {recurseIntoArrays: true}> {
return {...CART, ...options};
}

export const CART_WITH_LINES: PartialDeep<Cart, {recurseIntoArrays: true}> = {
...CART,
lines: {edges: [{node: CART_LINE}]},
};

export const CART_WITH_LINES_FLATTENED: PartialDeep<
Cart,
{recurseIntoArrays: true}
> & {
lines: PartialDeep<CartLine[], {recurseIntoArrays: true}>;
} = {
...CART,
lines: flattenConnection(CART_WITH_LINES.lines),
};

export function getCartLineMock(
options?: Partial<CartLine>
): PartialDeep<CartLine, {recurseIntoArrays: true}> {
return {...CART_LINE, ...options};
}

0 comments on commit b9c3940

Please sign in to comment.