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

Fix the OptimisticCart type to properly retain the generic of line items #2327

Merged
merged 4 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
blittle marked this conversation as resolved.
Show resolved Hide resolved
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[];
Comment on lines -68 to +70
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to change, because just using the OptimisticCartLine directly without a generic means the default API query is used.

}) {
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
}
}
#/***********************************************/
Comment on lines +25 to +32
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding sellingPlanAllocation here.

#/********** 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
Loading