Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mastodon
GitHub Repository: mastodon/joinmastodon
Path: blob/main/components/donate/DonateCheckout.tsx
1006 views
1
import { PaymentElement, useCheckout } from "@stripe/react-stripe-js"
2
import classNames from "classnames"
3
import Link from "next/link"
4
import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from "react"
5
import { FormattedMessage } from "react-intl"
6
7
import LoadingIcon from "../../public/icons/loading.svg?inline"
8
import ArrowLeftIcon from "../../public/ui/arrow-left.svg?inline"
9
import ExternalLinkIcon from "../../public/ui/external-link.svg?inline"
10
11
import { Button } from "../Button"
12
import { Input } from "../Input"
13
import { isInIframe } from "../../donate/utils"
14
15
interface DonateCheckoutProps {
16
backUrl?: string
17
className?: string
18
onComplete: () => void
19
}
20
21
export function DonateCheckout({
22
className,
23
backUrl,
24
onComplete,
25
}: DonateCheckoutProps) {
26
const checkout = useCheckout()
27
28
const [email, setEmail] = useState("")
29
const [errorMessage, setErrorMessage] = useState<string | null>(null)
30
const [isLoading, setIsLoading] = useState(false)
31
32
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
33
setErrorMessage(null)
34
setEmail(e.target.value)
35
}, [])
36
37
const handleEmailBlur = useCallback(async () => {
38
if (!email) {
39
return
40
}
41
42
const result = await checkout.updateEmail(email)
43
if (result.type === "error") {
44
setErrorMessage(result.error.message)
45
} else {
46
setErrorMessage(null)
47
}
48
}, [checkout, email])
49
50
const handleCheckout = useCallback(
51
async (e: FormEvent<HTMLFormElement>) => {
52
e.preventDefault()
53
54
setIsLoading(true)
55
56
const result = await checkout.updateEmail(email)
57
if (result.type === "error") {
58
setErrorMessage(result.error.message)
59
setIsLoading(false)
60
return
61
}
62
63
const confirmResult = await checkout.confirm({
64
redirect: "if_required", // Only redirect if required
65
})
66
67
if (confirmResult.type === "error") {
68
setErrorMessage(confirmResult.error.message)
69
setIsLoading(false)
70
} else {
71
onComplete()
72
}
73
},
74
[checkout, email, onComplete]
75
)
76
77
// Determine if we are in an external iframe in client-side.
78
const [isExternal, setIsExternal] = useState(false)
79
useEffect(() => {
80
setIsExternal(
81
isInIframe() && window.parent.location.host !== window.location.host
82
)
83
}, [])
84
85
return (
86
<form className={className} onSubmit={handleCheckout}>
87
<header className="mb-4 flex flex-col gap-2">
88
<div className="border dark:border-gray-1 p-4 rounded-md">
89
<h3 className="text-b1">
90
{checkout.recurring ? (
91
<FormattedMessage
92
id="donate_widget.checkout.header.recurring"
93
defaultMessage="You are donating {total} every {frequency}"
94
values={{
95
total: checkout.total.total.amount,
96
frequency: checkout.recurring.interval,
97
}}
98
/>
99
) : (
100
<FormattedMessage
101
id="donate_widget.checkout.header.one_time"
102
defaultMessage="You are donating {total} once"
103
values={{
104
total: checkout.total.total.amount,
105
}}
106
/>
107
)}
108
</h3>
109
{checkout.recurring && (
110
<p className="text-b4 mt-2 text-gray-1 dark:text-gray-2">
111
<FormattedMessage
112
id="donate_widget.checkout.header.recurring_info"
113
defaultMessage="You will be charged {total} today and every following {frequency}. You can cancel your recurring donation at any time."
114
values={{
115
total: checkout.total.total.amount,
116
frequency: checkout.recurring.interval,
117
}}
118
/>
119
</p>
120
)}
121
</div>
122
{backUrl && (
123
<Link
124
href={backUrl}
125
className="text-gray-1 dark:text-gray-2 text-b3 mt-2 flex gap-1 items-center -order-1"
126
>
127
<ArrowLeftIcon className="size-4 fill-current" />
128
<FormattedMessage
129
id="donate_widget.checkout.header.back"
130
defaultMessage="Edit your donation"
131
/>
132
</Link>
133
)}
134
</header>
135
136
<div className="flex flex-col gap-4 mb-4">
137
<label className="w-full">
138
<FormattedMessage
139
id="donate_widget.checkout.email"
140
defaultMessage="Email"
141
>
142
{(text) => <p className="mb-2">{text}</p>}
143
</FormattedMessage>
144
<Input
145
type="email"
146
value={email}
147
onChange={handleChange}
148
placeholder="[email protected]"
149
onBlur={handleEmailBlur}
150
disabled={isLoading}
151
fullWidth
152
/>
153
</label>
154
155
<div className="w-full">
156
<h4 className="mb-2">
157
<FormattedMessage
158
id="donate_widget.checkout.payment"
159
defaultMessage="Payment"
160
/>
161
</h4>
162
<PaymentElement className="" />
163
</div>
164
</div>
165
166
{errorMessage && (
167
<p className="text-error text-b3 mb-2">{errorMessage}</p>
168
)}
169
<Button
170
disabled={isLoading}
171
dark
172
className={classNames(
173
"flex gap-2 items-center justify-center",
174
isLoading && "text-gray-2"
175
)}
176
fullWidth
177
type="submit"
178
>
179
<DonateCheckoutButtonText isLoading={isLoading} />
180
{isExternal && <ExternalLinkIcon className="fill-current" />}
181
</Button>
182
{isExternal && (
183
<p className="text-b4 text-center mt-2 text-gray-1 dark:text-gray-2">
184
<FormattedMessage
185
id="donate_widget.checkout.external_notice"
186
defaultMessage="You will be redirected to {url} for secure payment."
187
values={{
188
url: typeof window !== "undefined" ? window.location.host : "",
189
}}
190
/>
191
</p>
192
)}
193
</form>
194
)
195
}
196
197
function DonateCheckoutButtonText({ isLoading }: { isLoading: boolean }) {
198
const checkout = useCheckout()
199
if (isLoading) {
200
return (
201
<>
202
<LoadingIcon className="motion-safe:animate-spin size-5" />
203
<FormattedMessage
204
id="donate_widget.checkout.submitting"
205
defaultMessage="Submitting…"
206
/>
207
</>
208
)
209
}
210
211
if (checkout.recurring) {
212
return (
213
<FormattedMessage
214
id="donate_widget.checkout.subscribe_button"
215
defaultMessage="Subscribe for {total} every {frequency}"
216
values={{
217
total: checkout.total.total.amount,
218
frequency: checkout.recurring.interval,
219
}}
220
/>
221
)
222
}
223
224
return (
225
<FormattedMessage
226
id="donate_widget.checkout.pay_button"
227
defaultMessage="Pay {total} now"
228
values={{
229
total: checkout.total.total.amount,
230
}}
231
/>
232
)
233
}
234
235