ApiAuthorization.Valid.tsx206 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { useEffect, useMemo, useState, type ReactNode } from 'react'
3import { useForm, type UseFormReturn } from 'react-hook-form'
4import { toast } from 'sonner'
5
6import { ApiAuthorizationApprovedScreen } from './ApiAuthorization.Approved'
7import { ApiAuthorizationErrorScreen } from './ApiAuthorization.Error'
8import { ApiAuthorizationMainView } from './ApiAuthorization.Form'
9import { ApiAuthorizationLoadingScreen } from './ApiAuthorization.Loading'
10import {
11 approvalFormSchema,
12 type ApprovalState,
13 type IApprovalFormSchema,
14} from './ApiAuthorization.Schema'
15import { useApiAuthorizationApproveMutation } from '@/data/api-authorization/api-authorization-approve-mutation'
16import { useApiAuthorizationDeclineMutation } from '@/data/api-authorization/api-authorization-decline-mutation'
17import { useApiAuthorizationQuery } from '@/data/api-authorization/api-authorization-query'
18import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
19import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
20import type { Organization } from '@/types'
21
22function getMatchingOrganization(
23 organization_slug: string | undefined,
24 organizations: Array<Organization> | undefined
25): Organization | null {
26 if (!organization_slug || !organizations) return null
27 return organizations.find(({ slug }) => slug === organization_slug) ?? null
28}
29
30interface PreselectOrganizationSlugParameters {
31 form: UseFormReturn<IApprovalFormSchema>
32 organization_slug: string | undefined
33 organizations: Array<{ slug: string }>
34}
35
36function preselectOrganizationSlug({
37 form,
38 organization_slug,
39 organizations,
40}: PreselectOrganizationSlugParameters) {
41 if (organization_slug) {
42 const preselected = organizations.find(({ slug }) => slug === organization_slug)
43 if (preselected) form.setValue('selectedOrgSlug', preselected.slug)
44 } else if (!form.getValues('selectedOrgSlug') && organizations.length === 1) {
45 form.setValue('selectedOrgSlug', organizations[0].slug)
46 }
47}
48
49function useOrganizationsState(organization_slug: string | undefined) {
50 const {
51 data: organizations,
52 isPending: isLoadingOrganizations,
53 isError: isErrorOrganizations,
54 error: organizationsError,
55 } = useOrganizationsQuery()
56
57 const organizationsState = useMemo(
58 function calculateOrganizationsState() {
59 if (isLoadingOrganizations) {
60 return { _tag: 'loading' as const }
61 }
62 if (isErrorOrganizations) {
63 return { _tag: 'error' as const, error: organizationsError }
64 }
65 if (organizations.length === 0) {
66 return { _tag: 'empty' as const }
67 }
68 if (organization_slug) {
69 const matchingOrganization = getMatchingOrganization(organization_slug, organizations)
70 if (!matchingOrganization) {
71 return { _tag: 'not_member' as const }
72 }
73 }
74 return { _tag: 'success' as const, organizations }
75 },
76 [
77 isLoadingOrganizations,
78 isErrorOrganizations,
79 organizationsError,
80 organizations,
81 organization_slug,
82 ]
83 )
84
85 return organizationsState
86}
87
88function usePrefillFormOnOrganizationsSuccess(
89 form: UseFormReturn<IApprovalFormSchema>,
90 organizationsState: ReturnType<typeof useOrganizationsState>,
91 organization_slug: string | undefined
92) {
93 const prefillForm = useStaticEffectEvent(() => {
94 if (organizationsState._tag === 'success') {
95 preselectOrganizationSlug({
96 form,
97 organization_slug,
98 organizations: organizationsState.organizations,
99 })
100 }
101 })
102 useEffect(() => {
103 if (organizationsState._tag === 'success') {
104 prefillForm()
105 }
106 }, [organizationsState._tag, prefillForm])
107}
108
109export interface ApiAuthorizationValidScreenProps {
110 auth_id: string
111 organization_slug: string | undefined
112 navigate: (destination: string) => void
113}
114
115export function ApiAuthorizationValidScreen({
116 auth_id,
117 organization_slug,
118 navigate,
119}: ApiAuthorizationValidScreenProps): ReactNode {
120 const [approvalState, setApprovalState] = useState<ApprovalState>('indeterminate')
121
122 const form = useForm<IApprovalFormSchema>({
123 resolver: zodResolver(approvalFormSchema as any),
124 defaultValues: { selectedOrgSlug: '' },
125 mode: 'onSubmit',
126 reValidateMode: 'onBlur',
127 })
128
129 const organizationsState = useOrganizationsState(organization_slug)
130 usePrefillFormOnOrganizationsSuccess(form, organizationsState, organization_slug)
131
132 const {
133 data: requester,
134 isPending: isLoading,
135 isError,
136 error,
137 } = useApiAuthorizationQuery({ id: auth_id })
138 const isApproved = (requester?.approved_at ?? null) !== null
139
140 const { mutate: approveRequest } = useApiAuthorizationApproveMutation({
141 onSuccess: (res) => {
142 window.location.href = res.url
143 },
144 })
145 const { mutate: declineRequest } = useApiAuthorizationDeclineMutation({
146 onSuccess: () => {
147 toast.success('Declined API authorization request')
148 navigate('/organizations')
149 },
150 })
151
152 const onApproveRequest = form.handleSubmit((values) => {
153 if (approvalState !== 'indeterminate') {
154 return
155 }
156 setApprovalState('approving')
157 approveRequest(
158 { id: auth_id, slug: values.selectedOrgSlug },
159 { onError: () => setApprovalState('indeterminate') }
160 )
161 })
162
163 const onDeclineRequest = form.handleSubmit((values) => {
164 if (approvalState !== 'indeterminate') {
165 return
166 }
167 setApprovalState('declining')
168 declineRequest(
169 { id: auth_id, slug: values.selectedOrgSlug },
170 { onError: () => setApprovalState('indeterminate') }
171 )
172 })
173
174 if (isLoading) {
175 return <ApiAuthorizationLoadingScreen />
176 }
177
178 if (isError) {
179 return <ApiAuthorizationErrorScreen error={error} />
180 }
181
182 if (isApproved) {
183 const approvedOrganization =
184 organizationsState._tag === 'success'
185 ? organizationsState.organizations.find(
186 (org) => org.slug === requester.approved_organization_slug
187 )
188 : undefined
189
190 return (
191 <ApiAuthorizationApprovedScreen requester={requester} organization={approvedOrganization} />
192 )
193 }
194
195 return (
196 <ApiAuthorizationMainView
197 approvalState={approvalState}
198 form={form}
199 requester={requester}
200 requestedOrganizationSlug={organization_slug}
201 organizations={organizationsState}
202 onApprove={onApproveRequest}
203 onDecline={onDeclineRequest}
204 />
205 )
206}