Skip to content

Commit

Permalink
Avoid changing state during the organization creation promise chain
Browse files Browse the repository at this point in the history
Also removed the `signerAddress` in useAuth, we should be using
`account.address` from `useClient` instead.
  • Loading branch information
elboletaire committed Dec 27, 2024
1 parent f4ed617 commit 288c800
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 60 deletions.
9 changes: 5 additions & 4 deletions src/components/Account/useSaasAccountProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ const useSaasOrganization = ({
}: {
options?: Omit<UseQueryOptions<OrganizationData>, 'queryKey' | 'queryFn'>
} = {}) => {
const { bearedFetch, signerAddress } = useAuth()
const { bearedFetch } = useAuth()
const { account } = useClient()

return useQuery({
queryKey: ['organizations', 'info', signerAddress],
queryFn: () => bearedFetch<OrganizationData>(ApiEndpoints.Organization.replace('{address}', signerAddress)),
enabled: !!signerAddress,
queryKey: ['organizations', 'info', account?.address],
queryFn: () => bearedFetch<OrganizationData>(ApiEndpoints.Organization.replace('{address}', account?.address)),
enabled: !!account?.address,
...options,
})
}
Expand Down
29 changes: 19 additions & 10 deletions src/components/Auth/useAuthProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMutation } from '@tanstack/react-query'
import { useClient } from '@vocdoni/react-providers'
import { RemoteSigner } from '@vocdoni/sdk'
import { NoOrganizationsError, RemoteSigner } from '@vocdoni/sdk'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { api, ApiEndpoints, ApiParams, UnauthorizedApiError } from '~components/Auth/api'
import { LoginResponse, useLogin, useRegister, useVerifyMail } from '~components/Auth/authQueries'
Expand All @@ -23,12 +23,19 @@ const useSigner = () => {
token,
})
// Once the signer is set, try to get the signer address
const address = await signer.getAddress()
setSigner(signer)
return address
try {
await signer.getAddress()
setSigner(signer)
return signer
} catch (e) {
// If is NoOrganizationsError ignore the error
if (!(e instanceof NoOrganizationsError)) {
throw e
}
}
}, [])

return useMutation<string, Error, string>({ mutationFn: updateSigner })
return useMutation<RemoteSigner, Error, string>({ mutationFn: updateSigner })
}

export const useAuthProvider = () => {
Expand All @@ -46,7 +53,7 @@ export const useAuthProvider = () => {
storeLogin(data)
},
})
const { mutate: updateSigner, isIdle: signerIdle, isPending: signerPending, data: signerAddress } = useSigner()
const { mutateAsync: updateSigner, isIdle: signerIdle, isPending: signerPending } = useSigner()

const bearedFetch = useCallback(
<T>(path: string, { headers = new Headers({}), ...params }: ApiParams = {}) => {
Expand Down Expand Up @@ -85,15 +92,17 @@ export const useAuthProvider = () => {
}, [])

const signerRefresh = useCallback(() => {
if (bearer && !clientSigner) {
updateSigner(bearer)
if (bearer) {
return updateSigner(bearer)
}
}, [bearer, clientSigner])

// If no signer but berarer instantiate the signer
// For example when bearer is on local storage but no login was done to instantiate the signer
useEffect(() => {
signerRefresh()
if (!clientSigner) {
signerRefresh()
}
}, [bearer, clientSigner])

const isAuthenticated = useMemo(() => !!bearer, [bearer])
Expand All @@ -104,13 +113,13 @@ export const useAuthProvider = () => {

return {
isAuthenticated,
bearer,
login,
register,
mailVerify,
logout,
bearedFetch,
isAuthLoading,
signerAddress,
signerRefresh,
}
}
53 changes: 23 additions & 30 deletions src/components/Organization/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Button, Flex, FlexProps, Stack, Text } from '@chakra-ui/react'

import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { useClient } from '@vocdoni/react-providers'
import { useEffect, useState } from 'react'
import { Account, RemoteSigner } from '@vocdoni/sdk'
import { useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { Trans, useTranslation } from 'react-i18next'
import { Link as ReactRouterLink, useNavigate } from 'react-router-dom'
import { CreateOrgParams } from '~components/Account/AccountTypes'
import LogoutBtn from '~components/Account/LogoutBtn'
import { useAccountCreate } from '~components/Account/useAccountCreate'
import { ApiEndpoints } from '~components/Auth/api'
import { useAuth } from '~components/Auth/useAuth'
import { useAuthProvider } from '~components/Auth/useAuthProvider'
Expand Down Expand Up @@ -43,15 +43,14 @@ export const OrganizationCreate = ({
const [isPending, setIsPending] = useState(false)
const methods = useForm<FormData>()
const { handleSubmit } = methods
const [success, setSuccess] = useState<string | null>(null)
const { signerRefresh } = useAuthProvider()
const { signer } = useClient()
const { bearer, signerRefresh } = useAuthProvider()
const { client, fetchAccount } = useClient()
const [promiseError, setPromiseError] = useState<Error | null>(null)

const { create: createAccount, error: accountError } = useAccountCreate()
const { mutateAsync: createSaasAccount, isError: isSaasError, error: saasError } = useOrganizationCreate()

const error = saasError || accountError
const error = saasError || promiseError
const isError = isSaasError || !!promiseError

const onSubmit = (values: FormData) => {
setIsPending(true)
Expand All @@ -64,36 +63,30 @@ export const OrganizationCreate = ({
country: values.countrySelect?.value,
type: values.typeSelect?.value,
})
// ensure the signer is properly initialized
.then(() => {
signerRefresh()
})
// we need to ensure the SDK populated the signer with our account (otherwise "createAccount" would fail)
.then(() => signer.getAddress())
// then we create the account on the vochain
.then(() =>
createAccount({
name: typeof values.name === 'object' ? values.name.default : values.name,
description: typeof values.description === 'object' ? values.description.default : values.description,
const signer = new RemoteSigner({
url: import.meta.env.SAAS_URL,
token: bearer,
})
client.wallet = signer
return client.createAccount({
account: new Account({
name: typeof values.name === 'object' ? values.name.default : values.name,
description: typeof values.description === 'object' ? values.description.default : values.description,
}),
})
)
// save on success to redirect (cannot directly redirect due to a re-render during the promise chain)
.then(() => setSuccess(onSuccessRoute as unknown as string))
})
// update state info and redirect
.then(() => {
fetchAccount().then(() => signerRefresh())
return navigate(onSuccessRoute as unknown)
})
.catch((e) => {
setPromiseError(e)
})
.finally(() => setIsPending(false))
}

const isError = !!accountError || isSaasError || !!promiseError

// The promise chain breaks the redirection due to some re-render in between, so we need to redirect in an effect
useEffect(() => {
if (!success) return

navigate(success)
}, [success])

return (
<FormProvider {...methods}>
<Flex
Expand Down Expand Up @@ -122,7 +115,7 @@ export const OrganizationCreate = ({
{t('organization.create_org')}
</Button>
</Stack>
<FormSubmitMessage isError={isError} error={error || saasError || promiseError} />
<FormSubmitMessage isError={isError} error={error} />
<Text color={'account_create_text_secondary'} fontSize='sm' textAlign='center' mt='auto'>
<Trans i18nKey='create_org.already_profile'>
If your organization already have a profile, ask the admin to invite you to your organization.
Expand Down
5 changes: 3 additions & 2 deletions src/components/Organization/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ import fallback from '/assets/default-avatar.png'
type FormData = CustomOrgFormData & PrivateOrgFormData & CreateOrgParams

const useOrganizationEdit = (options?: Omit<UseMutationOptions<void, Error, CreateOrgParams>, 'mutationFn'>) => {
const { bearedFetch, signerAddress } = useAuth()
const { bearedFetch } = useAuth()
const { account } = useClient()
return useMutation<void, Error, CreateOrgParams>({
mutationFn: (params: CreateOrgParams) =>
bearedFetch<void>(ApiEndpoints.Organization.replace('{address}', signerAddress), {
bearedFetch<void>(ApiEndpoints.Organization.replace('{address}', account?.address), {
body: params,
method: 'PUT',
}),
Expand Down
15 changes: 9 additions & 6 deletions src/components/Organization/Team.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Avatar, Badge, Box, Flex, Table, TableContainer, Tbody, Td, Text, Th, Thead, Tr } from '@chakra-ui/react'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { useClient } from '@vocdoni/react-providers'
import { Trans } from 'react-i18next'
import { ApiEndpoints } from '~components/Auth/api'
import { useAuth } from '~components/Auth/useAuth'
Expand Down Expand Up @@ -33,11 +34,12 @@ export const useTeamMembers = ({
}: {
options?: Omit<UseQueryOptions<TeamMembersResponse>, 'queryKey' | 'queryFn'>
} = {}) => {
const { bearedFetch, signerAddress } = useAuth()
const { bearedFetch } = useAuth()
const { account } = useClient()
return useQuery({
queryKey: ['organizations', 'members', signerAddress],
queryKey: ['organizations', 'members', account?.address],
queryFn: () =>
bearedFetch<TeamMembersResponse>(ApiEndpoints.OrganizationMembers.replace('{address}', signerAddress)),
bearedFetch<TeamMembersResponse>(ApiEndpoints.OrganizationMembers.replace('{address}', account?.address)),
...options,
select: (data) => data.members,
})
Expand All @@ -49,12 +51,13 @@ export const usePendingTeamMembers = ({
}: {
options?: Omit<UseQueryOptions<PendingTeamMembersResponse>, 'queryKey' | 'queryFn'>
} = {}) => {
const { bearedFetch, signerAddress } = useAuth()
const { bearedFetch } = useAuth()
const { account } = useClient()
return useQuery({
queryKey: ['organizations', 'members', 'pending', signerAddress],
queryKey: ['organizations', 'members', 'pending', account?.address],
queryFn: () =>
bearedFetch<PendingTeamMembersResponse>(
ApiEndpoints.OrganizationPendingMembers.replace('{address}', signerAddress)
ApiEndpoints.OrganizationPendingMembers.replace('{address}', account?.address)
),
...options,
select: (data) => data.pending,
Expand Down
12 changes: 8 additions & 4 deletions src/components/Pricing/SubscriptionPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js'
import { loadStripe, Stripe } from '@stripe/stripe-js'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useClient } from '@vocdoni/react-providers'
import { ensure0x } from '@vocdoni/sdk'
import { useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -40,16 +41,18 @@ type CheckoutResponse = {
}

const useUpdateSubscription = () => {
const { bearedFetch, signerAddress } = useAuth()
const { bearedFetch } = useAuth()
const { account } = useClient()

return useMutation<SubscriptionType>({
mutationFn: async () =>
await bearedFetch(ApiEndpoints.OrganizationSubscription.replace('{address}', ensure0x(signerAddress))),
await bearedFetch(ApiEndpoints.OrganizationSubscription.replace('{address}', ensure0x(account?.address))),
})
}

export const SubscriptionPayment = ({ amount, lookupKey }: SubscriptionPaymentData) => {
const { signerAddress, bearedFetch } = useAuth()
const { bearedFetch } = useAuth()
const { signer } = useClient()
const { i18n } = useTranslation()
const { subscription } = useSubscription()
const { mutateAsync: checkSubscription } = useUpdateSubscription()
Expand All @@ -64,6 +67,7 @@ export const SubscriptionPayment = ({ amount, lookupKey }: SubscriptionPaymentDa
const queryClient = useQueryClient()

const fetchClientSecret = useCallback(async () => {
const signerAddress = await signer.getAddress()
if (signerAddress) {
const body = {
lookupKey,
Expand All @@ -80,7 +84,7 @@ export const SubscriptionPayment = ({ amount, lookupKey }: SubscriptionPaymentDa
.then((data) => data.clientSecret)
}
return await Promise.resolve('')
}, [signerAddress])
}, [signer])

const onComplete = async () => {
let nsub = await checkSubscription()
Expand Down
10 changes: 9 additions & 1 deletion src/elements/dashboard/organization/create.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useOutletContext } from 'react-router-dom'
import { useNavigate, useOutletContext } from 'react-router-dom'
import { useAccountHealthTools } from '~components/Account/use-account-health-tools'
import { DashboardContents } from '~components/Layout/Dashboard'
import { OrganizationCreate } from '~components/Organization/Create'
import { DashboardLayoutContext } from '~elements/LayoutDashboard'
import { Routes } from '~src/router/routes'

const DashBoardCreateOrg = () => {
const { t } = useTranslation()
const [onSuccessRoute, setOnSuccessRoute] = useState<number>(null)
const { setTitle } = useOutletContext<DashboardLayoutContext>()
const { exists } = useAccountHealthTools()
const navigate = useNavigate()

// Set layout title and subtitle and back button
useEffect(() => {
setTitle(t('create_org.title', { defaultValue: 'Organization' }))
// redirect to profile if organization already exists
if (exists) {
navigate(Routes.dashboard.organization)
}
if (window.history.state.idx) {
setOnSuccessRoute(-1)
}
Expand Down
4 changes: 1 addition & 3 deletions src/router/OrganizationProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Outlet, useOutletContext } from 'react-router-dom'
import { useAccountHealthTools } from '~components/Account/use-account-health-tools'
import { useAuth } from '~components/Auth/useAuth'
import { NoOrganizationsPage } from '~components/Organization/NoOrganizations' // This protected routes are supposed to be inside of a AccountProtectedRoute

// This protected routes are supposed to be inside of a AccountProtectedRoute
// So no auth/loading checks are performed here
const OrganizationProtectedRoute = () => {
const context = useOutletContext()
const { exists } = useAccountHealthTools()
const { signerAddress } = useAuth()

if (!exists && !signerAddress) {
if (!exists) {
return <NoOrganizationsPage />
}

Expand Down

0 comments on commit 288c800

Please sign in to comment.