Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mastodon
GitHub Repository: mastodon/joinmastodon
Path: blob/main/pages/donate/checkout.tsx
1006 views
1
import { CheckoutProvider } from "@stripe/react-stripe-js"
2
import { loadStripe } from "@stripe/stripe-js"
3
import { GetServerSideProps, InferGetServerSidePropsType } from "next"
4
import { useRouter } from "next/navigation"
5
import { useCallback, useEffect, useMemo } from "react"
6
import { z } from "zod"
7
import { FormattedMessage } from "react-intl"
8
9
import { DonateCheckout } from "../../components/donate/DonateCheckout"
10
import { isInIframe, sendMessage } from "../../donate/utils"
11
import { CURRENCIES, DONATION_FREQUENCIES } from "../../types/api"
12
13
import { themeSchema } from "../../donate/utils"
14
import { DonateFooter } from "../../components/donate/DonateFooter"
15
import { DonateWrapper } from "../../components/donate/DonateWrapper"
16
import { Theme } from "../../donate/types"
17
18
const primaryColor = "#6364ff"
19
const hoverColor = "#563acc"
20
21
export default function DonateCheckoutPage({
22
clientSecret,
23
stripePublicKey,
24
theme,
25
backUrl,
26
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
27
const loadStripePromise = useMemo(
28
() => loadStripe(stripePublicKey),
29
[stripePublicKey]
30
)
31
const fetchClientSecret = useCallback(
32
async () => clientSecret,
33
[clientSecret]
34
)
35
const router = useRouter()
36
37
useEffect(() => {
38
sendMessage("checkout-loaded")
39
}, [])
40
const handleDonate = useCallback(() => {
41
sendMessage("checkout-complete")
42
router.push(`/donate/complete?theme=${theme}`)
43
}, [router, theme])
44
45
return (
46
<DonateWrapper theme={theme} belowModal={<BelowModalLink />}>
47
<CheckoutProvider
48
stripe={loadStripePromise}
49
options={{
50
fetchClientSecret,
51
elementsOptions: {
52
appearance: {
53
theme: "flat",
54
variables: {
55
colorPrimary: primaryColor,
56
colorText: "#000000",
57
colorBackground: "#ffffff",
58
colorTextSecondary: primaryColor,
59
borderRadius: "0.5rem",
60
logoColor: theme,
61
},
62
rules: {
63
".AccordionItem": {
64
border: `1px solid ${primaryColor}`,
65
padding: "1rem",
66
},
67
".AccordionItem:hover": {
68
color: hoverColor,
69
borderColor: hoverColor,
70
},
71
".Input": {
72
border: `1px solid ${primaryColor}`,
73
},
74
".Input::placeholder": {
75
color: "#9b9b9b",
76
},
77
".Block": {
78
border: `1px solid #d4d4d4`,
79
boxShadow: "none",
80
},
81
},
82
},
83
},
84
}}
85
>
86
<DonateCheckout
87
onComplete={handleDonate}
88
className="p-8 pb-2 grow"
89
backUrl={backUrl}
90
/>
91
</CheckoutProvider>
92
<DonateFooter />
93
</DonateWrapper>
94
)
95
}
96
97
function BelowModalLink() {
98
if (isInIframe()) {
99
return null
100
}
101
return (
102
<p className="text-center text-b3 dark:text-gray-2 my-4">
103
<FormattedMessage
104
id="donate_widget.checkout.footer"
105
defaultMessage="For more options on how to donate, visit {link}"
106
values={{
107
link: (
108
<a href="https://joinmastodon.org/donate" className="underline">
109
joinmastodon.org/donate
110
</a>
111
),
112
}}
113
/>
114
</p>
115
)
116
}
117
118
interface DonateCheckoutPageProps {
119
clientSecret: string
120
stripePublicKey: string
121
theme: Theme
122
backUrl: string
123
}
124
125
const querySchema = z.object({
126
url: z.string().url(),
127
frequency: z.enum(DONATION_FREQUENCIES),
128
amount: z.coerce.number().int().positive().gte(100),
129
currency: z.enum(CURRENCIES),
130
theme: themeSchema,
131
})
132
133
const responseSchema = z.promise(
134
z.object({
135
clientSecret: z.string(),
136
})
137
)
138
139
export const getServerSideProps: GetServerSideProps<
140
DonateCheckoutPageProps
141
> = async ({ query, locale }) => {
142
try {
143
const { url, frequency, amount, currency, theme } = querySchema.parse(query)
144
145
const params = new URLSearchParams({
146
platform: "web",
147
frequency,
148
amount: amount.toString(),
149
currency,
150
locale,
151
source: "menu",
152
})
153
if (process.env.APP_ENV !== "production") {
154
params.set("environment", "staging")
155
}
156
const response = await fetch(`${url}?${params}`, {
157
headers: {
158
"Content-Type": "application/json",
159
},
160
})
161
if (!response.ok) {
162
throw new Error(
163
`Failed to get donation URL: ${response.status} ${response.statusText}`
164
)
165
}
166
const { clientSecret } = await responseSchema.parse(response.json())
167
return {
168
props: {
169
clientSecret,
170
stripePublicKey: process.env.STRIPE_PUBLIC_KEY ?? "",
171
theme,
172
backUrl: `/donate?${new URLSearchParams({ frequency, amount: amount.toString(), currency, theme }).toString()}`,
173
},
174
}
175
} catch (error) {
176
console.error("Error with checkout:", error)
177
return {
178
redirect: {
179
statusCode: 302,
180
destination: "/donate",
181
},
182
}
183
}
184
}
185
186