From 798f65c4008f7fa265da584a6a8b4a72870b1ecc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:50:04 +0000 Subject: [PATCH 1/7] feat: add custom Stripe checkout page using coss/ui components Co-Authored-By: sean@cal.com --- apps/ui/app/checkout/page.tsx | 360 ++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 apps/ui/app/checkout/page.tsx diff --git a/apps/ui/app/checkout/page.tsx b/apps/ui/app/checkout/page.tsx new file mode 100644 index 000000000..60a2831c9 --- /dev/null +++ b/apps/ui/app/checkout/page.tsx @@ -0,0 +1,360 @@ +"use client"; + +import { CreditCardIcon, LockIcon, ShieldCheckIcon } from "lucide-react"; +import { type FormEvent, useState } from "react"; + +import { Badge } from "@/registry/default/ui/badge"; +import { Button } from "@/registry/default/ui/button"; +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardPanel, + CardTitle, +} from "@/registry/default/ui/card"; +import { Field, FieldError, FieldLabel } from "@/registry/default/ui/field"; +import { Form } from "@/registry/default/ui/form"; +import { Input } from "@/registry/default/ui/input"; +import { Label } from "@/registry/default/ui/label"; +import { + Select, + SelectItem, + SelectPopup, + SelectTrigger, + SelectValue, +} from "@/registry/default/ui/select"; +import { Separator } from "@/registry/default/ui/separator"; +import { Spinner } from "@/registry/default/ui/spinner"; + +const countryOptions = [ + { label: "United States", value: "us" }, + { label: "Canada", value: "ca" }, + { label: "United Kingdom", value: "uk" }, + { label: "Germany", value: "de" }, + { label: "France", value: "fr" }, + { label: "Australia", value: "au" }, +]; + +const orderItems = [ + { id: "pro-plan", name: "Pro Plan (Annual)", price: 199.0, quantity: 1 }, + { id: "seats", name: "Additional Seats (5)", price: 49.0, quantity: 1 }, +]; + +export default function CheckoutPage() { + const [loading, setLoading] = useState(false); + const [cardNumber, setCardNumber] = useState(""); + const [expiry, setExpiry] = useState(""); + const [cvc, setCvc] = useState(""); + + const subtotal = orderItems.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + const tax = subtotal * 0.1; + const total = subtotal + tax; + + const formatCardNumber = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + const matches = v.match(/\d{4,16}/g); + const match = matches?.[0] || ""; + const parts = []; + for (let i = 0, len = match.length; i < len; i += 4) { + parts.push(match.substring(i, i + 4)); + } + if (parts.length) { + return parts.join(" "); + } + return value; + }; + + const formatExpiry = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + if (v.length >= 2) { + return `${v.substring(0, 2)}/${v.substring(2, 4)}`; + } + return v; + }; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert("Payment successful! Thank you for your purchase."); + }; + + return ( +
+
+ + +
+
+ + Enter your payment information to complete your purchase + +
+
+ +
+
+ +
+ + First Name + + Please enter your first name. + + + Last Name + + Please enter your last name. + +
+ + Email + + Please enter a valid email. + +
+ + + +
+ + + Card Number + + setCardNumber(formatCardNumber(e.target.value)) + } + placeholder="4242 4242 4242 4242" + required + type="text" + value={cardNumber} + /> + Please enter a valid card number. + +
+ + Expiration Date + + setExpiry(formatExpiry(e.target.value)) + } + placeholder="MM/YY" + required + type="text" + value={expiry} + /> + Please enter a valid expiry date. + + + CVC + + setCvc(e.target.value.replace(/[^0-9]/g, "")) + } + placeholder="123" + required + type="text" + value={cvc} + /> + Please enter a valid CVC. + +
+
+ + + +
+ + + Country + + + + Street Address + + Please enter your address. + +
+ + City + + Please enter your city. + + + State + + Please enter your state. + + + ZIP Code + + Please enter your ZIP code. + +
+
+
+
+ + +
+
+
+
+
+ +
+ + + Order Summary + + +
+ {orderItems.map((item) => ( +
+
+ {item.name} + + Qty: {item.quantity} + +
+ + ${(item.price * item.quantity).toFixed(2)} + +
+ ))} + +
+ + Subtotal + + ${subtotal.toFixed(2)} +
+
+ + Tax (10%) + + ${tax.toFixed(2)} +
+ +
+ Total + + ${total.toFixed(2)} + +
+
+
+
+ + + +
+
+ + + + 256-bit SSL Encryption + +
+

+ Your payment information is processed securely. We do not + store credit card details nor have access to your credit card + information. +

+
+
+
+
+
+
+ ); +} From ae950ed3fdc630e926220c9bb2bfe8ce76aa9047 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:57:29 +0000 Subject: [PATCH 2/7] feat: add Apple Pay, Google Pay, and SEPA payment methods to checkout page Co-Authored-By: sean@cal.com --- apps/ui/app/checkout/page.tsx | 645 ++++++++++++++++++++++++---------- 1 file changed, 467 insertions(+), 178 deletions(-) diff --git a/apps/ui/app/checkout/page.tsx b/apps/ui/app/checkout/page.tsx index 60a2831c9..4de6d42df 100644 --- a/apps/ui/app/checkout/page.tsx +++ b/apps/ui/app/checkout/page.tsx @@ -1,6 +1,12 @@ "use client"; -import { CreditCardIcon, LockIcon, ShieldCheckIcon } from "lucide-react"; +import { + BanknoteIcon, + CreditCardIcon, + LockIcon, + ShieldCheckIcon, + SmartphoneIcon, +} from "lucide-react"; import { type FormEvent, useState } from "react"; import { Badge } from "@/registry/default/ui/badge"; @@ -8,7 +14,6 @@ import { Button } from "@/registry/default/ui/button"; import { Card, CardDescription, - CardFooter, CardHeader, CardPanel, CardTitle, @@ -26,6 +31,7 @@ import { } from "@/registry/default/ui/select"; import { Separator } from "@/registry/default/ui/separator"; import { Spinner } from "@/registry/default/ui/spinner"; +import { Tabs, TabsList, TabsPanel, TabsTab } from "@/registry/default/ui/tabs"; const countryOptions = [ { label: "United States", value: "us" }, @@ -36,16 +42,57 @@ const countryOptions = [ { label: "Australia", value: "au" }, ]; +const sepaCountryOptions = [ + { label: "Germany", value: "de" }, + { label: "France", value: "fr" }, + { label: "Netherlands", value: "nl" }, + { label: "Belgium", value: "be" }, + { label: "Austria", value: "at" }, + { label: "Spain", value: "es" }, + { label: "Italy", value: "it" }, +]; + const orderItems = [ { id: "pro-plan", name: "Pro Plan (Annual)", price: 199.0, quantity: 1 }, { id: "seats", name: "Additional Seats (5)", price: 49.0, quantity: 1 }, ]; +function ApplePayIcon({ className }: { className?: string }) { + return ( + + ); +} + +function GooglePayIcon({ className }: { className?: string }) { + return ( + + ); +} + export default function CheckoutPage() { const [loading, setLoading] = useState(false); const [cardNumber, setCardNumber] = useState(""); const [expiry, setExpiry] = useState(""); const [cvc, setCvc] = useState(""); + const [iban, setIban] = useState(""); const subtotal = orderItems.reduce( (sum, item) => sum + item.price * item.quantity, @@ -76,6 +123,15 @@ export default function CheckoutPage() { return v; }; + const formatIban = (value: string) => { + const v = value.replace(/\s+/g, "").toUpperCase(); + const parts = []; + for (let i = 0, len = v.length; i < len; i += 4) { + parts.push(v.substring(i, i + 4)); + } + return parts.join(" "); + }; + const onSubmit = async (e: FormEvent) => { e.preventDefault(); setLoading(true); @@ -84,6 +140,13 @@ export default function CheckoutPage() { alert("Payment successful! Thank you for your purchase."); }; + const handleWalletPayment = async (wallet: string) => { + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert(`${wallet} payment successful! Thank you for your purchase.`); + }; + return (
@@ -94,196 +157,422 @@ export default function CheckoutPage() { Payment Details
- Enter your payment information to complete your purchase + Choose your preferred payment method -
- -
-
- -
- - First Name - - Please enter your first name. - - - Last Name - - Please enter your last name. - + + + + + + + + + + + + + +
+
+ +
+ + First Name + + Please enter your first name. + + + Last Name + + Please enter your last name. + +
+ + Email + + Please enter a valid email. + +
+ + + +
+ + + Card Number + + setCardNumber(formatCardNumber(e.target.value)) + } + placeholder="4242 4242 4242 4242" + required + type="text" + value={cardNumber} + /> + + Please enter a valid card number. + + +
+ + Expiration Date + + setExpiry(formatExpiry(e.target.value)) + } + placeholder="MM/YY" + required + type="text" + value={expiry} + /> + + Please enter a valid expiry date. + + + + CVC + + setCvc(e.target.value.replace(/[^0-9]/g, "")) + } + placeholder="123" + required + type="text" + value={cvc} + /> + Please enter a valid CVC. + +
+
+ + + +
+ + + Country + + + + Street Address + + Please enter your address. + +
+ + City + + Please enter your city. + + + State + + Please enter your state. + + + ZIP Code + + Please enter your ZIP code. + +
+
- - Email - +
+ size="lg" + type="submit" + > + {loading ? ( + <> +
+ + - + +
+
+ +

+ Pay quickly and securely with your preferred digital + wallet. +

+
-
- - - Card Number - + + + +
+ + + +
+
+
+
+ +
+
+
- + +
+
+
+ +

+ Pay directly from your European bank account. Available + for customers in the Single Euro Payments Area. +

+
-
- - - Country - - - - Street Address - + + + Full Name + + + Please enter the account holder name. + + + + Email + + Please enter a valid email. + +
+ + + +
+ + + IBAN + setIban(formatIban(e.target.value))} + placeholder="DE89 3704 0044 0532 0130 00" + required + type="text" + value={iban} + /> + Please enter a valid IBAN. + + + Country + + +
+ +
+

+ By providing your IBAN and confirming this payment, you + authorize (A) this company and Stripe, our payment + service provider, to send instructions to your bank to + debit your account and (B) your bank to debit your + account in accordance with those instructions. You are + entitled to a refund from your bank under the terms and + conditions of your agreement with your bank. +

+
+
+
+ +
+
-
- -
- - -
-
-
- + + + +
From a788693165b3cc0ae34c9965239890c97835c93f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:07:20 +0000 Subject: [PATCH 3/7] feat: convert checkout page to particle component (p-form-3) Co-Authored-By: sean@cal.com --- .../registry/default/particles/p-form-3.tsx | 637 ++++++++++++++++++ apps/ui/registry/registry-particles.ts | 24 + 2 files changed, 661 insertions(+) create mode 100644 apps/ui/registry/default/particles/p-form-3.tsx diff --git a/apps/ui/registry/default/particles/p-form-3.tsx b/apps/ui/registry/default/particles/p-form-3.tsx new file mode 100644 index 000000000..6809003f3 --- /dev/null +++ b/apps/ui/registry/default/particles/p-form-3.tsx @@ -0,0 +1,637 @@ +"use client"; + +import { + BanknoteIcon, + CreditCardIcon, + LockIcon, + ShieldCheckIcon, + SmartphoneIcon, +} from "lucide-react"; +import { type FormEvent, useState } from "react"; + +import { Badge } from "@/registry/default/ui/badge"; +import { Button } from "@/registry/default/ui/button"; +import { + Card, + CardDescription, + CardHeader, + CardPanel, + CardTitle, +} from "@/registry/default/ui/card"; +import { Field, FieldError, FieldLabel } from "@/registry/default/ui/field"; +import { Form } from "@/registry/default/ui/form"; +import { Input } from "@/registry/default/ui/input"; +import { Label } from "@/registry/default/ui/label"; +import { + Select, + SelectItem, + SelectPopup, + SelectTrigger, + SelectValue, +} from "@/registry/default/ui/select"; +import { Separator } from "@/registry/default/ui/separator"; +import { Spinner } from "@/registry/default/ui/spinner"; +import { Tabs, TabsList, TabsPanel, TabsTab } from "@/registry/default/ui/tabs"; + +const countryOptions = [ + { label: "United States", value: "us" }, + { label: "Canada", value: "ca" }, + { label: "United Kingdom", value: "uk" }, + { label: "Germany", value: "de" }, + { label: "France", value: "fr" }, + { label: "Australia", value: "au" }, +]; + +const sepaCountryOptions = [ + { label: "Germany", value: "de" }, + { label: "France", value: "fr" }, + { label: "Netherlands", value: "nl" }, + { label: "Belgium", value: "be" }, + { label: "Austria", value: "at" }, + { label: "Spain", value: "es" }, + { label: "Italy", value: "it" }, +]; + +const orderItems = [ + { id: "pro-plan", name: "Pro Plan (Annual)", price: 199.0, quantity: 1 }, + { id: "seats", name: "Additional Seats (5)", price: 49.0, quantity: 1 }, +]; + +function ApplePayIcon({ className }: { className?: string }) { + return ( + + ); +} + +function GooglePayIcon({ className }: { className?: string }) { + return ( + + ); +} + +export default function Particle() { + const [loading, setLoading] = useState(false); + const [cardNumber, setCardNumber] = useState(""); + const [expiry, setExpiry] = useState(""); + const [cvc, setCvc] = useState(""); + const [iban, setIban] = useState(""); + + const subtotal = orderItems.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + const tax = subtotal * 0.1; + const total = subtotal + tax; + + const formatCardNumber = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + const matches = v.match(/\d{4,16}/g); + const match = matches?.[0] || ""; + const parts = []; + for (let i = 0, len = match.length; i < len; i += 4) { + parts.push(match.substring(i, i + 4)); + } + if (parts.length) { + return parts.join(" "); + } + return value; + }; + + const formatExpiry = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + if (v.length >= 2) { + return `${v.substring(0, 2)}/${v.substring(2, 4)}`; + } + return v; + }; + + const formatIban = (value: string) => { + const v = value.replace(/\s+/g, "").toUpperCase(); + const parts = []; + for (let i = 0, len = v.length; i < len; i += 4) { + parts.push(v.substring(i, i + 4)); + } + return parts.join(" "); + }; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert("Payment successful! Thank you for your purchase."); + }; + + const handleWalletPayment = async (wallet: string) => { + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert(`${wallet} payment successful! Thank you for your purchase.`); + }; + + return ( +
+ + +
+
+ + Choose your preferred payment method + +
+ + + + + + + + + + + + +
+
+
+ +
+ + First Name + + Please enter your first name. + + + Last Name + + Please enter your last name. + +
+ + Email + + Please enter a valid email. + +
+ + + +
+ + + Card Number + + setCardNumber(formatCardNumber(e.target.value)) + } + placeholder="4242 4242 4242 4242" + required + type="text" + value={cardNumber} + /> + Please enter a valid card number. + +
+ + Expiration Date + + setExpiry(formatExpiry(e.target.value)) + } + placeholder="MM/YY" + required + type="text" + value={expiry} + /> + + Please enter a valid expiry date. + + + + CVC + + setCvc(e.target.value.replace(/[^0-9]/g, "")) + } + placeholder="123" + required + type="text" + value={cvc} + /> + Please enter a valid CVC. + +
+
+ + + +
+ + + Country + + + + Street Address + + Please enter your address. + +
+ + City + + Please enter your city. + + + State + + Please enter your state. + + + ZIP Code + + Please enter your ZIP code. + +
+
+
+
+ +
+
+
+
+
+ + +
+
+ +

+ Pay quickly and securely with your preferred digital wallet. +

+
+ +
+ + + +
+ + + +
+
+
+
+ +
+
+
+
+ + +
+
+
+ +

+ Pay directly from your European bank account. Available + for customers in the Single Euro Payments Area. +

+
+ +
+ + + Full Name + + + Please enter the account holder name. + + + + Email + + Please enter a valid email. + +
+ + + +
+ + + IBAN + setIban(formatIban(e.target.value))} + placeholder="DE89 3704 0044 0532 0130 00" + required + type="text" + value={iban} + /> + Please enter a valid IBAN. + + + Country + + +
+ +
+

+ By providing your IBAN and confirming this payment, you + authorize (A) this company and Stripe, our payment service + provider, to send instructions to your bank to debit your + account and (B) your bank to debit your account in + accordance with those instructions. You are entitled to a + refund from your bank under the terms and conditions of + your agreement with your bank. +

+
+
+
+ +
+
+
+
+
+
+
+
+ +
+ + + Order Summary + + +
+ {orderItems.map((item) => ( +
+
+ {item.name} + + Qty: {item.quantity} + +
+ + ${(item.price * item.quantity).toFixed(2)} + +
+ ))} + +
+ Subtotal + ${subtotal.toFixed(2)} +
+
+ Tax (10%) + ${tax.toFixed(2)} +
+ +
+ Total + + ${total.toFixed(2)} + +
+
+
+
+ + + +
+
+ + + + 256-bit SSL Encryption + +
+

+ Your payment information is processed securely. We do not store + credit card details nor have access to your credit card + information. +

+
+
+
+
+
+ ); +} diff --git a/apps/ui/registry/registry-particles.ts b/apps/ui/registry/registry-particles.ts index bc4afcef5..c47f7073a 100644 --- a/apps/ui/registry/registry-particles.ts +++ b/apps/ui/registry/registry-particles.ts @@ -1649,6 +1649,30 @@ export const particles: ParticleItem[] = [ ], type: "registry:block", }, + { + categories: categories("card", "form", "input", "select", "tabs"), + dependencies: ["lucide-react"], + description: "Stripe checkout form with multiple payment methods", + files: [{ path: "particles/p-form-3.tsx", type: "registry:block" }], + meta: { + className: "**:data-[slot=preview]:w-full", + }, + name: "p-form-3", + registryDependencies: [ + "@coss/badge", + "@coss/button", + "@coss/card", + "@coss/field", + "@coss/form", + "@coss/input", + "@coss/label", + "@coss/select", + "@coss/separator", + "@coss/spinner", + "@coss/tabs", + ], + type: "registry:block", + }, { categories: categories("frame"), description: "Basic frame", From 5e6acf9ba6ec5776882500d245d15872b951f3de Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:07:28 +0000 Subject: [PATCH 4/7] chore: update registry with p-form-3 particle Co-Authored-By: sean@cal.com --- apps/ui/public/r/p-form-3.json | 39 ++++++++++++++++++++++++++++++++++ apps/ui/public/r/registry.json | 37 ++++++++++++++++++++++++++++++++ apps/ui/registry.json | 37 ++++++++++++++++++++++++++++++++ apps/ui/registry/__index__.tsx | 18 ++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 apps/ui/public/r/p-form-3.json diff --git a/apps/ui/public/r/p-form-3.json b/apps/ui/public/r/p-form-3.json new file mode 100644 index 000000000..c9aed4085 --- /dev/null +++ b/apps/ui/public/r/p-form-3.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "p-form-3", + "type": "registry:block", + "description": "Stripe checkout form with multiple payment methods", + "dependencies": [ + "lucide-react" + ], + "registryDependencies": [ + "@coss/badge", + "@coss/button", + "@coss/card", + "@coss/field", + "@coss/form", + "@coss/input", + "@coss/label", + "@coss/select", + "@coss/separator", + "@coss/spinner", + "@coss/tabs" + ], + "files": [ + { + "path": "registry/default/particles/p-form-3.tsx", + "content": "\"use client\";\n\nimport {\n BanknoteIcon,\n CreditCardIcon,\n LockIcon,\n ShieldCheckIcon,\n SmartphoneIcon,\n} from \"lucide-react\";\nimport { type FormEvent, useState } from \"react\";\n\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Card,\n CardDescription,\n CardHeader,\n CardPanel,\n CardTitle,\n} from \"@/registry/default/ui/card\";\nimport { Field, FieldError, FieldLabel } from \"@/registry/default/ui/field\";\nimport { Form } from \"@/registry/default/ui/form\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport { Label } from \"@/registry/default/ui/label\";\nimport {\n Select,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\nimport { Separator } from \"@/registry/default/ui/separator\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\nimport { Tabs, TabsList, TabsPanel, TabsTab } from \"@/registry/default/ui/tabs\";\n\nconst countryOptions = [\n { label: \"United States\", value: \"us\" },\n { label: \"Canada\", value: \"ca\" },\n { label: \"United Kingdom\", value: \"uk\" },\n { label: \"Germany\", value: \"de\" },\n { label: \"France\", value: \"fr\" },\n { label: \"Australia\", value: \"au\" },\n];\n\nconst sepaCountryOptions = [\n { label: \"Germany\", value: \"de\" },\n { label: \"France\", value: \"fr\" },\n { label: \"Netherlands\", value: \"nl\" },\n { label: \"Belgium\", value: \"be\" },\n { label: \"Austria\", value: \"at\" },\n { label: \"Spain\", value: \"es\" },\n { label: \"Italy\", value: \"it\" },\n];\n\nconst orderItems = [\n { id: \"pro-plan\", name: \"Pro Plan (Annual)\", price: 199.0, quantity: 1 },\n { id: \"seats\", name: \"Additional Seats (5)\", price: 49.0, quantity: 1 },\n];\n\nfunction ApplePayIcon({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nfunction GooglePayIcon({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nexport default function Particle() {\n const [loading, setLoading] = useState(false);\n const [cardNumber, setCardNumber] = useState(\"\");\n const [expiry, setExpiry] = useState(\"\");\n const [cvc, setCvc] = useState(\"\");\n const [iban, setIban] = useState(\"\");\n\n const subtotal = orderItems.reduce(\n (sum, item) => sum + item.price * item.quantity,\n 0,\n );\n const tax = subtotal * 0.1;\n const total = subtotal + tax;\n\n const formatCardNumber = (value: string) => {\n const v = value.replace(/\\s+/g, \"\").replace(/[^0-9]/gi, \"\");\n const matches = v.match(/\\d{4,16}/g);\n const match = matches?.[0] || \"\";\n const parts = [];\n for (let i = 0, len = match.length; i < len; i += 4) {\n parts.push(match.substring(i, i + 4));\n }\n if (parts.length) {\n return parts.join(\" \");\n }\n return value;\n };\n\n const formatExpiry = (value: string) => {\n const v = value.replace(/\\s+/g, \"\").replace(/[^0-9]/gi, \"\");\n if (v.length >= 2) {\n return `${v.substring(0, 2)}/${v.substring(2, 4)}`;\n }\n return v;\n };\n\n const formatIban = (value: string) => {\n const v = value.replace(/\\s+/g, \"\").toUpperCase();\n const parts = [];\n for (let i = 0, len = v.length; i < len; i += 4) {\n parts.push(v.substring(i, i + 4));\n }\n return parts.join(\" \");\n };\n\n const onSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setLoading(true);\n await new Promise((r) => setTimeout(r, 2000));\n setLoading(false);\n alert(\"Payment successful! Thank you for your purchase.\");\n };\n\n const handleWalletPayment = async (wallet: string) => {\n setLoading(true);\n await new Promise((r) => setTimeout(r, 2000));\n setLoading(false);\n alert(`${wallet} payment successful! Thank you for your purchase.`);\n };\n\n return (\n
\n \n \n
\n \n Payment Details\n
\n \n Choose your preferred payment method\n \n
\n \n \n \n \n \n Card\n \n \n \n Wallet\n \n \n \n SEPA\n \n \n\n \n
\n
\n
\n \n
\n \n First Name\n \n Please enter your first name.\n \n \n Last Name\n \n Please enter your last name.\n \n
\n \n Email\n \n Please enter a valid email.\n \n
\n\n \n\n
\n \n \n Card Number\n \n setCardNumber(formatCardNumber(e.target.value))\n }\n placeholder=\"4242 4242 4242 4242\"\n required\n type=\"text\"\n value={cardNumber}\n />\n Please enter a valid card number.\n \n
\n \n Expiration Date\n \n setExpiry(formatExpiry(e.target.value))\n }\n placeholder=\"MM/YY\"\n required\n type=\"text\"\n value={expiry}\n />\n \n Please enter a valid expiry date.\n \n \n \n CVC\n \n setCvc(e.target.value.replace(/[^0-9]/g, \"\"))\n }\n placeholder=\"123\"\n required\n type=\"text\"\n value={cvc}\n />\n Please enter a valid CVC.\n \n
\n
\n\n \n\n
\n \n \n Country\n \n \n \n Street Address\n \n Please enter your address.\n \n
\n \n City\n \n Please enter your city.\n \n \n State\n \n Please enter your state.\n \n \n ZIP Code\n \n Please enter your ZIP code.\n \n
\n
\n
\n
\n \n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay ${total.toFixed(2)}\n \n )}\n \n
\n \n \n Secured by Stripe. Your payment info is encrypted.\n \n
\n
\n
\n
\n\n \n
\n
\n \n

\n Pay quickly and securely with your preferred digital wallet.\n

\n
\n\n
\n handleWalletPayment(\"Apple Pay\")}\n size=\"lg\"\n type=\"button\"\n >\n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay with Apple Pay\n \n )}\n \n\n handleWalletPayment(\"Google Pay\")}\n size=\"lg\"\n type=\"button\"\n variant=\"outline\"\n >\n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay with Google Pay\n \n )}\n \n
\n\n \n\n
\n
\n \n

Digital Wallet Payment

\n

\n When you click a wallet button, you'll be prompted to\n authenticate with your device (Face ID, Touch ID, or\n fingerprint).\n

\n
\n
\n\n
\n \n \n Secured by Stripe. Your payment info is encrypted.\n \n
\n
\n
\n\n \n
\n
\n
\n \n

\n Pay directly from your European bank account. Available\n for customers in the Single Euro Payments Area.\n

\n
\n\n
\n \n \n Full Name\n \n \n Please enter the account holder name.\n \n \n \n Email\n \n Please enter a valid email.\n \n
\n\n \n\n
\n \n \n IBAN\n setIban(formatIban(e.target.value))}\n placeholder=\"DE89 3704 0044 0532 0130 00\"\n required\n type=\"text\"\n value={iban}\n />\n Please enter a valid IBAN.\n \n \n Country\n \n \n
\n\n
\n

\n By providing your IBAN and confirming this payment, you\n authorize (A) this company and Stripe, our payment service\n provider, to send instructions to your bank to debit your\n account and (B) your bank to debit your account in\n accordance with those instructions. You are entitled to a\n refund from your bank under the terms and conditions of\n your agreement with your bank.\n

\n
\n
\n
\n \n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay ${total.toFixed(2)} via SEPA\n \n )}\n \n
\n \n \n Secured by Stripe. Your payment info is encrypted.\n \n
\n
\n
\n
\n
\n
\n
\n\n
\n \n \n Order Summary\n \n \n
\n {orderItems.map((item) => (\n
\n
\n {item.name}\n \n Qty: {item.quantity}\n \n
\n \n ${(item.price * item.quantity).toFixed(2)}\n \n
\n ))}\n \n
\n Subtotal\n ${subtotal.toFixed(2)}\n
\n
\n Tax (10%)\n ${tax.toFixed(2)}\n
\n \n
\n Total\n \n ${total.toFixed(2)}\n \n
\n
\n
\n
\n\n \n \n
\n
\n \n \n Secure\n \n \n 256-bit SSL Encryption\n \n
\n

\n Your payment information is processed securely. We do not store\n credit card details nor have access to your credit card\n information.\n

\n
\n
\n
\n
\n
\n );\n}\n", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full" + }, + "categories": [ + "card", + "form", + "input", + "select", + "tabs" + ] +} \ No newline at end of file diff --git a/apps/ui/public/r/registry.json b/apps/ui/public/r/registry.json index dd906ac34..70449a41c 100644 --- a/apps/ui/public/r/registry.json +++ b/apps/ui/public/r/registry.json @@ -3872,6 +3872,43 @@ ], "type": "registry:block" }, + { + "categories": [ + "card", + "form", + "input", + "select", + "tabs" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Stripe checkout form with multiple payment methods", + "files": [ + { + "path": "registry/default/particles/p-form-3.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full" + }, + "name": "p-form-3", + "registryDependencies": [ + "@coss/badge", + "@coss/button", + "@coss/card", + "@coss/field", + "@coss/form", + "@coss/input", + "@coss/label", + "@coss/select", + "@coss/separator", + "@coss/spinner", + "@coss/tabs" + ], + "type": "registry:block" + }, { "categories": [ "frame" diff --git a/apps/ui/registry.json b/apps/ui/registry.json index dd906ac34..70449a41c 100644 --- a/apps/ui/registry.json +++ b/apps/ui/registry.json @@ -3872,6 +3872,43 @@ ], "type": "registry:block" }, + { + "categories": [ + "card", + "form", + "input", + "select", + "tabs" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Stripe checkout form with multiple payment methods", + "files": [ + { + "path": "registry/default/particles/p-form-3.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full" + }, + "name": "p-form-3", + "registryDependencies": [ + "@coss/badge", + "@coss/button", + "@coss/card", + "@coss/field", + "@coss/form", + "@coss/input", + "@coss/label", + "@coss/select", + "@coss/separator", + "@coss/spinner", + "@coss/tabs" + ], + "type": "registry:block" + }, { "categories": [ "frame" diff --git a/apps/ui/registry/__index__.tsx b/apps/ui/registry/__index__.tsx index 737b21392..fd04b3267 100644 --- a/apps/ui/registry/__index__.tsx +++ b/apps/ui/registry/__index__.tsx @@ -3607,6 +3607,24 @@ export const Index: Record = { categories: ["button","field","form","label","validation","zod"], meta: {"className":"**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-64"}, }, + "p-form-3": { + name: "p-form-3", + description: "Stripe checkout form with multiple payment methods", + type: "registry:block", + registryDependencies: ["@coss/badge","@coss/button","@coss/card","@coss/field","@coss/form","@coss/input","@coss/label","@coss/select","@coss/separator","@coss/spinner","@coss/tabs"], + files: [{ + path: "registry/default/particles/p-form-3.tsx", + type: "registry:block", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/default/particles/p-form-3.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name + return { default: mod.default || mod[exportName] } + }), + categories: ["card","form","input","select","tabs"], + meta: {"className":"**:data-[slot=preview]:w-full"}, + }, "p-frame-1": { name: "p-frame-1", description: "Basic frame", From ab93f8d6e4610b0512855d5aa6262a9d22b68f72 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:18:55 +0000 Subject: [PATCH 5/7] feat: add Stripe-like checkout page variant and revert particle changes Co-Authored-By: sean@cal.com --- apps/ui/app/checkout-stripe/page.tsx | 550 +++++++++++++++ apps/ui/public/r/registry.json | 37 - apps/ui/registry.json | 37 - apps/ui/registry/__index__.tsx | 18 - .../registry/default/particles/p-form-3.tsx | 637 ------------------ apps/ui/registry/registry-particles.ts | 24 - 6 files changed, 550 insertions(+), 753 deletions(-) create mode 100644 apps/ui/app/checkout-stripe/page.tsx delete mode 100644 apps/ui/registry/default/particles/p-form-3.tsx diff --git a/apps/ui/app/checkout-stripe/page.tsx b/apps/ui/app/checkout-stripe/page.tsx new file mode 100644 index 000000000..48aeb1f9e --- /dev/null +++ b/apps/ui/app/checkout-stripe/page.tsx @@ -0,0 +1,550 @@ +"use client"; + +import { + CheckIcon, + ChevronDownIcon, + CreditCardIcon, + LockIcon, + TagIcon, +} from "lucide-react"; +import { type FormEvent, useState } from "react"; + +import { Button } from "@/registry/default/ui/button"; +import { Checkbox } from "@/registry/default/ui/checkbox"; +import { Field, FieldLabel } from "@/registry/default/ui/field"; +import { Form } from "@/registry/default/ui/form"; +import { Input } from "@/registry/default/ui/input"; +import { Label } from "@/registry/default/ui/label"; +import { + Select, + SelectItem, + SelectPopup, + SelectTrigger, + SelectValue, +} from "@/registry/default/ui/select"; +import { Separator } from "@/registry/default/ui/separator"; +import { Spinner } from "@/registry/default/ui/spinner"; + +const countryOptions = [ + { label: "United States", value: "US" }, + { label: "Canada", value: "CA" }, + { label: "United Kingdom", value: "GB" }, + { label: "Germany", value: "DE" }, + { label: "France", value: "FR" }, + { label: "Australia", value: "AU" }, + { label: "Japan", value: "JP" }, + { label: "Netherlands", value: "NL" }, + { label: "Spain", value: "ES" }, + { label: "Italy", value: "IT" }, +]; + +const orderItems = [ + { + id: "pure-glow", + image: "https://placehold.co/48x48/e2e8f0/64748b?text=PG", + name: "Pure Glow Cream", + price: 32.0, + quantity: 1, + }, +]; + +const promoCode = { code: "SAVE10", discount: 0.1 }; + +function ApplePayIcon({ className }: { className?: string }) { + return ( + + ); +} + +function VisaIcon({ className }: { className?: string }) { + return ( + + ); +} + +function MastercardIcon({ className }: { className?: string }) { + return ( + + ); +} + +function AmexIcon({ className }: { className?: string }) { + return ( + + ); +} + +function StripeLogo({ className }: { className?: string }) { + return ( + + + + ); +} + +export default function StripeCheckoutPage() { + const [loading, setLoading] = useState(false); + const [cardNumber, setCardNumber] = useState(""); + const [expiry, setExpiry] = useState(""); + const [cvc, setCvc] = useState(""); + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState("card"); + const [billingIsSameAsShipping, setBillingIsSameAsShipping] = useState(true); + const [saveInfo, setSaveInfo] = useState(false); + + const subtotal = orderItems.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + const discount = subtotal * promoCode.discount; + const total = subtotal - discount; + + const formatCardNumber = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + const matches = v.match(/\d{4,16}/g); + const match = matches?.[0] || ""; + const parts = []; + for (let i = 0, len = match.length; i < len; i += 4) { + parts.push(match.substring(i, i + 4)); + } + if (parts.length) { + return parts.join(" "); + } + return value; + }; + + const formatExpiry = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + if (v.length >= 2) { + return `${v.substring(0, 2)} / ${v.substring(2, 4)}`; + } + return v; + }; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert("Payment successful! Thank you for your purchase."); + }; + + const handleApplePay = async () => { + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert("Apple Pay payment successful! Thank you for your purchase."); + }; + + return ( +
+
+
+
+
+ P +
+
+

Pay Powdur

+

${total.toFixed(2)}

+
+
+ +
+ {orderItems.map((item) => ( +
+ {item.name} +
+

{item.name}

+
+

${item.price.toFixed(2)}

+
+ ))} + + + +
+
+ Subtotal + ${subtotal.toFixed(2)} +
+
+
+
+ -${discount.toFixed(2)} +
+

10% off

+
+ Tax + + Enter address to calculate + +
+
+ + + +
+ Total due + ${total.toFixed(2)} +
+
+
+ +
+
+
+ + +
+ + + Or + + +
+ +
+

Shipping information

+ + + Email + + + +
+ +
+ + + +
+ +
+
+ +
+

Payment method

+ +
+ + + {selectedPaymentMethod === "card" && ( +
+
+ +
+
+ + setCardNumber(formatCardNumber(e.target.value)) + } + placeholder="1234 1234 1234 1234" + required + type="text" + value={cardNumber} + /> +
+ + + +
+
+
+ + setExpiry(formatExpiry(e.target.value)) + } + placeholder="MM / YY" + required + type="text" + value={expiry} + /> +
+ + setCvc(e.target.value.replace(/[^0-9]/g, "")) + } + placeholder="CVC" + required + type="text" + value={cvc} + /> +
+
+
+
+ +
+ + setBillingIsSameAsShipping(checked === true) + } + /> + +
+
+ )} + + + + + + +
+
+ +
+ setSaveInfo(checked === true)} + /> +
+ +

+ Pay securely at Powdur and everywhere{" "} + + Link + {" "} + is accepted. +

+
+
+ + +
+
+ +
+
+ Powered by + +
+
+ + + +
+
+
+
+
+ ); +} diff --git a/apps/ui/public/r/registry.json b/apps/ui/public/r/registry.json index 70449a41c..dd906ac34 100644 --- a/apps/ui/public/r/registry.json +++ b/apps/ui/public/r/registry.json @@ -3872,43 +3872,6 @@ ], "type": "registry:block" }, - { - "categories": [ - "card", - "form", - "input", - "select", - "tabs" - ], - "dependencies": [ - "lucide-react" - ], - "description": "Stripe checkout form with multiple payment methods", - "files": [ - { - "path": "registry/default/particles/p-form-3.tsx", - "type": "registry:block" - } - ], - "meta": { - "className": "**:data-[slot=preview]:w-full" - }, - "name": "p-form-3", - "registryDependencies": [ - "@coss/badge", - "@coss/button", - "@coss/card", - "@coss/field", - "@coss/form", - "@coss/input", - "@coss/label", - "@coss/select", - "@coss/separator", - "@coss/spinner", - "@coss/tabs" - ], - "type": "registry:block" - }, { "categories": [ "frame" diff --git a/apps/ui/registry.json b/apps/ui/registry.json index 70449a41c..dd906ac34 100644 --- a/apps/ui/registry.json +++ b/apps/ui/registry.json @@ -3872,43 +3872,6 @@ ], "type": "registry:block" }, - { - "categories": [ - "card", - "form", - "input", - "select", - "tabs" - ], - "dependencies": [ - "lucide-react" - ], - "description": "Stripe checkout form with multiple payment methods", - "files": [ - { - "path": "registry/default/particles/p-form-3.tsx", - "type": "registry:block" - } - ], - "meta": { - "className": "**:data-[slot=preview]:w-full" - }, - "name": "p-form-3", - "registryDependencies": [ - "@coss/badge", - "@coss/button", - "@coss/card", - "@coss/field", - "@coss/form", - "@coss/input", - "@coss/label", - "@coss/select", - "@coss/separator", - "@coss/spinner", - "@coss/tabs" - ], - "type": "registry:block" - }, { "categories": [ "frame" diff --git a/apps/ui/registry/__index__.tsx b/apps/ui/registry/__index__.tsx index fd04b3267..737b21392 100644 --- a/apps/ui/registry/__index__.tsx +++ b/apps/ui/registry/__index__.tsx @@ -3607,24 +3607,6 @@ export const Index: Record = { categories: ["button","field","form","label","validation","zod"], meta: {"className":"**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-64"}, }, - "p-form-3": { - name: "p-form-3", - description: "Stripe checkout form with multiple payment methods", - type: "registry:block", - registryDependencies: ["@coss/badge","@coss/button","@coss/card","@coss/field","@coss/form","@coss/input","@coss/label","@coss/select","@coss/separator","@coss/spinner","@coss/tabs"], - files: [{ - path: "registry/default/particles/p-form-3.tsx", - type: "registry:block", - target: "" - }], - component: React.lazy(async () => { - const mod = await import("@/registry/default/particles/p-form-3.tsx") - const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name - return { default: mod.default || mod[exportName] } - }), - categories: ["card","form","input","select","tabs"], - meta: {"className":"**:data-[slot=preview]:w-full"}, - }, "p-frame-1": { name: "p-frame-1", description: "Basic frame", diff --git a/apps/ui/registry/default/particles/p-form-3.tsx b/apps/ui/registry/default/particles/p-form-3.tsx deleted file mode 100644 index 6809003f3..000000000 --- a/apps/ui/registry/default/particles/p-form-3.tsx +++ /dev/null @@ -1,637 +0,0 @@ -"use client"; - -import { - BanknoteIcon, - CreditCardIcon, - LockIcon, - ShieldCheckIcon, - SmartphoneIcon, -} from "lucide-react"; -import { type FormEvent, useState } from "react"; - -import { Badge } from "@/registry/default/ui/badge"; -import { Button } from "@/registry/default/ui/button"; -import { - Card, - CardDescription, - CardHeader, - CardPanel, - CardTitle, -} from "@/registry/default/ui/card"; -import { Field, FieldError, FieldLabel } from "@/registry/default/ui/field"; -import { Form } from "@/registry/default/ui/form"; -import { Input } from "@/registry/default/ui/input"; -import { Label } from "@/registry/default/ui/label"; -import { - Select, - SelectItem, - SelectPopup, - SelectTrigger, - SelectValue, -} from "@/registry/default/ui/select"; -import { Separator } from "@/registry/default/ui/separator"; -import { Spinner } from "@/registry/default/ui/spinner"; -import { Tabs, TabsList, TabsPanel, TabsTab } from "@/registry/default/ui/tabs"; - -const countryOptions = [ - { label: "United States", value: "us" }, - { label: "Canada", value: "ca" }, - { label: "United Kingdom", value: "uk" }, - { label: "Germany", value: "de" }, - { label: "France", value: "fr" }, - { label: "Australia", value: "au" }, -]; - -const sepaCountryOptions = [ - { label: "Germany", value: "de" }, - { label: "France", value: "fr" }, - { label: "Netherlands", value: "nl" }, - { label: "Belgium", value: "be" }, - { label: "Austria", value: "at" }, - { label: "Spain", value: "es" }, - { label: "Italy", value: "it" }, -]; - -const orderItems = [ - { id: "pro-plan", name: "Pro Plan (Annual)", price: 199.0, quantity: 1 }, - { id: "seats", name: "Additional Seats (5)", price: 49.0, quantity: 1 }, -]; - -function ApplePayIcon({ className }: { className?: string }) { - return ( - - ); -} - -function GooglePayIcon({ className }: { className?: string }) { - return ( - - ); -} - -export default function Particle() { - const [loading, setLoading] = useState(false); - const [cardNumber, setCardNumber] = useState(""); - const [expiry, setExpiry] = useState(""); - const [cvc, setCvc] = useState(""); - const [iban, setIban] = useState(""); - - const subtotal = orderItems.reduce( - (sum, item) => sum + item.price * item.quantity, - 0, - ); - const tax = subtotal * 0.1; - const total = subtotal + tax; - - const formatCardNumber = (value: string) => { - const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); - const matches = v.match(/\d{4,16}/g); - const match = matches?.[0] || ""; - const parts = []; - for (let i = 0, len = match.length; i < len; i += 4) { - parts.push(match.substring(i, i + 4)); - } - if (parts.length) { - return parts.join(" "); - } - return value; - }; - - const formatExpiry = (value: string) => { - const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); - if (v.length >= 2) { - return `${v.substring(0, 2)}/${v.substring(2, 4)}`; - } - return v; - }; - - const formatIban = (value: string) => { - const v = value.replace(/\s+/g, "").toUpperCase(); - const parts = []; - for (let i = 0, len = v.length; i < len; i += 4) { - parts.push(v.substring(i, i + 4)); - } - return parts.join(" "); - }; - - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - setLoading(true); - await new Promise((r) => setTimeout(r, 2000)); - setLoading(false); - alert("Payment successful! Thank you for your purchase."); - }; - - const handleWalletPayment = async (wallet: string) => { - setLoading(true); - await new Promise((r) => setTimeout(r, 2000)); - setLoading(false); - alert(`${wallet} payment successful! Thank you for your purchase.`); - }; - - return ( -
- - -
-
- - Choose your preferred payment method - -
- - - - - - - - - - - - -
-
-
- -
- - First Name - - Please enter your first name. - - - Last Name - - Please enter your last name. - -
- - Email - - Please enter a valid email. - -
- - - -
- - - Card Number - - setCardNumber(formatCardNumber(e.target.value)) - } - placeholder="4242 4242 4242 4242" - required - type="text" - value={cardNumber} - /> - Please enter a valid card number. - -
- - Expiration Date - - setExpiry(formatExpiry(e.target.value)) - } - placeholder="MM/YY" - required - type="text" - value={expiry} - /> - - Please enter a valid expiry date. - - - - CVC - - setCvc(e.target.value.replace(/[^0-9]/g, "")) - } - placeholder="123" - required - type="text" - value={cvc} - /> - Please enter a valid CVC. - -
-
- - - -
- - - Country - - - - Street Address - - Please enter your address. - -
- - City - - Please enter your city. - - - State - - Please enter your state. - - - ZIP Code - - Please enter your ZIP code. - -
-
-
-
- -
-
-
-
-
- - -
-
- -

- Pay quickly and securely with your preferred digital wallet. -

-
- -
- - - -
- - - -
-
-
-
- -
-
-
-
- - -
-
-
- -

- Pay directly from your European bank account. Available - for customers in the Single Euro Payments Area. -

-
- -
- - - Full Name - - - Please enter the account holder name. - - - - Email - - Please enter a valid email. - -
- - - -
- - - IBAN - setIban(formatIban(e.target.value))} - placeholder="DE89 3704 0044 0532 0130 00" - required - type="text" - value={iban} - /> - Please enter a valid IBAN. - - - Country - - -
- -
-

- By providing your IBAN and confirming this payment, you - authorize (A) this company and Stripe, our payment service - provider, to send instructions to your bank to debit your - account and (B) your bank to debit your account in - accordance with those instructions. You are entitled to a - refund from your bank under the terms and conditions of - your agreement with your bank. -

-
-
-
- -
-
-
-
-
-
-
-
- -
- - - Order Summary - - -
- {orderItems.map((item) => ( -
-
- {item.name} - - Qty: {item.quantity} - -
- - ${(item.price * item.quantity).toFixed(2)} - -
- ))} - -
- Subtotal - ${subtotal.toFixed(2)} -
-
- Tax (10%) - ${tax.toFixed(2)} -
- -
- Total - - ${total.toFixed(2)} - -
-
-
-
- - - -
-
- - - - 256-bit SSL Encryption - -
-

- Your payment information is processed securely. We do not store - credit card details nor have access to your credit card - information. -

-
-
-
-
-
- ); -} diff --git a/apps/ui/registry/registry-particles.ts b/apps/ui/registry/registry-particles.ts index c47f7073a..bc4afcef5 100644 --- a/apps/ui/registry/registry-particles.ts +++ b/apps/ui/registry/registry-particles.ts @@ -1649,30 +1649,6 @@ export const particles: ParticleItem[] = [ ], type: "registry:block", }, - { - categories: categories("card", "form", "input", "select", "tabs"), - dependencies: ["lucide-react"], - description: "Stripe checkout form with multiple payment methods", - files: [{ path: "particles/p-form-3.tsx", type: "registry:block" }], - meta: { - className: "**:data-[slot=preview]:w-full", - }, - name: "p-form-3", - registryDependencies: [ - "@coss/badge", - "@coss/button", - "@coss/card", - "@coss/field", - "@coss/form", - "@coss/input", - "@coss/label", - "@coss/select", - "@coss/separator", - "@coss/spinner", - "@coss/tabs", - ], - type: "registry:block", - }, { categories: categories("frame"), description: "Basic frame", From c387cced86cba480fc50cbaf15541ac872d72c01 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:30:37 +0000 Subject: [PATCH 6/7] refactor: redesign checkout-stripe to use coss/ui Card components Co-Authored-By: sean@cal.com --- apps/ui/app/checkout-stripe/page.tsx | 721 +++++++++++++-------------- 1 file changed, 351 insertions(+), 370 deletions(-) diff --git a/apps/ui/app/checkout-stripe/page.tsx b/apps/ui/app/checkout-stripe/page.tsx index 48aeb1f9e..efb64ac43 100644 --- a/apps/ui/app/checkout-stripe/page.tsx +++ b/apps/ui/app/checkout-stripe/page.tsx @@ -1,17 +1,26 @@ "use client"; import { + BuildingIcon, CheckIcon, - ChevronDownIcon, CreditCardIcon, LockIcon, + ShieldCheckIcon, TagIcon, } from "lucide-react"; import { type FormEvent, useState } from "react"; +import { Badge } from "@/registry/default/ui/badge"; import { Button } from "@/registry/default/ui/button"; +import { + Card, + CardDescription, + CardHeader, + CardPanel, + CardTitle, +} from "@/registry/default/ui/card"; import { Checkbox } from "@/registry/default/ui/checkbox"; -import { Field, FieldLabel } from "@/registry/default/ui/field"; +import { Field, FieldError, FieldLabel } from "@/registry/default/ui/field"; import { Form } from "@/registry/default/ui/form"; import { Input } from "@/registry/default/ui/input"; import { Label } from "@/registry/default/ui/label"; @@ -26,22 +35,17 @@ import { Separator } from "@/registry/default/ui/separator"; import { Spinner } from "@/registry/default/ui/spinner"; const countryOptions = [ - { label: "United States", value: "US" }, - { label: "Canada", value: "CA" }, - { label: "United Kingdom", value: "GB" }, - { label: "Germany", value: "DE" }, - { label: "France", value: "FR" }, - { label: "Australia", value: "AU" }, - { label: "Japan", value: "JP" }, - { label: "Netherlands", value: "NL" }, - { label: "Spain", value: "ES" }, - { label: "Italy", value: "IT" }, + { label: "United States", value: "us" }, + { label: "Canada", value: "ca" }, + { label: "United Kingdom", value: "uk" }, + { label: "Germany", value: "de" }, + { label: "France", value: "fr" }, + { label: "Australia", value: "au" }, ]; const orderItems = [ { id: "pure-glow", - image: "https://placehold.co/48x48/e2e8f0/64748b?text=PG", name: "Pure Glow Cream", price: 32.0, quantity: 1, @@ -64,81 +68,32 @@ function ApplePayIcon({ className }: { className?: string }) { ); } -function VisaIcon({ className }: { className?: string }) { - return ( - - ); -} - -function MastercardIcon({ className }: { className?: string }) { +function GooglePayIcon({ className }: { className?: string }) { return ( ); } -function AmexIcon({ className }: { className?: string }) { - return ( - - ); -} - -function StripeLogo({ className }: { className?: string }) { - return ( - - - - ); -} +type PaymentMethod = "card" | "alipay" | "bank"; export default function StripeCheckoutPage() { const [loading, setLoading] = useState(false); const [cardNumber, setCardNumber] = useState(""); const [expiry, setExpiry] = useState(""); const [cvc, setCvc] = useState(""); - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState("card"); + const [selectedPaymentMethod, setSelectedPaymentMethod] = + useState("card"); const [billingIsSameAsShipping, setBillingIsSameAsShipping] = useState(true); - const [saveInfo, setSaveInfo] = useState(false); const subtotal = orderItems.reduce( (sum, item) => sum + item.price * item.quantity, @@ -164,7 +119,7 @@ export default function StripeCheckoutPage() { const formatExpiry = (value: string) => { const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); if (v.length >= 2) { - return `${v.substring(0, 2)} / ${v.substring(2, 4)}`; + return `${v.substring(0, 2)}/${v.substring(2, 4)}`; } return v; }; @@ -177,133 +132,175 @@ export default function StripeCheckoutPage() { alert("Payment successful! Thank you for your purchase."); }; - const handleApplePay = async () => { + const handleWalletPayment = async (wallet: string) => { setLoading(true); await new Promise((r) => setTimeout(r, 2000)); setLoading(false); - alert("Apple Pay payment successful! Thank you for your purchase."); + alert(`${wallet} payment successful! Thank you for your purchase.`); }; return ( -
-
-
-
-
- P -
-
-

Pay Powdur

-

${total.toFixed(2)}

-
-
- -
- {orderItems.map((item) => ( -
- {item.name} -
-

{item.name}

+
+
+ + + Order Summary + Review your order details + + +
+ {orderItems.map((item) => ( +
+
+

{item.name}

+

+ Qty: {item.quantity} +

+
+

${item.price.toFixed(2)}

-

${item.price.toFixed(2)}

-
- ))} + ))} - + -
-
- Subtotal - ${subtotal.toFixed(2)} -
-
-
-
- + -
- Total due - ${total.toFixed(2)} -
-
-
- -
-
-
- - -
- - - Or - - +
+ Total + ${total.toFixed(2)}
-
-

Shipping information

+
+
+
+ + + + + +
+
+ Complete your purchase securely +
+ + +
+
+ +
+ + +
+
- - Email - - - -
-
-
-

Payment method

+ -
-
-
+ + +
); From c14c174cc97cf843982e391d64dfe38235965b25 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:39:48 +0000 Subject: [PATCH 7/7] revert: remove checkout-stripe variant, keep only original checkout page Co-Authored-By: sean@cal.com --- apps/ui/app/checkout-stripe/page.tsx | 531 --------------------------- 1 file changed, 531 deletions(-) delete mode 100644 apps/ui/app/checkout-stripe/page.tsx diff --git a/apps/ui/app/checkout-stripe/page.tsx b/apps/ui/app/checkout-stripe/page.tsx deleted file mode 100644 index efb64ac43..000000000 --- a/apps/ui/app/checkout-stripe/page.tsx +++ /dev/null @@ -1,531 +0,0 @@ -"use client"; - -import { - BuildingIcon, - CheckIcon, - CreditCardIcon, - LockIcon, - ShieldCheckIcon, - TagIcon, -} from "lucide-react"; -import { type FormEvent, useState } from "react"; - -import { Badge } from "@/registry/default/ui/badge"; -import { Button } from "@/registry/default/ui/button"; -import { - Card, - CardDescription, - CardHeader, - CardPanel, - CardTitle, -} from "@/registry/default/ui/card"; -import { Checkbox } from "@/registry/default/ui/checkbox"; -import { Field, FieldError, FieldLabel } from "@/registry/default/ui/field"; -import { Form } from "@/registry/default/ui/form"; -import { Input } from "@/registry/default/ui/input"; -import { Label } from "@/registry/default/ui/label"; -import { - Select, - SelectItem, - SelectPopup, - SelectTrigger, - SelectValue, -} from "@/registry/default/ui/select"; -import { Separator } from "@/registry/default/ui/separator"; -import { Spinner } from "@/registry/default/ui/spinner"; - -const countryOptions = [ - { label: "United States", value: "us" }, - { label: "Canada", value: "ca" }, - { label: "United Kingdom", value: "uk" }, - { label: "Germany", value: "de" }, - { label: "France", value: "fr" }, - { label: "Australia", value: "au" }, -]; - -const orderItems = [ - { - id: "pure-glow", - name: "Pure Glow Cream", - price: 32.0, - quantity: 1, - }, -]; - -const promoCode = { code: "SAVE10", discount: 0.1 }; - -function ApplePayIcon({ className }: { className?: string }) { - return ( - - ); -} - -function GooglePayIcon({ className }: { className?: string }) { - return ( - - ); -} - -type PaymentMethod = "card" | "alipay" | "bank"; - -export default function StripeCheckoutPage() { - const [loading, setLoading] = useState(false); - const [cardNumber, setCardNumber] = useState(""); - const [expiry, setExpiry] = useState(""); - const [cvc, setCvc] = useState(""); - const [selectedPaymentMethod, setSelectedPaymentMethod] = - useState("card"); - const [billingIsSameAsShipping, setBillingIsSameAsShipping] = useState(true); - - const subtotal = orderItems.reduce( - (sum, item) => sum + item.price * item.quantity, - 0, - ); - const discount = subtotal * promoCode.discount; - const total = subtotal - discount; - - const formatCardNumber = (value: string) => { - const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); - const matches = v.match(/\d{4,16}/g); - const match = matches?.[0] || ""; - const parts = []; - for (let i = 0, len = match.length; i < len; i += 4) { - parts.push(match.substring(i, i + 4)); - } - if (parts.length) { - return parts.join(" "); - } - return value; - }; - - const formatExpiry = (value: string) => { - const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); - if (v.length >= 2) { - return `${v.substring(0, 2)}/${v.substring(2, 4)}`; - } - return v; - }; - - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - setLoading(true); - await new Promise((r) => setTimeout(r, 2000)); - setLoading(false); - alert("Payment successful! Thank you for your purchase."); - }; - - const handleWalletPayment = async (wallet: string) => { - setLoading(true); - await new Promise((r) => setTimeout(r, 2000)); - setLoading(false); - alert(`${wallet} payment successful! Thank you for your purchase.`); - }; - - return ( -
-
- - - Order Summary - Review your order details - - -
- {orderItems.map((item) => ( -
-
-

{item.name}

-

- Qty: {item.quantity} -

-
-

${item.price.toFixed(2)}

-
- ))} - - - -
-
- Subtotal - ${subtotal.toFixed(2)} -
-
-
- - -
- - -${discount.toFixed(2)} - -
-
- - - -
- Total - ${total.toFixed(2)} -
- -
-
-
-
-
- - - -
-
- Complete your purchase securely -
- -
-
-
- -
- - -
-
- -
- - - Or pay with card - - -
- -
- - - Email - - Please enter a valid email. - -
- - - -
- -
- - First Name - - Please enter your first name. - - - Last Name - - Please enter your last name. - -
- - Country - - - - Address - - Please enter your address. - -
- - City - - Please enter your city. - - - State - - Please enter your state. - - - ZIP Code - - Please enter your ZIP code. - -
-
- - - -
- - -
- - - {selectedPaymentMethod === "card" && ( -
- - Card Number - - setCardNumber(formatCardNumber(e.target.value)) - } - placeholder="4242 4242 4242 4242" - required - type="text" - value={cardNumber} - /> - - Please enter a valid card number. - - -
- - Expiration Date - - setExpiry(formatExpiry(e.target.value)) - } - placeholder="MM/YY" - required - type="text" - value={expiry} - /> - - Please enter a valid expiry date. - - - - CVC - - setCvc(e.target.value.replace(/[^0-9]/g, "")) - } - placeholder="123" - required - type="text" - value={cvc} - /> - Please enter a valid CVC. - -
-
- - setBillingIsSameAsShipping(checked === true) - } - /> - -
-
- )} - - - - -
-
- -
- -
-
-
-
-
-
-
-
-
- ); -}