Skip to content

Commit

Permalink
feat: handle no client (#2)
Browse files Browse the repository at this point in the history
* feat: handle no client

* delete duplicated files

* revert deleted files

* fix theme
  • Loading branch information
Clement-Muth committed Feb 27, 2024
1 parent fdd35fb commit 4121c3e
Show file tree
Hide file tree
Showing 28 changed files with 1,206 additions and 525 deletions.
3 changes: 2 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY
STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY
}
};

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"react-syntax-highlighter": "^15.5.0",
"stripe": "^14.17.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-syntax-highlighter": "^15.5.11",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"rome": "^12.1.3",
Expand Down
20 changes: 20 additions & 0 deletions src/app/(stripe)/add-payment-methods/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { cookies } from "next/headers";
import AddPaymentMethods from "~/app/(stripe)/add-payment-methods/views";
import getClient from "~/app/api/getClient";
import PageContainer from "~/components/PageContainer/PageContainer";
import ElementsProvider from "~/libraries/stripe/ElementsProvider";

const AddPaymentMethodPage = async () => {
const clientId = await getClient(cookies().get("clientId")?.value);

return (
<PageContainer>
<h1 className="font-bold text-3xl">Add your payment method</h1>
<ElementsProvider>
<AddPaymentMethods clientId={clientId.id} />
</ElementsProvider>
</PageContainer>
);
};

export default AddPaymentMethodPage;
120 changes: 120 additions & 0 deletions src/app/(stripe)/add-payment-methods/views/Form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

import { Input, Select, SelectItem, Spacer } from "@nextui-org/react";
import {
CardCvcElement,
CardExpiryElement,
CardNumberElement,
useElements,
useStripe
} from "@stripe/react-stripe-js";
import { useRouter } from "next/navigation";
import { useMemo, useState, useTransition } from "react";
import countries from "~/app/(stripe)/add-payment-methods/views/Form/countries";
import newPaymentMethod from "~/app/(stripe)/add-payment-methods/views/Form/newPaymentMethod";
import Form from "~/components/Form/Form";

interface AddPaymentMethodFormProps {
clientId: string;
}

const AddPaymentMethodForm = ({ clientId }: AddPaymentMethodFormProps) => {
const [isPending, startTransition] = useTransition();
const [zipCode, setZipCode] = useState<undefined | string>(undefined);
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
const [country, setCountry] = useState<any>(new Set([]));
const [email, setEmail] = useState<undefined | string>(undefined);
const [fullname, setFullname] = useState<undefined | string>(undefined);
const [isPaymentMethodAdded, setIsPaymentMethodAdded] = useState<boolean>(false);
const elements = useElements();
const stripe = useStripe();
const { push } = useRouter();

const isInvalid = useMemo(() => {
if (zipCode === undefined) return false;

return zipCode.match(/^\d{5}$/) ? false : true;
}, [zipCode]);

return (
<Form
data-form-type="payment"
onSubmit={(e) =>
startTransition(async () => {
e.preventDefault();
if (!elements || !stripe) return;

const cardElement = elements.getElement(CardNumberElement)!;

const createNewPaymentMethod = new newPaymentMethod(
clientId,
(country as { currentKey: string }).currentKey,
zipCode!,
email!,
fullname!
);
if ((await createNewPaymentMethod.addNewPaymentMethod(stripe, cardElement)).success)
setIsPaymentMethodAdded(true);
})
}
isLoading={isPending}
onClick={() => isPaymentMethodAdded && push("/get-payment-methods")}
submitted={isPaymentMethodAdded}
buttonTitle={isPaymentMethodAdded ? "See my payment methods" : "Add payment method"}
>
<div className="flex">
<Input
label="Email"
isInvalid={isInvalid}
color={isInvalid ? "danger" : "default"}
errorMessage={isInvalid && "Please enter a valid email"}
onValueChange={setEmail}
data-form-type="email"
type="email"
isRequired
/>
<Spacer x={4} />
<Input
label="Full name"
isInvalid={isInvalid}
color={isInvalid ? "danger" : "default"}
onValueChange={setFullname}
data-form-type="fullname"
isRequired
/>
</div>
<CardNumberElement id="card-number-element" options={{ showIcon: true }} />
<div className="flex">
<CardExpiryElement id="card-number-element" />
<Spacer x={4} />
<CardCvcElement id="card-number-element" />
</div>
<Input
placeholder="92084"
label="Zip Code"
maxLength={5}
minLength={5}
isInvalid={isInvalid}
color={isInvalid ? "danger" : "default"}
errorMessage={isInvalid && "Please enter a zip code"}
onValueChange={setZipCode}
isRequired
/>
<Select
label="Country"
selectedKeys={country}
onSelectionChange={setCountry}
autoComplete="false"
isRequired
>
{countries.map((country) => (
<SelectItem key={country.value} value={country.value}>
{country.label}
</SelectItem>
))}
</Select>
</Form>
);
};

export default AddPaymentMethodForm;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Stripe, StripeCardNumberElement } from "@stripe/stripe-js";
import StripeError from "stripe";
import attachPaymentMethod from "~/app/add-payment-methods/api/attachPaymentMethod";
import createNewClient from "~/app/add-payment-methods/api/createNewClient";
import createPaymentMethod from "~/app/add-payment-methods/api/createPaymentMethod";
import createPaymentMethod from "~/app/(stripe)/add-payment-methods/views/Form/createPaymentMethod";
import attachPaymentMethod from "~/app/api/attachPaymentMethod";
import createNewClient from "~/app/api/createNewClient";
import FormError from "~/core/FormError";
import StripeErr from "~/libraries/stripe/stripeError";

Expand Down
117 changes: 117 additions & 0 deletions src/app/(stripe)/add-payment-methods/views/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"use client";

import { Tab, Tabs } from "@nextui-org/tabs";
import AddPaymentMethodForm from "~/app/(stripe)/add-payment-methods/views/Form";
import onDark from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus";
import { Prism } from "react-syntax-highlighter";

export interface AddPaymentMethodsProps {
clientId: string;
}

const AddPaymentMethods = ({ clientId }: AddPaymentMethodsProps) => {
return (
<Tabs variant="underlined">
<Tab key="form" title="preview">
<AddPaymentMethodForm clientId={clientId} />
</Tab>
<Tab key="code" title="code" aria-label="Code demo tabs" className="max-h-[50vh]">
<Prism language="typescript" className="h-full rounded-lg !bg-[rgb(14_12_12)]" style={onDark}>
{`import type { Stripe, StripeCardNumberElement } from "@stripe/stripe-js";
import StripeError from "~/libraries/stripe/stripeError";
export type Brand =
| "American Express"
| "Diners Club"
| "Discover"
| "JCB"
| "mastercard"
| "UnionPay"
| "visa";
export class PaymentMethod {
constructor(
public readonly paymentMethodId: string,
public readonly fingerprint: string,
public readonly brand: Brand,
public readonly last4: string
) {
Object.freeze(this);
}
}
export type BillingDetails = {
address: {
zipCode: string;
country: string;
};
name?: string;
phone?: string;
email?: string;
};
type Params = {
stripe: Stripe;
cardElement: StripeCardNumberElement;
billingDetails: BillingDetails;
};
const createPaymentMethod = async ({
stripe,
cardElement,
billingDetails
}: Params): Promise<PaymentMethod> => {
const { error, paymentMethod } = await stripe.createPaymentMethod({
card: cardElement,
type: "card",
billing_details: {
...billingDetails,
address: {
country: billingDetails.address.country,
postal_code: billingDetails.address.zipCode
}
}
});
if (error) {
switch (error.code) {
case "expired_card":
throw new StripeError.Error({
code: StripeError.Errors.cardCode.expiredCard,
message: "The card has expired. Check the expiration date or use a different card."
});
case "incorrect_cvc":
throw new StripeError.Error({
code: StripeError.Errors.cardCode.incorrectCvc,
message:
"The card’s security code is incorrect. Check the card’s security code or use a different card."
});
case "incorrect_number":
throw new StripeError.Error({
code: StripeError.Errors.cardCode.incorrectNumber,
message: "The card number is incorrect. Check the card’s number or use a different card."
});
default:
throw new StripeError.Error({
code: StripeError.Errors.cardCode.unknown,
message: error?.message ?? "An error occured during the payment, please try again."
});
}
} else
return {
paymentMethodId: paymentMethod.id,
brand: paymentMethod.card?.brand as Brand,
fingerprint: paymentMethod.card?.fingerprint!,
last4: paymentMethod.card?.last4!
};
};
export default createPaymentMethod;
`}
</Prism>
</Tab>
</Tabs>
);
};

export default AddPaymentMethods;
62 changes: 62 additions & 0 deletions src/app/(stripe)/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import { Input } from "@nextui-org/react";
import { useMemo, useState, useTransition } from "react";
import createNewClient from "~/app/api/createNewClient";
import Form from "~/components/Form/Form";
import PageContainer from "~/components/PageContainer/PageContainer";

const Error = ({ reset }: { reset: () => void }) => {
const [isPending, startTransition] = useTransition();
const [email, setEmail] = useState<string | undefined>(undefined);
const [fullname, setFullname] = useState<string | undefined>(undefined);

const isInvalidEmail = useMemo(() => {
if (email === undefined) return false;

return email.match(/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,4})$/) ? false : true;
}, [email]);

return (
<PageContainer>
<h1 className="font-bold text-3xl">Create a customer first</h1>
<Form
data-form-type="payment"
onSubmit={(e) =>
startTransition(async () => {
e.preventDefault();

if (!email || !fullname) return;
await createNewClient({ email, fullname });

reset();
})
}
isLoading={isPending}
buttonTitle="Create a customer"
>
<Input
placeholder="email@example.com"
label="Email Address"
type="email"
maxLength={500}
isInvalid={isInvalidEmail}
color={isInvalidEmail ? "danger" : "default"}
errorMessage={isInvalidEmail && "Please enter a valid email address"}
onValueChange={setEmail}
isRequired
/>
<Input
placeholder="Clément Muth"
label="Full name"
type="text"
maxLength={100}
onValueChange={setFullname}
isRequired
/>
</Form>
</PageContainer>
);
};

export default Error;
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Card, CardBody, CardFooter, CardHeader, Image, ScrollShadow } from "@nextui-org/react";
import getPaymentMethods from "~/app/get-payment-methods/api/getPaymentMethods";
import Mastercard from "/public/cards/brands/mastercard.svg";
import AmericanExpress from "/public/cards/brands/american-express.svg";
import Visa from "/public/cards/brands/visa.svg";
Expand All @@ -8,12 +7,17 @@ import DinersClub from "/public/cards/brands/diners-club.svg";
import Jcb from "/public/cards/brands/jcb.svg";
import UnionPay from "/public/cards/brands/unionpay.svg";
import NextImage from "next/image";
import PageContainer from "~/components/PageContainer/PageContainer";
import getPaymentMethods from "~/app/api/getPaymentMethods";
import getClient from "~/app/api/getClient";
import { cookies } from "next/headers";

const AddPaymentMethodPage = async () => {
const paymentMethods = await getPaymentMethods("cus_Pbe2kpjgnWl3pR");
const clientId = await getClient(cookies().get("clientId")?.value);
const paymentMethods = await getPaymentMethods(clientId.id);

return (
<main className="flex flex-col justify-center items-center min-h-screen gap-y-10">
<PageContainer>
<h1 className="font-bold text-3xl">My payment methods</h1>
<ScrollShadow orientation="horizontal" className="flex gap-5 max-w-2xl p-5" hideScrollBar>
{paymentMethods?.map((paymentMethod, i) => (
Expand Down Expand Up @@ -55,7 +59,7 @@ const AddPaymentMethodPage = async () => {
</Card>
)) ?? <p>No card added</p>}
</ScrollShadow>
</main>
</PageContainer>
);
};

Expand Down
Loading

0 comments on commit 4121c3e

Please sign in to comment.