Path: blob/main/components/donate/DonateCheckout.tsx
1006 views
import { PaymentElement, useCheckout } from "@stripe/react-stripe-js"1import classNames from "classnames"2import Link from "next/link"3import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from "react"4import { FormattedMessage } from "react-intl"56import LoadingIcon from "../../public/icons/loading.svg?inline"7import ArrowLeftIcon from "../../public/ui/arrow-left.svg?inline"8import ExternalLinkIcon from "../../public/ui/external-link.svg?inline"910import { Button } from "../Button"11import { Input } from "../Input"12import { isInIframe } from "../../donate/utils"1314interface DonateCheckoutProps {15backUrl?: string16className?: string17onComplete: () => void18}1920export function DonateCheckout({21className,22backUrl,23onComplete,24}: DonateCheckoutProps) {25const checkout = useCheckout()2627const [email, setEmail] = useState("")28const [errorMessage, setErrorMessage] = useState<string | null>(null)29const [isLoading, setIsLoading] = useState(false)3031const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {32setErrorMessage(null)33setEmail(e.target.value)34}, [])3536const handleEmailBlur = useCallback(async () => {37if (!email) {38return39}4041const result = await checkout.updateEmail(email)42if (result.type === "error") {43setErrorMessage(result.error.message)44} else {45setErrorMessage(null)46}47}, [checkout, email])4849const handleCheckout = useCallback(50async (e: FormEvent<HTMLFormElement>) => {51e.preventDefault()5253setIsLoading(true)5455const result = await checkout.updateEmail(email)56if (result.type === "error") {57setErrorMessage(result.error.message)58setIsLoading(false)59return60}6162const confirmResult = await checkout.confirm({63redirect: "if_required", // Only redirect if required64})6566if (confirmResult.type === "error") {67setErrorMessage(confirmResult.error.message)68setIsLoading(false)69} else {70onComplete()71}72},73[checkout, email, onComplete]74)7576// Determine if we are in an external iframe in client-side.77const [isExternal, setIsExternal] = useState(false)78useEffect(() => {79setIsExternal(80isInIframe() && window.parent.location.host !== window.location.host81)82}, [])8384return (85<form className={className} onSubmit={handleCheckout}>86<header className="mb-4 flex flex-col gap-2">87<div className="border dark:border-gray-1 p-4 rounded-md">88<h3 className="text-b1">89{checkout.recurring ? (90<FormattedMessage91id="donate_widget.checkout.header.recurring"92defaultMessage="You are donating {total} every {frequency}"93values={{94total: checkout.total.total.amount,95frequency: checkout.recurring.interval,96}}97/>98) : (99<FormattedMessage100id="donate_widget.checkout.header.one_time"101defaultMessage="You are donating {total} once"102values={{103total: checkout.total.total.amount,104}}105/>106)}107</h3>108{checkout.recurring && (109<p className="text-b4 mt-2 text-gray-1 dark:text-gray-2">110<FormattedMessage111id="donate_widget.checkout.header.recurring_info"112defaultMessage="You will be charged {total} today and every following {frequency}. You can cancel your recurring donation at any time."113values={{114total: checkout.total.total.amount,115frequency: checkout.recurring.interval,116}}117/>118</p>119)}120</div>121{backUrl && (122<Link123href={backUrl}124className="text-gray-1 dark:text-gray-2 text-b3 mt-2 flex gap-1 items-center -order-1"125>126<ArrowLeftIcon className="size-4 fill-current" />127<FormattedMessage128id="donate_widget.checkout.header.back"129defaultMessage="Edit your donation"130/>131</Link>132)}133</header>134135<div className="flex flex-col gap-4 mb-4">136<label className="w-full">137<FormattedMessage138id="donate_widget.checkout.email"139defaultMessage="Email"140>141{(text) => <p className="mb-2">{text}</p>}142</FormattedMessage>143<Input144type="email"145value={email}146onChange={handleChange}147placeholder="[email protected]"148onBlur={handleEmailBlur}149disabled={isLoading}150fullWidth151/>152</label>153154<div className="w-full">155<h4 className="mb-2">156<FormattedMessage157id="donate_widget.checkout.payment"158defaultMessage="Payment"159/>160</h4>161<PaymentElement className="" />162</div>163</div>164165{errorMessage && (166<p className="text-error text-b3 mb-2">{errorMessage}</p>167)}168<Button169disabled={isLoading}170dark171className={classNames(172"flex gap-2 items-center justify-center",173isLoading && "text-gray-2"174)}175fullWidth176type="submit"177>178<DonateCheckoutButtonText isLoading={isLoading} />179{isExternal && <ExternalLinkIcon className="fill-current" />}180</Button>181{isExternal && (182<p className="text-b4 text-center mt-2 text-gray-1 dark:text-gray-2">183<FormattedMessage184id="donate_widget.checkout.external_notice"185defaultMessage="You will be redirected to {url} for secure payment."186values={{187url: typeof window !== "undefined" ? window.location.host : "",188}}189/>190</p>191)}192</form>193)194}195196function DonateCheckoutButtonText({ isLoading }: { isLoading: boolean }) {197const checkout = useCheckout()198if (isLoading) {199return (200<>201<LoadingIcon className="motion-safe:animate-spin size-5" />202<FormattedMessage203id="donate_widget.checkout.submitting"204defaultMessage="Submitting…"205/>206</>207)208}209210if (checkout.recurring) {211return (212<FormattedMessage213id="donate_widget.checkout.subscribe_button"214defaultMessage="Subscribe for {total} every {frequency}"215values={{216total: checkout.total.total.amount,217frequency: checkout.recurring.interval,218}}219/>220)221}222223return (224<FormattedMessage225id="donate_widget.checkout.pay_button"226defaultMessage="Pay {total} now"227values={{228total: checkout.total.total.amount,229}}230/>231)232}233234235