RedeemCredits.tsx206 lines · main
1import { FeatureFlagContext } from 'common'
2import { useRouter } from 'next/router'
3import { ReactNode, useContext, useEffect, useRef, useState } from 'react'
4import { Button, Card, CardContent } from 'ui'
5import { Admonition, ShimmeringLoader } from 'ui-patterns'
6
7import { OrganizationSelector } from '../Connect/OrganizationSelector'
8import { CreditCodeRedemption } from '@/components/interfaces/Organization/BillingSettings/CreditCodeRedemption'
9import {
10 InterstitialAccountRow,
11 InterstitialLayout,
12 BrivenLogo,
13} from '@/components/layouts/InterstitialLayout'
14import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
15import { useProfile } from '@/lib/profile'
16import { EMPTY_ARR } from '@/lib/void'
17
18const RETURN_TO_SELECTED_ORG_PARAM = 'selected_org'
19
20const RedeemCreditsInterstitial = ({
21 title,
22 description,
23 children,
24}: {
25 title: ReactNode
26 description?: ReactNode
27 children: ReactNode
28}) => (
29 <InterstitialLayout logo={<BrivenLogo />} title={title} description={description}>
30 <div className="px-6 pb-6">{children}</div>
31 </InterstitialLayout>
32)
33
34export const RedeemCreditsScreen = () => {
35 const router = useRouter()
36 const { profile, isLoading: isLoadingProfile } = useProfile()
37 const { hasLoaded } = useContext(FeatureFlagContext)
38
39 const [selectedOrgSlug, setSelectedOrgSlug] = useState<string | null>(null)
40 const [redemptionModalOrgSlug, setRedemptionModalOrgSlug] = useState<string | null>(null)
41 const appliedReturnSelectedOrgRef = useRef<string | null>(null)
42
43 const {
44 data: organizationOptions = EMPTY_ARR,
45 error: organizationsError,
46 isLoading: isLoadingOrganizations,
47 isError: isOrganizationsError,
48 } = useOrganizationsQuery()
49
50 const returnSelectedOrgSlug =
51 router.isReady && typeof router.query[RETURN_TO_SELECTED_ORG_PARAM] === 'string'
52 ? router.query[RETURN_TO_SELECTED_ORG_PARAM]
53 : null
54
55 const displayName = profile?.primary_email ?? profile?.username
56
57 const isLoading = isLoadingProfile || isLoadingOrganizations || !hasLoaded
58
59 useEffect(() => {
60 if (!returnSelectedOrgSlug) return
61 if (appliedReturnSelectedOrgRef.current === returnSelectedOrgSlug) return
62
63 const hasReturnedOrganization = (organizationOptions ?? []).some(
64 (organization) => organization.slug === returnSelectedOrgSlug
65 )
66
67 if (hasReturnedOrganization) {
68 setSelectedOrgSlug(returnSelectedOrgSlug)
69 appliedReturnSelectedOrgRef.current = returnSelectedOrgSlug
70 }
71 }, [organizationOptions, returnSelectedOrgSlug])
72
73 if (isLoading) {
74 return (
75 <RedeemCreditsInterstitial
76 title={<ShimmeringLoader className="mx-auto h-7 w-32 max-w-full py-0" />}
77 description={<ShimmeringLoader className="mx-auto h-4 w-56 max-w-full py-0" />}
78 >
79 <ConnectLoadingCards />
80 </RedeemCreditsInterstitial>
81 )
82 }
83
84 if (isOrganizationsError) {
85 return (
86 <RedeemCreditsInterstitial
87 title="Unable to load credit redemption"
88 description="Please try again before redeeming this code"
89 >
90 <div className="flex flex-col gap-3">
91 <Admonition
92 type="warning"
93 description={
94 <>
95 We could not load your organizations.
96 {organizationsError && (
97 <span className="mt-1 block text-foreground-lighter">
98 Error: {organizationsError.message}
99 </span>
100 )}
101 </>
102 }
103 />
104 </div>
105 </RedeemCreditsInterstitial>
106 )
107 }
108
109 const createOrganizationParams = {
110 returnTo: router.asPath || '/redeem',
111 returnToOrgParam: RETURN_TO_SELECTED_ORG_PARAM,
112 }
113
114 const openRedemption = () => {
115 if (!selectedOrgSlug) return
116 setRedemptionModalOrgSlug(selectedOrgSlug)
117 }
118
119 return (
120 <>
121 <RedeemCreditsInterstitial
122 title="Redeem credits"
123 description="Choose an organization to redeem this code"
124 >
125 <div className="flex flex-col gap-5">
126 <InterstitialAccountRow displayName={displayName} />
127
128 <OrganizationSelector
129 organizations={organizationOptions}
130 selectedSlug={selectedOrgSlug}
131 onSelect={setSelectedOrgSlug}
132 getOrganizationDescription={(organization) => `${organization.plan.name} Plan`}
133 createLabel={
134 organizationOptions.length === 0
135 ? 'Create your first organization'
136 : 'Create new organization'
137 }
138 createHrefParams={createOrganizationParams}
139 />
140
141 {organizationOptions.length === 0 && (
142 <Admonition
143 type="warning"
144 description="Create an organization before redeeming this credit code."
145 />
146 )}
147
148 <div className="flex flex-col gap-2">
149 <Button
150 block
151 type="primary"
152 disabled={!selectedOrgSlug || organizationOptions.length === 0}
153 onClick={openRedemption}
154 >
155 Redeem credits
156 </Button>
157 <p className="text-center text-xs text-foreground-lighter text-balance">
158 Credits apply to one organization and are used toward future invoices before your
159 payment method is charged.
160 </p>
161 </div>
162 </div>
163 </RedeemCreditsInterstitial>
164
165 {redemptionModalOrgSlug && (
166 <CreditCodeRedemption
167 modalVisible
168 slug={redemptionModalOrgSlug}
169 onClose={() => setRedemptionModalOrgSlug(null)}
170 />
171 )}
172 </>
173 )
174}
175
176const ConnectLoadingCards = () => (
177 <div className="flex flex-col gap-5">
178 <Card className="shadow-none">
179 <CardContent className="flex items-center gap-3 border-none px-4 py-3">
180 <ShimmeringLoader className="size-8 flex-shrink-0 rounded-full py-0" />
181 <div className="min-w-0 flex-1 space-y-2">
182 <ShimmeringLoader className="h-3 w-20 py-0" />
183 <ShimmeringLoader className="h-4 w-40 max-w-full py-0" />
184 </div>
185 </CardContent>
186 </Card>
187 <section className="space-y-2" aria-label="Organizations">
188 <ShimmeringLoader className="h-3 w-24 py-0" />
189 {Array.from({ length: 3 }).map((_, index) => (
190 <Card key={index} className="shadow-none">
191 <CardContent className="flex items-center gap-3 border-none px-4 py-3">
192 <ShimmeringLoader className="size-9 flex-shrink-0 rounded-lg py-0" />
193 <div className="min-w-0 flex-1 space-y-2">
194 <ShimmeringLoader className="h-4 w-32 py-0" />
195 <ShimmeringLoader className="h-3 w-20 py-0" />
196 </div>
197 </CardContent>
198 </Card>
199 ))}
200 </section>
201 <div className="flex flex-col gap-2">
202 <ShimmeringLoader className="h-10 w-full py-0" />
203 <ShimmeringLoader className="h-10 w-full py-0" />
204 </div>
205 </div>
206)