Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support cart metadata #60

Merged
merged 14 commits into from
Feb 16, 2021
69 changes: 51 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,15 @@ ReactDOM.render(

#### Props

- `id`: (_optional_) `id` for your cart to enable automatic cart retrieval via `window.localStorage`. If you pass a `id` then you can use multiple instances of `CartProvider`.
- `onSetItems`: Triggered only when `setItems` invoked
- `onItemAdd`: Triggered on items added to your cart, unless the item already exists, then `onItemUpdate` will be invoked
- `onItemUpdate`: Triggered on items updated in your cart, unless you are setting the quantity to `0`, then `onItemRemove` will be invoked
- `onItemRemove`: Triggered on items removed from your cart
- `storage`: Must return `[getter, setter]`
| Prop | Required | Description |
| -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | _No_ | `id` for your cart to enable automatic cart retrieval via `window.localStorage`. If you pass a `id` then you can use multiple instances of `CartProvider`. |
| `onSetItems` | _No_ | Triggered only when `setItems` invoked. |
| `onItemAdd` | _No_ | Triggered on items added to your cart, unless the item already exists, then `onItemUpdate` will be invoked. |
| `onItemUpdate` | _No_ | Triggered on items updated in your cart, unless you are setting the quantity to `0`, then `onItemRemove` will be invoked. |
| `onItemRemove` | _No_ | Triggered on items removed from your cart. |
| `storage` | _No_ | Must return `[getter, setter]`. |
| `metadata` | _No_ | Custom global state on the cart. Stored inside of `metadata`. |

## `useCart`

Expand Down Expand Up @@ -304,9 +307,27 @@ const { emptyCart } = useCart();
emptyCart();
```

### `items`
### `updateCartMetadata(object)`

This will return the current cart items.
The `updateCartMetadata()` will update the `metadata` object on the cart. You must pass it a object.

#### Args

- `object`: A object with key/value pairs. The key being a string.

#### Usage

```js
import { useCart } from "react-use-cart";

const { updateCartMetadata } = useCart();

updateCartMetadata({ notes: "Leave in shed" });
```

### `items = []`

This will return the current cart items in an array.

#### Usage

Expand All @@ -316,9 +337,9 @@ import { useCart } from "react-use-cart";
const { items } = useCart();
```

### `isEmpty`
### `isEmpty = false`

A quick and easy way to check if the cart is empty.
A quick and easy way to check if the cart is empty. Returned as a boolean.

#### Usage

Expand All @@ -330,7 +351,7 @@ const { isEmpty } = useCart();

### `getItem(itemId)`

Get a specific cart item by `id`.
Get a specific cart item by `id`. Returns the item object.

#### Args

Expand All @@ -348,7 +369,7 @@ const myItem = getItem("cjld2cjxh0000qzrmn831i7rn");

### `inCart(itemId)`

Quickly check if an item is in the cart.
Quickly check if an item is in the cart. Returned as a boolean.

#### Args

Expand All @@ -364,9 +385,9 @@ const { inCart } = useCart();
inCart("cjld2cjxh0000qzrmn831i7rn") ? "In cart" : "Not in cart";
```

### `totalItems`
### `totalItems = 0`

This method returns the totaly quantity of items in the cart.
This returns the totaly quantity of items in the cart as an integer.

#### Usage

Expand All @@ -376,9 +397,9 @@ import { useCart } from "react-use-cart";
const { totalItems } = useCart();
```

### `totalUniqueItems`
### `totalUniqueItems = 0`

This method returns the total unique items in the cart.
This returns the total unique items in the cart as an integer.

#### Usage

Expand All @@ -388,9 +409,9 @@ import { useCart } from "react-use-cart";
const { totalUniqueItems } = useCart();
```

### `cartTotal`
### `cartTotal = 0`

This method returns the total value of all items in the cart.
This returns the total value of all items in the cart.

#### Usage

Expand All @@ -399,3 +420,15 @@ import { useCart } from "react-use-cart";

const { cartTotal } = useCart();
```

### `metadata = {}`

This returns the metadata set with `updateCartMetadata`. This is useful for storing additional cart, or checkout values.

#### Usage

```js
import { useCart } from "react-use-cart";

const { metadata } = useCart();
```
71 changes: 71 additions & 0 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ describe("CartProvider", () => {
);
expect(result.current.isEmpty).toBe(true);
});

test("sets cart metadata", () => {
const metadata = {
coupon: "abc123",
notes: "Leave on door step",
};

const wrapper = ({ children }) => (
<CartProvider id="test" metadata={metadata}>
{children}
</CartProvider>
);

const { result } = renderHook(() => useCart(), {
wrapper,
});

expect(result.current.metadata).toEqual(metadata);
});
});

describe("addItem", () => {
Expand Down Expand Up @@ -415,3 +434,55 @@ describe("emptyCart", () => {
expect(result.current.isEmpty).toBe(true);
});
});

describe("updateCartMetadata", () => {
test("updates cart metadata", () => {
const wrapper = ({ children }) => (
<CartProvider id="test">{children}</CartProvider>
);

const { result } = renderHook(() => useCart(), {
wrapper,
});

const metadata = {
coupon: "abc123",
notes: "Leave on door step",
};

act(() => {
result.current.updateCartMetadata(metadata);
});

expect(result.current.metadata).toEqual(metadata);
});

test("merge new metadata with existing", () => {
const initialMetadata = {
coupon: "abc123",
};

const wrapper = ({ children }) => (
<CartProvider id="test" metadata={initialMetadata}>
{children}
</CartProvider>
);

const { result } = renderHook(() => useCart(), {
wrapper,
});

const metadata = {
notes: "Leave on door step",
};

act(() => {
result.current.updateCartMetadata(metadata);
});

expect(result.current.metadata).toEqual({
...initialMetadata,
...metadata,
});
});
});
32 changes: 29 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from "react";

import useLocalStorage from "./useLocalStorage";

// Interfaces:
interface Item {
id: string;
price: number;
Expand All @@ -17,6 +16,11 @@ interface InitialState {
totalItems: number;
totalUniqueItems: number;
cartTotal: number;
metadata?: Metadata;
}

interface Metadata {
[key: string]: any;
}

interface CartProviderState extends InitialState {
Expand All @@ -38,14 +42,16 @@ export type Actions =
id: Item["id"];
payload: object;
}
| { type: "EMPTY_CART" };
| { type: "EMPTY_CART" }
| { type: "UPDATE_CART_META"; payload: Metadata };

export const initialState: any = {
items: [],
isEmpty: true,
totalItems: 0,
totalUniqueItems: 0,
cartTotal: 0,
metadata: {},
};

const CartContext = React.createContext<CartProviderState | undefined>(
Expand All @@ -56,9 +62,10 @@ export const createCartIdentifier = (len = 12) =>
[...Array(len)].map(() => (~~(Math.random() * 36)).toString(36)).join("");

export const useCart = () => {
// This makes sure that the cart functions are always defined before calling it.
const context = React.useContext(CartContext);

if (!context) throw new Error("Expected to be wrapped in a CartProvider");

return context;
};

Expand Down Expand Up @@ -95,6 +102,15 @@ function reducer(state: CartProviderState, action: Actions) {
case "EMPTY_CART":
return initialState;

case "UPDATE_CART_META":
return {
...state,
metadata: {
...state.metadata,
...action.payload,
},
};

default:
throw new Error("No action specified");
}
Expand Down Expand Up @@ -141,6 +157,7 @@ export const CartProvider: React.FC<{
key: string,
initialValue: string
) => [string, (value: Function | string) => void];
metadata?: Metadata;
}> = ({
children,
id: cartId,
Expand All @@ -150,6 +167,7 @@ export const CartProvider: React.FC<{
onItemUpdate,
onItemRemove,
storage = useLocalStorage,
metadata,
}) => {
const id = cartId ? cartId : createCartIdentifier();

Expand All @@ -159,6 +177,7 @@ export const CartProvider: React.FC<{
id,
...initialState,
items: defaultItems,
metadata,
})
);

Expand Down Expand Up @@ -252,6 +271,12 @@ export const CartProvider: React.FC<{

const inCart = (id: Item["id"]) => state.items.some((i: Item) => i.id === id);

const updateCartMetadata = (metadata: Metadata) =>
dispatch({
type: "UPDATE_CART_META",
payload: metadata,
});

return (
<CartContext.Provider
value={{
Expand All @@ -264,6 +289,7 @@ export const CartProvider: React.FC<{
updateItemQuantity,
removeItem,
emptyCart,
updateCartMetadata,
}}
>
{children}
Expand Down