Skip to content

Commit

Permalink
Implement saas org provider (#780)
Browse files Browse the repository at this point in the history
* Implement useSaasAccount

* Implement useSaasAccount on needed screens

* Implement update account

* Improve FormSubmitMessage

* Expose fetch account

* Bulk fixes

* Usa await instead of chaining promises
  • Loading branch information
selankon authored and elboletaire committed Oct 18, 2024
1 parent 392bbdf commit 1ec530e
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 73 deletions.
16 changes: 14 additions & 2 deletions src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { translations } from './i18n/components'
import { datesLocale } from './i18n/locales'
import { RoutesProvider } from './router/Router'
import { RainbowKitTheme, Theme } from './Theme'
import { SaasAccountProvider } from '~components/AccountSaas/SaasAccountContext'

const queryClient = new QueryClient()

Expand All @@ -29,6 +30,17 @@ export const Providers = () => {
)
}

const SaasProviders = ({ children }: PropsWithChildren<{}>) => {
if (!import.meta.env.SAAS_URL) {
return children
}
return (
<AuthProvider>
<SaasAccountProvider>{children}</SaasAccountProvider>
</AuthProvider>
)
}

export const AppProviders = () => {
const { data } = useWalletClient()
const { address } = useAccount()
Expand All @@ -48,10 +60,10 @@ export const AppProviders = () => {
datesLocale={datesLocale(i18n.language)}
options={{ faucet_url: import.meta.env.CUSTOM_FAUCET_URL }}
>
<AuthProvider>
<SaasProviders>
<ColorModeScript />
<RoutesProvider />
</AuthProvider>
</SaasProviders>
</ClientProvider>
</RainbowKitTheme>
)
Expand Down
14 changes: 9 additions & 5 deletions src/components/AccountSaas/AccountTypes.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
export interface OrgInterface {
import { AccountData, IAccount } from '@vocdoni/sdk'

export type SaasOrganizationData = {
active: boolean
address: string
createdAt: string
name: string
website: string
description: string
size: string
type: string
country: string
timezone: string
language: string
logo: string
header: string
subdomain: string
color: string
communications: boolean
}

export type CreateOrgParams = Partial<Omit<OrgInterface, 'active' | 'address' | 'createdAt'>>
export type OrganizationData = SaasOrganizationData & AccountData

export type CreateOrgParams = Partial<
Pick<IAccount, 'name' | 'description' | 'logo' | 'header'> &
Omit<SaasOrganizationData, 'active' | 'address' | 'createdAt'>
>
4 changes: 2 additions & 2 deletions src/components/AccountSaas/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ export const AccountCreate = ({ children, ...props }: FlexProps) => {
.then(() => signer.getAddress()) // Get the address of newly created signer
.then(() =>
createAccount({
name: values.name,
description: values.description,
name: typeof values.name === 'object' ? values.name.default : values.name,
description: typeof values.description === 'object' ? values.description.default : values.description,
})
) // Create the new account on the vochain
.finally(() => setIsPending(false))
Expand Down
86 changes: 59 additions & 27 deletions src/components/AccountSaas/EditProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,95 @@ import {
import { REGEX_AVATAR } from '~constants'
import useDarkMode from '~src/themes/saas/hooks/useDarkMode'
import fallback from '/assets/default-avatar.png'
import { useEditSaasOrganization, useSaasOrganization } from '~components/AccountSaas/queries'
import FormSubmitMessage from '~components/Layout/FormSubmitMessage'
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { useAuth } from '~components/Auth/useAuth'
import { ApiEndpoints } from '~components/Auth/api'
import { useSaasAccount } from '~components/AccountSaas/useSaasAccount'
import { useClient } from '@vocdoni/react-providers'
import { Account } from '@vocdoni/sdk'

type FormData = CustomOrgFormData & PrivateOrgFormData & CreateOrgParams

const useEditSaasOrganization = (options?: Omit<UseMutationOptions<void, Error, CreateOrgParams>, 'mutationFn'>) => {
const { bearedFetch, signerAddress } = useAuth()
return useMutation<void, Error, CreateOrgParams>({
mutationFn: (params: CreateOrgParams) =>
bearedFetch<void>(ApiEndpoints.ORGANIZATION.replace('{address}', signerAddress), {
body: params,
method: 'PUT',
}),
...options,
})
}

const EditProfile = () => {
const { t } = useTranslation()
const {
updateAccount,
loading: { update: isUpdateLoading },
errors: { update: updateError },
} = useClient()
const { organization } = useSaasAccount()

const { data } = useSaasOrganization()
const { mutate, isPending, isError, error, isSuccess } = useEditSaasOrganization()
const {
mutateAsync,
isPending: isSaasPending,
isError: isSaasError,
error: saasError,
isSuccess,
} = useEditSaasOrganization()

const methods = useForm<FormData>({
defaultValues: {
name: data?.name || '',
website: data?.website || '',
description: data?.description || '',
sizeSelect: data?.size && {
value: data.size,
name: organization?.account.name.default || '',
website: organization?.website || '',
description: organization?.account.description.default || '',
sizeSelect: organization?.size && {
value: organization.size,
},
typeSelect: data?.type && {
value: data.type,
typeSelect: organization?.type && {
value: organization.type,
},
countrySelect: data?.country && {
value: data.country || '',
countrySelect: organization?.country && {
value: organization.country || '',
},
communications: data?.communications || false,
timeZoneSelect: data?.timezone && {
value: data.timezone,
communications: organization?.communications || false,
timeZoneSelect: organization?.timezone && {
value: organization.timezone,
},
languageSelect: data?.language && {
value: data.language,
languageSelect: organization?.language && {
value: organization.language,
},
logo: data?.logo || '',
header: data?.header || '',
logo: organization?.account.avatar || '',
header: organization?.header || '',
},
})

const { handleSubmit } = methods

const onSubmit: SubmitHandler<FormData> = async (values: FormData) => {
const newInfo: CreateOrgParams = {
name: values.name,
website: values.website,
description: values.description,
size: values.sizeSelect?.value,
type: values.typeSelect?.value,
country: values.countrySelect?.value,
timezone: values.timeZoneSelect.value,
language: values.languageSelect.value,
logo: values.logo,
header: values.header,
}
mutate({
...data,
...newInfo,
})

await mutateAsync({ ...organization, ...newInfo })
const newAccount = new Account({ ...organization?.account, ...values })
// Check if account changed before trying to update
if (JSON.stringify(newAccount.generateMetadata()) !== JSON.stringify(organization?.account.generateMetadata())) {
updateAccount(newAccount)
}
}

const isPending = isUpdateLoading || isSaasPending
const isError = isSaasError || !!updateError
const error = saasError || updateError

return (
<FormProvider {...methods}>
<Box height='100%' maxH='100%' overflowY='auto'>
Expand Down Expand Up @@ -106,6 +137,7 @@ const EditProfile = () => {
})}
</Button>
<FormSubmitMessage
isLoading={isPending}
isError={isError}
error={error}
isSuccess={isSuccess}
Expand Down
13 changes: 13 additions & 0 deletions src/components/AccountSaas/SaasAccountContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContext, ReactNode } from 'react'
import { useSaasAccountProvider } from './useSaasAccountProvider'

export const SaasAccountContext = createContext<ReturnType<typeof useSaasAccountProvider> | undefined>(undefined)

export const SaasAccountProvider = ({ children }: { children: ReactNode }) => {
const isSaas = !!import.meta.env.SAAS_URL
const saasAcount = useSaasAccountProvider({ options: { enabled: isSaas } })
if (!isSaas) {
return children
}
return <SaasAccountContext.Provider value={saasAcount}>{children}</SaasAccountContext.Provider>
}
33 changes: 0 additions & 33 deletions src/components/AccountSaas/queries.ts

This file was deleted.

10 changes: 10 additions & 0 deletions src/components/AccountSaas/useSaasAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useContext } from 'react'
import { SaasAccountContext } from '~components/AccountSaas/SaasAccountContext'

export const useSaasAccount = () => {
const context = useContext(SaasAccountContext)
if (!context) {
throw new Error('useSaasAccount must be used within an SaasAccountProvider')
}
return context
}
50 changes: 50 additions & 0 deletions src/components/AccountSaas/useSaasAccountProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useClient } from '@vocdoni/react-providers'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { OrganizationData } from '~components/AccountSaas/AccountTypes'
import { useAuth } from '~components/Auth/useAuth'
import { ApiEndpoints } from '~components/Auth/api'
import { useCallback } from 'react'

const useSaasOrganization = ({
options,
}: {
options?: Omit<UseQueryOptions<OrganizationData>, 'queryKey' | 'queryFn'>
} = {}) => {
const { bearedFetch, signerAddress } = useAuth()

return useQuery({
queryKey: ['organizations', 'info', signerAddress],
queryFn: () => bearedFetch<OrganizationData>(ApiEndpoints.ORGANIZATION.replace('{address}', signerAddress)),
enabled: !!signerAddress,
...options,
})
}

export const useSaasAccountProvider = (options?: Parameters<typeof useSaasOrganization>[0]) => {
const {
account: accountSDK,
fetchAccount,
errors: { fetch: sdkAccountError },
loading: { fetch: sdkAccountLoading },
} = useClient()
const {
data: saasData,
refetch,
isLoading: isSaasLoading,
isError: isSaasError,
error: saasError,
} = useSaasOrganization(options)

const refetchAccount = useCallback(() => {
refetch()
fetchAccount()
}, [refetch, fetchAccount])

const organization: OrganizationData = { ...accountSDK, ...saasData }

const isLoading = isSaasLoading || sdkAccountLoading
const isError = isSaasError || !!sdkAccountError
const error = saasError || sdkAccountError

return { organization, refetchAccount, isLoading, isError, error }
}
2 changes: 1 addition & 1 deletion src/components/Layout/ErrorComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Flex, FlexProps, Text } from '@chakra-ui/react'
import { WarningIcon } from '@chakra-ui/icons'
import { useTranslation } from 'react-i18next'

const ErrorComponent = ({ error, ...props }: { error: Error } & FlexProps) => {
const ErrorComponent = ({ error, ...props }: { error: Error | string } & FlexProps) => {
const { t } = useTranslation()

return (
Expand Down
5 changes: 5 additions & 0 deletions src/components/Layout/FormSubmitMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { Box, BoxProps, FormControl, FormErrorMessage } from '@chakra-ui/react'

const FormSubmitMessage = ({
isLoading,
error,
isError,
isSuccess,
success,
...boxProps
}: {
isLoading?: boolean
error?: Error | string
isError?: boolean
isSuccess?: boolean
success?: string
} & BoxProps) => {
if (isLoading) {
return null
}
if (isSuccess) {
return (
<Box pt={2} {...boxProps}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/QueryDataLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface IUserQueryLayout {
isLoading: boolean
isEmpty?: boolean
isError: boolean
error?: Error
error?: Error | string
}

const QueryDataLayout = ({ isLoading, isEmpty, isError, error, children }: IUserQueryLayout & PropsWithChildren) => {
Expand Down
4 changes: 2 additions & 2 deletions src/elements/OrganizationSaas/Edit.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import EditProfile from '~components/AccountSaas/EditProfile'
import { useSaasOrganization } from '~components/AccountSaas/queries'
import QueryDataLayout from '~components/Layout/QueryDataLayout'
import { useSaasAccount } from '~components/AccountSaas/useSaasAccount'

const OrganizationEdit = () => {
const { isLoading, isError, error } = useSaasOrganization()
const { isLoading, isError, error } = useSaasAccount()

return (
<QueryDataLayout isLoading={isLoading} isError={isError} error={error}>
Expand Down

0 comments on commit 1ec530e

Please sign in to comment.