Skip to content

Commit

Permalink
Fix the OptimisticCart type to properly retain the generic of line …
Browse files Browse the repository at this point in the history
…items (#2327)

* Fix the `OptimisticCart` type to properly retain the generic of line items
* Make `OptimisticCartLine` support both Cart and CartLine as a generic
  • Loading branch information
blittle committed Jul 31, 2024
1 parent 31ea19e commit dfb9be7
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-flowers-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/hydrogen': patch
---

Fix the `OptimisticCart` type to properly retain the generic of line items. The `OptimisticCartLine` type now takes a cart or cart line item generic.
10 changes: 6 additions & 4 deletions examples/multipass/app/components/Cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {MultipassCheckoutButton} from './MultipassCheckoutButton';
/********** EXAMPLE UPDATE END ************/
/***********************************************/

type CartLine = OptimisticCartLine<CartApiQueryFragment>;

type CartMainProps = {
cart: CartApiQueryFragment | null;
layout: 'page' | 'aside';
Expand Down Expand Up @@ -65,7 +67,7 @@ function CartLines({
layout,
}: {
layout: CartMainProps['layout'];
lines: OptimisticCartLine[];
lines: CartLine[];
}) {
if (!lines) return null;

Expand All @@ -85,7 +87,7 @@ function CartLineItem({
line,
}: {
layout: CartMainProps['layout'];
line: OptimisticCartLine;
line: CartLine;
}) {
const {id, merchandise} = line;
const {product, title, image, selectedOptions} = merchandise;
Expand Down Expand Up @@ -199,7 +201,7 @@ function CartLineRemoveButton({
);
}

function CartLineQuantity({line}: {line: OptimisticCartLine}) {
function CartLineQuantity({line}: {line: CartLine}) {
if (!line || typeof line?.quantity === 'undefined') return null;
const {id: lineId, quantity, isOptimistic} = line;
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
Expand Down Expand Up @@ -240,7 +242,7 @@ function CartLinePrice({
priceType = 'regular',
...passthroughProps
}: {
line: OptimisticCartLine;
line: CartLine;
priceType?: 'regular' | 'compareAt';
[key: string]: any;
}) {
Expand Down
10 changes: 6 additions & 4 deletions examples/subscriptions/app/components/Cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {Link} from '@remix-run/react';
import type {CartApiQueryFragment} from 'storefrontapi.generated';
import {useVariantUrl} from '~/lib/variants';

type CartLine = OptimisticCartLine<CartApiQueryFragment>;

type CartMainProps = {
cart: CartApiQueryFragment;
layout: 'page' | 'aside';
Expand Down Expand Up @@ -60,7 +62,7 @@ function CartLines({
layout,
}: {
layout: CartMainProps['layout'];
lines: OptimisticCartLine[];
lines: CartLine[];
}) {
if (!lines) return null;

Expand All @@ -80,7 +82,7 @@ function CartLineItem({
line,
}: {
layout: CartMainProps['layout'];
line: OptimisticCartLine;
line: CartLine;
}) {
/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
Expand Down Expand Up @@ -208,7 +210,7 @@ function CartLineRemoveButton({
);
}

function CartLineQuantity({line}: {line: OptimisticCartLine}) {
function CartLineQuantity({line}: {line: CartLine}) {
if (!line || typeof line?.quantity === 'undefined') return null;
const {id: lineId, quantity, isOptimistic} = line;
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
Expand Down Expand Up @@ -249,7 +251,7 @@ function CartLinePrice({
priceType = 'regular',
...passthroughProps
}: {
line: OptimisticCartLine;
line: CartLine;
priceType?: 'regular' | 'compareAt';
[key: string]: any;
}) {
Expand Down
7 changes: 5 additions & 2 deletions examples/subscriptions/app/components/CartLineItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {useVariantUrl} from '~/lib/variants';
import {Link} from '@remix-run/react';
import {ProductPrice} from '~/components/ProductPrice';
import {useAside} from '~/components/Aside';
import type {CartApiQueryFragment} from 'storefrontapi.generated';

type CartLine = OptimisticCartLine<CartApiQueryFragment>;

/**
* A single line item in the cart. It displays the product image, title, price.
Expand All @@ -15,7 +18,7 @@ export function CartLineItem({
line,
}: {
layout: CartLayout;
line: OptimisticCartLine;
line: CartLine;
}) {
/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
Expand Down Expand Up @@ -85,7 +88,7 @@ export function CartLineItem({
* These controls are disabled when the line item is new, and the server
* hasn't yet responded that it was successfully added to the cart.
*/
function CartLineQuantity({line}: {line: OptimisticCartLine}) {
function CartLineQuantity({line}: {line: CartLine}) {
if (!line || typeof line?.quantity === 'undefined') return null;
const {id: lineId, quantity, isOptimistic} = line;
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
Expand Down
183 changes: 183 additions & 0 deletions examples/subscriptions/app/lib/fragments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/cart
export const CART_QUERY_FRAGMENT = `#graphql
fragment Money on MoneyV2 {
currencyCode
amount
}
fragment CartLine on CartLine {
id
quantity
attributes {
key
value
}
cost {
totalAmount {
...Money
}
amountPerQuantity {
...Money
}
compareAtAmountPerQuantity {
...Money
}
}
#/***********************************************/
#/********** EXAMPLE UPDATE STARTS ************/
sellingPlanAllocation {
sellingPlan {
name
}
}
#/***********************************************/
#/********** EXAMPLE UPDATE ENDS ************/
merchandise {
... on ProductVariant {
id
availableForSale
compareAtPrice {
...Money
}
price {
...Money
}
requiresShipping
title
image {
id
url
altText
width
height
}
product {
handle
title
id
vendor
}
selectedOptions {
name
value
}
}
}
}
fragment CartApiQuery on Cart {
updatedAt
id
checkoutUrl
totalQuantity
buyerIdentity {
countryCode
customer {
id
email
firstName
lastName
displayName
}
email
phone
}
lines(first: $numCartLines) {
nodes {
...CartLine
}
}
cost {
subtotalAmount {
...Money
}
totalAmount {
...Money
}
totalDutyAmount {
...Money
}
totalTaxAmount {
...Money
}
}
note
attributes {
key
value
}
discountCodes {
code
applicable
}
}
` as const;

const MENU_FRAGMENT = `#graphql
fragment MenuItem on MenuItem {
id
resourceId
tags
title
type
url
}
fragment ChildMenuItem on MenuItem {
...MenuItem
}
fragment ParentMenuItem on MenuItem {
...MenuItem
items {
...ChildMenuItem
}
}
fragment Menu on Menu {
id
items {
...ParentMenuItem
}
}
` as const;

export const HEADER_QUERY = `#graphql
fragment Shop on Shop {
id
name
description
primaryDomain {
url
}
brand {
logo {
image {
url
}
}
}
}
query Header(
$country: CountryCode
$headerMenuHandle: String!
$language: LanguageCode
) @inContext(language: $language, country: $country) {
shop {
...Shop
}
menu(handle: $headerMenuHandle) {
...Menu
}
}
${MENU_FRAGMENT}
` as const;

export const FOOTER_QUERY = `#graphql
query Footer(
$country: CountryCode
$footerMenuHandle: String!
$language: LanguageCode
) @inContext(language: $language, country: $country) {
menu(handle: $footerMenuHandle) {
...Menu
}
}
${MENU_FRAGMENT}
` as const;
6 changes: 6 additions & 0 deletions examples/subscriptions/storefrontapi.generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export type CartLineFragment = Pick<
Pick<StorefrontAPI.MoneyV2, 'currencyCode' | 'amount'>
>;
};
sellingPlanAllocation?: StorefrontAPI.Maybe<{
sellingPlan: Pick<StorefrontAPI.SellingPlan, 'name'>;
}>;
merchandise: Pick<
StorefrontAPI.ProductVariant,
'id' | 'availableForSale' | 'requiresShipping' | 'title'
Expand Down Expand Up @@ -67,6 +70,9 @@ export type CartApiQueryFragment = Pick<
Pick<StorefrontAPI.MoneyV2, 'currencyCode' | 'amount'>
>;
};
sellingPlanAllocation?: StorefrontAPI.Maybe<{
sellingPlan: Pick<StorefrontAPI.SellingPlan, 'name'>;
}>;
merchandise: Pick<
StorefrontAPI.ProductVariant,
'id' | 'availableForSale' | 'requiresShipping' | 'title'
Expand Down
14 changes: 11 additions & 3 deletions packages/hydrogen/src/cart/optimistic/useOptimisticCart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import {
import type {PartialDeep} from 'type-fest';
import type {CartReturn} from '../queries/cart-types';

export type OptimisticCartLine<T = CartLine> = T & {isOptimistic?: boolean};
type LikeACart = {
lines: {
nodes: Array<unknown>;
};
};

export type OptimisticCartLine<T = CartLine | CartReturn> = T extends LikeACart
? T['lines']['nodes'][number] & {isOptimistic?: boolean}
: T & {isOptimistic?: boolean};

export type OptimisticCart<T = CartReturn> = T extends undefined | null
? // This is the null/undefined case, where the cart has yet to be created.
Expand All @@ -25,7 +33,7 @@ export type OptimisticCart<T = CartReturn> = T extends undefined | null
: Omit<T, 'lines'> & {
isOptimistic?: boolean;
lines: {
nodes: Array<OptimisticCartLine>;
nodes: Array<OptimisticCartLine<T>>;
};
};

Expand All @@ -49,7 +57,7 @@ export function useOptimisticCart<
? (structuredClone(cart) as OptimisticCart<DefaultCart>)
: ({lines: {nodes: []}} as unknown as OptimisticCart<DefaultCart>);

const cartLines = optimisticCart.lines.nodes;
const cartLines = optimisticCart.lines.nodes as OptimisticCartLine[];

let isOptimistic = false;

Expand Down
7 changes: 5 additions & 2 deletions templates/skeleton/app/components/CartLineItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {useVariantUrl} from '~/lib/variants';
import {Link} from '@remix-run/react';
import {ProductPrice} from './ProductPrice';
import {useAside} from './Aside';
import type {CartApiQueryFragment} from 'storefrontapi.generated';

type CartLine = OptimisticCartLine<CartApiQueryFragment>;

/**
* A single line item in the cart. It displays the product image, title, price.
Expand All @@ -15,7 +18,7 @@ export function CartLineItem({
line,
}: {
layout: CartLayout;
line: OptimisticCartLine;
line: CartLine;
}) {
const {id, merchandise} = line;
const {product, title, image, selectedOptions} = merchandise;
Expand Down Expand Up @@ -70,7 +73,7 @@ export function CartLineItem({
* These controls are disabled when the line item is new, and the server
* hasn't yet responded that it was successfully added to the cart.
*/
function CartLineQuantity({line}: {line: OptimisticCartLine}) {
function CartLineQuantity({line}: {line: CartLine}) {
if (!line || typeof line?.quantity === 'undefined') return null;
const {id: lineId, quantity, isOptimistic} = line;
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
Expand Down
Loading

0 comments on commit dfb9be7

Please sign in to comment.