eny.space Landingpage

feat(app): implement payment processing with Stripe integration, including checkout and payment elements, and update project structure and dependencies

+7
.env.local.example
··· 1 + # Stripe keys 2 + # https://dashboard.stripe.com/apikeys 3 + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_12345 4 + STRIPE_SECRET_KEY=sk_12345 5 + STRIPE_PAYMENT_DESCRIPTION='Software development services' 6 + # https://stripe.com/docs/webhooks/signatures 7 + STRIPE_WEBHOOK_SECRET=whsec_1234
+101 -1
README.md
··· 1 - # eny.space 1 + # Easy PDS 2 + 3 + A full-stack TypeScript application using Next.js for processing donations. 4 + 5 + ## Features 6 + 7 + - **Checkout** - Custom amount donations with hosted and embedded checkout 8 + - **Payment Elements** - Custom payment form with Payment Element 9 + - **Webhook handling** - Server-side webhook processing for payment events 10 + 11 + ## Tech Stack 12 + 13 + - **Frontend**: Next.js, React, TypeScript 14 + - **Backend**: Next.js Server Actions and Route Handlers 15 + 16 + ## Getting Started 17 + 18 + ### Prerequisites 19 + 20 + - Node.js 18+ installed 21 + - A payment processor account 22 + 23 + ### Installation 24 + 25 + 1. Clone the repository and install dependencies: 26 + 27 + ```bash 28 + npm install 29 + # or 30 + yarn install 31 + # or 32 + pnpm install 33 + ``` 34 + 35 + 2. Set up environment variables: 36 + 37 + Create a `.env.local` file in the root directory: 38 + 39 + ```bash 40 + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_publishable_key 41 + STRIPE_SECRET_KEY=your_secret_key 42 + STRIPE_WEBHOOK_SECRET=your_webhook_secret 43 + ``` 44 + 45 + Get your API keys from your payment processor dashboard. 46 + 47 + 3. Start the development server: 48 + 49 + ```bash 50 + npm run dev 51 + # or 52 + yarn dev 53 + # or 54 + pnpm dev 55 + ``` 56 + 57 + The application will be available at `http://localhost:3000`. 58 + 59 + ### Webhook Setup 60 + 61 + #### Local Development 62 + 63 + 1. Install the payment processor CLI and link your account. 64 + 65 + 2. Start webhook forwarding to your local server: 66 + 67 + ```bash 68 + # Example command - adjust based on your payment processor 69 + webhook listen --forward-to localhost:3000/api/webhooks 70 + ``` 71 + 72 + 3. Copy the webhook secret from the CLI output and add it to your `.env.local` file. 73 + 74 + #### Production 75 + 76 + 1. Deploy your application and copy the webhook URL (e.g., `https://your-domain.com/api/webhooks`). 77 + 78 + 2. Create a webhook endpoint in your payment processor dashboard. 79 + 80 + 3. Add the webhook signing secret to your production environment variables. 81 + 82 + ## Testing 83 + 84 + Use test cards for testing payments. Common test cards: 85 + - `4242 4242 4242 4242` - Successful payment 86 + - `4000 0027 6000 3184` - 3D Secure authentication required 87 + 88 + ## Deployment 89 + 90 + This application can be deployed to any platform that supports Next.js, such as [Vercel](https://vercel.com), Netlify, or your own infrastructure. 91 + 92 + Make sure to set all required environment variables in your deployment platform. 93 + 94 + ## Project Structure 95 + 96 + - `app/` - Next.js app directory with pages and components 97 + - `app/actions/` - Server actions for payment operations 98 + - `app/api/webhooks/` - Webhook handler route 99 + - `lib/` - Payment processor client configuration 100 + - `components/` - React components for payment forms 101 + - `utils/` - Utility functions
+80
app/actions/stripe.ts
··· 1 + "use server"; 2 + 3 + import type { Stripe } from "stripe"; 4 + 5 + import { headers } from "next/headers"; 6 + 7 + import { CURRENCY } from "@/config"; 8 + import { formatAmountForStripe } from "@/utils/stripe-helpers"; 9 + import { stripe } from "@/lib/stripe"; 10 + 11 + export async function createCheckoutSession( 12 + data: FormData, 13 + ): Promise<{ client_secret: string | null; url: string | null }> { 14 + const ui_mode = data.get( 15 + "uiMode", 16 + ) as Stripe.Checkout.SessionCreateParams.UiMode; 17 + 18 + const headersList = await headers(); 19 + const originHeader = headersList.get("origin"); 20 + const hostHeader = headersList.get("host"); 21 + 22 + let origin: string; 23 + if (originHeader) { 24 + origin = originHeader; 25 + } else if (hostHeader) { 26 + origin = `https://${hostHeader}`; 27 + } else { 28 + origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; 29 + } 30 + 31 + const checkoutSession: Stripe.Checkout.Session = 32 + await stripe.checkout.sessions.create({ 33 + mode: "payment", 34 + submit_type: "donate", 35 + line_items: [ 36 + { 37 + quantity: 1, 38 + price_data: { 39 + currency: CURRENCY, 40 + product_data: { 41 + name: "Custom amount donation", 42 + }, 43 + unit_amount: formatAmountForStripe( 44 + Number(data.get("customDonation") as string), 45 + CURRENCY, 46 + ), 47 + }, 48 + }, 49 + ], 50 + ...(ui_mode === "hosted" && { 51 + success_url: `${origin}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`, 52 + cancel_url: `${origin}/donate-with-checkout`, 53 + }), 54 + ...(ui_mode === "embedded" && { 55 + return_url: `${origin}/donate-with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`, 56 + }), 57 + ui_mode, 58 + }); 59 + 60 + return { 61 + client_secret: checkoutSession.client_secret, 62 + url: checkoutSession.url, 63 + }; 64 + } 65 + 66 + export async function createPaymentIntent( 67 + data: FormData, 68 + ): Promise<{ client_secret: string }> { 69 + const paymentIntent: Stripe.PaymentIntent = 70 + await stripe.paymentIntents.create({ 71 + amount: formatAmountForStripe( 72 + Number(data.get("customDonation") as string), 73 + CURRENCY, 74 + ), 75 + automatic_payment_methods: { enabled: true }, 76 + currency: CURRENCY, 77 + }); 78 + 79 + return { client_secret: paymentIntent.client_secret as string }; 80 + }
+66
app/api/webhooks/route.ts
··· 1 + import type { Stripe } from "stripe"; 2 + 3 + import { NextResponse } from "next/server"; 4 + 5 + import { stripe } from "@/lib/stripe"; 6 + 7 + export async function POST(req: Request) { 8 + let event: Stripe.Event; 9 + 10 + try { 11 + event = stripe.webhooks.constructEvent( 12 + await (await req.blob()).text(), 13 + req.headers.get("stripe-signature") as string, 14 + process.env.STRIPE_WEBHOOK_SECRET as string, 15 + ); 16 + } catch (err) { 17 + const errorMessage = err instanceof Error ? err.message : "Unknown error"; 18 + // On error, log and return the error message. 19 + if (!(err instanceof Error)) console.log(err); 20 + console.log(`❌ Error message: ${errorMessage}`); 21 + return NextResponse.json( 22 + { message: `Webhook Error: ${errorMessage}` }, 23 + { status: 400 }, 24 + ); 25 + } 26 + 27 + // Successfully constructed event. 28 + console.log("✅ Success:", event.id); 29 + 30 + const permittedEvents: string[] = [ 31 + "checkout.session.completed", 32 + "payment_intent.succeeded", 33 + "payment_intent.payment_failed", 34 + ]; 35 + 36 + if (permittedEvents.includes(event.type)) { 37 + let data; 38 + 39 + try { 40 + switch (event.type) { 41 + case "checkout.session.completed": 42 + data = event.data.object as Stripe.Checkout.Session; 43 + console.log(`💰 CheckoutSession status: ${data.payment_status}`); 44 + break; 45 + case "payment_intent.payment_failed": 46 + data = event.data.object as Stripe.PaymentIntent; 47 + console.log(`❌ Payment failed: ${data.last_payment_error?.message}`); 48 + break; 49 + case "payment_intent.succeeded": 50 + data = event.data.object as Stripe.PaymentIntent; 51 + console.log(`💰 PaymentIntent status: ${data.status}`); 52 + break; 53 + default: 54 + throw new Error(`Unhandled event: ${event.type}`); 55 + } 56 + } catch (error) { 57 + console.log(error); 58 + return NextResponse.json( 59 + { message: "Webhook handler failed" }, 60 + { status: 500 }, 61 + ); 62 + } 63 + } 64 + // Return a response to acknowledge receipt of the event. 65 + return NextResponse.json({ message: "Received" }, { status: 200 }); 66 + }
+82
app/components/CheckoutForm.tsx
··· 1 + "use client"; 2 + 3 + import type Stripe from "stripe"; 4 + 5 + import React, { useState } from "react"; 6 + 7 + import CustomDonationInput from "@/components/CustomDonationInput"; 8 + import TestCards from "@/components/TestCards"; 9 + 10 + import { formatAmountForDisplay } from "@/utils/stripe-helpers"; 11 + import * as config from "@/config"; 12 + import { createCheckoutSession } from "@/actions/stripe"; 13 + import getStripe from "@/utils/get-stripejs"; 14 + import { 15 + EmbeddedCheckout, 16 + EmbeddedCheckoutProvider, 17 + } from "@stripe/react-stripe-js"; 18 + 19 + interface CheckoutFormProps { 20 + uiMode: Stripe.Checkout.SessionCreateParams.UiMode; 21 + } 22 + 23 + export default function CheckoutForm(props: CheckoutFormProps): JSX.Element { 24 + const [loading] = useState<boolean>(false); 25 + const [input, setInput] = useState<{ customDonation: number }>({ 26 + customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP), 27 + }); 28 + const [clientSecret, setClientSecret] = useState<string | null>(null); 29 + 30 + const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = ( 31 + e, 32 + ): void => 33 + setInput({ 34 + ...input, 35 + [e.currentTarget.name]: e.currentTarget.value, 36 + }); 37 + 38 + const formAction = async (data: FormData): Promise<void> => { 39 + const uiMode = data.get( 40 + "uiMode", 41 + ) as Stripe.Checkout.SessionCreateParams.UiMode; 42 + const { client_secret, url } = await createCheckoutSession(data); 43 + 44 + if (uiMode === "embedded") return setClientSecret(client_secret); 45 + 46 + window.location.assign(url as string); 47 + }; 48 + 49 + return ( 50 + <> 51 + <form action={formAction}> 52 + <input type="hidden" name="uiMode" value={props.uiMode} /> 53 + <CustomDonationInput 54 + className="checkout-style" 55 + name="customDonation" 56 + min={config.MIN_AMOUNT} 57 + max={config.MAX_AMOUNT} 58 + step={config.AMOUNT_STEP} 59 + currency={config.CURRENCY} 60 + onChange={handleInputChange} 61 + value={input.customDonation} 62 + /> 63 + <TestCards /> 64 + <button 65 + className="checkout-style-background" 66 + type="submit" 67 + disabled={loading} 68 + > 69 + Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)} 70 + </button> 71 + </form> 72 + {clientSecret ? ( 73 + <EmbeddedCheckoutProvider 74 + stripe={getStripe()} 75 + options={{ clientSecret }} 76 + > 77 + <EmbeddedCheckout /> 78 + </EmbeddedCheckoutProvider> 79 + ) : null} 80 + </> 81 + ); 82 + }
+38
app/components/CustomDonationInput.tsx
··· 1 + import { formatAmountForDisplay } from "@/utils/stripe-helpers"; 2 + 3 + export default function CustomDonationInput({ 4 + name, 5 + min, 6 + max, 7 + currency, 8 + step, 9 + onChange, 10 + value, 11 + className, 12 + }: { 13 + name: string; 14 + min: number; 15 + max: number; 16 + currency: string; 17 + step: number; 18 + onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; 19 + value: number; 20 + className?: string; 21 + }): JSX.Element { 22 + return ( 23 + <label> 24 + Custom donation amount ({formatAmountForDisplay(min, currency)}- 25 + {formatAmountForDisplay(max, currency)}): 26 + <input 27 + type="range" 28 + name={name} 29 + min={min} 30 + max={max} 31 + step={step} 32 + onChange={onChange} 33 + value={value} 34 + className={className} 35 + ></input> 36 + </label> 37 + ); 38 + }
+191
app/components/ElementsForm.tsx
··· 1 + "use client"; 2 + 3 + import type { StripeError } from "@stripe/stripe-js"; 4 + 5 + import * as React from "react"; 6 + import { 7 + useStripe, 8 + useElements, 9 + PaymentElement, 10 + Elements, 11 + } from "@stripe/react-stripe-js"; 12 + 13 + import CustomDonationInput from "./CustomDonationInput"; 14 + import TestCards from "./TestCards"; 15 + 16 + import { formatAmountForDisplay } from "@/utils/stripe-helpers"; 17 + import * as config from "@/config"; 18 + import getStripe from "@/utils/get-stripejs"; 19 + import { createPaymentIntent } from "@/actions/stripe"; 20 + 21 + function CheckoutForm(): JSX.Element { 22 + const [input, setInput] = React.useState<{ 23 + customDonation: number; 24 + cardholderName: string; 25 + }>({ 26 + customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP), 27 + cardholderName: "", 28 + }); 29 + const [paymentType, setPaymentType] = React.useState<string>(""); 30 + const [payment, setPayment] = React.useState<{ 31 + status: "initial" | "processing" | "error"; 32 + }>({ status: "initial" }); 33 + const [errorMessage, setErrorMessage] = React.useState<string>(""); 34 + 35 + const stripe = useStripe(); 36 + const elements = useElements(); 37 + 38 + const PaymentStatus = ({ status }: { status: string }) => { 39 + switch (status) { 40 + case "processing": 41 + case "requires_payment_method": 42 + case "requires_confirmation": 43 + return <h2>Processing...</h2>; 44 + 45 + case "requires_action": 46 + return <h2>Authenticating...</h2>; 47 + 48 + case "succeeded": 49 + return <h2>Payment Succeeded 🥳</h2>; 50 + 51 + case "error": 52 + return ( 53 + <> 54 + <h2>Error 😭</h2> 55 + <p className="error-message">{errorMessage}</p> 56 + </> 57 + ); 58 + 59 + default: 60 + return null; 61 + } 62 + }; 63 + 64 + const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { 65 + setInput({ 66 + ...input, 67 + [e.currentTarget.name]: e.currentTarget.value, 68 + }); 69 + 70 + elements?.update({ amount: input.customDonation * 100 }); 71 + }; 72 + 73 + const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => { 74 + try { 75 + e.preventDefault(); 76 + // Abort if form isn't valid 77 + if (!e.currentTarget.reportValidity()) return; 78 + if (!elements || !stripe) return; 79 + 80 + setPayment({ status: "processing" }); 81 + 82 + const { error: submitError } = await elements.submit(); 83 + 84 + if (submitError) { 85 + setPayment({ status: "error" }); 86 + setErrorMessage(submitError.message ?? "An unknown error occurred"); 87 + 88 + return; 89 + } 90 + 91 + // Create a PaymentIntent with the specified amount. 92 + const { client_secret: clientSecret } = await createPaymentIntent( 93 + new FormData(e.target as HTMLFormElement), 94 + ); 95 + 96 + // Confirm payment with the payment processor 97 + const { error: confirmError } = await stripe!.confirmPayment({ 98 + elements, 99 + clientSecret, 100 + confirmParams: { 101 + return_url: `${window.location.origin}/donate-with-elements/result`, 102 + payment_method_data: { 103 + billing_details: { 104 + name: input.cardholderName, 105 + }, 106 + }, 107 + }, 108 + }); 109 + 110 + if (confirmError) { 111 + setPayment({ status: "error" }); 112 + setErrorMessage(confirmError.message ?? "An unknown error occurred"); 113 + } 114 + } catch (err) { 115 + const { message } = err as StripeError; 116 + 117 + setPayment({ status: "error" }); 118 + setErrorMessage(message ?? "An unknown error occurred"); 119 + } 120 + }; 121 + 122 + return ( 123 + <> 124 + <form onSubmit={handleSubmit}> 125 + <CustomDonationInput 126 + className="elements-style" 127 + name="customDonation" 128 + value={input.customDonation} 129 + min={config.MIN_AMOUNT} 130 + max={config.MAX_AMOUNT} 131 + step={config.AMOUNT_STEP} 132 + currency={config.CURRENCY} 133 + onChange={handleInputChange} 134 + /> 135 + <TestCards /> 136 + <fieldset className="elements-style"> 137 + <legend>Your payment details:</legend> 138 + {paymentType === "card" ? ( 139 + <input 140 + placeholder="Cardholder name" 141 + className="elements-style" 142 + type="Text" 143 + name="cardholderName" 144 + onChange={handleInputChange} 145 + required 146 + /> 147 + ) : null} 148 + <div className="FormRow elements-style"> 149 + <PaymentElement 150 + onChange={(e) => { 151 + setPaymentType(e.value.type); 152 + }} 153 + /> 154 + </div> 155 + </fieldset> 156 + <button 157 + className="elements-style-background" 158 + type="submit" 159 + disabled={ 160 + !["initial", "succeeded", "error"].includes(payment.status) || 161 + !stripe 162 + } 163 + > 164 + Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)} 165 + </button> 166 + </form> 167 + <PaymentStatus status={payment.status} /> 168 + </> 169 + ); 170 + } 171 + 172 + export default function ElementsForm(): JSX.Element { 173 + return ( 174 + <Elements 175 + stripe={getStripe()} 176 + options={{ 177 + appearance: { 178 + variables: { 179 + colorIcon: "#6772e5", 180 + fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif", 181 + }, 182 + }, 183 + currency: config.CURRENCY, 184 + mode: "payment", 185 + amount: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP), 186 + }} 187 + > 188 + <CheckoutForm /> 189 + </Elements> 190 + ); 191 + }
+10
app/components/PrintObject.tsx
··· 1 + import type { Stripe } from "stripe"; 2 + 3 + export default function PrintObject({ 4 + content, 5 + }: { 6 + content: Stripe.PaymentIntent | Stripe.Checkout.Session; 7 + }): JSX.Element { 8 + const formattedContent: string = JSON.stringify(content, null, 2); 9 + return <pre>{formattedContent}</pre>; 10 + }
+11
app/components/StripeTestCards.tsx
··· 1 + export default function StripeTestCards(): JSX.Element { 2 + return ( 3 + <div className="test-card-notice"> 4 + Use test cards for testing, e.g.{" "} 5 + <div className="card-number"> 6 + 4242<span></span>4242<span></span>4242<span></span>4242 7 + </div> 8 + . 9 + </div> 10 + ); 11 + }
+11
app/components/TestCards.tsx
··· 1 + export default function TestCards(): JSX.Element { 2 + return ( 3 + <div className="test-card-notice"> 4 + Use test cards for testing, e.g.{" "} 5 + <div className="card-number"> 6 + 4242<span></span>4242<span></span>4242<span></span>4242 7 + </div> 8 + . 9 + </div> 10 + ); 11 + }
+17
app/donate-with-checkout/page.tsx
··· 1 + import type { Metadata } from "next"; 2 + 3 + import CheckoutForm from "@/components/CheckoutForm"; 4 + 5 + export const metadata: Metadata = { 6 + title: "Donate", 7 + }; 8 + 9 + export default function DonatePage(): JSX.Element { 10 + return ( 11 + <div className="page-container"> 12 + <h1>Donate</h1> 13 + <p>Donate to our project 💖</p> 14 + <CheckoutForm uiMode="hosted" /> 15 + </div> 16 + ); 17 + }
+5
app/donate-with-checkout/result/error.tsx
··· 1 + "use client"; 2 + 3 + export default function Error({ error }: { error: Error }) { 4 + return <h2>{error.message}</h2>; 5 + }
+18
app/donate-with-checkout/result/layout.tsx
··· 1 + import type { Metadata } from "next"; 2 + 3 + export const metadata: Metadata = { 4 + title: "Checkout Session Result", 5 + }; 6 + 7 + export default function ResultLayout({ 8 + children, 9 + }: { 10 + children: React.ReactNode; 11 + }): JSX.Element { 12 + return ( 13 + <div className="page-container"> 14 + <h1>Checkout Session Result</h1> 15 + {children} 16 + </div> 17 + ); 18 + }
+28
app/donate-with-checkout/result/page.tsx
··· 1 + import type { Stripe } from "stripe"; 2 + 3 + import PrintObject from "@/components/PrintObject"; 4 + import { stripe } from "@/lib/stripe"; 5 + 6 + export default async function ResultPage({ 7 + searchParams, 8 + }: { 9 + searchParams: { session_id: string }; 10 + }): Promise<JSX.Element> { 11 + if (!searchParams.session_id) 12 + throw new Error("Please provide a valid session_id (`cs_test_...`)"); 13 + 14 + const checkoutSession: Stripe.Checkout.Session = 15 + await stripe.checkout.sessions.retrieve(searchParams.session_id, { 16 + expand: ["line_items", "payment_intent"], 17 + }); 18 + 19 + const paymentIntent = checkoutSession.payment_intent as Stripe.PaymentIntent; 20 + 21 + return ( 22 + <> 23 + <h2>Status: {paymentIntent.status}</h2> 24 + <h3>Checkout Session response:</h3> 25 + <PrintObject content={checkoutSession} /> 26 + </> 27 + ); 28 + }
+5
app/donate-with-elements/page.tsx
··· 1 + import { redirect } from "next/navigation"; 2 + 3 + export default function PaymentElementPage() { 4 + redirect("/donate-with-checkout"); 5 + }
+5
app/donate-with-elements/result/error.tsx
··· 1 + "use client"; 2 + 3 + export default function Error({ error }: { error: Error }) { 4 + return <h2>{error.message}</h2>; 5 + }
+18
app/donate-with-elements/result/layout.tsx
··· 1 + import type { Metadata } from "next"; 2 + 3 + export const metadata: Metadata = { 4 + title: "Payment Intent Result", 5 + }; 6 + 7 + export default function ResultLayout({ 8 + children, 9 + }: { 10 + children: React.ReactNode; 11 + }): JSX.Element { 12 + return ( 13 + <div className="page-container"> 14 + <h1>Payment Intent Result</h1> 15 + {children} 16 + </div> 17 + ); 18 + }
+24
app/donate-with-elements/result/page.tsx
··· 1 + import type { Stripe } from "stripe"; 2 + 3 + import PrintObject from "@/components/PrintObject"; 4 + import { stripe } from "@/lib/stripe"; 5 + 6 + export default async function ResultPage({ 7 + searchParams, 8 + }: { 9 + searchParams: { payment_intent: string }; 10 + }): Promise<JSX.Element> { 11 + if (!searchParams.payment_intent) 12 + throw new Error("Please provide a valid payment_intent (`pi_...`)"); 13 + 14 + const paymentIntent: Stripe.PaymentIntent = 15 + await stripe.paymentIntents.retrieve(searchParams.payment_intent); 16 + 17 + return ( 18 + <> 19 + <h2>Status: {paymentIntent.status}</h2> 20 + <h3>Payment Intent response:</h3> 21 + <PrintObject content={paymentIntent} /> 22 + </> 23 + ); 24 + }
+5
app/donate-with-embedded-checkout/page.tsx
··· 1 + import { redirect } from "next/navigation"; 2 + 3 + export default function DonatePage() { 4 + redirect("/donate-with-checkout"); 5 + }
+5
app/donate-with-embedded-checkout/result/error.tsx
··· 1 + "use client"; 2 + 3 + export default function Error({ error }: { error: Error }) { 4 + return <h2>{error.message}</h2>; 5 + }
+18
app/donate-with-embedded-checkout/result/layout.tsx
··· 1 + import type { Metadata } from "next"; 2 + 3 + export const metadata: Metadata = { 4 + title: "Checkout Session Result", 5 + }; 6 + 7 + export default function ResultLayout({ 8 + children, 9 + }: { 10 + children: React.ReactNode; 11 + }): JSX.Element { 12 + return ( 13 + <div className="page-container"> 14 + <h1>Checkout Session Result</h1> 15 + {children} 16 + </div> 17 + ); 18 + }
+28
app/donate-with-embedded-checkout/result/page.tsx
··· 1 + import type { Stripe } from "stripe"; 2 + 3 + import PrintObject from "@/components/PrintObject"; 4 + import { stripe } from "@/lib/stripe"; 5 + 6 + export default async function ResultPage({ 7 + searchParams, 8 + }: { 9 + searchParams: { session_id: string }; 10 + }): Promise<JSX.Element> { 11 + if (!searchParams.session_id) 12 + throw new Error("Please provide a valid session_id (`cs_test_...`)"); 13 + 14 + const checkoutSession: Stripe.Checkout.Session = 15 + await stripe.checkout.sessions.retrieve(searchParams.session_id, { 16 + expand: ["line_items", "payment_intent"], 17 + }); 18 + 19 + const paymentIntent = checkoutSession.payment_intent as Stripe.PaymentIntent; 20 + 21 + return ( 22 + <> 23 + <h2>Status: {paymentIntent.status}</h2> 24 + <h3>Checkout Session response:</h3> 25 + <PrintObject content={checkoutSession} /> 26 + </> 27 + ); 28 + }
+37
app/layout.tsx
··· 1 + import type { Metadata } from "next"; 2 + 3 + import "../styles.css"; 4 + 5 + interface LayoutProps { 6 + children: React.ReactNode; 7 + } 8 + 9 + export const metadata: Metadata = { 10 + title: { 11 + default: "Easy PDS", 12 + template: "%s | Easy PDS", 13 + }, 14 + twitter: { 15 + card: "summary_large_image", 16 + description: "Easy PDS - Secure donation platform.", 17 + }, 18 + }; 19 + 20 + export default function RootLayout({ children }: LayoutProps) { 21 + return ( 22 + <html lang="en"> 23 + <body> 24 + <div className="container"> 25 + <header> 26 + <div className="header-content"> 27 + <h1> 28 + Easy PDS 29 + </h1> 30 + </div> 31 + </header> 32 + {children} 33 + </div> 34 + </body> 35 + </html> 36 + ); 37 + }
+23
app/page.tsx
··· 1 + import type { Metadata } from "next"; 2 + 3 + import Link from "next/link"; 4 + 5 + export const metadata: Metadata = { 6 + title: "Home", 7 + }; 8 + 9 + export default function IndexPage(): JSX.Element { 10 + return ( 11 + <ul className="card-list"> 12 + <li> 13 + <Link 14 + href="/donate-with-checkout" 15 + className="card checkout-style-background" 16 + > 17 + <h2 className="bottom">Donate</h2> 18 + <img src="/checkout-one-time-payments.svg" /> 19 + </Link> 20 + </li> 21 + </ul> 22 + ); 23 + }
-32
components/Layout.tsx
··· 1 - import React, { ReactNode } from "react"; 2 - import Link from "next/link"; 3 - import Head from "next/head"; 4 - 5 - type Props = { 6 - children?: ReactNode; 7 - title?: string; 8 - }; 9 - 10 - const Layout = ({ children, title = "This is the default title" }: Props) => ( 11 - <div> 12 - <Head> 13 - <title>{title}</title> 14 - <meta charSet="utf-8" /> 15 - <meta name="viewport" content="initial-scale=1.0, width=device-width" /> 16 - </Head> 17 - <header> 18 - <nav> 19 - <Link href="/">Home</Link> | <Link href="/about">About</Link> |{" "} 20 - <Link href="/users">Users List</Link> |{" "} 21 - <a href="/api/users">Users API</a> 22 - </nav> 23 - </header> 24 - {children} 25 - <footer> 26 - <hr /> 27 - <span>I'm here to stay (Footer)</span> 28 - </footer> 29 - </div> 30 - ); 31 - 32 - export default Layout;
-19
components/List.tsx
··· 1 - import * as React from "react"; 2 - import ListItem from "./ListItem"; 3 - import { User } from "../interfaces"; 4 - 5 - type Props = { 6 - items: User[]; 7 - }; 8 - 9 - const List = ({ items }: Props) => ( 10 - <ul> 11 - {items.map((item) => ( 12 - <li key={item.id}> 13 - <ListItem data={item} /> 14 - </li> 15 - ))} 16 - </ul> 17 - ); 18 - 19 - export default List;
-16
components/ListDetail.tsx
··· 1 - import * as React from "react"; 2 - 3 - import { User } from "../interfaces"; 4 - 5 - type ListDetailProps = { 6 - item: User; 7 - }; 8 - 9 - const ListDetail = ({ item: user }: ListDetailProps) => ( 10 - <div> 11 - <h1>Detail for {user.name}</h1> 12 - <p>ID: {user.id}</p> 13 - </div> 14 - ); 15 - 16 - export default ListDetail;
-16
components/ListItem.tsx
··· 1 - import React from "react"; 2 - import Link from "next/link"; 3 - 4 - import { User } from "../interfaces"; 5 - 6 - type Props = { 7 - data: User; 8 - }; 9 - 10 - const ListItem = ({ data }: Props) => ( 11 - <Link href="/users/[id]" as={`/users/${data.id}`}> 12 - {data.id}:{data.name} 13 - </Link> 14 - ); 15 - 16 - export default ListItem;
+6
config/index.ts
··· 1 + export const CURRENCY = "usd"; 2 + // Set your amount limits: Use float for decimal currencies and 3 + // Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal. 4 + export const MIN_AMOUNT = 10.0; 5 + export const MAX_AMOUNT = 5000.0; 6 + export const AMOUNT_STEP = 5.0;
-10
interfaces/index.ts
··· 1 - // You can include shared interfaces/types in a separate file 2 - // and then use them in any component by importing them. For 3 - // example, to import the interface below do: 4 - // 5 - // import { User } from 'path/to/interfaces'; 6 - 7 - export type User = { 8 - id: number; 9 - name: string; 10 - };
+8
lib/stripe.ts
··· 1 + import "server-only"; 2 + 3 + import Stripe from "stripe"; 4 + 5 + export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { 6 + // https://github.com/stripe/stripe-node#configuration 7 + apiVersion: "2023-10-16", 8 + });
+1
next.config.js
··· 1 + module.exports = {};
+357 -47
package-lock.json
··· 1 1 { 2 - "name": "eny-space", 2 + "name": "with-stripe-typescript-app", 3 3 "lockfileVersion": 3, 4 4 "requires": true, 5 5 "packages": { 6 6 "": { 7 7 "dependencies": { 8 - "@stripe/stripe-js": "^8.6.0", 8 + "@stripe/react-stripe-js": "2.4.0", 9 + "@stripe/stripe-js": "2.2.2", 9 10 "next": "latest", 10 - "react": "^18.2.0", 11 - "react-dom": "^18.2.0" 11 + "react": "18.2.0", 12 + "react-dom": "18.2.0", 13 + "server-only": "0.0.1", 14 + "stripe": "14.8.0" 12 15 }, 13 16 "devDependencies": { 14 - "@types/node": "^12.12.21", 15 - "@types/react": "^17.0.2", 16 - "@types/react-dom": "^17.0.1", 17 - "typescript": "^4.8.3" 17 + "@types/node": "20.4.6", 18 + "@types/react": "18.2.8", 19 + "typescript": "5.1.6" 18 20 } 19 21 }, 20 22 "node_modules/@emnapi/runtime": { ··· 627 629 "node": ">= 10" 628 630 } 629 631 }, 630 - "node_modules/@stripe/stripe-js": { 631 - "version": "8.6.0", 632 - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.0.tgz", 633 - "integrity": "sha512-EB0/GGgs4hfezzkiMkinlRgWtjz8fSdwVQhwYS7Sg/RQrSvuNOz+ssPjD+lAzqaYTCB0zlbrt0fcqVziLJrufQ==", 632 + "node_modules/@stripe/react-stripe-js": { 633 + "version": "2.4.0", 634 + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.4.0.tgz", 635 + "integrity": "sha512-1jVQEL3OuhuzNlf4OdfqovHt+MkWh8Uh8xpLxx/xUFUDdF+7/kDOrGKy+xJO3WLCfZUL7NAy+/ypwXbbYZi0tg==", 634 636 "license": "MIT", 635 - "engines": { 636 - "node": ">=12.16" 637 + "dependencies": { 638 + "prop-types": "^15.7.2" 639 + }, 640 + "peerDependencies": { 641 + "@stripe/stripe-js": "^1.44.1 || ^2.0.0", 642 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", 643 + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" 637 644 } 645 + }, 646 + "node_modules/@stripe/stripe-js": { 647 + "version": "2.2.2", 648 + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.2.2.tgz", 649 + "integrity": "sha512-LvFZRZEBoMe6vXC6RoOAIbXWo/0JDdndq43ekL9M6affcM7PtF5KALmwt91BazW7q49sbSl0l7TunWhhSwEW4w==", 650 + "license": "MIT" 638 651 }, 639 652 "node_modules/@swc/helpers": { 640 653 "version": "0.5.15", ··· 646 659 } 647 660 }, 648 661 "node_modules/@types/node": { 649 - "version": "12.20.55", 650 - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", 651 - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", 652 - "dev": true, 662 + "version": "20.4.6", 663 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz", 664 + "integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA==", 653 665 "license": "MIT" 654 666 }, 655 667 "node_modules/@types/prop-types": { ··· 660 672 "license": "MIT" 661 673 }, 662 674 "node_modules/@types/react": { 663 - "version": "17.0.90", 664 - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.90.tgz", 665 - "integrity": "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw==", 675 + "version": "18.2.8", 676 + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.8.tgz", 677 + "integrity": "sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==", 666 678 "dev": true, 667 679 "license": "MIT", 668 680 "dependencies": { 669 681 "@types/prop-types": "*", 670 - "@types/scheduler": "^0.16", 671 - "csstype": "^3.2.2" 682 + "@types/scheduler": "*", 683 + "csstype": "^3.0.2" 672 684 } 673 685 }, 674 - "node_modules/@types/react-dom": { 675 - "version": "17.0.26", 676 - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz", 677 - "integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==", 686 + "node_modules/@types/scheduler": { 687 + "version": "0.26.0", 688 + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz", 689 + "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==", 678 690 "dev": true, 691 + "license": "MIT" 692 + }, 693 + "node_modules/call-bind-apply-helpers": { 694 + "version": "1.0.2", 695 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 696 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 679 697 "license": "MIT", 680 - "peerDependencies": { 681 - "@types/react": "^17.0.0" 698 + "dependencies": { 699 + "es-errors": "^1.3.0", 700 + "function-bind": "^1.1.2" 701 + }, 702 + "engines": { 703 + "node": ">= 0.4" 682 704 } 683 705 }, 684 - "node_modules/@types/scheduler": { 685 - "version": "0.16.8", 686 - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", 687 - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", 688 - "dev": true, 689 - "license": "MIT" 706 + "node_modules/call-bound": { 707 + "version": "1.0.4", 708 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 709 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 710 + "license": "MIT", 711 + "dependencies": { 712 + "call-bind-apply-helpers": "^1.0.2", 713 + "get-intrinsic": "^1.3.0" 714 + }, 715 + "engines": { 716 + "node": ">= 0.4" 717 + }, 718 + "funding": { 719 + "url": "https://github.com/sponsors/ljharb" 720 + } 690 721 }, 691 722 "node_modules/caniuse-lite": { 692 723 "version": "1.0.30001760", ··· 731 762 "node": ">=8" 732 763 } 733 764 }, 765 + "node_modules/dunder-proto": { 766 + "version": "1.0.1", 767 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 768 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 769 + "license": "MIT", 770 + "dependencies": { 771 + "call-bind-apply-helpers": "^1.0.1", 772 + "es-errors": "^1.3.0", 773 + "gopd": "^1.2.0" 774 + }, 775 + "engines": { 776 + "node": ">= 0.4" 777 + } 778 + }, 779 + "node_modules/es-define-property": { 780 + "version": "1.0.1", 781 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 782 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 783 + "license": "MIT", 784 + "engines": { 785 + "node": ">= 0.4" 786 + } 787 + }, 788 + "node_modules/es-errors": { 789 + "version": "1.3.0", 790 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 791 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 792 + "license": "MIT", 793 + "engines": { 794 + "node": ">= 0.4" 795 + } 796 + }, 797 + "node_modules/es-object-atoms": { 798 + "version": "1.1.1", 799 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 800 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 801 + "license": "MIT", 802 + "dependencies": { 803 + "es-errors": "^1.3.0" 804 + }, 805 + "engines": { 806 + "node": ">= 0.4" 807 + } 808 + }, 809 + "node_modules/function-bind": { 810 + "version": "1.1.2", 811 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 812 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 813 + "license": "MIT", 814 + "funding": { 815 + "url": "https://github.com/sponsors/ljharb" 816 + } 817 + }, 818 + "node_modules/get-intrinsic": { 819 + "version": "1.3.0", 820 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 821 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 822 + "license": "MIT", 823 + "dependencies": { 824 + "call-bind-apply-helpers": "^1.0.2", 825 + "es-define-property": "^1.0.1", 826 + "es-errors": "^1.3.0", 827 + "es-object-atoms": "^1.1.1", 828 + "function-bind": "^1.1.2", 829 + "get-proto": "^1.0.1", 830 + "gopd": "^1.2.0", 831 + "has-symbols": "^1.1.0", 832 + "hasown": "^2.0.2", 833 + "math-intrinsics": "^1.1.0" 834 + }, 835 + "engines": { 836 + "node": ">= 0.4" 837 + }, 838 + "funding": { 839 + "url": "https://github.com/sponsors/ljharb" 840 + } 841 + }, 842 + "node_modules/get-proto": { 843 + "version": "1.0.1", 844 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 845 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 846 + "license": "MIT", 847 + "dependencies": { 848 + "dunder-proto": "^1.0.1", 849 + "es-object-atoms": "^1.0.0" 850 + }, 851 + "engines": { 852 + "node": ">= 0.4" 853 + } 854 + }, 855 + "node_modules/gopd": { 856 + "version": "1.2.0", 857 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 858 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 859 + "license": "MIT", 860 + "engines": { 861 + "node": ">= 0.4" 862 + }, 863 + "funding": { 864 + "url": "https://github.com/sponsors/ljharb" 865 + } 866 + }, 867 + "node_modules/has-symbols": { 868 + "version": "1.1.0", 869 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 870 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 871 + "license": "MIT", 872 + "engines": { 873 + "node": ">= 0.4" 874 + }, 875 + "funding": { 876 + "url": "https://github.com/sponsors/ljharb" 877 + } 878 + }, 879 + "node_modules/hasown": { 880 + "version": "2.0.2", 881 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 882 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 883 + "license": "MIT", 884 + "dependencies": { 885 + "function-bind": "^1.1.2" 886 + }, 887 + "engines": { 888 + "node": ">= 0.4" 889 + } 890 + }, 734 891 "node_modules/js-tokens": { 735 892 "version": "4.0.0", 736 893 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", ··· 747 904 }, 748 905 "bin": { 749 906 "loose-envify": "cli.js" 907 + } 908 + }, 909 + "node_modules/math-intrinsics": { 910 + "version": "1.1.0", 911 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 912 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 913 + "license": "MIT", 914 + "engines": { 915 + "node": ">= 0.4" 750 916 } 751 917 }, 752 918 "node_modules/nanoid": { ··· 819 985 } 820 986 } 821 987 }, 988 + "node_modules/object-assign": { 989 + "version": "4.1.1", 990 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 991 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 992 + "license": "MIT", 993 + "engines": { 994 + "node": ">=0.10.0" 995 + } 996 + }, 997 + "node_modules/object-inspect": { 998 + "version": "1.13.4", 999 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 1000 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 1001 + "license": "MIT", 1002 + "engines": { 1003 + "node": ">= 0.4" 1004 + }, 1005 + "funding": { 1006 + "url": "https://github.com/sponsors/ljharb" 1007 + } 1008 + }, 822 1009 "node_modules/picocolors": { 823 1010 "version": "1.1.1", 824 1011 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", ··· 853 1040 "node": "^10 || ^12 || >=14" 854 1041 } 855 1042 }, 1043 + "node_modules/prop-types": { 1044 + "version": "15.8.1", 1045 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 1046 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 1047 + "license": "MIT", 1048 + "dependencies": { 1049 + "loose-envify": "^1.4.0", 1050 + "object-assign": "^4.1.1", 1051 + "react-is": "^16.13.1" 1052 + } 1053 + }, 1054 + "node_modules/qs": { 1055 + "version": "6.14.0", 1056 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 1057 + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 1058 + "license": "BSD-3-Clause", 1059 + "dependencies": { 1060 + "side-channel": "^1.1.0" 1061 + }, 1062 + "engines": { 1063 + "node": ">=0.6" 1064 + }, 1065 + "funding": { 1066 + "url": "https://github.com/sponsors/ljharb" 1067 + } 1068 + }, 856 1069 "node_modules/react": { 857 - "version": "18.3.1", 858 - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", 859 - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", 1070 + "version": "18.2.0", 1071 + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 1072 + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 860 1073 "license": "MIT", 861 1074 "dependencies": { 862 1075 "loose-envify": "^1.1.0" ··· 866 1079 } 867 1080 }, 868 1081 "node_modules/react-dom": { 869 - "version": "18.3.1", 870 - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", 871 - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", 1082 + "version": "18.2.0", 1083 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 1084 + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 872 1085 "license": "MIT", 873 1086 "dependencies": { 874 1087 "loose-envify": "^1.1.0", 875 - "scheduler": "^0.23.2" 1088 + "scheduler": "^0.23.0" 876 1089 }, 877 1090 "peerDependencies": { 878 - "react": "^18.3.1" 1091 + "react": "^18.2.0" 879 1092 } 880 1093 }, 1094 + "node_modules/react-is": { 1095 + "version": "16.13.1", 1096 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 1097 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 1098 + "license": "MIT" 1099 + }, 881 1100 "node_modules/scheduler": { 882 1101 "version": "0.23.2", 883 1102 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", ··· 899 1118 "engines": { 900 1119 "node": ">=10" 901 1120 } 1121 + }, 1122 + "node_modules/server-only": { 1123 + "version": "0.0.1", 1124 + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", 1125 + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", 1126 + "license": "MIT" 902 1127 }, 903 1128 "node_modules/sharp": { 904 1129 "version": "0.34.5", ··· 945 1170 "@img/sharp-win32-x64": "0.34.5" 946 1171 } 947 1172 }, 1173 + "node_modules/side-channel": { 1174 + "version": "1.1.0", 1175 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1176 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1177 + "license": "MIT", 1178 + "dependencies": { 1179 + "es-errors": "^1.3.0", 1180 + "object-inspect": "^1.13.3", 1181 + "side-channel-list": "^1.0.0", 1182 + "side-channel-map": "^1.0.1", 1183 + "side-channel-weakmap": "^1.0.2" 1184 + }, 1185 + "engines": { 1186 + "node": ">= 0.4" 1187 + }, 1188 + "funding": { 1189 + "url": "https://github.com/sponsors/ljharb" 1190 + } 1191 + }, 1192 + "node_modules/side-channel-list": { 1193 + "version": "1.0.0", 1194 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1195 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1196 + "license": "MIT", 1197 + "dependencies": { 1198 + "es-errors": "^1.3.0", 1199 + "object-inspect": "^1.13.3" 1200 + }, 1201 + "engines": { 1202 + "node": ">= 0.4" 1203 + }, 1204 + "funding": { 1205 + "url": "https://github.com/sponsors/ljharb" 1206 + } 1207 + }, 1208 + "node_modules/side-channel-map": { 1209 + "version": "1.0.1", 1210 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1211 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1212 + "license": "MIT", 1213 + "dependencies": { 1214 + "call-bound": "^1.0.2", 1215 + "es-errors": "^1.3.0", 1216 + "get-intrinsic": "^1.2.5", 1217 + "object-inspect": "^1.13.3" 1218 + }, 1219 + "engines": { 1220 + "node": ">= 0.4" 1221 + }, 1222 + "funding": { 1223 + "url": "https://github.com/sponsors/ljharb" 1224 + } 1225 + }, 1226 + "node_modules/side-channel-weakmap": { 1227 + "version": "1.0.2", 1228 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1229 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1230 + "license": "MIT", 1231 + "dependencies": { 1232 + "call-bound": "^1.0.2", 1233 + "es-errors": "^1.3.0", 1234 + "get-intrinsic": "^1.2.5", 1235 + "object-inspect": "^1.13.3", 1236 + "side-channel-map": "^1.0.1" 1237 + }, 1238 + "engines": { 1239 + "node": ">= 0.4" 1240 + }, 1241 + "funding": { 1242 + "url": "https://github.com/sponsors/ljharb" 1243 + } 1244 + }, 948 1245 "node_modules/source-map-js": { 949 1246 "version": "1.2.1", 950 1247 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", ··· 954 1251 "node": ">=0.10.0" 955 1252 } 956 1253 }, 1254 + "node_modules/stripe": { 1255 + "version": "14.8.0", 1256 + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.8.0.tgz", 1257 + "integrity": "sha512-Qdecqk7lx095BE829NWxrG1+69NjPuHrpZqeR61i2KO00fpKkjMX9TYjwFU+WjQD6ZcNmCGOwNfnz5VnI5bjIg==", 1258 + "license": "MIT", 1259 + "dependencies": { 1260 + "@types/node": ">=8.1.0", 1261 + "qs": "^6.11.0" 1262 + }, 1263 + "engines": { 1264 + "node": ">=12.*" 1265 + } 1266 + }, 957 1267 "node_modules/styled-jsx": { 958 1268 "version": "5.1.6", 959 1269 "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", ··· 984 1294 "license": "0BSD" 985 1295 }, 986 1296 "node_modules/typescript": { 987 - "version": "4.9.5", 988 - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 989 - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 1297 + "version": "5.1.6", 1298 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 1299 + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", 990 1300 "dev": true, 991 1301 "license": "Apache-2.0", 992 1302 "bin": { ··· 994 1304 "tsserver": "bin/tsserver" 995 1305 }, 996 1306 "engines": { 997 - "node": ">=4.2.0" 1307 + "node": ">=14.17" 998 1308 } 999 1309 } 1000 1310 }
+10 -14
package.json
··· 1 1 { 2 - "name": "eny.space", 3 - "version": "1.0.0", 4 - "description": "eny.space", 5 - "author": "Krekeny GmbH", 6 - "license": "MIT", 7 2 "private": true, 8 3 "scripts": { 9 4 "dev": "next", 10 5 "build": "next build", 11 - "start": "next start", 12 - "type-check": "tsc" 6 + "start": "next start" 13 7 }, 14 8 "dependencies": { 15 - "@stripe/stripe-js": "^8.6.0", 9 + "@stripe/react-stripe-js": "2.4.0", 10 + "@stripe/stripe-js": "2.2.2", 16 11 "next": "latest", 17 - "react": "^18.2.0", 18 - "react-dom": "^18.2.0" 12 + "react": "18.2.0", 13 + "react-dom": "18.2.0", 14 + "server-only": "0.0.1", 15 + "stripe": "14.8.0" 19 16 }, 20 17 "devDependencies": { 21 - "@types/node": "^12.12.21", 22 - "@types/react": "^17.0.2", 23 - "@types/react-dom": "^17.0.1", 24 - "typescript": "^4.8.3" 18 + "@types/node": "20.4.6", 19 + "@types/react": "18.2.8", 20 + "typescript": "5.1.6" 25 21 } 26 22 }
-14
pages/about.tsx
··· 1 - import Link from "next/link"; 2 - import Layout from "../components/Layout"; 3 - 4 - const AboutPage = () => ( 5 - <Layout title="About | Next.js + TypeScript Example"> 6 - <h1>About</h1> 7 - <p>This is the about page</p> 8 - <p> 9 - <Link href="/">Go home</Link> 10 - </p> 11 - </Layout> 12 - ); 13 - 14 - export default AboutPage;
-16
pages/api/users/index.ts
··· 1 - import { NextApiRequest, NextApiResponse } from "next"; 2 - import { sampleUserData } from "@/utils/sample-data"; 3 - 4 - const handler = (_req: NextApiRequest, res: NextApiResponse) => { 5 - try { 6 - if (!Array.isArray(sampleUserData)) { 7 - throw new Error("Cannot find user data"); 8 - } 9 - 10 - res.status(200).json(sampleUserData); 11 - } catch (err: any) { 12 - res.status(500).json({ statusCode: 500, message: err.message }); 13 - } 14 - }; 15 - 16 - export default handler;
-13
pages/index.tsx
··· 1 - import Link from "next/link"; 2 - import Layout from "../components/Layout"; 3 - 4 - const IndexPage = () => ( 5 - <Layout title="Home | Next.js + TypeScript Example"> 6 - <h1>Hello Next.js 👋</h1> 7 - <p> 8 - <Link href="/about">About</Link> 9 - </p> 10 - </Layout> 11 - ); 12 - 13 - export default IndexPage;
-61
pages/users/[id].tsx
··· 1 - import { GetStaticProps, GetStaticPaths } from "next"; 2 - 3 - import { User } from "../../interfaces"; 4 - import { sampleUserData } from "../../utils/sample-data"; 5 - import Layout from "../../components/Layout"; 6 - import ListDetail from "../../components/ListDetail"; 7 - 8 - type Props = { 9 - item?: User; 10 - errors?: string; 11 - }; 12 - 13 - const StaticPropsDetail = ({ item, errors }: Props) => { 14 - if (errors) { 15 - return ( 16 - <Layout title="Error | Next.js + TypeScript Example"> 17 - <p> 18 - <span style={{ color: "red" }}>Error:</span> {errors} 19 - </p> 20 - </Layout> 21 - ); 22 - } 23 - 24 - return ( 25 - <Layout 26 - title={`${ 27 - item ? item.name : "User Detail" 28 - } | Next.js + TypeScript Example`} 29 - > 30 - {item && <ListDetail item={item} />} 31 - </Layout> 32 - ); 33 - }; 34 - 35 - export default StaticPropsDetail; 36 - 37 - export const getStaticPaths: GetStaticPaths = async () => { 38 - // Get the paths we want to pre-render based on users 39 - const paths = sampleUserData.map((user) => ({ 40 - params: { id: user.id.toString() }, 41 - })); 42 - 43 - // We'll pre-render only these paths at build time. 44 - // { fallback: false } means other routes should 404. 45 - return { paths, fallback: false }; 46 - }; 47 - 48 - // This function gets called at build time on server-side. 49 - // It won't be called on client-side, so you can even do 50 - // direct database queries. 51 - export const getStaticProps: GetStaticProps = async ({ params }) => { 52 - try { 53 - const id = params?.id; 54 - const item = sampleUserData.find((data) => data.id === Number(id)); 55 - // By returning { props: item }, the StaticPropsDetail component 56 - // will receive `item` as a prop at build time 57 - return { props: { item } }; 58 - } catch (err: any) { 59 - return { props: { errors: err.message } }; 60 - } 61 - };
-35
pages/users/index.tsx
··· 1 - import { GetStaticProps } from "next"; 2 - import Link from "next/link"; 3 - 4 - import { User } from "../../interfaces"; 5 - import { sampleUserData } from "../../utils/sample-data"; 6 - import Layout from "../../components/Layout"; 7 - import List from "../../components/List"; 8 - 9 - type Props = { 10 - items: User[]; 11 - }; 12 - 13 - const WithStaticProps = ({ items }: Props) => ( 14 - <Layout title="Users List | Next.js + TypeScript Example"> 15 - <h1>Users List</h1> 16 - <p> 17 - Example fetching data from inside <code>getStaticProps()</code>. 18 - </p> 19 - <p>You are currently on: /users</p> 20 - <List items={items} /> 21 - <p> 22 - <Link href="/">Go home</Link> 23 - </p> 24 - </Layout> 25 - ); 26 - 27 - export const getStaticProps: GetStaticProps = async () => { 28 - // Example for including static props in a Next.js function component page. 29 - // Don't forget to include the respective types for any props passed into 30 - // the component. 31 - const items: User[] = sampleUserData; 32 - return { props: { items } }; 33 - }; 34 - 35 - export default WithStaticProps;
+1
public/checkout-one-time-payments.svg
··· 1 + <svg height="156" viewBox="0 0 240 156" width="240" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd" transform="translate(0 1)"><rect fill="#fff" height="154" opacity=".8" rx="4" width="240"/><path d="m99 0v154" opacity=".068034" stroke="#000" stroke-linecap="square"/><text style="opacity:.836844;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:18;font-weight:500;letter-spacing:-.45;fill:#8f6ed5"><tspan x="14" y="48">$29.90</tspan></text><path d="m18 17h23c1.1045695 0 2 .8954305 2 2s-.8954305 2-2 2h-23c-1.1045695 0-2-.8954305-2-2s.8954305-2 2-2z" fill="#8f6ed5"/><g transform="translate(112 109)"><path d="m3.58943928 0h109.02112172c1.248126 0 1.700727.12995586 2.157023.37398579s.8144.60213393 1.05843 1.05843023c.24403.45629629.373986.90889702.373986 2.15702326v12.42112142c0 1.2481263-.129956 1.700727-.373986 2.1570233s-.602134.8144003-1.05843 1.0584302-.908897.3739858-2.157023.3739858h-109.02112172c-1.24812624 0-1.70072697-.1299559-2.15702326-.3739858-.4562963-.2440299-.8144003-.6021339-1.05843023-1.0584302s-.37398579-.908897-.37398579-2.1570233v-12.42112142c0-1.24812624.12995586-1.70072697.37398579-2.15702326.24402993-.4562963.60213393-.8144003 1.05843023-1.05843023.45629629-.24402993.90889702-.37398579 2.15702326-.37398579z" fill="#8f6ed5"/><text style="opacity:.7;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:10;font-weight:500;letter-spacing:.12;fill:#f7fafc"><tspan x="29" y="14">Pay $29.90</tspan></text></g><g transform="translate(112 83)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><g fill-rule="nonzero" opacity=".5" transform="translate(95.2 4.2)"><rect fill="#ebf1f8" height="10.7" rx="1.4" stroke="#a450b5" stroke-width=".5" width="16.3" x=".25" y=".25"/><path d="m2.30141283 4.14010612c-.36619921-.20085195-.7841356-.36238968-1.25141283-.47449094l.01960035-.08729771h1.91588511c.25968832.00910242.47040833.08723662.53897699.36318047l.41637443 1.98511543.12755149.59789495 1.1661881-2.94619085h1.2591758l-1.87173549 4.31595242h-1.25924092zm5.11856981 3.75874553h-1.1908025l.7448132-4.32053418h1.19073738zm4.31676486-4.21490945-.1619471.93296757-.1077042-.04587865c-.2154736-.0873588-.4999065-.17465651-.8870297-.16543191-.4701478 0-.68099808.18834069-.68588189.3722218 0 .20226923.25506499.33556776.67155609.53325523.6860773.30795505 1.0043062.6848197.9995526 1.17665586-.0096374.89631352-.8230844 1.47550848-2.07255766 1.47550848-.53422342-.00464284-1.04878137-.11057303-1.32807004-.22994303l.16663551-.96986596.15680278.06903178c.38712314.16103343.64186253.22982085 1.11728491.22982085.34284328 0 .7104312-.13342072.7151196-.42286548 0-.18846286-.1566074-.32646533-.61731322-.53789807-.45074287-.20691207-1.05340471-.55158225-1.04363709-1.17213519.00507916-.8410881.83317755-1.42938548 2.00913331-1.42938548.4606407 0 .8331775.09652231 1.0680561.1839422zm1.5826792 2.68429772h.989785c-.0489683-.21601449-.27447-1.2502083-.27447-1.2502083l-.0832201-.37228289c-.058801.16085015-.1616214.42286547-.1566725.41364087 0 0-.3773555.96064136-.4754224 1.20885032zm1.4698958-2.78992245.9606775 4.32047309h-1.1025684s-.1078996-.49641792-.1420862-.64810457h-1.5288922c-.0442148.11484934-.2499207.64810457-.2499207.64810457h-1.2494733l1.7687848-3.96199655c.122551-.28040343.3383502-.35847654.6223924-.35847654z" fill="#8f6ed5"/></g><g fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500"><text opacity=".67"><tspan x="5.6" y="12.9">4242</tspan></text><text opacity=".67"><tspan x="33.6" y="12.9">11/26</tspan></text><text opacity=".67"><tspan x="61.6" y="12.9">934</tspan></text></g></g><g transform="translate(112 57)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500" opacity=".67"><tspan x="5.6" y="13.6">Jenny Rosen</tspan></text></g><g transform="translate(112 31)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500" opacity=".67"><tspan x="5.6" y="13.6">jenny@rosen.com</tspan></text></g></g></svg>
public/checkout_demo.gif

This is a binary file and will not be displayed.

+1
public/elements-card-payment.svg
··· 1 + <svg height="64" viewBox="0 0 202 64" width="202" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="#fff" height="28" opacity=".8" rx="4" width="202"/><g transform="translate(0 36)"><rect fill="#4e5663" height="28" rx="4" width="202"/><text style="opacity:.7;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:14;font-weight:500;letter-spacing:-.154;fill:#fff"><tspan x="63" y="19">Pay $29.90</tspan></text></g><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="500" opacity=".797503"><tspan x="9" y="19">4242 4242 4242 4242</tspan></text><g fill-rule="nonzero" opacity=".5" transform="translate(171 6)"><rect height="15.5" rx="2" stroke="#4f566b" stroke-width=".5" width="23.5" x=".25" y=".25"/><path d="m3.28773262 5.91443732c-.52314173-.28693137-1.12019372-.51769955-1.78773262-.6778442l.0280005-.12471102h2.73697872c.37098332.01300346.67201191.12462375.76996714.51882925l.59482061 2.83587917.18221641.85413565 1.66598301-4.20884407h1.79882257l-2.67390785 6.1656463h-1.79891559zm7.31224258 5.36963648h-1.70114643l1.06401886-6.1721917h1.70105337zm6.1668069-6.02129922-.2313529 1.3328108-.1538632-.06554092c-.3078194-.12479829-.7141522-.2495093-1.2671852-.2363313-.6716398 0-.9728545.26905813-.9798314.53174543 0 .28895604.3643786.47938251.9593659.76179318.9801104.43993579 1.4347231.97831385 1.4279323 1.68093694-.0137677 1.28044789-1.1758348 2.10786929-2.9607967 2.10786929-.7631763-.0066327-1.4982591-.1579615-1.8972429-.3284901l.2380507-1.38552277.224004.09861682c.5530331.23004776.9169465.32831555 1.5961213.32831555.4897761 0 1.0149017-.19060107 1.0215995-.60409358 0-.26923267-.2237249-.46637906-.8818761-.76842582-.6439184-.29558867-1.5048639-.78797464-1.4909101-1.67447884.0072559-1.20155443 1.1902536-2.04197926 2.8701904-2.04197926.6580581 0 1.1902536.13788902 1.5257944.26277458zm2.2609703 3.83471102h1.4139786c-.0699548-.30859213-.3921-1.78601186-.3921-1.78601186l-.1188858-.5318327c-.0840015.22978594-.2308878.60409353-.2238179.59091553 0 0-.5390794 1.3723448-.6791749 1.72692903zm2.0998512-3.9856035 1.3723964 6.1721044h-1.5750977s-.1541422-.7091684-.2029803-.9258637h-2.1841317c-.0631639.1640705-.3570296.9258637-.3570296.9258637h-1.7849619l2.5268355-5.65999506c.1750729-.40057632.4833574-.51210934.889132-.51210934z" fill="#3c4257"/></g></g></svg>
public/elements_demo.gif

This is a binary file and will not be displayed.

public/logo.png

This is a binary file and will not be displayed.

public/social_card.png

This is a binary file and will not be displayed.

+315
styles.css
··· 1 + /* Variables */ 2 + :root { 3 + --body-color: #fcfdfe; 4 + --checkout-color: #8f6ed5; 5 + --elements-color: #6772e5; 6 + --body-font-family: 7 + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 8 + --h1-color: #1a1f36; 9 + --h2-color: #7b818a; 10 + --h3-color: #a3acb9; 11 + --radius: 6px; 12 + --container-width-max: 1280px; 13 + --page-width-max: 600px; 14 + --transition-duration: 2s; 15 + } 16 + 17 + body { 18 + margin: 0; 19 + padding: 0; 20 + background: var(--body-color); 21 + overflow-y: scroll; 22 + } 23 + 24 + * { 25 + box-sizing: border-box; 26 + font-family: var(--body-font-family); 27 + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 28 + } 29 + 30 + #__next { 31 + display: flex; 32 + justify-content: center; 33 + } 34 + 35 + .container { 36 + max-width: var(--container-width-max); 37 + padding: 45px 25px; 38 + display: flex; 39 + flex-direction: row; 40 + } 41 + 42 + .page-container { 43 + padding-bottom: 60px; 44 + max-width: var(--page-width-max); 45 + } 46 + 47 + h1 { 48 + font-weight: 600; 49 + color: var(--h1-color); 50 + margin: 6px 0 12px; 51 + font-size: 27px; 52 + line-height: 32px; 53 + } 54 + 55 + h1 span.light { 56 + color: var(--h3-color); 57 + } 58 + 59 + h2 { 60 + color: var(--h2-color); 61 + margin: 8px 0; 62 + } 63 + 64 + h3 { 65 + font-size: 17px; 66 + color: var(--h3-color); 67 + margin: 8px 0; 68 + } 69 + 70 + a { 71 + color: var(--checkout-color); 72 + text-decoration: none; 73 + } 74 + 75 + header { 76 + position: relative; 77 + flex: 0 0 250px; 78 + padding-right: 48px; 79 + } 80 + 81 + .header-content { 82 + position: sticky; 83 + top: 45px; 84 + } 85 + 86 + .logo img { 87 + height: 20px; 88 + margin-bottom: 52px; 89 + } 90 + 91 + ul, 92 + li { 93 + list-style: none; 94 + padding: 0; 95 + margin: 0; 96 + } 97 + 98 + .card-list { 99 + display: flex; 100 + flex-wrap: wrap; 101 + align-content: flex-start; 102 + padding-top: 64px; 103 + } 104 + 105 + .card { 106 + display: block; 107 + border-radius: 10px; 108 + position: relative; 109 + padding: 12px; 110 + height: 320px; 111 + flex: 0 0 33%; 112 + min-width: 304px; 113 + width: 33%; 114 + margin: 0 20px 20px 0; 115 + text-decoration: none; 116 + box-shadow: 117 + -20px 20px 60px #abacad, 118 + 20px -20px 60px #ffffff; 119 + } 120 + .card h2 { 121 + color: #fff; 122 + } 123 + .card h2.bottom { 124 + position: absolute; 125 + bottom: 10px; 126 + } 127 + 128 + .card img { 129 + width: 80%; 130 + position: absolute; 131 + top: 50%; 132 + left: 50%; 133 + transform: translate(-50%, -50%); 134 + } 135 + 136 + .error-message { 137 + color: #ef2961; 138 + } 139 + 140 + .FormRow, 141 + fieldset, 142 + input[type="number"], 143 + input[type="text"] { 144 + border-radius: var(--radius); 145 + padding: 5px 12px; 146 + width: 100%; 147 + background: #fff; 148 + appearance: none; 149 + font-size: 16px; 150 + margin-top: 10px; 151 + } 152 + 153 + input[type="range"] { 154 + margin: 5px 0; 155 + width: 100%; 156 + } 157 + 158 + button { 159 + border-radius: var(--radius); 160 + color: white; 161 + font-size: larger; 162 + border: 0; 163 + padding: 12px 16px; 164 + margin-top: 10px; 165 + font-weight: 600; 166 + cursor: pointer; 167 + transition: all 0.2s ease; 168 + display: block; 169 + width: 100%; 170 + } 171 + button:disabled { 172 + opacity: 0.5; 173 + cursor: not-allowed; 174 + } 175 + 176 + .elements-style { 177 + color: var(--elements-color); 178 + border: 1px solid var(--elements-color); 179 + } 180 + .elements-style-background { 181 + background: var(--elements-color); 182 + transition: box-shadow var(--transition-duration); 183 + } 184 + .card.elements-style-background:hover { 185 + box-shadow: 186 + 20px 20px 60px #464e9c, 187 + -20px -20px 60px #8896ff; 188 + } 189 + .checkout-style { 190 + color: var(--checkout-color); 191 + border: 1px solid var(--checkout-color); 192 + } 193 + .checkout-style-background { 194 + background: var(--checkout-color); 195 + transition: box-shadow var(--transition-duration); 196 + } 197 + .card.checkout-style-background:hover { 198 + box-shadow: 199 + 20px 20px 60px #614b91, 200 + -20px -20px 60px #bd91ff; 201 + } 202 + 203 + /* Test card number */ 204 + .test-card-notice { 205 + display: block; 206 + margin-block-start: 1em; 207 + margin-block-end: 1em; 208 + margin-inline-start: 0px; 209 + margin-inline-end: 0px; 210 + } 211 + .card-number { 212 + display: inline; 213 + white-space: nowrap; 214 + font-family: Menlo, Consolas, monospace; 215 + color: #3c4257; 216 + font-weight: 500; 217 + } 218 + .card-number span { 219 + display: inline-block; 220 + width: 4px; 221 + } 222 + 223 + /* Code block */ 224 + code, 225 + pre { 226 + font-family: "SF Mono", "IBM Plex Mono", "Menlo", monospace; 227 + font-size: 12px; 228 + background: rgba(0, 0, 0, 0.03); 229 + padding: 12px; 230 + border-radius: var(--radius); 231 + max-height: 500px; 232 + width: var(--page-width-max); 233 + overflow: auto; 234 + } 235 + 236 + .banner { 237 + max-width: 825px; 238 + margin: 0 auto; 239 + font-size: 14px; 240 + background: var(--body-color); 241 + color: #6a7c94; 242 + border-radius: 50px; 243 + box-shadow: 244 + -20px 20px 60px #abacad, 245 + 20px -20px 60px #ffffff; 246 + display: flex; 247 + align-items: center; 248 + box-sizing: border-box; 249 + padding: 15px; 250 + line-height: 1.15; 251 + position: fixed; 252 + bottom: 2vh; 253 + left: 0; 254 + right: 0; 255 + text-align: center; 256 + justify-content: center; 257 + } 258 + 259 + @media only screen and (max-width: 980px) { 260 + .container { 261 + flex-direction: column; 262 + } 263 + 264 + .header-content { 265 + max-width: 280px; 266 + position: relative; 267 + top: 0; 268 + } 269 + 270 + .card { 271 + margin: 0 20px 20px 0; 272 + box-shadow: none; 273 + } 274 + 275 + .card-list { 276 + padding-top: 0; 277 + } 278 + 279 + .banner { 280 + box-shadow: none; 281 + bottom: 0; 282 + } 283 + } 284 + 285 + @media only screen and (max-width: 600px) { 286 + .container { 287 + flex-direction: column; 288 + } 289 + 290 + .card { 291 + display: block; 292 + border-radius: 8px; 293 + flex: 1 0 100%; 294 + max-width: 100%; 295 + padding-left: 0; 296 + padding-right: 0; 297 + margin: 0 0 20px 0; 298 + box-shadow: none; 299 + } 300 + 301 + .card-list { 302 + padding-top: 0; 303 + } 304 + 305 + code, 306 + pre, 307 + h3 { 308 + display: none; 309 + } 310 + 311 + .banner { 312 + box-shadow: none; 313 + bottom: 0; 314 + } 315 + }
+30 -9
tsconfig.json
··· 1 1 { 2 2 "compilerOptions": { 3 - "target": "es5", 3 + "target": "ES2019", 4 4 "lib": [ 5 5 "dom", 6 6 "dom.iterable", ··· 8 8 ], 9 9 "allowJs": true, 10 10 "skipLibCheck": true, 11 - "strict": false, 11 + "strict": true, 12 12 "forceConsistentCasingInFileNames": true, 13 13 "noEmit": true, 14 14 "esModuleInterop": true, 15 15 "module": "esnext", 16 16 "moduleResolution": "node", 17 17 "resolveJsonModule": true, 18 + "allowSyntheticDefaultImports": true, 18 19 "isolatedModules": true, 20 + "incremental": true, 19 21 "jsx": "react-jsx", 22 + "baseUrl": ".", 20 23 "paths": { 21 - "@/*": [ 22 - "./*" 24 + "@/actions/*": [ 25 + "app/actions/*" 26 + ], 27 + "@/components/*": [ 28 + "app/components/*" 29 + ], 30 + "@/config": [ 31 + "config/" 32 + ], 33 + "@/lib/*": [ 34 + "lib/*" 35 + ], 36 + "@/utils/*": [ 37 + "utils/*" 23 38 ] 24 39 }, 25 - "incremental": true 40 + "plugins": [ 41 + { 42 + "name": "next" 43 + } 44 + ] 26 45 }, 46 + "exclude": [ 47 + "node_modules" 48 + ], 27 49 "include": [ 28 50 "next-env.d.ts", 29 51 "**/*.ts", 30 - "**/*.tsx" 31 - ], 32 - "exclude": [ 33 - "node_modules" 52 + "**/*.tsx", 53 + ".next/types/**/*.ts", 54 + ".next/dev/types/**/*.ts" 34 55 ] 35 56 }
+31 -8
types/routes.d.ts
··· 1 1 // This file is generated automatically by Next.js 2 2 // Do not edit this file manually 3 3 4 - type AppRoutes = never 5 - type PageRoutes = "/" | "/about" | "/users" | "/users/[id]" 6 - type LayoutRoutes = never 4 + type AppRoutes = "/" | "/donate-with-checkout" | "/donate-with-checkout/result" | "/donate-with-elements" | "/donate-with-elements/result" | "/donate-with-embedded-checkout" | "/donate-with-embedded-checkout/result" 5 + type AppRouteHandlerRoutes = "/api/webhooks" 6 + type PageRoutes = never 7 + type LayoutRoutes = "/" | "/donate-with-checkout/result" | "/donate-with-elements/result" | "/donate-with-embedded-checkout/result" 7 8 type RedirectRoutes = never 8 9 type RewriteRoutes = never 9 - type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes 10 + type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes | AppRouteHandlerRoutes 10 11 11 12 12 13 interface ParamMap { 13 14 "/": {} 14 - "/about": {} 15 - "/users": {} 16 - "/users/[id]": { "id": string; } 15 + "/api/webhooks": {} 16 + "/donate-with-checkout": {} 17 + "/donate-with-checkout/result": {} 18 + "/donate-with-elements": {} 19 + "/donate-with-elements/result": {} 20 + "/donate-with-embedded-checkout": {} 21 + "/donate-with-embedded-checkout/result": {} 17 22 } 18 23 19 24 20 25 export type ParamsOf<Route extends Routes> = ParamMap[Route] 21 26 22 27 interface LayoutSlotMap { 28 + "/": never 29 + "/donate-with-checkout/result": never 30 + "/donate-with-elements/result": never 31 + "/donate-with-embedded-checkout/result": never 23 32 } 24 33 25 34 26 - export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap } 35 + export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap, AppRouteHandlerRoutes } 27 36 28 37 declare global { 29 38 /** ··· 55 64 children: React.ReactNode 56 65 } & { 57 66 [K in LayoutSlotMap[LayoutRoute]]: React.ReactNode 67 + } 68 + 69 + /** 70 + * Context for Next.js App Router route handlers 71 + * @example 72 + * ```tsx 73 + * export async function GET(request: NextRequest, context: RouteContext<'/api/users/[id]'>) { 74 + * const { id } = await context.params 75 + * return Response.json({ id }) 76 + * } 77 + * ``` 78 + */ 79 + interface RouteContext<AppRouteHandlerRoute extends AppRouteHandlerRoutes> { 80 + params: Promise<ParamMap[AppRouteHandlerRoute]> 58 81 } 59 82 }
+114 -42
types/validator.ts
··· 2 2 // Do not edit this file manually 3 3 // This file validates that all pages and layouts export the correct types 4 4 5 + import type { AppRoutes, LayoutRoutes, ParamMap, AppRouteHandlerRoutes } from "./routes.js" 6 + import type { ResolvingMetadata, ResolvingViewport } from "next/types.js" 7 + import type { NextRequest } from 'next/server.js' 5 8 6 - import type { NextApiHandler } from "next/types.js" 9 + type AppPageConfig<Route extends AppRoutes = AppRoutes> = { 10 + default: React.ComponentType<{ params: Promise<ParamMap[Route]> } & any> | ((props: { params: Promise<ParamMap[Route]> } & any) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>) 11 + generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[] 12 + generateMetadata?: ( 13 + props: { params: Promise<ParamMap[Route]> } & any, 14 + parent: ResolvingMetadata 15 + ) => Promise<any> | any 16 + generateViewport?: ( 17 + props: { params: Promise<ParamMap[Route]> } & any, 18 + parent: ResolvingViewport 19 + ) => Promise<any> | any 20 + metadata?: any 21 + viewport?: any 22 + } 7 23 8 - type PagesPageConfig = { 9 - default: React.ComponentType<any> | ((props: any) => React.ReactNode | Promise<React.ReactNode> | never | void) 10 - getStaticProps?: (context: any) => Promise<any> | any 11 - getStaticPaths?: (context: any) => Promise<any> | any 12 - getServerSideProps?: (context: any) => Promise<any> | any 13 - getInitialProps?: (context: any) => Promise<any> | any 14 - /** 15 - * Segment configuration for legacy Pages Router pages. 16 - * Validated at build-time by parsePagesSegmentConfig. 17 - */ 18 - config?: { 19 - maxDuration?: number 20 - runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const 21 - regions?: string[] 22 - } 24 + type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = { 25 + default: React.ComponentType<LayoutProps<Route>> | ((props: LayoutProps<Route>) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>) 26 + generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[] 27 + generateMetadata?: ( 28 + props: { params: Promise<ParamMap[Route]> } & any, 29 + parent: ResolvingMetadata 30 + ) => Promise<any> | any 31 + generateViewport?: ( 32 + props: { params: Promise<ParamMap[Route]> } & any, 33 + parent: ResolvingViewport 34 + ) => Promise<any> | any 35 + metadata?: any 36 + viewport?: any 23 37 } 24 38 25 - type ApiRouteConfig = { 26 - default: (req: any, res: any) => ReturnType<NextApiHandler> 27 - config?: { 28 - api?: { 29 - bodyParser?: boolean | { sizeLimit?: string } 30 - responseLimit?: string | number | boolean 31 - externalResolver?: boolean 32 - } 33 - runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const 34 - maxDuration?: number 35 - } 39 + type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRoutes> = { 40 + GET?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void 41 + POST?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void 42 + PUT?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void 43 + PATCH?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void 44 + DELETE?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void 45 + HEAD?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void 46 + OPTIONS?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void 36 47 } 37 48 38 49 50 + // Validate ../../app/donate-with-checkout/page.tsx 51 + { 52 + type __IsExpected<Specific extends AppPageConfig<"/donate-with-checkout">> = Specific 53 + const handler = {} as typeof import("../../app/donate-with-checkout/page.js") 54 + type __Check = __IsExpected<typeof handler> 55 + // @ts-ignore 56 + type __Unused = __Check 57 + } 39 58 59 + // Validate ../../app/donate-with-checkout/result/page.tsx 60 + { 61 + type __IsExpected<Specific extends AppPageConfig<"/donate-with-checkout/result">> = Specific 62 + const handler = {} as typeof import("../../app/donate-with-checkout/result/page.js") 63 + type __Check = __IsExpected<typeof handler> 64 + // @ts-ignore 65 + type __Unused = __Check 66 + } 40 67 68 + // Validate ../../app/donate-with-elements/page.tsx 69 + { 70 + type __IsExpected<Specific extends AppPageConfig<"/donate-with-elements">> = Specific 71 + const handler = {} as typeof import("../../app/donate-with-elements/page.js") 72 + type __Check = __IsExpected<typeof handler> 73 + // @ts-ignore 74 + type __Unused = __Check 75 + } 41 76 77 + // Validate ../../app/donate-with-elements/result/page.tsx 78 + { 79 + type __IsExpected<Specific extends AppPageConfig<"/donate-with-elements/result">> = Specific 80 + const handler = {} as typeof import("../../app/donate-with-elements/result/page.js") 81 + type __Check = __IsExpected<typeof handler> 82 + // @ts-ignore 83 + type __Unused = __Check 84 + } 42 85 43 - // Validate ../../pages/about.tsx 86 + // Validate ../../app/donate-with-embedded-checkout/page.tsx 44 87 { 45 - type __IsExpected<Specific extends PagesPageConfig> = Specific 46 - const handler = {} as typeof import("../../pages/about.js") 88 + type __IsExpected<Specific extends AppPageConfig<"/donate-with-embedded-checkout">> = Specific 89 + const handler = {} as typeof import("../../app/donate-with-embedded-checkout/page.js") 47 90 type __Check = __IsExpected<typeof handler> 48 91 // @ts-ignore 49 92 type __Unused = __Check 50 93 } 51 94 52 - // Validate ../../pages/index.tsx 95 + // Validate ../../app/donate-with-embedded-checkout/result/page.tsx 53 96 { 54 - type __IsExpected<Specific extends PagesPageConfig> = Specific 55 - const handler = {} as typeof import("../../pages/index.js") 97 + type __IsExpected<Specific extends AppPageConfig<"/donate-with-embedded-checkout/result">> = Specific 98 + const handler = {} as typeof import("../../app/donate-with-embedded-checkout/result/page.js") 56 99 type __Check = __IsExpected<typeof handler> 57 100 // @ts-ignore 58 101 type __Unused = __Check 59 102 } 60 103 61 - // Validate ../../pages/users/[id].tsx 104 + // Validate ../../app/page.tsx 62 105 { 63 - type __IsExpected<Specific extends PagesPageConfig> = Specific 64 - const handler = {} as typeof import("../../pages/users/[id].js") 106 + type __IsExpected<Specific extends AppPageConfig<"/">> = Specific 107 + const handler = {} as typeof import("../../app/page.js") 65 108 type __Check = __IsExpected<typeof handler> 66 109 // @ts-ignore 67 110 type __Unused = __Check 68 111 } 69 112 70 - // Validate ../../pages/users/index.tsx 113 + // Validate ../../app/api/webhooks/route.ts 71 114 { 72 - type __IsExpected<Specific extends PagesPageConfig> = Specific 73 - const handler = {} as typeof import("../../pages/users/index.js") 115 + type __IsExpected<Specific extends RouteHandlerConfig<"/api/webhooks">> = Specific 116 + const handler = {} as typeof import("../../app/api/webhooks/route.js") 74 117 type __Check = __IsExpected<typeof handler> 75 118 // @ts-ignore 76 119 type __Unused = __Check 77 120 } 78 121 79 - // Validate ../../pages/api/users/index.ts 122 + 123 + 124 + 125 + 126 + // Validate ../../app/donate-with-checkout/result/layout.tsx 80 127 { 81 - type __IsExpected<Specific extends ApiRouteConfig> = Specific 82 - const handler = {} as typeof import("../../pages/api/users/index.js") 128 + type __IsExpected<Specific extends LayoutConfig<"/donate-with-checkout/result">> = Specific 129 + const handler = {} as typeof import("../../app/donate-with-checkout/result/layout.js") 130 + type __Check = __IsExpected<typeof handler> 131 + // @ts-ignore 132 + type __Unused = __Check 133 + } 134 + 135 + // Validate ../../app/donate-with-elements/result/layout.tsx 136 + { 137 + type __IsExpected<Specific extends LayoutConfig<"/donate-with-elements/result">> = Specific 138 + const handler = {} as typeof import("../../app/donate-with-elements/result/layout.js") 83 139 type __Check = __IsExpected<typeof handler> 84 140 // @ts-ignore 85 141 type __Unused = __Check 86 142 } 87 143 144 + // Validate ../../app/donate-with-embedded-checkout/result/layout.tsx 145 + { 146 + type __IsExpected<Specific extends LayoutConfig<"/donate-with-embedded-checkout/result">> = Specific 147 + const handler = {} as typeof import("../../app/donate-with-embedded-checkout/result/layout.js") 148 + type __Check = __IsExpected<typeof handler> 149 + // @ts-ignore 150 + type __Unused = __Check 151 + } 88 152 153 + // Validate ../../app/layout.tsx 154 + { 155 + type __IsExpected<Specific extends LayoutConfig<"/">> = Specific 156 + const handler = {} as typeof import("../../app/layout.js") 157 + type __Check = __IsExpected<typeof handler> 158 + // @ts-ignore 159 + type __Unused = __Check 160 + }
+11 -7
utils/get-stripejs.ts
··· 1 + /** 2 + * This is a singleton to ensure we only instantiate Stripe once. 3 + */ 1 4 import { Stripe, loadStripe } from "@stripe/stripe-js"; 2 5 3 6 let stripePromise: Promise<Stripe | null>; 4 - const getStripe = () => { 5 - if (!stripePromise) { 6 - stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); 7 - } 8 - return stripePromise; 9 - }; 10 7 11 - export default getStripe; 8 + export default function getStripe(): Promise<Stripe | null> { 9 + if (!stripePromise) 10 + stripePromise = loadStripe( 11 + process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string, 12 + ); 13 + 14 + return stripePromise; 15 + }
-9
utils/sample-data.ts
··· 1 - import { User } from "../interfaces"; 2 - 3 - /** Dummy user data. */ 4 - export const sampleUserData: User[] = [ 5 - { id: 101, name: "Alice" }, 6 - { id: 102, name: "Bob" }, 7 - { id: 103, name: "Caroline" }, 8 - { id: 104, name: "Dave" }, 9 - ];
+30
utils/stripe-helpers.ts
··· 1 + export function formatAmountForDisplay( 2 + amount: number, 3 + currency: string, 4 + ): string { 5 + let numberFormat = new Intl.NumberFormat(["en-US"], { 6 + style: "currency", 7 + currency: currency, 8 + currencyDisplay: "symbol", 9 + }); 10 + return numberFormat.format(amount); 11 + } 12 + 13 + export function formatAmountForStripe( 14 + amount: number, 15 + currency: string, 16 + ): number { 17 + let numberFormat = new Intl.NumberFormat(["en-US"], { 18 + style: "currency", 19 + currency: currency, 20 + currencyDisplay: "symbol", 21 + }); 22 + const parts = numberFormat.formatToParts(amount); 23 + let zeroDecimalCurrency: boolean = true; 24 + for (let part of parts) { 25 + if (part.type === "decimal") { 26 + zeroDecimalCurrency = false; 27 + } 28 + } 29 + return zeroDecimalCurrency ? amount : Math.round(amount * 100); 30 + }