login.tsx219 lines · main
| 1 | import { useQuery } from '@tanstack/react-query' |
| 2 | import { useParams } from 'common' |
| 3 | import { LogOut } from 'lucide-react' |
| 4 | import Head from 'next/head' |
| 5 | import { useRouter } from 'next/router' |
| 6 | import { useEffect } from 'react' |
| 7 | import { Button } from 'ui' |
| 8 | import { Admonition, ShimmeringLoader } from 'ui-patterns' |
| 9 | |
| 10 | import { |
| 11 | InterstitialAccountRow, |
| 12 | InterstitialLayout, |
| 13 | LogoPair, |
| 14 | PartnerLogo, |
| 15 | BrivenLogo, |
| 16 | } from '@/components/layouts/InterstitialLayout' |
| 17 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 18 | import { useConfirmAccountRequestMutation } from '@/data/partners/stripe-projects-confirm-mutation' |
| 19 | import { accountRequestQueryOptions } from '@/data/partners/stripe-projects-query' |
| 20 | import { withAuth } from '@/hooks/misc/withAuth' |
| 21 | import { useSignOut } from '@/lib/auth' |
| 22 | import { BASE_PATH } from '@/lib/constants' |
| 23 | import { buildStudioPageTitle } from '@/lib/page-title' |
| 24 | import { useProfileNameAndPicture } from '@/lib/profile' |
| 25 | import type { NextPageWithLayout } from '@/types' |
| 26 | |
| 27 | const PAGE_TITLE = buildStudioPageTitle({ section: 'Authorize Stripe Projects', brand: 'Briven' }) |
| 28 | |
| 29 | const StripeProjectsLoginPage: NextPageWithLayout = () => { |
| 30 | const router = useRouter() |
| 31 | const { ar_id } = useParams() |
| 32 | const signOut = useSignOut() |
| 33 | const { username, primaryEmail, avatarUrl } = useProfileNameAndPicture() |
| 34 | |
| 35 | const { |
| 36 | data: accountRequest, |
| 37 | isPending: isQueryPending, |
| 38 | isSuccess: isQuerySuccess, |
| 39 | isError: isQueryError, |
| 40 | error, |
| 41 | } = useQuery({ |
| 42 | ...accountRequestQueryOptions({ arId: ar_id }), |
| 43 | enabled: typeof ar_id !== 'undefined', |
| 44 | }) |
| 45 | |
| 46 | const { |
| 47 | mutate: confirmAccountRequest, |
| 48 | isPending: isConfirmationPending, |
| 49 | isSuccess: isConfirmationSuccess, |
| 50 | } = useConfirmAccountRequestMutation() |
| 51 | |
| 52 | useEffect(() => { |
| 53 | if (!router.isReady) return |
| 54 | |
| 55 | if (!ar_id) { |
| 56 | router.push('/404') |
| 57 | } |
| 58 | }, [router.isReady, ar_id, router]) |
| 59 | |
| 60 | const handleApprove = async () => { |
| 61 | if (!ar_id || isConfirmationPending) return |
| 62 | confirmAccountRequest({ arId: ar_id }) |
| 63 | } |
| 64 | |
| 65 | const linkedOrg = accountRequest?.linked_organization |
| 66 | const emailMatches = accountRequest?.email_matches ?? false |
| 67 | const displayName = primaryEmail ?? username ?? accountRequest?.email ?? '' |
| 68 | const isPending = router.isReady && isQueryPending |
| 69 | const isConfirmed = isConfirmationSuccess |
| 70 | const isConfirming = isConfirmationPending |
| 71 | const isSuccess = isQuerySuccess |
| 72 | const isError = isQueryError |
| 73 | const showAuthorizationState = isSuccess && !isConfirmed |
| 74 | const interstitialDescription = isConfirmed |
| 75 | ? undefined |
| 76 | : 'This will create an organization on your behalf in Briven' |
| 77 | |
| 78 | return ( |
| 79 | <> |
| 80 | <Head> |
| 81 | <title>{PAGE_TITLE}</title> |
| 82 | </Head> |
| 83 | |
| 84 | <InterstitialLayout |
| 85 | logo={ |
| 86 | <LogoPair |
| 87 | left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />} |
| 88 | right={<BrivenLogo />} |
| 89 | /> |
| 90 | } |
| 91 | title="Authorize Stripe Projects" |
| 92 | description={interstitialDescription} |
| 93 | > |
| 94 | <div className="px-6 pb-6"> |
| 95 | {isPending && ( |
| 96 | <div className="flex flex-col gap-6"> |
| 97 | <div className="flex items-center gap-3 rounded-lg border border-secondary p-3"> |
| 98 | <ShimmeringLoader className="size-9 flex-shrink-0 rounded-full py-0" /> |
| 99 | <div className="min-w-0 flex-1 space-y-2"> |
| 100 | <ShimmeringLoader className="h-3 w-20 py-0" /> |
| 101 | <ShimmeringLoader className="h-4 w-40 max-w-full py-0" /> |
| 102 | </div> |
| 103 | <div className="h-8 w-8 flex-shrink-0" /> |
| 104 | </div> |
| 105 | <div className="flex flex-col gap-2"> |
| 106 | <ShimmeringLoader className="h-10 w-full py-0" /> |
| 107 | <ShimmeringLoader className="h-10 w-full py-0" /> |
| 108 | </div> |
| 109 | </div> |
| 110 | )} |
| 111 | |
| 112 | {isConfirmed && ( |
| 113 | <div className="flex flex-col gap-4"> |
| 114 | <Admonition type="success" title="Stripe Projects authorized" /> |
| 115 | <p className="text-center text-xs text-foreground-lighter text-balance"> |
| 116 | You can now close this tab. |
| 117 | </p> |
| 118 | </div> |
| 119 | )} |
| 120 | |
| 121 | {showAuthorizationState && !emailMatches && ( |
| 122 | <div className="flex flex-col gap-3"> |
| 123 | <Admonition |
| 124 | type="warning" |
| 125 | description={ |
| 126 | <> |
| 127 | You're signed in to a different account. Sign out and sign back in as{' '} |
| 128 | <span className="font-medium text-foreground">{accountRequest?.email}</span>. |
| 129 | Then return to Stripe to restart the request. |
| 130 | </> |
| 131 | } |
| 132 | /> |
| 133 | <Button type="default" block onClick={() => signOut()}> |
| 134 | Sign out |
| 135 | </Button> |
| 136 | </div> |
| 137 | )} |
| 138 | |
| 139 | {showAuthorizationState && emailMatches && linkedOrg && ( |
| 140 | <div className="flex flex-col gap-3"> |
| 141 | <Admonition |
| 142 | type="tip" |
| 143 | description={ |
| 144 | <> |
| 145 | <span className="font-medium text-foreground">{linkedOrg.name}</span> is already |
| 146 | linked to this Stripe account, and just needs to be confirmed. |
| 147 | </> |
| 148 | } |
| 149 | /> |
| 150 | <div className="flex flex-col gap-2"> |
| 151 | <Button type="primary" block loading={isConfirming} onClick={handleApprove}> |
| 152 | Authorize Stripe Projects |
| 153 | </Button> |
| 154 | <Button type="text" block onClick={() => router.push('/')}> |
| 155 | Cancel |
| 156 | </Button> |
| 157 | </div> |
| 158 | </div> |
| 159 | )} |
| 160 | |
| 161 | {showAuthorizationState && emailMatches && !linkedOrg && ( |
| 162 | <div className="flex flex-col gap-6"> |
| 163 | <InterstitialAccountRow |
| 164 | avatarUrl={avatarUrl} |
| 165 | displayName={displayName} |
| 166 | action={ |
| 167 | <ButtonTooltip |
| 168 | type="text" |
| 169 | size="small" |
| 170 | className="h-8 w-8 px-0" |
| 171 | onClick={() => signOut()} |
| 172 | icon={ |
| 173 | <LogOut size={16} strokeWidth={1.5} className="text-foreground-lighter" /> |
| 174 | } |
| 175 | tooltip={{ |
| 176 | content: { |
| 177 | side: 'top', |
| 178 | text: 'Sign out', |
| 179 | }, |
| 180 | }} |
| 181 | /> |
| 182 | } |
| 183 | /> |
| 184 | |
| 185 | <div className="flex flex-col gap-2"> |
| 186 | <Button |
| 187 | type="primary" |
| 188 | loading={isConfirming} |
| 189 | disabled={isConfirming} |
| 190 | onClick={handleApprove} |
| 191 | > |
| 192 | Create organization |
| 193 | </Button> |
| 194 | <Button type="text" onClick={() => router.push('/')}> |
| 195 | Cancel |
| 196 | </Button> |
| 197 | </div> |
| 198 | </div> |
| 199 | )} |
| 200 | |
| 201 | {isError && ( |
| 202 | <div className="flex flex-col gap-3"> |
| 203 | <Admonition |
| 204 | type="danger" |
| 205 | title="Unable to load authorization" |
| 206 | description={error?.message} |
| 207 | /> |
| 208 | <Button type="default" block onClick={() => signOut()}> |
| 209 | Sign out |
| 210 | </Button> |
| 211 | </div> |
| 212 | )} |
| 213 | </div> |
| 214 | </InterstitialLayout> |
| 215 | </> |
| 216 | ) |
| 217 | } |
| 218 | |
| 219 | export default withAuth(StripeProjectsLoginPage) |