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

Stripe integration #990

Merged
merged 6 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 5 additions & 4 deletions __tests__/STF_01.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
9 changes: 5 additions & 4 deletions __tests__/STF_03.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"@adyen/adyen-web": "5.53.2",
"@adyen/api-library": "14.3.0",
"@headlessui/react": "1.7.17",
"@stripe/react-stripe-js": "2.3.1",
"@stripe/stripe-js": "2.1.10",
"@saleor/auth-sdk": "0.14.0",
"clsx": "2.0.0",
"editorjs-html": "3.4.3",
Expand Down
24 changes: 23 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/app/(main)/cart/CheckoutLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<a
aria-disabled={disabled}
onClick={(e) => 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
</a>
Expand Down
106 changes: 60 additions & 46 deletions src/app/(main)/cart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<section className="mx-auto max-w-7xl p-8">
<h1 className="mt-8 text-3xl font-bold text-neutral-900">Your Shopping Cart is empty</h1>
<p className="my-12 text-sm text-neutral-500">
Looks like you haven’t added any items to the cart yet.
</p>
<Link
href={"/"}
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"
>
Go back
</Link>
</section>
);
}

return (
<section className="mx-auto max-w-7xl p-8">
<h1 className="mt-8 text-3xl font-bold text-neutral-900">Your Shopping Cart</h1>
<form className="mt-12">
<div>
<ul
data-testid="CartProductList"
role="list"
className="divide-y divide-neutral-200 border-b border-t border-neutral-200"
>
{lines.map((item) => (
<li key={item.id} className="flex py-4">
<div className="aspect-square h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border bg-neutral-50 sm:h-32 sm:w-32">
{item.variant?.product?.thumbnail?.url && (
<Image
src={item.variant.product.thumbnail.url}
alt={item.variant.product.thumbnail.alt ?? ""}
width={200}
height={200}
className="h-full w-full object-contain object-center"
/>
)}
</div>
<div className="relative flex flex-1 flex-col justify-between p-4 py-2">
<div className="flex justify-between justify-items-start gap-4">
<div className="">
<Link href={`/products/${item.variant.product.slug}?variant=${item.variant.id}`}>
<h2 className="font-medium text-neutral-700">{item.variant?.product?.name}</h2>
</Link>
<p className="mt-1 text-sm text-neutral-500">{item.variant?.product?.category?.name}</p>
{item.variant.name !== item.variant.id && Boolean(item.variant.name) && (
<p className="mt-1 text-sm text-neutral-500">Variant: {item.variant.name}</p>
)}
</div>
<p className="text-right font-semibold text-neutral-900">
{formatMoney(item.totalPrice.gross.amount, item.totalPrice.gross.currency)}
</p>
</div>
<div className="flex justify-between">
<div className="text-sm font-bold">Qty: {item.quantity}</div>
<DeleteLineButton checkoutId={checkoutId} lineId={item.id} />
<ul
data-testid="CartProductList"
role="list"
className="divide-y divide-neutral-200 border-b border-t border-neutral-200"
>
{checkout.lines.map((item) => (
<li key={item.id} className="flex py-4">
<div className="aspect-square h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border bg-neutral-50 sm:h-32 sm:w-32">
{item.variant?.product?.thumbnail?.url && (
<Image
src={item.variant.product.thumbnail.url}
alt={item.variant.product.thumbnail.alt ?? ""}
width={200}
height={200}
className="h-full w-full object-contain object-center"
/>
)}
</div>
<div className="relative flex flex-1 flex-col justify-between p-4 py-2">
<div className="flex justify-between justify-items-start gap-4">
<div className="">
<Link href={`/products/${item.variant.product.slug}?variant=${item.variant.id}`}>
<h2 className="font-medium text-neutral-700">{item.variant?.product?.name}</h2>
</Link>
<p className="mt-1 text-sm text-neutral-500">{item.variant?.product?.category?.name}</p>
{item.variant.name !== item.variant.id && Boolean(item.variant.name) && (
<p className="mt-1 text-sm text-neutral-500">Variant: {item.variant.name}</p>
)}
</div>
<p className="text-right font-semibold text-neutral-900">
{formatMoney(item.totalPrice.gross.amount, item.totalPrice.gross.currency)}
</p>
</div>
</li>
))}
</ul>
</div>
<div className="flex justify-between">
<div className="text-sm font-bold">Qty: {item.quantity}</div>
<DeleteLineButton checkoutId={checkoutId} lineId={item.id} />
</div>
</div>
</li>
))}
</ul>

<div className="mt-12">
<div className="rounded border bg-neutral-50 px-4 py-2">
<div className="flex items-center justify-between py-2">
Expand All @@ -71,13 +86,12 @@ export default async function Page() {
<p className="mt-1 text-sm text-neutral-500">Shipping will be calculated in the next step</p>
</div>
<div className="font-medium text-neutral-900">
{checkout &&
formatMoney(checkout.totalPrice.gross.amount, checkout.totalPrice.gross.currency)}
{formatMoney(checkout.totalPrice.gross.amount, checkout.totalPrice.gross.currency)}
</div>
</div>
</div>
<div className="mt-10 grid sm:grid-cols-3">
<CheckoutLink checkoutId={checkoutId} disabled={lines.length < 1} />
<div className="mt-10 text-center">
<CheckoutLink checkoutId={checkoutId} disabled={!checkout.lines.length} className="w-1/3" />
</div>
</div>
</form>
Expand Down
6 changes: 4 additions & 2 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export default function RootLayout(props: { children: ReactNode }) {
return (
<>
<Header />
<main className="min-h-[calc(100vh-106px)] flex-grow">{props.children}</main>
<Footer />
<div className="flex h-[calc(100%-64px)] flex-col">
<main className="flex-1">{props.children}</main>
<Footer />
</div>
</>
);
}
52 changes: 19 additions & 33 deletions src/app/(main)/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/app/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const metadata: Metadata = {

export default function RootLayout(props: { children: ReactNode }) {
return (
<html lang="en">
<body className={`${inter.className} flex min-h-screen flex-col`}>
<html lang="en" className="h-full">
<body className={`${inter.className} h-full`}>
<AuthProvider>{props.children}</AuthProvider>
<DraftModeNotification />
</body>
Expand Down
11 changes: 8 additions & 3 deletions src/checkout/components/AddressForm/AddressForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ export const AddressForm: FC<PropsWithChildren<AddressFormProps>> = ({
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<Record<AddressField, FieldValidator>> = {
phone: isValidPhoneNumber,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({}),
),
Expand Down
2 changes: 1 addition & 1 deletion src/checkout/components/AddressForm/useAddressFormUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,6 @@ export const useAddressFormUtils = (countryCode: CountryCode = defaultCountry) =
hasAllRequiredFields,
getMissingFieldsFromAddress,
...validationRules,
allowedFields: validationRules?.allowedFields as AddressField[],
allowedFields: validationRules?.allowedFields as AddressField[] | undefined,
};
};
2 changes: 1 addition & 1 deletion src/checkout/hooks/useAlerts/useAlerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>) => void;
Expand Down
Loading