eny.space Landingpage

feat(app): add Vercel Speed Insights, adjust settings and clean up my previous mess

Changed files
+54 -330
app
actions
components
donate-with-elements
donate-with-embedded-checkout
config
+6 -6
app/actions/stripe.ts
··· 9 9 import { stripe } from "@/lib/stripe"; 10 10 11 11 export async function createCheckoutSession( 12 - data: FormData, 12 + data: FormData 13 13 ): Promise<{ client_secret: string | null; url: string | null }> { 14 14 const ui_mode = data.get( 15 - "uiMode", 15 + "uiMode" 16 16 ) as Stripe.Checkout.SessionCreateParams.UiMode; 17 17 18 18 const headersList = await headers(); 19 19 const originHeader = headersList.get("origin"); 20 20 const hostHeader = headersList.get("host"); 21 - 21 + 22 22 let origin: string; 23 23 if (originHeader) { 24 24 origin = originHeader; ··· 42 42 }, 43 43 unit_amount: formatAmountForStripe( 44 44 Number(data.get("customDonation") as string), 45 - CURRENCY, 45 + CURRENCY 46 46 ), 47 47 }, 48 48 }, ··· 64 64 } 65 65 66 66 export async function createPaymentIntent( 67 - data: FormData, 67 + data: FormData 68 68 ): Promise<{ client_secret: string }> { 69 69 const paymentIntent: Stripe.PaymentIntent = 70 70 await stripe.paymentIntents.create({ 71 71 amount: formatAmountForStripe( 72 72 Number(data.get("customDonation") as string), 73 - CURRENCY, 73 + CURRENCY 74 74 ), 75 75 automatic_payment_methods: { enabled: true }, 76 76 currency: CURRENCY,
+4 -3
app/components/CheckoutForm.tsx
··· 28 28 const [clientSecret, setClientSecret] = useState<string | null>(null); 29 29 30 30 const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = ( 31 - e, 31 + e 32 32 ): void => 33 33 setInput({ 34 34 ...input, ··· 37 37 38 38 const formAction = async (data: FormData): Promise<void> => { 39 39 const uiMode = data.get( 40 - "uiMode", 40 + "uiMode" 41 41 ) as Stripe.Checkout.SessionCreateParams.UiMode; 42 42 const { client_secret, url } = await createCheckoutSession(data); 43 43 ··· 66 66 type="submit" 67 67 disabled={loading} 68 68 > 69 - Purchase {formatAmountForDisplay(input.customDonation, config.CURRENCY)} 69 + Purchase{" "} 70 + {formatAmountForDisplay(input.customDonation, config.CURRENCY)} 70 71 </button> 71 72 </form> 72 73 {clientSecret ? (
-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 - Purchase {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 - }
-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 - }
-5
app/donate-with-elements/page.tsx
··· 1 - import { redirect } from "next/navigation"; 2 - 3 - export default function PaymentElementPage() { 4 - redirect("/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("/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 - }
+3 -3
app/layout.tsx
··· 1 1 import type { Metadata } from "next"; 2 + import { SpeedInsights } from "@vercel/speed-insights/next"; 2 3 3 4 import "../styles.css"; 4 5 ··· 24 25 <div className="container"> 25 26 <header> 26 27 <div className="header-content"> 27 - <h1> 28 - Easy PDS 29 - </h1> 28 + <h1>Easy PDS</h1> 30 29 </div> 31 30 </header> 32 31 {children} 33 32 </div> 33 + <SpeedInsights /> 34 34 </body> 35 35 </html> 36 36 );
+1 -4
app/page.tsx
··· 10 10 return ( 11 11 <ul className="card-list"> 12 12 <li> 13 - <Link 14 - href="/checkout" 15 - className="card checkout-style-background" 16 - > 13 + <Link href="/checkout" className="card checkout-style-background"> 17 14 <h2 className="bottom">Purchase Hosting</h2> 18 15 <img src="/checkout-one-time-payments.svg" /> 19 16 </Link>
+3 -3
config/index.ts
··· 1 - export const CURRENCY = "usd"; 1 + export const CURRENCY = "eur"; 2 2 // Set your amount limits: Use float for decimal currencies and 3 3 // Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal. 4 4 export const MIN_AMOUNT = 10.0; 5 - export const MAX_AMOUNT = 5000.0; 6 - export const AMOUNT_STEP = 5.0; 5 + export const MAX_AMOUNT = 100.0; 6 + export const AMOUNT_STEP = 1.0;
+36 -1
package-lock.json
··· 1 1 { 2 - "name": "with-stripe-typescript-app", 2 + "name": "eny-space", 3 3 "lockfileVersion": 3, 4 4 "requires": true, 5 5 "packages": { ··· 7 7 "dependencies": { 8 8 "@stripe/react-stripe-js": "2.4.0", 9 9 "@stripe/stripe-js": "2.2.2", 10 + "@vercel/speed-insights": "^1.3.1", 10 11 "next": "latest", 11 12 "react": "18.2.0", 12 13 "react-dom": "18.2.0", ··· 689 690 "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==", 690 691 "dev": true, 691 692 "license": "MIT" 693 + }, 694 + "node_modules/@vercel/speed-insights": { 695 + "version": "1.3.1", 696 + "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.3.1.tgz", 697 + "integrity": "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==", 698 + "license": "Apache-2.0", 699 + "peerDependencies": { 700 + "@sveltejs/kit": "^1 || ^2", 701 + "next": ">= 13", 702 + "react": "^18 || ^19 || ^19.0.0-rc", 703 + "svelte": ">= 4", 704 + "vue": "^3", 705 + "vue-router": "^4" 706 + }, 707 + "peerDependenciesMeta": { 708 + "@sveltejs/kit": { 709 + "optional": true 710 + }, 711 + "next": { 712 + "optional": true 713 + }, 714 + "react": { 715 + "optional": true 716 + }, 717 + "svelte": { 718 + "optional": true 719 + }, 720 + "vue": { 721 + "optional": true 722 + }, 723 + "vue-router": { 724 + "optional": true 725 + } 726 + } 692 727 }, 693 728 "node_modules/call-bind-apply-helpers": { 694 729 "version": "1.0.2",
+1
package.json
··· 8 8 "dependencies": { 9 9 "@stripe/react-stripe-js": "2.4.0", 10 10 "@stripe/stripe-js": "2.2.2", 11 + "@vercel/speed-insights": "^1.3.1", 11 12 "next": "latest", 12 13 "react": "18.2.0", 13 14 "react-dom": "18.2.0",