-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: handle no client * delete duplicated files * revert deleted files * fix theme
- Loading branch information
1 parent
fdd35fb
commit 4121c3e
Showing
28 changed files
with
1,206 additions
and
525 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
File renamed without changes.
File renamed without changes.
120 changes: 120 additions & 0 deletions
120
src/app/(stripe)/add-payment-methods/views/Form/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
6 changes: 3 additions & 3 deletions
6
...nt-methods/views/Form/newPaymentMethod.ts → ...nt-methods/views/Form/newPaymentMethod.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.