diff --git a/.changeset/chatty-bugs-invite.md b/.changeset/chatty-bugs-invite.md new file mode 100644 index 0000000000..590f7ac617 --- /dev/null +++ b/.changeset/chatty-bugs-invite.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli-hydrogen': patch +'@shopify/create-hydrogen': patch +--- + +Add magic cart and discount routes to skeleton template diff --git a/packages/cli/src/lib/setups/routes/generate.ts b/packages/cli/src/lib/setups/routes/generate.ts index 00ced633c7..7b1e522d0e 100644 --- a/packages/cli/src/lib/setups/routes/generate.ts +++ b/packages/cli/src/lib/setups/routes/generate.ts @@ -43,7 +43,7 @@ const NO_LOCALE_PATTERNS = [/robots\.txt/]; const ROUTE_MAP = { home: ['_index', '$'], page: 'pages*', - cart: 'cart', + cart: ['cart', 'cart.$lines', 'discount.$code'], products: 'products*', collections: 'collections*', policies: 'policies*', diff --git a/templates/skeleton/app/routes/cart.$lines.tsx b/templates/skeleton/app/routes/cart.$lines.tsx new file mode 100644 index 0000000000..045554953f --- /dev/null +++ b/templates/skeleton/app/routes/cart.$lines.tsx @@ -0,0 +1,70 @@ +import {redirect, type LoaderArgs} from '@shopify/remix-oxygen'; + +/** + * Automatically creates a new cart based on the URL and redirects straight to checkout. + * Expected URL structure: + * ```ts + * /cart/: + * + * ``` + * More than one `:` separated by a comma, can be supplied in the URL, for + * carts with more than one product variant. + * + * @param `?discount` an optional discount code to apply to the cart + * @example + * Example path creating a cart with two product variants, different quantities, and a discount code: + * ```ts + * /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD + * + * ``` + * @preserve + */ +export async function loader({request, context, params}: LoaderArgs) { + const {cart} = context; + const {lines} = params; + if (!lines) return redirect('/cart'); + const linesMap = lines.split(',').map((line) => { + const lineDetails = line.split(':'); + const variantId = lineDetails[0]; + const quantity = parseInt(lineDetails[1], 10); + + return { + merchandiseId: `gid://shopify/ProductVariant/${variantId}`, + quantity, + }; + }); + + const url = new URL(request.url); + const searchParams = new URLSearchParams(url.search); + + const discount = searchParams.get('discount'); + const discountArray = discount ? [discount] : []; + + // create a cart + const result = await cart.create({ + lines: linesMap, + discountCodes: discountArray, + }); + + const cartResult = result.cart; + + if (result.errors?.length || !cartResult) { + throw new Response('Link may be expired. Try checking the URL.', { + status: 410, + }); + } + + // Update cart id in cookie + const headers = cart.setCartId(cartResult.id); + + // redirect to checkout + if (cartResult.checkoutUrl) { + return redirect(cartResult.checkoutUrl, {headers}); + } else { + throw new Error('No checkout URL found'); + } +} + +export default function Component() { + return null; +} diff --git a/templates/skeleton/app/routes/discount.$code.tsx b/templates/skeleton/app/routes/discount.$code.tsx new file mode 100644 index 0000000000..9ae62d7cb2 --- /dev/null +++ b/templates/skeleton/app/routes/discount.$code.tsx @@ -0,0 +1,43 @@ +import {redirect, type LoaderArgs} from '@shopify/remix-oxygen'; + +/** + * Automatically applies a discount found on the url + * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied + * @param ?redirect an optional path to return to otherwise return to the home page + * @example + * Example path applying a discount and redirecting + * ```ts + * /discount/FREESHIPPING?redirect=/products + * + * ``` + * @preserve + */ +export async function loader({request, context, params}: LoaderArgs) { + const {cart} = context; + const {code} = params; + + const url = new URL(request.url); + const searchParams = new URLSearchParams(url.search); + const redirectParam = + searchParams.get('redirect') || searchParams.get('return_to') || '/'; + + searchParams.delete('redirect'); + searchParams.delete('return_to'); + + const redirectUrl = `${redirectParam}?${searchParams}`; + + if (!code) { + return redirect(redirectUrl); + } + + const result = await cart.updateDiscountCodes([code]); + const headers = cart.setCartId(result.cart.id); + + // Using set-cookie on a 303 redirect will not work if the domain origin have port number (:3000) + // If there is no cart id and a new cart id is created in the progress, it will not be set in the cookie + // on localhost:3000 + return redirect(redirectUrl, { + status: 303, + headers, + }); +}