From 05a4d746a54faa60b6ddc50ae558455799de0dc0 Mon Sep 17 00:00:00 2001 From: Michal Miszczyszyn Date: Wed, 25 Oct 2023 20:05:07 +0200 Subject: [PATCH 1/4] Improve test cases --- __tests__/STF_01.spec.ts | 9 +++++---- __tests__/STF_03.spec.ts | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/__tests__/STF_01.spec.ts b/__tests__/STF_01.spec.ts index 6f2649d5d..f7f0936fd 100644 --- a/__tests__/STF_01.spec.ts +++ b/__tests__/STF_01.spec.ts @@ -21,8 +21,9 @@ test("STF_01: Add items to the basket", async ({ page }) => { await openCart({ page }); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toHaveCount(1); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toContainText(product.name); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toContainText(`Qty: 1`); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toContainText(price.toFixed(2)); + const productInCart = page.getByTestId("CartProductList").getByRole("listitem"); + await expect(productInCart).toHaveCount(1); + await expect(productInCart).toContainText(product.name); + await expect(productInCart).toContainText(`Qty: 1`); + await expect(productInCart).toContainText(price.toFixed(2)); }); diff --git a/__tests__/STF_03.spec.ts b/__tests__/STF_03.spec.ts index 3d5a32a47..9b5113a7f 100644 --- a/__tests__/STF_03.spec.ts +++ b/__tests__/STF_03.spec.ts @@ -24,8 +24,9 @@ test("STF_03: Check if price are calculating correctly", async ({ page }) => { await openCart({ page }); const totalPrice = (price * 2).toFixed(2); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toHaveCount(1); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toContainText(product.name); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toContainText(`Qty: 2`); - await expect(page.getByTestId("CartProductList").getByRole("listitem")).toContainText(totalPrice); + const productInCart = page.getByTestId("CartProductList").getByRole("listitem"); + await expect(productInCart).toHaveCount(1); + await expect(productInCart).toContainText(product.name); + await expect(productInCart).toContainText(`Qty: 2`); + await expect(productInCart).toContainText(totalPrice); }); From 4e6d36f3a3a03d7f3dcd0d93e73479f879371643 Mon Sep 17 00:00:00 2001 From: Michal Miszczyszyn Date: Wed, 25 Oct 2023 20:52:20 +0200 Subject: [PATCH 2/4] Stripe integration wip --- package.json | 2 + pnpm-lock.yaml | 23 +++++++ .../AdyenDropIn/useAdyenDropin.ts | 8 +-- .../PaymentSection/PaymentMethods.tsx | 19 ++++-- .../StripeElements/StripePaymentComponent.tsx | 0 .../StripeElements/stripeComponent.tsx | 24 +++++++ .../StripeElements/stripeElementsForm.tsx | 63 +++++++++++++++++++ .../PaymentSection/StripeElements/types.ts | 2 + .../PaymentSection/supportedPaymentApps.ts | 9 +++ src/checkout/sections/PaymentSection/types.ts | 14 ++--- .../usePaymentGatewaysInitialize.ts | 11 ++-- .../sections/PaymentSection/usePayments.ts | 2 +- src/checkout/sections/PaymentSection/utils.ts | 46 +++----------- 13 files changed, 161 insertions(+), 62 deletions(-) create mode 100644 src/checkout/sections/PaymentSection/StripeElements/StripePaymentComponent.tsx create mode 100644 src/checkout/sections/PaymentSection/StripeElements/stripeComponent.tsx create mode 100644 src/checkout/sections/PaymentSection/StripeElements/stripeElementsForm.tsx create mode 100644 src/checkout/sections/PaymentSection/StripeElements/types.ts create mode 100644 src/checkout/sections/PaymentSection/supportedPaymentApps.ts diff --git a/package.json b/package.json index 28ca98a92..e4b368044 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@apollo/client": "3.8.5", "@headlessui/react": "1.7.17", "@saleor/auth-sdk": "0.13.1", + "@stripe/react-stripe-js": "2.3.1", + "@stripe/stripe-js": "2.1.10", "clsx": "2.0.0", "editorjs-html": "3.4.3", "formik": "2.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a3fd220b..050ee864b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ dependencies: '@saleor/auth-sdk': specifier: 0.13.1 version: 0.13.1(@apollo/client@3.8.5)(next@13.5.6)(react-dom@18.2.0)(react@18.2.0)(urql@4.0.5) + '@stripe/react-stripe-js': + specifier: 2.3.1 + version: 2.3.1(@stripe/stripe-js@2.1.10)(react-dom@18.2.0)(react@18.2.0) + '@stripe/stripe-js': + specifier: 2.1.10 + version: 2.1.10 clsx: specifier: 2.0.0 version: 2.0.0 @@ -1857,6 +1863,23 @@ packages: urql: 4.0.5(graphql@16.8.1)(react@18.2.0) dev: false + /@stripe/react-stripe-js@2.3.1(@stripe/stripe-js@2.1.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vXiwcG2ZjAF4AezjP7DJ8jiwxfCWCen/X2rBhyXaKrfQ7+pwmXhsoUlKRa0eLWioY1oelOQOafauNUiwTwFHgQ==} + peerDependencies: + '@stripe/stripe-js': ^1.44.1 || ^2.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@stripe/stripe-js': 2.1.10 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@stripe/stripe-js@2.1.10: + resolution: {integrity: sha512-h79zhwvxAJVAvtVjtMoz++DtwI7GdcEItmTC0P2gciZoFUeAO6XX9DL+UXm9uADiEaUvTKqrExYwtBTlMYAaPA==} + dev: false + /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: diff --git a/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts b/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts index 13c61f0b0..7cfdc8e44 100644 --- a/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts +++ b/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts @@ -70,7 +70,7 @@ export const useAdyenDropin = (props: AdyenDropinProps) => { getQueryParams().transaction, ); const [, transactionInitialize] = useTransactionInitializeMutation(); - const [, transactionProccess] = useTransactionProcessMutation(); + const [, transactionProcess] = useTransactionProcessMutation(); const { onCheckoutComplete } = useCheckoutComplete(); const [adyenCheckoutSubmitParams, setAdyenCheckoutSubmitParams] = useState<{ @@ -171,10 +171,10 @@ export const useAdyenDropin = (props: AdyenDropinProps) => { ), ); - const onTransactionProccess = useSubmit( + const onTransactionProccess = useSubmit( useMemo( () => ({ - onSubmit: transactionProccess, + onSubmit: transactionProcess, onError: () => { // will tell the processing screen to disappear setIsProcessingPayment(false); @@ -213,7 +213,7 @@ export const useAdyenDropin = (props: AdyenDropinProps) => { handlePaymentResult, setIsProcessingPayment, showCustomErrors, - transactionProccess, + transactionProcess, ], ), ); diff --git a/src/checkout/sections/PaymentSection/PaymentMethods.tsx b/src/checkout/sections/PaymentSection/PaymentMethods.tsx index 503cca483..61c71d7ef 100644 --- a/src/checkout/sections/PaymentSection/PaymentMethods.tsx +++ b/src/checkout/sections/PaymentSection/PaymentMethods.tsx @@ -1,4 +1,4 @@ -import { AdyenDropIn } from "@/checkout/sections/PaymentSection/AdyenDropIn/AdyenDropIn"; +import { paymentMethodToComponent } from "./supportedPaymentApps"; import { PaymentSectionSkeleton } from "@/checkout/sections/PaymentSection/PaymentSectionSkeleton"; import { usePayments } from "@/checkout/sections/PaymentSection/usePayments"; import { useCheckoutUpdateState } from "@/checkout/state/updateStateStore"; @@ -10,12 +10,23 @@ export const PaymentMethods = () => { updateState: { checkoutDeliveryMethodUpdate }, } = useCheckoutUpdateState(); - const { adyen } = availablePaymentGateways; - // delivery methods change total price so we want to wait until the change is done if (changingBillingCountry || fetching || checkoutDeliveryMethodUpdate === "loading") { return ; } - return
{adyen ? : null}
; + return ( +
+ {availablePaymentGateways.map((gateway) => { + const Component = paymentMethodToComponent[gateway.id]; + return ( + + ); + })} +
+ ); }; diff --git a/src/checkout/sections/PaymentSection/StripeElements/StripePaymentComponent.tsx b/src/checkout/sections/PaymentSection/StripeElements/StripePaymentComponent.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/checkout/sections/PaymentSection/StripeElements/stripeComponent.tsx b/src/checkout/sections/PaymentSection/StripeElements/stripeComponent.tsx new file mode 100644 index 000000000..41d5facc2 --- /dev/null +++ b/src/checkout/sections/PaymentSection/StripeElements/stripeComponent.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { loadStripe } from "@stripe/stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; +import { useMemo } from "react"; +import CheckoutForm from "./stripeElementsForm"; + +export const StripeComponent = ({ + clientSecret, + publishableKey, + returnUrl, +}: { + clientSecret: string; + publishableKey: string; + returnUrl: string; +}) => { + const stripePromise = useMemo(() => loadStripe(publishableKey), [publishableKey]); + + return ( + + + + ); +}; diff --git a/src/checkout/sections/PaymentSection/StripeElements/stripeElementsForm.tsx b/src/checkout/sections/PaymentSection/StripeElements/stripeElementsForm.tsx new file mode 100644 index 000000000..4eda60e5b --- /dev/null +++ b/src/checkout/sections/PaymentSection/StripeElements/stripeElementsForm.tsx @@ -0,0 +1,63 @@ +// Copied from https://stripe.com/docs/payments/quickstart +/* eslint-disable */ +// @ts-nocheck +import { useState } from "react"; +import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js"; + +export default function CheckoutForm({ returnUrl }: { returnUrl: string }) { + const stripe = useStripe(); + const elements = useElements(); + + const [message, setMessage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!stripe || !elements) { + // Stripe.js hasn't yet loaded. + // Make sure to disable form submission until Stripe.js has loaded. + return; + } + + setIsLoading(true); + + const { error } = await stripe.confirmPayment({ + elements, + confirmParams: { + // Make sure to change this to your payment completion page + return_url: returnUrl, + }, + }); + + // This point will only be reached if there is an immediate error when + // confirming the payment. Otherwise, your customer will be redirected to + // your `return_url`. For some payment methods like iDEAL, your customer will + // be redirected to an intermediate site first to authorize the payment, then + // redirected to the `return_url`. + if (error.type === "card_error" || error.type === "validation_error") { + setMessage(error.message); + } else { + setMessage("An unexpected error occurred."); + } + + setIsLoading(false); + }; + + const paymentElementOptions = { + layout: "tabs", + }; + + return ( +
+
+ + + {/* Show any error or success messages */} + {message &&
{message}
} + +
+ ); +} diff --git a/src/checkout/sections/PaymentSection/StripeElements/types.ts b/src/checkout/sections/PaymentSection/StripeElements/types.ts new file mode 100644 index 000000000..5ee00bd10 --- /dev/null +++ b/src/checkout/sections/PaymentSection/StripeElements/types.ts @@ -0,0 +1,2 @@ +export const stripeGatewayId = "app.saleor.stripe"; +export type StripeGatewayId = typeof stripeGatewayId; diff --git a/src/checkout/sections/PaymentSection/supportedPaymentApps.ts b/src/checkout/sections/PaymentSection/supportedPaymentApps.ts new file mode 100644 index 000000000..a462d6859 --- /dev/null +++ b/src/checkout/sections/PaymentSection/supportedPaymentApps.ts @@ -0,0 +1,9 @@ +import { AdyenDropIn } from "./AdyenDropIn/AdyenDropIn"; +import { adyenGatewayId } from "./AdyenDropIn/types"; +import { stripeGatewayId } from "./StripeElements/types"; +import { type ParsedStripeGateway } from "./types"; + +export const paymentMethodToComponent = { + [adyenGatewayId]: AdyenDropIn, + [stripeGatewayId]: ({}: { config: ParsedStripeGateway }) => null, +}; diff --git a/src/checkout/sections/PaymentSection/types.ts b/src/checkout/sections/PaymentSection/types.ts index 136eaf02c..51cba5810 100644 --- a/src/checkout/sections/PaymentSection/types.ts +++ b/src/checkout/sections/PaymentSection/types.ts @@ -1,21 +1,21 @@ +import { type StripeGatewayId } from "./StripeElements/types"; import { type PaymentGatewayConfig } from "@/checkout/graphql"; import { type AdyenGatewayId, type AdyenGatewayInitializePayload, } from "@/checkout/sections/PaymentSection/AdyenDropIn/types"; -export type PaymentGatewayId = AdyenGatewayId; +export type PaymentGatewayId = AdyenGatewayId | StripeGatewayId; -export type ParsedAdyenGateway = ParsedPaymentGateway; +export type ParsedAdyenGateway = ParsedPaymentGateway; +export type ParsedStripeGateway = ParsedPaymentGateway; -export type ParsedPaymentGateways = { - adyen?: ParsedAdyenGateway; -}; +export type ParsedPaymentGateways = ReadonlyArray; -export interface ParsedPaymentGateway> +export interface ParsedPaymentGateway> extends Omit { data: TData; - id: PaymentGatewayId; + id: ID; } export type PaymentStatus = "paidInFull" | "overpaid" | "none" | "authorized"; diff --git a/src/checkout/sections/PaymentSection/usePaymentGatewaysInitialize.ts b/src/checkout/sections/PaymentSection/usePaymentGatewaysInitialize.ts index f4b9cc557..8842f92bf 100644 --- a/src/checkout/sections/PaymentSection/usePaymentGatewaysInitialize.ts +++ b/src/checkout/sections/PaymentSection/usePaymentGatewaysInitialize.ts @@ -4,10 +4,7 @@ import { useCheckout } from "@/checkout/hooks/useCheckout"; import { useSubmit } from "@/checkout/hooks/useSubmit"; import { type MightNotExist } from "@/checkout/lib/globalTypes"; import { type ParsedPaymentGateways } from "@/checkout/sections/PaymentSection/types"; -import { - getFilteredPaymentGateways, - getParsedPaymentGatewayConfigs, -} from "@/checkout/sections/PaymentSection/utils"; +import { getFilteredPaymentGateways } from "@/checkout/sections/PaymentSection/utils"; export const usePaymentGatewaysInitialize = () => { const { @@ -19,7 +16,7 @@ export const usePaymentGatewaysInitialize = () => { const billingCountry = billingAddress?.country.code as MightNotExist; - const [gatewayConfigs, setGatewayConfigs] = useState({}); + const [gatewayConfigs, setGatewayConfigs] = useState([]); const previousBillingCountry = useRef(billingCountry); const [{ fetching }, paymentGatewaysInitialize] = usePaymentGatewaysInitializeMutation(); @@ -39,9 +36,9 @@ export const usePaymentGatewaysInitialize = () => { })), }), onSuccess: ({ data }) => { - const parsedConfigs = getParsedPaymentGatewayConfigs(data.gatewayConfigs); + const parsedConfigs = (data.gatewayConfigs || []) as ParsedPaymentGateways; - if (!Object.keys(parsedConfigs).length) { + if (!parsedConfigs.length) { throw new Error("No available payment gateways"); } diff --git a/src/checkout/sections/PaymentSection/usePayments.ts b/src/checkout/sections/PaymentSection/usePayments.ts index b86c8079b..9c61250b5 100644 --- a/src/checkout/sections/PaymentSection/usePayments.ts +++ b/src/checkout/sections/PaymentSection/usePayments.ts @@ -20,7 +20,7 @@ export const usePayments = () => { if (!completingCheckout && paidStatuses.includes(paymentStatus)) { void onCheckoutComplete(); } - }, []); + }, [completingCheckout, onCheckoutComplete, paymentStatus]); return { fetching, availablePaymentGateways }; }; diff --git a/src/checkout/sections/PaymentSection/utils.ts b/src/checkout/sections/PaymentSection/utils.ts index 9b4bfd233..0fab66b13 100644 --- a/src/checkout/sections/PaymentSection/utils.ts +++ b/src/checkout/sections/PaymentSection/utils.ts @@ -1,50 +1,18 @@ import { compact } from "lodash-es"; +import { adyenGatewayId } from "./AdyenDropIn/types"; +import { stripeGatewayId } from "./StripeElements/types"; import { type CheckoutAuthorizeStatusEnum, type CheckoutChargeStatusEnum, type OrderAuthorizeStatusEnum, type OrderChargeStatusEnum, type PaymentGateway, - type PaymentGatewayConfig, } from "@/checkout/graphql"; import { type MightNotExist } from "@/checkout/lib/globalTypes"; import { getUrl } from "@/checkout/lib/utils/url"; -import { adyenGatewayId } from "@/checkout/sections/PaymentSection/AdyenDropIn/types"; -import { - type ParsedPaymentGateways, - type PaymentGatewayId, - type PaymentStatus, -} from "@/checkout/sections/PaymentSection/types"; - -const paymentGatewayMap: Record = { - [adyenGatewayId]: "adyen", -}; - -export const getParsedPaymentGatewayConfigs = ( - gatewayConfigs: MightNotExist, -): ParsedPaymentGateways => { - if (!gatewayConfigs) { - return {}; - } +import { type PaymentStatus } from "@/checkout/sections/PaymentSection/types"; - return gatewayConfigs.reduce((result, gatewayConfig) => { - const hasError = !gatewayConfig?.data && !!gatewayConfig?.errors?.length; - - if (!gatewayConfig || hasError) { - return result; - } - - const { id, ...rest } = gatewayConfig; - - return { - ...result, - [paymentGatewayMap[id as PaymentGatewayId]]: { - id, - ...rest, - }, - }; - }, {}); -}; +export const supportedPaymentGateways = [adyenGatewayId, stripeGatewayId] as const; export const getFilteredPaymentGateways = ( paymentGateways: MightNotExist, @@ -55,13 +23,13 @@ export const getFilteredPaymentGateways = ( // we want to use only payment apps, not plugins return compact(paymentGateways).filter(({ id, name }) => { - const shouldBeIncluded = Object.keys(paymentGatewayMap).includes(id); - const isAPlugin = !id.startsWith("app.saleor."); + const shouldBeIncluded = supportedPaymentGateways.includes(id); + const isAPlugin = !id.startsWith("app."); // app is missing in our codebase but is an app and not a plugin // hence we'd like to have it handled by default if (!shouldBeIncluded && !isAPlugin) { - console.warn(`Unhandled payment gateway - name: ${name}, id: ${id as string}`); + console.warn(`Unhandled payment gateway - name: ${name}, id: ${id}`); return false; } From 15deea4754abd4d23b6a03662b538cad26e950d0 Mon Sep 17 00:00:00 2001 From: Michal Miszczyszyn Date: Thu, 26 Oct 2023 16:59:20 +0200 Subject: [PATCH 3/4] Stripe working --- src/app/(main)/cart/CheckoutLink.tsx | 5 +- src/app/(main)/cart/page.tsx | 106 +++++---- src/app/(main)/layout.tsx | 6 +- src/app/(main)/products/[slug]/page.tsx | 52 ++--- src/app/checkout/page.tsx | 8 +- src/app/layout.tsx | 4 +- .../AddressForm/useAddressFormSchema.ts | 2 +- .../AddressForm/useAddressFormUtils.ts | 2 +- src/checkout/hooks/useAlerts/useAlerts.tsx | 2 +- src/checkout/hooks/useCheckout.ts | 8 +- src/checkout/hooks/useSubmit/utils.ts | 3 +- .../AdyenDropIn/errorMessages.ts | 34 --- .../AdyenDropIn/useAdyenDropin.ts | 8 +- .../StripeElements/stripeComponent.tsx | 70 ++++-- .../StripeElements/stripeElementsForm.tsx | 208 ++++++++++++++---- .../sections/PaymentSection/errorMessages.ts | 33 +++ .../PaymentSection/supportedPaymentApps.ts | 4 +- .../checkoutValidationStateStore.ts | 58 ++--- .../updateStateStore/updateStateStore.ts | 97 ++++---- src/graphql/CheckoutCreate.graphql | 41 ++++ src/lib/checkout.ts | 30 ++- .../components/nav/components/CartNavItem.tsx | 4 +- 22 files changed, 506 insertions(+), 279 deletions(-) create mode 100644 src/checkout/sections/PaymentSection/errorMessages.ts diff --git a/src/app/(main)/cart/CheckoutLink.tsx b/src/app/(main)/cart/CheckoutLink.tsx index 3701257c8..b5ec40885 100644 --- a/src/app/(main)/cart/CheckoutLink.tsx +++ b/src/app/(main)/cart/CheckoutLink.tsx @@ -3,15 +3,16 @@ type Props = { disabled?: boolean; checkoutId?: string; + className?: string; }; -export const CheckoutLink = ({ disabled, checkoutId }: Props) => { +export const CheckoutLink = ({ disabled, checkoutId, className = "" }: Props) => { return ( disabled && e.preventDefault()} href={`/checkout?checkout=${checkoutId}`} - className="w-full rounded border border-transparent bg-neutral-900 px-6 py-3 text-center font-medium text-neutral-50 hover:bg-neutral-800 aria-disabled:cursor-not-allowed aria-disabled:bg-neutral-500 sm:col-start-2" + className={`inline-block max-w-full rounded border border-transparent bg-neutral-900 px-6 py-3 text-center font-medium text-neutral-50 hover:bg-neutral-800 aria-disabled:cursor-not-allowed aria-disabled:bg-neutral-500 sm:px-16 ${className}`} > Checkout diff --git a/src/app/(main)/cart/page.tsx b/src/app/(main)/cart/page.tsx index 4a03e8731..57ae3fb38 100644 --- a/src/app/(main)/cart/page.tsx +++ b/src/app/(main)/cart/page.tsx @@ -14,55 +14,70 @@ export default async function Page() { const checkoutId = cookies().get("checkoutId")?.value || ""; const checkout = await Checkout.find(checkoutId); - const lines = checkout ? checkout.lines : []; + + if (!checkout) { + return ( +
+

Your Shopping Cart is empty

+

+ Looks like you haven’t added any items to the cart yet. +

+ + Go back + +
+ ); + } return (

Your Shopping Cart

-
-
    - {lines.map((item) => ( -
  • -
    - {item.variant?.product?.thumbnail?.url && ( - {item.variant.product.thumbnail.alt - )} -
    -
    -
    -
    - -

    {item.variant?.product?.name}

    - -

    {item.variant?.product?.category?.name}

    - {item.variant.name !== item.variant.id && Boolean(item.variant.name) && ( -

    Variant: {item.variant.name}

    - )} -
    -

    - {formatMoney(item.totalPrice.gross.amount, item.totalPrice.gross.currency)} -

    -
    -
    -
    Qty: {item.quantity}
    - +
      + {checkout.lines.map((item) => ( +
    • +
      + {item.variant?.product?.thumbnail?.url && ( + {item.variant.product.thumbnail.alt + )} +
      +
      +
      +
      + +

      {item.variant?.product?.name}

      + +

      {item.variant?.product?.category?.name}

      + {item.variant.name !== item.variant.id && Boolean(item.variant.name) && ( +

      Variant: {item.variant.name}

      + )}
      +

      + {formatMoney(item.totalPrice.gross.amount, item.totalPrice.gross.currency)} +

      -
    • - ))} -
    -
    +
    +
    Qty: {item.quantity}
    + +
    +
    +
  • + ))} +
+
@@ -71,13 +86,12 @@ export default async function Page() {

Shipping will be calculated in the next step

- {checkout && - formatMoney(checkout.totalPrice.gross.amount, checkout.totalPrice.gross.currency)} + {formatMoney(checkout.totalPrice.gross.amount, checkout.totalPrice.gross.currency)}
-
- +
+
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 16cb0dbbb..84c0547c0 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -11,8 +11,10 @@ export default function RootLayout(props: { children: ReactNode }) { return ( <>
-
{props.children}
-