-
-
-
-
{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}
-
+
+ {props.children}
+
+
>
);
}
diff --git a/src/app/(main)/products/[slug]/page.tsx b/src/app/(main)/products/[slug]/page.tsx
index ade2c9ecc..f2c823356 100644
--- a/src/app/(main)/products/[slug]/page.tsx
+++ b/src/app/(main)/products/[slug]/page.tsx
@@ -4,6 +4,7 @@ import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { type Metadata } from "next";
import xss from "xss";
+import invariant from "ts-invariant";
import { AddButton } from "./AddButton";
import { VariantSelector } from "@/ui/components/VariantSelector";
import { ProductImageWrapper } from "@/ui/atoms/ProductImageWrapper";
@@ -85,44 +86,29 @@ export default async function Page(props: { params: { slug: string }; searchPara
async function addItem() {
"use server";
- let checkoutId = cookies().get("checkoutId")?.value;
+ const checkout = await Checkout.findOrCreate(cookies().get("checkoutId")?.value);
+ invariant(checkout, "This should never happen");
- if (!checkoutId) {
- const { checkoutCreate } = await Checkout.create();
+ cookies().set("checkoutId", checkout.id, {
+ secure: shouldUseHttps,
+ sameSite: "lax",
+ httpOnly: true,
+ });
- if (checkoutCreate && checkoutCreate?.checkout?.id) {
- cookies().set("checkoutId", checkoutCreate.checkout?.id, {
- secure: shouldUseHttps,
- sameSite: "lax",
- httpOnly: true,
- });
-
- checkoutId = checkoutCreate.checkout.id;
- }
+ if (!selectedVariantID) {
+ return;
}
- checkoutId = cookies().get("checkoutId")?.value;
-
- if (checkoutId && selectedVariantID) {
- const checkout = await Checkout.find(checkoutId);
-
- if (!checkout) {
- cookies().delete("checkoutId");
- }
+ // TODO: error handling
+ await executeGraphQL(CheckoutAddLineDocument, {
+ variables: {
+ id: checkout.id,
+ productVariantId: decodeURIComponent(selectedVariantID),
+ },
+ cache: "no-cache",
+ });
- // TODO: error handling
- await executeGraphQL(CheckoutAddLineDocument, {
- variables: {
- id: checkoutId,
- productVariantId: decodeURIComponent(selectedVariantID),
- },
- cache: "no-cache",
- });
-
- revalidatePath("/cart");
- } else {
- throw new Error("Cart not found");
- }
+ revalidatePath("/cart");
}
const isAvailable = variants?.some((variant) => variant.quantityAvailable) ?? false;
diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx
index 1b6ba09b5..ebb4fee08 100644
--- a/src/app/checkout/page.tsx
+++ b/src/app/checkout/page.tsx
@@ -5,10 +5,14 @@ export const metadata = {
title: "Shopping Cart · Saleor Storefront example",
};
-export default function CheckoutPage({ searchParams }: { searchParams: { checkout?: string } }) {
+export default function CheckoutPage({
+ searchParams,
+}: {
+ searchParams: { checkout?: string; order?: string };
+}) {
invariant(process.env.NEXT_PUBLIC_SALEOR_API_URL, "Missing NEXT_PUBLIC_SALEOR_API_URL env variable");
- if (!searchParams.checkout) {
+ if (!searchParams.checkout && !searchParams.order) {
return null;
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f5308c03d..e40956302 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -17,8 +17,8 @@ export const metadata: Metadata = {
export default function RootLayout(props: { children: ReactNode }) {
return (
-
-
+
+
{props.children}
diff --git a/src/checkout/components/AddressForm/AddressForm.tsx b/src/checkout/components/AddressForm/AddressForm.tsx
index 0fc05bdfe..6272df578 100644
--- a/src/checkout/components/AddressForm/AddressForm.tsx
+++ b/src/checkout/components/AddressForm/AddressForm.tsx
@@ -32,10 +32,15 @@ export const AddressForm: FC
> = ({
const isValidPhoneNumber = usePhoneNumberValidator(values.countryCode);
const previousValues = useRef(values);
- const { orderedAddressFields, getFieldLabel, isRequiredField, countryAreaChoices, allowedFields } =
- useAddressFormUtils(values.countryCode);
+ const {
+ orderedAddressFields,
+ getFieldLabel,
+ isRequiredField,
+ countryAreaChoices,
+ allowedFields = [],
+ } = useAddressFormUtils(values.countryCode);
- const allowedFieldsRef = useRef(allowedFields || []);
+ const allowedFieldsRef = useRef(allowedFields);
const customValidators: Partial> = {
phone: isValidPhoneNumber,
diff --git a/src/checkout/components/AddressForm/useAddressFormSchema.ts b/src/checkout/components/AddressForm/useAddressFormSchema.ts
index 6dc5fa90e..eec077dbf 100644
--- a/src/checkout/components/AddressForm/useAddressFormSchema.ts
+++ b/src/checkout/components/AddressForm/useAddressFormSchema.ts
@@ -23,7 +23,7 @@ export const useAddressFormSchema = (initialCountryCode?: CountryCode) => {
const validationSchema = useMemo(
() =>
- allowedFields.reduce(
+ allowedFields?.reduce(
(schema, field) => schema.concat(object().shape({ [field]: getFieldValidator(field) })),
object().shape({}),
),
diff --git a/src/checkout/components/AddressForm/useAddressFormUtils.ts b/src/checkout/components/AddressForm/useAddressFormUtils.ts
index d430eaad6..d674d65f2 100644
--- a/src/checkout/components/AddressForm/useAddressFormUtils.ts
+++ b/src/checkout/components/AddressForm/useAddressFormUtils.ts
@@ -118,6 +118,6 @@ export const useAddressFormUtils = (countryCode: CountryCode = defaultCountry) =
hasAllRequiredFields,
getMissingFieldsFromAddress,
...validationRules,
- allowedFields: validationRules?.allowedFields as AddressField[],
+ allowedFields: validationRules?.allowedFields as AddressField[] | undefined,
};
};
diff --git a/src/checkout/hooks/useAlerts/useAlerts.tsx b/src/checkout/hooks/useAlerts/useAlerts.tsx
index 9d1fcaceb..421860d40 100644
--- a/src/checkout/hooks/useAlerts/useAlerts.tsx
+++ b/src/checkout/hooks/useAlerts/useAlerts.tsx
@@ -12,7 +12,7 @@ import {
import { type ErrorCode } from "@/checkout/lib/globalTypes";
import { type ApiErrors } from "@/checkout/hooks/useGetParsedErrors/types";
import { useGetParsedErrors } from "@/checkout/hooks/useGetParsedErrors";
-import { apiErrorMessages } from "@/checkout/sections/PaymentSection/AdyenDropIn/errorMessages";
+import { apiErrorMessages } from "@/checkout/sections/PaymentSection/errorMessages";
function useAlerts(scope: CheckoutScope): {
showErrors: (errors: ApiErrors) => void;
diff --git a/src/checkout/hooks/useCheckout.ts b/src/checkout/hooks/useCheckout.ts
index c738b531f..0544c561f 100644
--- a/src/checkout/hooks/useCheckout.ts
+++ b/src/checkout/hooks/useCheckout.ts
@@ -8,15 +8,15 @@ export const useCheckout = ({ pause = false } = {}) => {
const id = useMemo(() => extractCheckoutIdFromUrl(), []);
const { setLoadingCheckout } = useCheckoutUpdateStateActions();
- const [{ data, fetching: loading, stale }, refetch] = useCheckoutQuery({
+ const [{ data, fetching, stale }, refetch] = useCheckoutQuery({
variables: { id, languageCode: "EN_US" },
pause: pause,
});
- useEffect(() => setLoadingCheckout(loading || stale), [loading, setLoadingCheckout, stale]);
+ useEffect(() => setLoadingCheckout(fetching || stale), [fetching, setLoadingCheckout, stale]);
return useMemo(
- () => ({ checkout: data?.checkout as Checkout, loading: loading || stale, refetch }),
- [data?.checkout, loading, refetch, stale],
+ () => ({ checkout: data?.checkout as Checkout, fetching: fetching || stale, refetch }),
+ [data?.checkout, fetching, refetch, stale],
);
};
diff --git a/src/checkout/hooks/useCustomerAttach.ts b/src/checkout/hooks/useCustomerAttach.ts
index eec09bb3b..1a4f9be8f 100644
--- a/src/checkout/hooks/useCustomerAttach.ts
+++ b/src/checkout/hooks/useCustomerAttach.ts
@@ -5,17 +5,18 @@ import { useUser } from "@/checkout/hooks/useUser";
import { useCheckout } from "@/checkout/hooks/useCheckout";
export const useCustomerAttach = () => {
- const { checkout, loading, refetch } = useCheckout();
+ const { checkout, fetching: fetchingCheckout, refetch } = useCheckout();
const { authenticated } = useUser();
- const [{ fetching }, customerAttach] = useCheckoutCustomerAttachMutation();
+ const [{ fetching: fetchingCustomerAttach }, customerAttach] = useCheckoutCustomerAttachMutation();
const onSubmit = useSubmit<{}, typeof customerAttach>(
useMemo(
() => ({
hideAlerts: true,
scope: "checkoutCustomerAttach",
- shouldAbort: () => !!checkout?.user?.id || !authenticated || fetching || loading,
+ shouldAbort: () =>
+ !!checkout?.user?.id || !authenticated || fetchingCustomerAttach || fetchingCheckout,
onSubmit: customerAttach,
parse: ({ languageCode, checkoutId }) => ({ languageCode, checkoutId }),
onError: ({ errors }) => {
@@ -31,7 +32,7 @@ export const useCustomerAttach = () => {
}
},
}),
- [authenticated, checkout?.user?.id, customerAttach, fetching, loading, refetch],
+ [authenticated, checkout?.user?.id, customerAttach, fetchingCheckout, fetchingCustomerAttach, refetch],
),
);
diff --git a/src/checkout/hooks/useSubmit/utils.ts b/src/checkout/hooks/useSubmit/utils.ts
index e637de080..bb4dae8de 100644
--- a/src/checkout/hooks/useSubmit/utils.ts
+++ b/src/checkout/hooks/useSubmit/utils.ts
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { type CombinedError } from "urql";
+import { compact } from "lodash-es";
import { type FormDataBase } from "@/checkout/hooks/useForm";
import { type ApiErrors } from "@/checkout/hooks/useGetParsedErrors";
import {
@@ -75,7 +76,7 @@ export const extractMutationErrors = <
const customErrors = extractCustomErrors?.(result) || [];
- const allErrors = [...apiErrors, ...graphqlErrors, ...customErrors];
+ const allErrors = compact([...apiErrors, ...graphqlErrors, ...customErrors]);
return { hasErrors: allErrors.length > 0, apiErrors, graphqlErrors, customErrors };
};
diff --git a/src/checkout/sections/PaymentSection/AdyenDropIn/errorMessages.ts b/src/checkout/sections/PaymentSection/AdyenDropIn/errorMessages.ts
index b120cb509..e2ea332a6 100644
--- a/src/checkout/sections/PaymentSection/AdyenDropIn/errorMessages.ts
+++ b/src/checkout/sections/PaymentSection/AdyenDropIn/errorMessages.ts
@@ -46,37 +46,3 @@ export const adyenErrorMessages = {
"3DsAuthenticationError":
"The 3D Secure authentication failed due to an issue at the card network or issuer. Retry the transaction, or retry the transaction with a different payment method.",
};
-
-export const apiErrorMessages = {
- somethingWentWrong: "Sorry, something went wrong. Please try again in a moment.",
- requestPasswordResetEmailNotFoundError: "User with provided email has not been found",
- requestPasswordResetEmailInactiveError: "User account with provided email is inactive",
- checkoutShippingUpdateCountryAreaRequiredError: "Please select country area for shipping address",
- checkoutBillingUpdateCountryAreaRequiredError: "Please select country area for billing address",
- checkoutFinalizePasswordRequiredError: "Please set user password before finalizing checkout",
- checkoutEmailUpdateEmailInvalidError: "Provided email is invalid",
- checkoutAddPromoCodePromoCodeInvalidError: "Invalid promo code provided",
- userAddressUpdatePostalCodeInvalidError: "Invalid postal code provided to address form",
- userAddressCreatePostalCodeInvalidError: "Invalid postal code provided to address form",
- userRegisterPasswordPasswordTooShortError: "Provided password is too short",
- checkoutPayShippingMethodNotSetError: "Please choose delivery method before finalizing checkout",
- checkoutEmailUpdateEmailRequiredError: "Email cannot be empty",
- checkoutPayTotalAmountMismatchError: "Couldn't finalize checkout, please try again",
- checkoutPayEmailNotSetError: "Please fill in email before finalizing checkout",
- userRegisterEmailUniqueError: "Cannot create account with email that is already used",
- loginEmailInactiveError: "Account with provided email is inactive",
- loginEmailNotFoundError: "Account with provided email was not found",
- loginEmailAccountNotConfirmedError: "Account hasn't been confirmed",
- resetPasswordPasswordPasswordTooShortError: "Provided password is too short",
- resetPasswordTokenInvalidError: "Provided reset password token is expired or invalid",
- checkoutLinesUpdateQuantityQuantityGreaterThanLimitError:
- "Couldn't update line - buy limit for this item exceeded",
- checkoutLinesUpdateQuantityInsufficientStockError: "Couldn't update line - insufficient stock in warehouse",
- signInEmailInvalidCredentialsError: "Invalid credentials provided to login",
- signInEmailInactiveError: "The account you're trying to sign in to is inactive",
- checkoutShippingUpdatePostalCodeInvalidError: "Invalid postal code was provided for shipping address",
- checkoutShippingUpdatePhoneInvalidError: "Invalid phone number was provided for shipping address",
- checkoutBillingUpdatePostalCodeInvalidError: "Invalid postal code was provided for billing address",
- checkoutDeliveryMethodUpdatePostalCodeInvalidError: "Invalid postal code was provided for shipping address",
- checkoutDeliveryMethodUpdatePromoCodeInvalidError: "Please provide a valid discount code.",
-};
diff --git a/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts b/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts
index 13c61f0b0..f0cdaff53 100644
--- a/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts
+++ b/src/checkout/sections/PaymentSection/AdyenDropIn/useAdyenDropin.ts
@@ -3,6 +3,7 @@
import type DropinElement from "@adyen/adyen-web/dist/types/components/Dropin";
import { useCallback, useEffect, useMemo, useState } from "react";
import { camelCase } from "lodash-es";
+import { apiErrorMessages } from "../errorMessages";
import {
type TransactionInitializeMutationVariables,
type TransactionProcessMutationVariables,
@@ -35,10 +36,7 @@ import {
} from "@/checkout/state/updateStateStore";
import { useCheckoutComplete } from "@/checkout/hooks/useCheckoutComplete";
import { useErrorMessages } from "@/checkout/hooks/useErrorMessages";
-import {
- adyenErrorMessages,
- apiErrorMessages,
-} from "@/checkout/sections/PaymentSection/AdyenDropIn/errorMessages";
+import { adyenErrorMessages } from "@/checkout/sections/PaymentSection/AdyenDropIn/errorMessages";
import { type MightNotExist } from "@/checkout/lib/globalTypes";
import { useUser } from "@/checkout/hooks/useUser";
import { getUrlForTransactionInitialize } from "@/checkout/sections/PaymentSection/utils";
@@ -70,7 +68,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 +169,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 +211,7 @@ export const useAdyenDropin = (props: AdyenDropinProps) => {
handlePaymentResult,
setIsProcessingPayment,
showCustomErrors,
- transactionProccess,
+ transactionProcess,
],
),
);
@@ -310,7 +308,7 @@ export const useAdyenDropin = (props: AdyenDropinProps) => {
id: transaction,
data: { details: { redirectResult: decodedRedirectData } },
});
- }, []);
+ }, [onTransactionProccess]);
return { onSubmit: onSubmitInitialize, onAdditionalDetails };
};
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 ;
+ 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..c6c3a7c43
--- /dev/null
+++ b/src/checkout/sections/PaymentSection/StripeElements/stripeComponent.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { loadStripe } from "@stripe/stripe-js";
+import { Elements } from "@stripe/react-stripe-js";
+import { useEffect, useMemo } from "react";
+import { apiErrorMessages } from "../errorMessages";
+import { CheckoutForm } from "./stripeElementsForm";
+import { stripeGatewayId } from "./types";
+import { useTransactionInitializeMutation } from "@/checkout/graphql";
+import { useAlerts } from "@/checkout/hooks/useAlerts";
+import { useErrorMessages } from "@/checkout/hooks/useErrorMessages";
+import { useCheckout } from "@/checkout/hooks/useCheckout";
+
+export const StripeComponent = () => {
+ const { checkout } = useCheckout();
+
+ const [transactionInitializeResult, transactionInitialize] = useTransactionInitializeMutation();
+ const stripeData = transactionInitializeResult.data?.transactionInitialize?.data as
+ | undefined
+ | {
+ paymentIntent: {
+ client_secret: string;
+ };
+ publishableKey: string;
+ };
+
+ const { showCustomErrors } = useAlerts();
+ const { errorMessages: commonErrorMessages } = useErrorMessages(apiErrorMessages);
+
+ useEffect(() => {
+ transactionInitialize({
+ checkoutId: checkout.id,
+ paymentGateway: {
+ id: stripeGatewayId,
+ data: {
+ automatic_payment_methods: {
+ enabled: true,
+ },
+ },
+ },
+ }).catch((err) => {
+ console.error(err);
+ showCustomErrors([{ message: commonErrorMessages.somethingWentWrong }]);
+ });
+ }, [checkout.id, commonErrorMessages.somethingWentWrong, showCustomErrors, transactionInitialize]);
+
+ const stripePromise = useMemo(
+ () => stripeData?.publishableKey && loadStripe(stripeData.publishableKey),
+ [stripeData],
+ );
+
+ if (!stripePromise || !stripeData) {
+ return null;
+ }
+
+ 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..b7895e020
--- /dev/null
+++ b/src/checkout/sections/PaymentSection/StripeElements/stripeElementsForm.tsx
@@ -0,0 +1,191 @@
+import { type FormEventHandler, useEffect, useState } from "react";
+import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";
+import { type StripePaymentElementOptions } from "@stripe/stripe-js";
+import { getUrlForTransactionInitialize } from "../utils";
+import {
+ useCheckoutValidationActions,
+ useCheckoutValidationState,
+ anyFormsValidating,
+ areAllFormsValid,
+} from "@/checkout/state/checkoutValidationStateStore";
+import {
+ useCheckoutUpdateState,
+ useCheckoutUpdateStateActions,
+ areAnyRequestsInProgress,
+ hasFinishedApiChangesWithNoError,
+} from "@/checkout/state/updateStateStore";
+import { useEvent } from "@/checkout/hooks/useEvent";
+import { useUser } from "@/checkout/hooks/useUser";
+import { useAlerts } from "@/checkout/hooks/useAlerts";
+import { useCheckout } from "@/checkout/hooks/useCheckout";
+
+const paymentElementOptions: StripePaymentElementOptions = {
+ layout: "tabs",
+ fields: {
+ billingDetails: "never",
+ },
+};
+
+export function CheckoutForm() {
+ const [isLoading, setIsLoading] = useState(false);
+ const stripe = useStripe();
+ const elements = useElements();
+ const { checkout } = useCheckout();
+
+ const { authenticated } = useUser();
+ const { showCustomErrors } = useAlerts();
+
+ const checkoutUpdateState = useCheckoutUpdateState();
+ const anyRequestsInProgress = areAnyRequestsInProgress(checkoutUpdateState);
+ const finishedApiChangesWithNoError = hasFinishedApiChangesWithNoError(checkoutUpdateState);
+ const { setSubmitInProgress, setShouldRegisterUser } = useCheckoutUpdateStateActions();
+ const { validateAllForms } = useCheckoutValidationActions();
+ const { validationState } = useCheckoutValidationState();
+
+ // handler for when user presses submit
+ const onSubmitInitialize: FormEventHandler = useEvent(async (e) => {
+ e.preventDefault();
+
+ setIsLoading(true);
+ validateAllForms(authenticated);
+ setShouldRegisterUser(true);
+ setSubmitInProgress(true);
+ });
+
+ // when submission is initialized, awaits for all the other requests to finish,
+ // forms to validate, then either does transaction initialize or process
+ useEffect(() => {
+ const validating = anyFormsValidating(validationState);
+ const allFormsValid = areAllFormsValid(validationState);
+
+ if (!checkoutUpdateState.submitInProgress || validating || anyRequestsInProgress) {
+ return;
+ }
+ if (!stripe || !elements) {
+ // Stripe.js hasn't yet loaded.
+ // Make sure to disable form submission until Stripe.js has loaded.
+ return;
+ }
+
+ // submit was finished - we can mark it as complete
+ setSubmitInProgress(false);
+
+ // there was en error either in some other request or form validation
+ // - stop the submission altogether
+ if (!finishedApiChangesWithNoError || !allFormsValid) {
+ setIsLoading(false);
+ return;
+ }
+
+ setIsLoading(true);
+
+ stripe
+ .confirmPayment({
+ elements,
+ confirmParams: {
+ return_url: getUrlForTransactionInitialize().newUrl,
+ payment_method_data: {
+ billing_details: {
+ name: checkout.billingAddress?.firstName + " " + checkout.billingAddress?.lastName,
+ email: checkout.email ?? "",
+ phone: checkout.billingAddress?.phone ?? "",
+ address: {
+ city: checkout.billingAddress?.city ?? "",
+ country: checkout.billingAddress?.country.code ?? "",
+ line1: checkout.billingAddress?.streetAddress1 ?? "",
+ line2: checkout.billingAddress?.streetAddress2 ?? "",
+ postal_code: checkout.billingAddress?.postalCode ?? "",
+ state: checkout.billingAddress?.countryArea ?? "",
+ },
+ },
+ },
+ },
+ })
+ .then(({ error }) => {
+ console.error(error);
+ // 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") {
+ showCustomErrors([{ message: error.message ?? "Something went wrong" }]);
+ } else {
+ showCustomErrors([{ message: "An unexpected error occurred." }]);
+ }
+
+ setIsLoading(false);
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+
+ // @todo
+ // there is a previous transaction going on, we want to process instead of initialize
+ // if (currentTransactionId) {
+ // void onTransactionProccess({
+ // data: adyenCheckoutSubmitParams?.state.data,
+ // id: currentTransactionId,
+ // });
+ // return;
+ // }
+ }, [
+ anyRequestsInProgress,
+ checkout.billingAddress?.city,
+ checkout.billingAddress?.country.code,
+ checkout.billingAddress?.countryArea,
+ checkout.billingAddress?.firstName,
+ checkout.billingAddress?.lastName,
+ checkout.billingAddress?.phone,
+ checkout.billingAddress?.postalCode,
+ checkout.billingAddress?.streetAddress1,
+ checkout.billingAddress?.streetAddress2,
+ checkout.email,
+ checkoutUpdateState.submitInProgress,
+ elements,
+ finishedApiChangesWithNoError,
+ setSubmitInProgress,
+ showCustomErrors,
+ stripe,
+ validationState,
+ ]);
+
+ return (
+
+ );
+}
+
+function Loader() {
+ return (
+
+ );
+}
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/errorMessages.ts b/src/checkout/sections/PaymentSection/errorMessages.ts
new file mode 100644
index 000000000..3e68de79b
--- /dev/null
+++ b/src/checkout/sections/PaymentSection/errorMessages.ts
@@ -0,0 +1,33 @@
+export const apiErrorMessages = {
+ somethingWentWrong: "Sorry, something went wrong. Please try again in a moment.",
+ requestPasswordResetEmailNotFoundError: "User with provided email has not been found",
+ requestPasswordResetEmailInactiveError: "User account with provided email is inactive",
+ checkoutShippingUpdateCountryAreaRequiredError: "Please select country area for shipping address",
+ checkoutBillingUpdateCountryAreaRequiredError: "Please select country area for billing address",
+ checkoutFinalizePasswordRequiredError: "Please set user password before finalizing checkout",
+ checkoutEmailUpdateEmailInvalidError: "Provided email is invalid",
+ checkoutAddPromoCodePromoCodeInvalidError: "Invalid promo code provided",
+ userAddressUpdatePostalCodeInvalidError: "Invalid postal code provided to address form",
+ userAddressCreatePostalCodeInvalidError: "Invalid postal code provided to address form",
+ userRegisterPasswordPasswordTooShortError: "Provided password is too short",
+ checkoutPayShippingMethodNotSetError: "Please choose delivery method before finalizing checkout",
+ checkoutEmailUpdateEmailRequiredError: "Email cannot be empty",
+ checkoutPayTotalAmountMismatchError: "Couldn't finalize checkout, please try again",
+ checkoutPayEmailNotSetError: "Please fill in email before finalizing checkout",
+ userRegisterEmailUniqueError: "Cannot create account with email that is already used",
+ loginEmailInactiveError: "Account with provided email is inactive",
+ loginEmailNotFoundError: "Account with provided email was not found",
+ loginEmailAccountNotConfirmedError: "Account hasn't been confirmed",
+ resetPasswordPasswordPasswordTooShortError: "Provided password is too short",
+ resetPasswordTokenInvalidError: "Provided reset password token is expired or invalid",
+ checkoutLinesUpdateQuantityQuantityGreaterThanLimitError:
+ "Couldn't update line - buy limit for this item exceeded",
+ checkoutLinesUpdateQuantityInsufficientStockError: "Couldn't update line - insufficient stock in warehouse",
+ signInEmailInvalidCredentialsError: "Invalid credentials provided to login",
+ signInEmailInactiveError: "The account you're trying to sign in to is inactive",
+ checkoutShippingUpdatePostalCodeInvalidError: "Invalid postal code was provided for shipping address",
+ checkoutShippingUpdatePhoneInvalidError: "Invalid phone number was provided for shipping address",
+ checkoutBillingUpdatePostalCodeInvalidError: "Invalid postal code was provided for billing address",
+ checkoutDeliveryMethodUpdatePostalCodeInvalidError: "Invalid postal code was provided for shipping address",
+ checkoutDeliveryMethodUpdatePromoCodeInvalidError: "Please provide a valid discount code.",
+};
diff --git a/src/checkout/sections/PaymentSection/supportedPaymentApps.ts b/src/checkout/sections/PaymentSection/supportedPaymentApps.ts
new file mode 100644
index 000000000..545c982f3
--- /dev/null
+++ b/src/checkout/sections/PaymentSection/supportedPaymentApps.ts
@@ -0,0 +1,9 @@
+import { AdyenDropIn } from "./AdyenDropIn/AdyenDropIn";
+import { adyenGatewayId } from "./AdyenDropIn/types";
+import { StripeComponent } from "./StripeElements/stripeComponent";
+import { stripeGatewayId } from "./StripeElements/types";
+
+export const paymentMethodToComponent = {
+ [adyenGatewayId]: AdyenDropIn,
+ [stripeGatewayId]: StripeComponent,
+};
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;
}
diff --git a/src/checkout/state/checkoutValidationStateStore/checkoutValidationStateStore.ts b/src/checkout/state/checkoutValidationStateStore/checkoutValidationStateStore.ts
index 4a15c15d1..8220bfc46 100644
--- a/src/checkout/state/checkoutValidationStateStore/checkoutValidationStateStore.ts
+++ b/src/checkout/state/checkoutValidationStateStore/checkoutValidationStateStore.ts
@@ -1,4 +1,4 @@
-import { create } from "zustand";
+import { createWithEqualityFn } from "zustand/traditional";
import { shallow } from "zustand/shallow";
@@ -18,35 +18,35 @@ interface UseCheckoutValidationStateStore extends CheckoutValidationState {
};
}
-const useCheckoutValidationStateStore = create((set) => ({
- validationState: { shippingAddress: "valid", guestUser: "valid", billingAddress: "valid" },
- actions: {
- validateAllForms: (signedIn: boolean) =>
- set((state) => {
- const keysToValidate = Object.keys(state.validationState).filter(
- (val) => !signedIn || val !== "guestUser",
- ) as CheckoutFormScope[];
-
- return {
- validationState: keysToValidate.reduce(
- (result, key) => ({ ...result, [key]: "validating" }),
- {} as ValidationState,
- ),
- };
- }),
- setValidationState: (scope: CheckoutFormScope, status: CheckoutFormValidationStatus) =>
- set((state) => ({
- validationState: { ...state.validationState, [scope]: status },
- })),
- },
-}));
+const useCheckoutValidationStateStore = createWithEqualityFn(
+ (set) => ({
+ validationState: { shippingAddress: "valid", guestUser: "valid", billingAddress: "valid" },
+ actions: {
+ validateAllForms: (signedIn: boolean) =>
+ set((state) => {
+ const keysToValidate = Object.keys(state.validationState).filter(
+ (val) => !signedIn || val !== "guestUser",
+ ) as CheckoutFormScope[];
+
+ return {
+ validationState: keysToValidate.reduce(
+ (result, key) => ({ ...result, [key]: "validating" }),
+ {} as ValidationState,
+ ),
+ };
+ }),
+ setValidationState: (scope: CheckoutFormScope, status: CheckoutFormValidationStatus) =>
+ set((state) => ({
+ validationState: { ...state.validationState, [scope]: status },
+ })),
+ },
+ }),
+ shallow,
+);
export const useCheckoutValidationActions = () => useCheckoutValidationStateStore((state) => state.actions);
export const useCheckoutValidationState = (): CheckoutValidationState =>
- useCheckoutValidationStateStore(
- ({ validationState }) => ({
- validationState,
- }),
- shallow,
- );
+ useCheckoutValidationStateStore(({ validationState }) => ({
+ validationState,
+ }));
diff --git a/src/checkout/state/updateStateStore/updateStateStore.ts b/src/checkout/state/updateStateStore/updateStateStore.ts
index 2841312ac..a9f8abf61 100644
--- a/src/checkout/state/updateStateStore/updateStateStore.ts
+++ b/src/checkout/state/updateStateStore/updateStateStore.ts
@@ -1,7 +1,7 @@
-import { create } from "zustand";
import { shallow } from "zustand/shallow";
import { useMemo } from "react";
import { memoize, omit } from "lodash-es";
+import { createWithEqualityFn } from "zustand/traditional";
import { type CheckoutScope } from "@/checkout/hooks/useAlerts";
export type CheckoutUpdateStateStatus = "success" | "loading" | "error";
@@ -26,52 +26,54 @@ export interface CheckoutUpdateStateStore extends CheckoutUpdateState {
};
}
-const useCheckoutUpdateStateStore = create((set) => ({
- shouldRegisterUser: false,
- submitInProgress: false,
- loadingCheckout: false,
- changingBillingCountry: false,
- updateState: {
- paymentGatewaysInitialize: "success",
- checkoutShippingUpdate: "success",
- checkoutCustomerAttach: "success",
- checkoutBillingUpdate: "success",
- checkoutAddPromoCode: "success",
- checkoutDeliveryMethodUpdate: "success",
- checkoutLinesUpdate: "success",
- checkoutEmailUpdate: "success",
- userRegister: "success",
- resetPassword: "success",
- signIn: "success",
- requestPasswordReset: "success",
- checkoutLinesDelete: "success",
- userAddressCreate: "success",
- userAddressDelete: "success",
- userAddressUpdate: "success",
- },
- actions: {
- setSubmitInProgress: (submitInProgress: boolean) => set({ submitInProgress }),
- setShouldRegisterUser: (shouldRegisterUser: boolean) =>
- set({
- shouldRegisterUser,
- }),
- setLoadingCheckout: (loading: boolean) => set({ loadingCheckout: loading }),
- setChangingBillingCountry: (changingBillingCountry: boolean) => set({ changingBillingCountry }),
- setUpdateState: memoize(
- (scope) => (status) =>
- set((state) => ({
- updateState: {
- ...state.updateState,
- [scope]: status,
- },
- // checkout will reload right after, this ensures there
- // are no rerenders in between where there's no state updating
- // also we might not need this once we get better caching
- loadingCheckout: status === "success" || state.loadingCheckout,
- })),
- ),
- },
-}));
+const useCheckoutUpdateStateStore = createWithEqualityFn(
+ (set) => ({
+ shouldRegisterUser: false,
+ submitInProgress: false,
+ loadingCheckout: false,
+ changingBillingCountry: false,
+ updateState: {
+ paymentGatewaysInitialize: "success",
+ checkoutShippingUpdate: "success",
+ checkoutCustomerAttach: "success",
+ checkoutBillingUpdate: "success",
+ checkoutAddPromoCode: "success",
+ checkoutDeliveryMethodUpdate: "success",
+ checkoutLinesUpdate: "success",
+ checkoutEmailUpdate: "success",
+ userRegister: "success",
+ resetPassword: "success",
+ signIn: "success",
+ requestPasswordReset: "success",
+ checkoutLinesDelete: "success",
+ userAddressCreate: "success",
+ userAddressDelete: "success",
+ userAddressUpdate: "success",
+ },
+ actions: {
+ setSubmitInProgress: (submitInProgress: boolean) => set({ submitInProgress }),
+ setShouldRegisterUser: (shouldRegisterUser: boolean) =>
+ set({
+ shouldRegisterUser,
+ }),
+ setLoadingCheckout: (loading: boolean) => set({ loadingCheckout: loading }),
+ setChangingBillingCountry: (changingBillingCountry: boolean) => set({ changingBillingCountry }),
+ setUpdateState: memoize(
+ (scope) => (status) =>
+ set((state) => {
+ return {
+ updateState: {
+ ...state.updateState,
+ [scope]: status,
+ },
+ };
+ }),
+ ),
+ },
+ }),
+ shallow,
+);
+useCheckoutUpdateStateStore.subscribe(console.log);
export const useCheckoutUpdateState = (): CheckoutUpdateState => {
const { updateState, loadingCheckout, submitInProgress, changingBillingCountry } =
@@ -82,7 +84,6 @@ export const useCheckoutUpdateState = (): CheckoutUpdateState => {
loadingCheckout,
submitInProgress,
}),
- shallow,
);
return { updateState, loadingCheckout, submitInProgress, changingBillingCountry };
diff --git a/src/checkout/views/Checkout/Checkout.tsx b/src/checkout/views/Checkout/Checkout.tsx
index 58efbdf17..1e4fe8072 100644
--- a/src/checkout/views/Checkout/Checkout.tsx
+++ b/src/checkout/views/Checkout/Checkout.tsx
@@ -11,10 +11,10 @@ import { CheckoutSkeleton } from "@/checkout/views/Checkout/CheckoutSkeleton";
import { PAGE_ID } from "@/checkout/views/Checkout/consts";
export const Checkout = () => {
- const { checkout, loading } = useCheckout();
+ const { checkout, fetching: fetchingCheckout } = useCheckout();
const { loading: isAuthenticating } = useUser();
- const isCheckoutInvalid = !loading && !checkout && !isAuthenticating;
+ const isCheckoutInvalid = !fetchingCheckout && !checkout && !isAuthenticating;
const isInitiallyAuthenticating = isAuthenticating && !checkout;
diff --git a/src/graphql/CheckoutCreate.graphql b/src/graphql/CheckoutCreate.graphql
index 2ea866dd2..4f7ad6bab 100644
--- a/src/graphql/CheckoutCreate.graphql
+++ b/src/graphql/CheckoutCreate.graphql
@@ -2,6 +2,47 @@ mutation CheckoutCreate {
checkoutCreate(input: { channel: "default-channel", lines: [] }) {
checkout {
id
+ email
+ lines {
+ id
+ quantity
+ totalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
+ variant {
+ product {
+ id
+ name
+ slug
+ thumbnail {
+ url
+ alt
+ }
+ category {
+ name
+ }
+ }
+ pricing {
+ price {
+ gross {
+ amount
+ currency
+ }
+ }
+ }
+ name
+ id
+ }
+ }
+ totalPrice {
+ gross {
+ amount
+ currency
+ }
+ }
}
errors {
field
diff --git a/src/lib/checkout.ts b/src/lib/checkout.ts
index 90a0af98b..8847e528c 100644
--- a/src/lib/checkout.ts
+++ b/src/lib/checkout.ts
@@ -2,16 +2,28 @@ import { CheckoutCreateDocument, CheckoutFindDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
export async function find(checkoutId: string) {
- const { checkout } = checkoutId
- ? await executeGraphQL(CheckoutFindDocument, {
- variables: {
- id: checkoutId,
- },
- cache: "no-cache",
- })
- : { checkout: null };
+ try {
+ const { checkout } = checkoutId
+ ? await executeGraphQL(CheckoutFindDocument, {
+ variables: {
+ id: checkoutId,
+ },
+ cache: "no-cache",
+ })
+ : { checkout: null };
- return checkout;
+ return checkout;
+ } catch {
+ // we ignore invalid ID or checkout not found
+ }
+}
+
+export async function findOrCreate(checkoutId?: string) {
+ if (!checkoutId) {
+ return (await create()).checkoutCreate?.checkout;
+ }
+ const checkout = await find(checkoutId);
+ return checkout || (await create()).checkoutCreate?.checkout;
}
export const create = () => executeGraphQL(CheckoutCreateDocument, { cache: "no-cache" });
diff --git a/src/ui/components/nav/components/CartNavItem.tsx b/src/ui/components/nav/components/CartNavItem.tsx
index 9fa021e7f..bc49278ee 100644
--- a/src/ui/components/nav/components/CartNavItem.tsx
+++ b/src/ui/components/nav/components/CartNavItem.tsx
@@ -5,8 +5,8 @@ import clsx from "clsx";
import * as Checkout from "@/lib/checkout";
export const CartNavItem = async () => {
- const checkoutId = cookies().get("checkoutId")?.value || "";
- const checkout = await Checkout.find(checkoutId);
+ const checkoutId = cookies().get("checkoutId")?.value;
+ const checkout = checkoutId ? await Checkout.find(checkoutId) : null;
const lineCount = checkout ? checkout.lines.reduce((result, line) => result + line.quantity, 0) : 0;