/
purchase.tsx
377 lines (351 loc) · 12.6 KB
/
purchase.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
import * as React from 'react'
import {GetServerSideProps} from 'next'
import Layout from '@/components/app/layout'
import {
convertToSerializeForNextResponse,
determinePurchaseType,
PurchaseType,
} from '@skillrecordings/commerce-server'
import {
EXISTING_BULK_COUPON,
INDIVIDUAL_TO_BULK_UPGRADE,
NEW_BULK_COUPON,
NEW_INDIVIDUAL_PURCHASE,
} from '@skillrecordings/types'
import {getSdk, Purchase} from '@skillrecordings/database'
import CopyInviteLink from '@skillrecordings/skill-lesson/team/copy-invite-link'
import Image from 'next/image'
import Balancer from 'react-wrap-balancer'
import {SanityDocument} from '@sanity/client'
import {InvoiceCard} from '@/pages/invoices'
import {getProduct} from '@/lib/products'
import {isEmpty} from 'lodash'
import {Transfer} from '@/purchase-transfer/purchase-transfer'
import {trpc} from '@/trpc/trpc.client'
import {paymentOptions} from '../api/skill/[...skillRecordings]'
export const getServerSideProps: GetServerSideProps = async (context) => {
const {query} = context
const provider =
(query.provider instanceof Array ? query.provider[0] : query.provider) ||
'stripe'
const session_id =
query.session_id instanceof Array ? query.session_id[0] : query.session_id
const paymentProvider = paymentOptions.getProvider(provider)
if (!session_id || !paymentProvider) {
return {
notFound: true,
}
}
const purchaseInfo = await paymentProvider.getPurchaseInfo(session_id)
const {
email,
chargeIdentifier,
quantity: seatsPurchased,
product: merchantProduct,
} = purchaseInfo
const stripeProductName = merchantProduct.name
const purchase = await getSdk().getPurchaseForStripeCharge(chargeIdentifier)
if (!purchase || !email) {
return {
notFound: true,
}
}
const purchaseType = await determinePurchaseType({
checkoutSessionId: session_id,
})
const product = await getProduct(purchase.productId)
return {
props: {
// purchase: convertToSerializeForNextResponse(purchase),
purchase: convertToSerializeForNextResponse({
...purchase,
totalAmount: purchase.totalAmount.toString(),
}),
email,
seatsPurchased,
purchaseType,
bulkCouponId: purchase.bulkCoupon?.id || null,
product: product || null,
stripeProductName,
},
}
}
const InlineTeamInvite = ({
bulkCouponId,
seatsPurchased,
}: {
bulkCouponId?: string
seatsPurchased: number
}) => {
if (!bulkCouponId) return null
return (
<div className="mx-auto w-full px-5">
<h2 className="pb-2 text-sm font-medium">Invite your team</h2>
<div className="flex flex-col rounded-lg border border-indigo-600/50 p-5">
<p className="pb-2 font-semibold text-white">
You have purchased {seatsPurchased} seats.
</p>
<p className="pb-2 text-sm">
Invite your team to claim seats right away with this invite link.
Don't worry about saving this anywhere, it will always be available on
your{' '}
<a
className="font-semibold underline"
href={`${process.env.NEXT_PUBLIC_URL}/team`}
>
Team page
</a>{' '}
once you log in.
</p>
<CopyInviteLink
className="[&_[data-sr-button]]:bg-gray-900/75 [&_[data-sr-button]]:text-primary [&_[data-sr-button]]:text-white [&_[data-sr-button]]:hover:bg-gray-900 [&_[data-sr-button]]:dark:bg-gray-900/75 [&_[data-sr-button]]:dark:text-white [&_[data-sr-button]]:dark:hover:bg-gray-900 [&_input]:border-transparent [&_input]:bg-gray-900 [&_input]:text-sm [&_input]:font-medium [&_input]:dark:bg-gray-900"
bulkCouponId={bulkCouponId}
/>
</div>
</div>
)
}
type ThankYouProps = {
email: string
product: SanityDocument
title: string
byline: JSX.Element | null
}
const ThankYou: React.FC<ThankYouProps> = ({title, byline, product, email}) => {
return (
<header className="flex w-full flex-col items-center justify-center">
<div className="flex flex-col items-center">
{product?.image && (
<div className="flex flex-shrink-0 items-center justify-center">
<Image
className="overflow-hidden rounded-full"
src={product.image.url}
alt={product.title}
quality={100}
width={200}
height={200}
priority
/>
</div>
)}
<div className="flex flex-col items-center pt-8 text-center">
<h1 className="font-heading max-w-sm text-lg font-medium sm:text-xl lg:text-2xl">
<span className="font-heading block pb-2 text-sm font-black uppercase text-primary dark:text-emerald-300">
Success!
</span>
<Balancer>{title}</Balancer>
</h1>
<p className="pt-5 font-medium">{byline}</p>
</div>
</div>
</header>
)
}
const LoginLink: React.FC<{email: string}> = ({email}) => {
return (
<div className="relative mx-auto flex w-full items-center justify-center gap-5">
<div className="relative z-10 flex flex-col items-center justify-center text-center">
<div className="flex flex-col items-center justify-center gap-2">
<MailIcon />
<h2 className="flex flex-col items-center justify-center break-all pt-3 text-center text-xl font-semibold">
<span className="drop-shadow-sm">Login link sent to:</span>
<span className="font-normal drop-shadow-sm">{email}</span>
</h2>
</div>
<p className="max-w-sm pt-5 text-center text-sm opacity-90">
<Balancer>
As a final step to access the course you need to check your inbox (
<strong>{email}</strong>) where you will find an email from{' '}
<strong>{process.env.NEXT_PUBLIC_SUPPORT_EMAIL}</strong> with a link
to access your purchase and start learning.
</Balancer>
</p>
</div>
</div>
)
}
const ThanksVerify: React.FC<
React.PropsWithChildren<{
email: string
seatsPurchased: number
purchaseType: PurchaseType
bulkCouponId: string
product: SanityDocument
stripeProductName: string
purchase: Purchase
}>
> = ({
email,
seatsPurchased,
purchaseType,
bulkCouponId,
product,
stripeProductName,
purchase,
}) => {
let byline = null
let title = `Thank you for purchasing ${stripeProductName}`
let loginLink = null
let inviteTeam = (
<InlineTeamInvite
bulkCouponId={bulkCouponId}
seatsPurchased={seatsPurchased}
/>
)
switch (purchaseType) {
case NEW_INDIVIDUAL_PURCHASE:
loginLink = LoginLink
break
case NEW_BULK_COUPON:
byline = (
<>
Your purchase is for <strong>{seatsPurchased}</strong> seat
{seatsPurchased > 1 && 's'}. You can always add more seats later when
your team grows.
</>
)
loginLink = LoginLink
break
case EXISTING_BULK_COUPON:
title = `Thank you for purchasing more seats for ${
product?.name || process.env.NEXT_PUBLIC_SITE_TITLE
}!`
byline = (
<>
Your purchase is for <strong>{seatsPurchased}</strong> additional seat
{seatsPurchased > 1 && 's'}. You can always add more seats later when
your team grows.
</>
)
break
case INDIVIDUAL_TO_BULK_UPGRADE:
title = `Thank you for purchasing more seats for ${
product?.name || process.env.NEXT_PUBLIC_SITE_TITLE
}!`
byline = (
<>
Your purchase is for <strong>{seatsPurchased}</strong> additional seat
{seatsPurchased > 1 && 's'}. You can always add more seats later when
your team grows.
</>
)
break
}
return (
<>
<Layout meta={{title: 'Purchase Successful'}}>
<main className="mx-auto flex w-full max-w-screen-lg flex-col-reverse sm:flex-grow lg:grid lg:grid-cols-9 lg:py-8">
<div className="col-span-4 flex w-full flex-col items-center justify-center px-5 pb-16 pt-10 sm:px-10 lg:py-16">
<ThankYou
title={title}
byline={byline}
product={product}
email={email}
/>
<div className="w-full max-w-md pt-8">
<h3 className="pb-2 text-sm font-medium">Your invoice</h3>
<InvoiceCard
target="_blank"
className="w-full p-4 [&_[data-content]]:flex-col [&_[data-content]]:items-start"
purchase={{product: {name: stripeProductName}, ...purchase}}
/>
</div>
<PurchaseTransfer purchase={purchase} />
</div>
<div className="col-span-5 flex flex-col items-center justify-center bg-gradient-to-tr from-primary to-indigo-500 pb-10 pt-16 text-primary-foreground selection:bg-gray-900 sm:pb-24 sm:pt-24 lg:rounded-md">
<div className="flex max-w-screen-sm flex-col gap-10 sm:px-10 lg:px-16">
{loginLink && loginLink({email})}
{inviteTeam && inviteTeam}
</div>
</div>
</main>
</Layout>
</>
)
}
const PurchaseTransfer: React.FC<{
bulkCouponId?: string
purchase: {id: string; userId: string | null}
}> = ({bulkCouponId, purchase}) => {
const {data: purchaseUserTransfers, refetch} =
trpc.purchaseUserTransfer.forPurchaseId.useQuery({
id: purchase.id,
sourceUserId: purchase.userId || undefined,
})
if (bulkCouponId) return null
if (isEmpty(purchaseUserTransfers)) return null
return (
<div className="pt-5">
{purchaseUserTransfers && (
<Transfer
className="[&_] flex w-full items-start rounded-lg border border-gray-100 bg-white p-4 dark:border-gray-800 dark:bg-gray-900 [&_[data-content]]:flex-col [&_[data-content]]:items-start [&_h2]:text-lg [&_h2]:leading-tight [&_p]:text-sm"
purchaseUserTransfers={purchaseUserTransfers}
refetch={refetch}
/>
)}
</div>
)
}
export default ThanksVerify
const MailIcon = () => {
return (
<div className="rounded-full bg-black/5 p-5 shadow-inner">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
className="h-12 w-12 brightness-110 drop-shadow-lg"
>
<title>send</title>
<g>
<path
d="M13 43C12.798 43 17.046 25.702 17.046 25.702C17.15 25.368 17.421 25.112 17.761 25.029C18.1 24.943 18.46 25.046 18.707 25.293L25.707 32.293C25.912 32.498 26.018 32.781 25.997 33.071C25.976 33.36 25.832 33.626 25.6 33.8L13.6 42.8C13.423 42.934 13.211 43 13 43Z"
fill="url(#nc-ui-3-0_linear_119_134)"
></path>
<path
d="M35.992 44.9999C35.779 44.9999 35.567 44.9319 35.392 44.7999L3.4 20.7999C3.11 20.5829 2.961 20.2279 3.008 19.8689C3.055 19.5109 3.292 19.2059 3.628 19.0709L43.629 3.07095C43.97 2.93595 44.358 2.99495 44.64 3.23095C44.922 3.46495 45.053 3.83495 44.981 4.19595L36.973 44.1959C36.906 44.5329 36.67 44.8109 36.349 44.9339C36.234 44.9779 36.113 44.9989 35.993 44.9989L35.992 44.9999Z"
fill="url(#nc-ui-3-1_linear_119_134)"
></path>
<path
d="M13.001 43C12.947 43 12.894 42.996 12.84 42.987C12.356 42.908 12 42.49 12 42V27C12 26.684 12.15 26.386 12.404 26.197L43.404 3.19704C43.829 2.88204 44.423 2.95204 44.763 3.35304C45.104 3.75604 45.073 4.35404 44.693 4.72004L17.199 31.151L13.949 42.316C13.811 42.729 13.425 43 13.001 43Z"
fill="url(#nc-ui-3-2_linear_119_134)"
></path>
<defs>
<linearGradient
id="nc-ui-3-0_linear_119_134"
x1="19.4963"
y1="24.9991"
x2="19.4963"
y2="43"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#A2A3B4"></stop>
<stop offset="1" stopColor="#83849B"></stop>
</linearGradient>
<linearGradient
id="nc-ui-3-1_linear_119_134"
x1="24"
y1="2.99976"
x2="24"
y2="44.9999"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#E0E0E6"></stop>
<stop offset="1" stopColor="#C2C3CD"></stop>
</linearGradient>
<linearGradient
id="nc-ui-3-2_linear_119_134"
x1="28.4999"
y1="2.99988"
x2="28.4999"
y2="43"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#C2C3CD"></stop>
<stop offset="1" stopColor="#A2A3B4"></stop>
</linearGradient>
</defs>
</g>
</svg>
</div>
)
}