-
Notifications
You must be signed in to change notification settings - Fork 59
[#321] Blade/update stripe payment theming #415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
a7e6bb6
payment changes
myr124 d098c06
fixed linting errors
myr124 6cfdff7
added a small note for paymments being non-refundable
myr124 5f390cd
added required email field
myr124 d762d72
added a powered by stripe line after payment form
myr124 77002ea
added extra cases to account for payment failure, fulfillpaymentinten…
myr124 f31c1cd
removed eslint error
myr124 ad605c0
Merge branch 'main' into blade/update-stripe-payment-theming
myr124 dd5dfb8
change made for new dependencies
myr124 5115e8a
added coderabbit suggestion for different conditional to handle proce…
myr124 9b06fcf
more coderabbit suggestions implemented (race condition fix with memb…
myr124 6c7f72d
linting and typecheck fixes, removed unnecessary code as well
myr124 eabea23
more coderabbit suggestions implemented (mostly race condition fixes,…
myr124 11851b3
more fixes
myr124 0d0f05c
merge fix for pnpm-lock
myr124 ef15450
Merge branch 'main' into blade/update-stripe-payment-theming
myr124 c59a84e
eslint fix
myr124 67edda3
Merge branch 'main' into blade/update-stripe-payment-theming
myr124 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
234 changes: 234 additions & 0 deletions
234
apps/blade/src/app/_components/dashboard/member/checkout-form.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| "use client"; | ||
|
|
||
| import type { Appearance } from "@stripe/stripe-js"; | ||
| import { useEffect, useState } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { | ||
| Elements, | ||
| PaymentElement, | ||
| useElements, | ||
| useStripe, | ||
| } from "@stripe/react-stripe-js"; | ||
| import { loadStripe } from "@stripe/stripe-js"; | ||
| import { useTheme } from "next-themes"; | ||
| import { FaStripe } from "react-icons/fa"; | ||
|
|
||
| import { Button } from "@forge/ui/button"; | ||
| import { Input } from "@forge/ui/input"; | ||
| import { Label } from "@forge/ui/label"; | ||
| import { toast } from "@forge/ui/toast"; | ||
|
|
||
| import { env } from "~/env"; | ||
| import { api } from "~/trpc/react"; | ||
|
|
||
| const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); | ||
|
|
||
| function buildAppearance(isDark: boolean): Appearance { | ||
| return { | ||
| theme: isDark ? "night" : "stripe", | ||
| variables: { | ||
| colorPrimary: isDark ? "#6d28d9" : "#7c3aed", | ||
| colorBackground: isDark ? "#060b1a" : "#ffffff", | ||
| colorText: isDark ? "#f8fafc" : "#060b1a", | ||
| colorDanger: "#ef4444", | ||
| fontFamily: "GeistSans, system-ui, sans-serif", | ||
| borderRadius: "0.5rem", | ||
| colorTextPlaceholder: isDark ? "#94a3b8" : "#9ca3af", | ||
| }, | ||
| rules: { | ||
| ".Input": { | ||
| border: `1px solid ${isDark ? "#1e2d40" : "#e2e4eb"}`, | ||
| backgroundColor: isDark ? "#060b1a" : "#ffffff", | ||
| color: isDark ? "#f8fafc" : "#060b1a", | ||
| }, | ||
| ".Input:focus": { | ||
| borderColor: isDark ? "#6d28d9" : "#7c3aed", | ||
| boxShadow: `0 0 0 1px ${isDark ? "#6d28d9" : "#7c3aed"}`, | ||
| outline: "none", | ||
| }, | ||
| ".Label": { | ||
| color: isDark ? "#94a3b8" : "#6b7280", | ||
| fontSize: "0.875rem", | ||
| }, | ||
| ".Tab": { | ||
| border: `1px solid ${isDark ? "#1e2d40" : "#e2e4eb"}`, | ||
| backgroundColor: isDark ? "#060b1a" : "#ffffff", | ||
| }, | ||
| ".Tab:hover": { | ||
| backgroundColor: isDark ? "#1e2d40" : "#f3f4f6", | ||
| }, | ||
| ".Tab--selected": { | ||
| borderColor: "#7c3aed", | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function PaymentForm() { | ||
| const stripe = useStripe(); | ||
| const elements = useElements(); | ||
| const router = useRouter(); | ||
| const [isSubmitting, setIsSubmitting] = useState(false); | ||
| const [errorMessage, setErrorMessage] = useState<string | null>(null); | ||
| const [email, setEmail] = useState(""); | ||
|
|
||
| const handleSubmit = async (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| if (!stripe || !elements) return; | ||
|
|
||
| setIsSubmitting(true); | ||
| setErrorMessage(null); | ||
|
|
||
| const { error, paymentIntent } = await stripe.confirmPayment({ | ||
| elements, | ||
| redirect: "if_required", | ||
| confirmParams: { | ||
| return_url: `${window.location.origin}/member/success`, | ||
| receipt_email: email, | ||
| }, | ||
| }); | ||
|
|
||
| if (error) { | ||
| setErrorMessage(error.message ?? null); | ||
| setIsSubmitting(false); | ||
| return; | ||
| } | ||
|
|
||
| if (paymentIntent.status === "succeeded") { | ||
| router.push(`/member/success?payment_intent=${paymentIntent.id}`); | ||
| } else if (paymentIntent.status === "processing") { | ||
| setErrorMessage( | ||
| "Your payment is still processing. We'll update your membership once Stripe confirms it.", | ||
| ); | ||
| setIsSubmitting(false); | ||
| } else { | ||
| setErrorMessage("Payment could not be completed. Please try again."); | ||
| setIsSubmitting(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <form onSubmit={handleSubmit} className="flex flex-col gap-6"> | ||
| <div className="flex flex-col gap-1.5"> | ||
| <Label htmlFor="email">Email</Label> | ||
| <Input | ||
| id="email" | ||
| type="email" | ||
| placeholder="you@example.com" | ||
| value={email} | ||
| onChange={(e) => setEmail(e.target.value)} | ||
| required | ||
| /> | ||
| </div> | ||
| <PaymentElement /> | ||
| {errorMessage && ( | ||
| <p className="text-sm text-destructive">{errorMessage}</p> | ||
| )} | ||
| <p className="text-xs text-muted-foreground"> | ||
| All payments are non-refundable. | ||
| </p> | ||
| <div className="flex items-center justify-center gap-1 text-xs text-muted-foreground"> | ||
| <span>Powered by</span> | ||
| <FaStripe className="h-8 w-8 text-[#635BFF]" /> | ||
| <span className="font-medium"> | Privacy Terms</span> | ||
| </div> | ||
| <div className="flex justify-between"> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={() => router.push("/dashboard")} | ||
| disabled={isSubmitting} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button type="submit" disabled={isSubmitting || !stripe}> | ||
| {isSubmitting ? "Processing..." : "Pay"} | ||
| </Button> | ||
| </div> | ||
| </form> | ||
| ); | ||
| } | ||
|
|
||
| export function CheckoutForm() { | ||
| const { resolvedTheme } = useTheme(); | ||
| const isDark = resolvedTheme === "dark"; | ||
| const [clientSecret, setClientSecret] = useState<string | null>(null); | ||
| const [intentError, setIntentError] = useState<string | null>(null); | ||
|
|
||
| const { mutate: createPaymentIntent } = | ||
| api.duesPayment.createPaymentIntent.useMutation({ | ||
| onSuccess: (data) => { | ||
| if (data.clientSecret) { | ||
| setClientSecret(data.clientSecret); | ||
| } else { | ||
| setIntentError("Could not initialize payment. Please try again."); | ||
| } | ||
| }, | ||
| onError: (error) => { | ||
| setIntentError(error.message); | ||
| toast.error("Failed to start checkout."); | ||
| }, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| createPaymentIntent(); | ||
| }, [createPaymentIntent]); | ||
|
|
||
| return ( | ||
| <div className="mx-auto max-w-4xl overflow-hidden rounded-lg border bg-card shadow-sm"> | ||
| <div className="grid md:grid-cols-2"> | ||
| {/* Left — order summary */} | ||
| <div className="flex flex-col justify-between bg-muted p-8"> | ||
| <div> | ||
| <p className="text-sm font-medium text-muted-foreground"> | ||
| Knight Hacks | ||
| </p> | ||
| <h2 className="mt-2 text-4xl font-bold">$25.00</h2> | ||
| <p className="mt-1 text-sm text-muted-foreground"> | ||
| Club Membership | ||
| </p> | ||
| </div> | ||
| <div className="mt-8 border-t pt-6"> | ||
| <div className="flex items-center justify-between text-sm"> | ||
| <span className="text-muted-foreground"> | ||
| Knight Hacks Membership | ||
| </span> | ||
| <span className="font-medium">$25.00</span> | ||
| </div> | ||
| <div className="mt-3 flex items-center justify-between border-t pt-3 text-sm font-semibold"> | ||
| <span>Total due today</span> | ||
| <span>$25.00</span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Right — payment form */} | ||
| <div className="p-8"> | ||
| {intentError && ( | ||
| <p className="mb-4 text-sm text-destructive">{intentError}</p> | ||
| )} | ||
|
|
||
| {!clientSecret && !intentError && ( | ||
| <div className="space-y-3"> | ||
| <div className="h-10 animate-pulse rounded-md bg-muted" /> | ||
| <div className="h-10 animate-pulse rounded-md bg-muted" /> | ||
| <div className="h-10 animate-pulse rounded-md bg-muted" /> | ||
| </div> | ||
| )} | ||
|
|
||
| {clientSecret && ( | ||
| <Elements | ||
| stripe={stripePromise} | ||
| options={{ | ||
| clientSecret, | ||
| appearance: buildAppearance(isDark), | ||
| }} | ||
| > | ||
| <PaymentForm /> | ||
| </Elements> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.