mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 15:21:48 +03:00
Require accepting Terms of Service and Privacy Policy when registering (#10772)
- Frontend part of https://github.com/enso-org/cloud-v2/issues/1427 - Show ToS immediately, before login screen - Add entries to devtools to be able to clear ToS # Important Notes None
This commit is contained in:
parent
50d919d952
commit
0ef074b8e9
@ -765,7 +765,7 @@ export async function login(
|
|||||||
await locateLoginButton(page).click()
|
await locateLoginButton(page).click()
|
||||||
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
|
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
|
||||||
if (first) {
|
if (first) {
|
||||||
await passTermsAndConditionsDialog({ page, setupAPI })
|
await passAgreementsDialog({ page, setupAPI })
|
||||||
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
|
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -840,11 +840,12 @@ async function mockDate({ page }: MockParams) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pass the Terms and conditions dialog. */
|
/** Pass the Agreements dialog. */
|
||||||
export async function passTermsAndConditionsDialog({ page }: MockParams) {
|
export async function passAgreementsDialog({ page }: MockParams) {
|
||||||
await test.test.step('Accept Terms and Conditions', async () => {
|
await test.test.step('Accept Terms and Conditions', async () => {
|
||||||
await page.waitForSelector('#terms-of-service-modal')
|
await page.waitForSelector('#agreements-modal')
|
||||||
await page.getByRole('checkbox').click()
|
await page.getByRole('checkbox').and(page.getByTestId('terms-of-service-checkbox')).click()
|
||||||
|
await page.getByRole('checkbox').and(page.getByTestId('privacy-policy-checkbox')).click()
|
||||||
await page.getByRole('button', { name: 'Accept' }).click()
|
await page.getByRole('button', { name: 'Accept' }).click()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,14 @@ export default class RegisterPageActions extends BaseActions {
|
|||||||
await this.page.getByPlaceholder(TEXT.emailPlaceholder).fill(email)
|
await this.page.getByPlaceholder(TEXT.emailPlaceholder).fill(email)
|
||||||
await this.page.getByPlaceholder(TEXT.passwordPlaceholder).fill(password)
|
await this.page.getByPlaceholder(TEXT.passwordPlaceholder).fill(password)
|
||||||
await this.page.getByPlaceholder(TEXT.confirmPasswordPlaceholder).fill(confirmPassword)
|
await this.page.getByPlaceholder(TEXT.confirmPasswordPlaceholder).fill(confirmPassword)
|
||||||
|
await this.page
|
||||||
|
.getByRole('checkbox')
|
||||||
|
.and(this.page.getByTestId('terms-of-service-checkbox'))
|
||||||
|
.click()
|
||||||
|
await this.page
|
||||||
|
.getByRole('checkbox')
|
||||||
|
.and(this.page.getByTestId('privacy-policy-checkbox'))
|
||||||
|
.click()
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('button', { name: TEXT.register, exact: true })
|
.getByRole('button', { name: TEXT.register, exact: true })
|
||||||
.getByText(TEXT.register)
|
.getByText(TEXT.register)
|
||||||
|
@ -65,7 +65,7 @@ test.test('asset panel contents', ({ page }) =>
|
|||||||
})
|
})
|
||||||
.login()
|
.login()
|
||||||
.do(async (thePage) => {
|
.do(async (thePage) => {
|
||||||
await actions.passTermsAndConditionsDialog({ page: thePage })
|
await actions.passAgreementsDialog({ page: thePage })
|
||||||
})
|
})
|
||||||
.driveTable.clickRow(0)
|
.driveTable.clickRow(0)
|
||||||
.toggleAssetPanel()
|
.toggleAssetPanel()
|
||||||
|
@ -12,7 +12,7 @@ test.test('login and logout', ({ page }) =>
|
|||||||
.mockAll({ page })
|
.mockAll({ page })
|
||||||
.login()
|
.login()
|
||||||
.do(async (thePage) => {
|
.do(async (thePage) => {
|
||||||
await actions.passTermsAndConditionsDialog({ page: thePage })
|
await actions.passAgreementsDialog({ page: thePage })
|
||||||
await test.expect(actions.locateDriveView(thePage)).toBeVisible()
|
await test.expect(actions.locateDriveView(thePage)).toBeVisible()
|
||||||
await test.expect(actions.locateLoginButton(thePage)).not.toBeVisible()
|
await test.expect(actions.locateLoginButton(thePage)).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
@ -4,7 +4,7 @@ import * as test from '@playwright/test'
|
|||||||
import {
|
import {
|
||||||
INVALID_PASSWORD,
|
INVALID_PASSWORD,
|
||||||
mockAll,
|
mockAll,
|
||||||
passTermsAndConditionsDialog,
|
passAgreementsDialog,
|
||||||
TEXT,
|
TEXT,
|
||||||
VALID_EMAIL,
|
VALID_EMAIL,
|
||||||
VALID_PASSWORD,
|
VALID_PASSWORD,
|
||||||
@ -26,7 +26,7 @@ test.test('login screen', ({ page }) =>
|
|||||||
// Technically it should not be allowed, but
|
// Technically it should not be allowed, but
|
||||||
.login(VALID_EMAIL, INVALID_PASSWORD)
|
.login(VALID_EMAIL, INVALID_PASSWORD)
|
||||||
.do(async (thePage) => {
|
.do(async (thePage) => {
|
||||||
await passTermsAndConditionsDialog({ page: thePage })
|
await passAgreementsDialog({ page: thePage })
|
||||||
})
|
})
|
||||||
.withDriveView(async (driveView) => {
|
.withDriveView(async (driveView) => {
|
||||||
await test.expect(driveView).toBeVisible()
|
await test.expect(driveView).toBeVisible()
|
||||||
|
@ -80,8 +80,8 @@ import * as errorBoundary from '#/components/ErrorBoundary'
|
|||||||
import * as suspense from '#/components/Suspense'
|
import * as suspense from '#/components/Suspense'
|
||||||
|
|
||||||
import AboutModal from '#/modals/AboutModal'
|
import AboutModal from '#/modals/AboutModal'
|
||||||
|
import { AgreementsModal } from '#/modals/AgreementsModal'
|
||||||
import * as setOrganizationNameModal from '#/modals/SetOrganizationNameModal'
|
import * as setOrganizationNameModal from '#/modals/SetOrganizationNameModal'
|
||||||
import * as termsOfServiceModal from '#/modals/TermsOfServiceModal'
|
|
||||||
|
|
||||||
import LocalBackend from '#/services/LocalBackend'
|
import LocalBackend from '#/services/LocalBackend'
|
||||||
import ProjectManager, * as projectManager from '#/services/ProjectManager'
|
import ProjectManager, * as projectManager from '#/services/ProjectManager'
|
||||||
@ -353,9 +353,10 @@ function AppRouter(props: AppRouterProps) {
|
|||||||
}, [localStorage, inputBindingsRaw])
|
}, [localStorage, inputBindingsRaw])
|
||||||
const mainPageUrl = getMainPageUrl()
|
const mainPageUrl = getMainPageUrl()
|
||||||
|
|
||||||
// Subscribe to `termsOfService` updates to trigger a rerender when the terms of service
|
// Subscribe to `localStorage` updates to trigger a rerender when the terms of service
|
||||||
// has been accepted.
|
// or privacy policy have been accepted.
|
||||||
localStorageProvider.useLocalStorageState('termsOfService')
|
localStorageProvider.useLocalStorageState('termsOfService')
|
||||||
|
localStorageProvider.useLocalStorageState('privacyPolicy')
|
||||||
|
|
||||||
const authService = useInitAuthService(props)
|
const authService = useInitAuthService(props)
|
||||||
const userSession = authService?.cognito.userSession.bind(authService.cognito) ?? null
|
const userSession = authService?.cognito.userSession.bind(authService.cognito) ?? null
|
||||||
@ -430,7 +431,7 @@ function AppRouter(props: AppRouterProps) {
|
|||||||
{/* Protected pages are visible to authenticated users. */}
|
{/* Protected pages are visible to authenticated users. */}
|
||||||
<router.Route element={<authProvider.NotDeletedUserLayout />}>
|
<router.Route element={<authProvider.NotDeletedUserLayout />}>
|
||||||
<router.Route element={<authProvider.ProtectedLayout />}>
|
<router.Route element={<authProvider.ProtectedLayout />}>
|
||||||
<router.Route element={<termsOfServiceModal.TermsOfServiceModal />}>
|
<router.Route element={<AgreementsModal />}>
|
||||||
<router.Route element={<setOrganizationNameModal.SetOrganizationNameModal />}>
|
<router.Route element={<setOrganizationNameModal.SetOrganizationNameModal />}>
|
||||||
<router.Route element={<InvitedToOrganizationModal />}>
|
<router.Route element={<InvitedToOrganizationModal />}>
|
||||||
<router.Route element={<openAppWatcher.OpenAppWatcher />}>
|
<router.Route element={<openAppWatcher.OpenAppWatcher />}>
|
||||||
@ -467,7 +468,7 @@ function AppRouter(props: AppRouterProps) {
|
|||||||
</router.Route>
|
</router.Route>
|
||||||
</router.Route>
|
</router.Route>
|
||||||
|
|
||||||
<router.Route element={<termsOfServiceModal.TermsOfServiceModal />}>
|
<router.Route element={<AgreementsModal />}>
|
||||||
<router.Route element={<authProvider.NotDeletedUserLayout />}>
|
<router.Route element={<authProvider.NotDeletedUserLayout />}>
|
||||||
<router.Route path={appUtils.SETUP_PATH} element={<setup.Setup />} />
|
<router.Route path={appUtils.SETUP_PATH} element={<setup.Setup />} />
|
||||||
</router.Route>
|
</router.Route>
|
||||||
|
@ -23,11 +23,24 @@ import {
|
|||||||
} from '#/providers/EnsoDevtoolsProvider'
|
} from '#/providers/EnsoDevtoolsProvider'
|
||||||
import * as textProvider from '#/providers/TextProvider'
|
import * as textProvider from '#/providers/TextProvider'
|
||||||
|
|
||||||
import * as aria from '#/components/aria'
|
import { Switch } from '#/components/aria'
|
||||||
import * as ariaComponents from '#/components/AriaComponents'
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
DialogTrigger,
|
||||||
|
Form,
|
||||||
|
Popover,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Separator,
|
||||||
|
Text,
|
||||||
|
} from '#/components/AriaComponents'
|
||||||
import Portal from '#/components/Portal'
|
import Portal from '#/components/Portal'
|
||||||
|
|
||||||
|
import { useLocalStorage } from '#/providers/LocalStorageProvider'
|
||||||
import * as backend from '#/services/Backend'
|
import * as backend from '#/services/Backend'
|
||||||
|
import LocalStorage from '#/utilities/LocalStorage'
|
||||||
|
import { unsafeEntries } from 'enso-common/src/utilities/data/object'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for a paywall feature.
|
* Configuration for a paywall feature.
|
||||||
@ -64,6 +77,7 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
const { authQueryKey, session } = authProvider.useAuth()
|
const { authQueryKey, session } = authProvider.useAuth()
|
||||||
const enableVersionChecker = useEnableVersionChecker()
|
const enableVersionChecker = useEnableVersionChecker()
|
||||||
const setEnableVersionChecker = useSetEnableVersionChecker()
|
const setEnableVersionChecker = useSetEnableVersionChecker()
|
||||||
|
const { localStorage } = useLocalStorage()
|
||||||
|
|
||||||
const [features, setFeatures] = React.useState<
|
const [features, setFeatures] = React.useState<
|
||||||
Record<billing.PaywallFeatureName, PaywallDevtoolsFeatureConfiguration>
|
Record<billing.PaywallFeatureName, PaywallDevtoolsFeatureConfiguration>
|
||||||
@ -92,8 +106,8 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
{children}
|
{children}
|
||||||
|
|
||||||
<Portal>
|
<Portal>
|
||||||
<ariaComponents.DialogTrigger>
|
<DialogTrigger>
|
||||||
<ariaComponents.Button
|
<Button
|
||||||
icon={DevtoolsLogo}
|
icon={DevtoolsLogo}
|
||||||
aria-label={getText('paywallDevtoolsButtonLabel')}
|
aria-label={getText('paywallDevtoolsButtonLabel')}
|
||||||
variant="icon"
|
variant="icon"
|
||||||
@ -103,27 +117,25 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
data-ignore-click-outside
|
data-ignore-click-outside
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ariaComponents.Popover>
|
<Popover>
|
||||||
<ariaComponents.Text.Heading disableLineHeightCompensation>
|
<Text.Heading disableLineHeightCompensation>
|
||||||
{getText('paywallDevtoolsPopoverHeading')}
|
{getText('paywallDevtoolsPopoverHeading')}
|
||||||
</ariaComponents.Text.Heading>
|
</Text.Heading>
|
||||||
|
|
||||||
<ariaComponents.Separator orientation="horizontal" className="my-3" />
|
<Separator orientation="horizontal" className="my-3" />
|
||||||
|
|
||||||
{session?.type === UserSessionType.full && (
|
{session?.type === UserSessionType.full && (
|
||||||
<>
|
<>
|
||||||
<ariaComponents.Text variant="subtitle">
|
<Text variant="subtitle">{getText('paywallDevtoolsPlanSelectSubtitle')}</Text>
|
||||||
{getText('paywallDevtoolsPlanSelectSubtitle')}
|
|
||||||
</ariaComponents.Text>
|
|
||||||
|
|
||||||
<ariaComponents.Form
|
<Form
|
||||||
gap="small"
|
gap="small"
|
||||||
schema={(schema) => schema.object({ plan: schema.string() })}
|
schema={(schema) => schema.object({ plan: schema.string() })}
|
||||||
defaultValues={{ plan: session.user.plan ?? 'free' }}
|
defaultValues={{ plan: session.user.plan ?? 'free' }}
|
||||||
>
|
>
|
||||||
{({ form }) => (
|
{({ form }) => (
|
||||||
<>
|
<>
|
||||||
<ariaComponents.RadioGroup
|
<RadioGroup
|
||||||
form={form}
|
form={form}
|
||||||
name="plan"
|
name="plan"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@ -133,16 +145,13 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ariaComponents.Radio label={getText('free')} value={'free'} />
|
<Radio label={getText('free')} value={'free'} />
|
||||||
<ariaComponents.Radio label={getText('solo')} value={backend.Plan.solo} />
|
<Radio label={getText('solo')} value={backend.Plan.solo} />
|
||||||
<ariaComponents.Radio label={getText('team')} value={backend.Plan.team} />
|
<Radio label={getText('team')} value={backend.Plan.team} />
|
||||||
<ariaComponents.Radio
|
<Radio label={getText('enterprise')} value={backend.Plan.enterprise} />
|
||||||
label={getText('enterprise')}
|
</RadioGroup>
|
||||||
value={backend.Plan.enterprise}
|
|
||||||
/>
|
|
||||||
</ariaComponents.RadioGroup>
|
|
||||||
|
|
||||||
<ariaComponents.Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
@ -152,27 +161,27 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{getText('reset')}
|
{getText('reset')}
|
||||||
</ariaComponents.Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ariaComponents.Form>
|
</Form>
|
||||||
|
|
||||||
<ariaComponents.Separator orientation="horizontal" className="my-3" />
|
<Separator orientation="horizontal" className="my-3" />
|
||||||
|
|
||||||
{/* eslint-disable-next-line no-restricted-syntax */}
|
{/* eslint-disable-next-line no-restricted-syntax */}
|
||||||
<ariaComponents.Button variant="link" href={SETUP_PATH + '?__qd-debg__=true'}>
|
<Button variant="link" href={SETUP_PATH + '?__qd-debg__=true'}>
|
||||||
Open setup page
|
Open setup page
|
||||||
</ariaComponents.Button>
|
</Button>
|
||||||
|
|
||||||
<ariaComponents.Separator orientation="horizontal" className="my-3" />
|
<Separator orientation="horizontal" className="my-3" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ariaComponents.Text variant="subtitle" className="mb-2">
|
<Text variant="subtitle" className="mb-2">
|
||||||
{getText('productionOnlyFeatures')}
|
{getText('productionOnlyFeatures')}
|
||||||
</ariaComponents.Text>
|
</Text>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<aria.Switch
|
<Switch
|
||||||
className="group flex items-center gap-1"
|
className="group flex items-center gap-1"
|
||||||
isSelected={enableVersionChecker ?? !IS_DEV_MODE}
|
isSelected={enableVersionChecker ?? !IS_DEV_MODE}
|
||||||
onChange={setEnableVersionChecker}
|
onChange={setEnableVersionChecker}
|
||||||
@ -181,21 +190,45 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
<span className="aspect-square h-full flex-none translate-x-0 transform rounded-full bg-white transition duration-200 ease-in-out group-selected:translate-x-[100%]" />
|
<span className="aspect-square h-full flex-none translate-x-0 transform rounded-full bg-white transition duration-200 ease-in-out group-selected:translate-x-[100%]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ariaComponents.Text className="flex-1">
|
<Text className="flex-1">{getText('enableVersionChecker')}</Text>
|
||||||
{getText('enableVersionChecker')}
|
</Switch>
|
||||||
</ariaComponents.Text>
|
|
||||||
</aria.Switch>
|
|
||||||
|
|
||||||
<ariaComponents.Text variant="body" color="disabled">
|
<Text variant="body" color="disabled">
|
||||||
{getText('enableVersionCheckerDescription')}
|
{getText('enableVersionCheckerDescription')}
|
||||||
</ariaComponents.Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ariaComponents.Separator orientation="horizontal" className="my-3" />
|
<Separator orientation="horizontal" className="my-3" />
|
||||||
|
|
||||||
<ariaComponents.Text variant="subtitle" className="mb-2">
|
<Text variant="subtitle" className="mb-2">
|
||||||
|
{getText('localStorage')}
|
||||||
|
</Text>
|
||||||
|
{unsafeEntries(LocalStorage.keyMetadata).map(([key]) => (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<ButtonGroup className="grow-0">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outline"
|
||||||
|
onPress={() => {
|
||||||
|
localStorage.delete(key)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getText('delete')}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Text variant="body">
|
||||||
|
{key
|
||||||
|
.replace(/[A-Z]/g, (m) => ' ' + m.toLowerCase())
|
||||||
|
.replace(/^./, (m) => m.toUpperCase())}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Separator orientation="horizontal" className="my-3" />
|
||||||
|
|
||||||
|
<Text variant="subtitle" className="mb-2">
|
||||||
{getText('paywallDevtoolsPaywallFeaturesToggles')}
|
{getText('paywallDevtoolsPaywallFeaturesToggles')}
|
||||||
</ariaComponents.Text>
|
</Text>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{Object.entries(features).map(([feature, configuration]) => {
|
{Object.entries(features).map(([feature, configuration]) => {
|
||||||
@ -205,7 +238,7 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={feature} className="flex flex-col">
|
<div key={feature} className="flex flex-col">
|
||||||
<aria.Switch
|
<Switch
|
||||||
className="group flex items-center gap-1"
|
className="group flex items-center gap-1"
|
||||||
isSelected={configuration.isForceEnabled ?? true}
|
isSelected={configuration.isForceEnabled ?? true}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@ -218,18 +251,18 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
|||||||
<span className="aspect-square h-full flex-none translate-x-0 transform rounded-full bg-white transition duration-200 ease-in-out group-selected:translate-x-[100%]" />
|
<span className="aspect-square h-full flex-none translate-x-0 transform rounded-full bg-white transition duration-200 ease-in-out group-selected:translate-x-[100%]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ariaComponents.Text className="flex-1">{getText(label)}</ariaComponents.Text>
|
<Text className="flex-1">{getText(label)}</Text>
|
||||||
</aria.Switch>
|
</Switch>
|
||||||
|
|
||||||
<ariaComponents.Text variant="body" color="disabled">
|
<Text variant="body" color="disabled">
|
||||||
{getText(descriptionTextId)}
|
{getText(descriptionTextId)}
|
||||||
</ariaComponents.Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ariaComponents.Popover>
|
</Popover>
|
||||||
</ariaComponents.DialogTrigger>
|
</DialogTrigger>
|
||||||
</Portal>
|
</Portal>
|
||||||
</PaywallDevtoolsContext.Provider>
|
</PaywallDevtoolsContext.Provider>
|
||||||
)
|
)
|
||||||
|
214
app/dashboard/src/modals/AgreementsModal.tsx
Normal file
214
app/dashboard/src/modals/AgreementsModal.tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
/** @file Modal for accepting the terms of service. */
|
||||||
|
import { useId } from 'react'
|
||||||
|
|
||||||
|
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { Outlet } from 'react-router'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import { Input } from '#/components/aria'
|
||||||
|
import { Button, Dialog, Form, Text } from '#/components/AriaComponents'
|
||||||
|
import { useAuth } from '#/providers/AuthProvider'
|
||||||
|
import { useLocalStorage } from '#/providers/LocalStorageProvider'
|
||||||
|
import { useText } from '#/providers/TextProvider'
|
||||||
|
import LocalStorage from '#/utilities/LocalStorage'
|
||||||
|
import { omit } from '#/utilities/object'
|
||||||
|
import { twMerge } from '#/utilities/tailwindMerge'
|
||||||
|
|
||||||
|
// =================
|
||||||
|
// === Constants ===
|
||||||
|
// =================
|
||||||
|
|
||||||
|
const TEN_MINUTES_MS = 600_000
|
||||||
|
const TOS_SCHEMA = z.object({ versionHash: z.string() })
|
||||||
|
const PRIVACY_POLICY_SCHEMA = z.object({ versionHash: z.string() })
|
||||||
|
const TOS_ENDPOINT_SCHEMA = z.object({ hash: z.string() })
|
||||||
|
const PRIVACY_POLICY_ENDPOINT_SCHEMA = z.object({ hash: z.string() })
|
||||||
|
|
||||||
|
export const latestTermsOfServiceQueryOptions = queryOptions({
|
||||||
|
queryKey: ['termsOfService', 'currentVersion'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(new URL('/eula.json', process.env.ENSO_CLOUD_ENSO_HOST))
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch Terms of Service')
|
||||||
|
} else {
|
||||||
|
return TOS_ENDPOINT_SCHEMA.parse(await response.json())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchIntervalInBackground: true,
|
||||||
|
refetchInterval: TEN_MINUTES_MS,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const latestPrivacyPolicyQueryOptions = queryOptions({
|
||||||
|
queryKey: ['privacyPolicy', 'currentVersion'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(new URL('/privacy.json', process.env.ENSO_CLOUD_ENSO_HOST))
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch Privacy Policy')
|
||||||
|
} else {
|
||||||
|
return PRIVACY_POLICY_ENDPOINT_SCHEMA.parse(await response.json())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchIntervalInBackground: true,
|
||||||
|
refetchInterval: TEN_MINUTES_MS,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// === Global configuration ===
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
declare module '#/utilities/LocalStorage' {
|
||||||
|
/** Metadata containing the version hash of the terms of service that the user has accepted. */
|
||||||
|
interface LocalStorageData {
|
||||||
|
readonly termsOfService: z.infer<typeof TOS_SCHEMA>
|
||||||
|
readonly privacyPolicy: z.infer<typeof PRIVACY_POLICY_SCHEMA>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalStorage.registerKey('termsOfService', { schema: TOS_SCHEMA })
|
||||||
|
LocalStorage.registerKey('privacyPolicy', { schema: PRIVACY_POLICY_SCHEMA })
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// === AgreementsModal ===
|
||||||
|
// =======================
|
||||||
|
|
||||||
|
/** Modal for accepting the terms of service. */
|
||||||
|
export function AgreementsModal() {
|
||||||
|
const { getText } = useText()
|
||||||
|
const { localStorage } = useLocalStorage()
|
||||||
|
const checkboxId = useId()
|
||||||
|
const { session } = useAuth()
|
||||||
|
|
||||||
|
const cachedTosHash = localStorage.get('termsOfService')?.versionHash
|
||||||
|
const { data: tosHash } = useSuspenseQuery({
|
||||||
|
...latestTermsOfServiceQueryOptions,
|
||||||
|
// If the user has already accepted the EULA, we don't need to
|
||||||
|
// block user interaction with the app while we fetch the latest version.
|
||||||
|
// We can use the local version hash as the initial data.
|
||||||
|
// and refetch in the background to check for updates.
|
||||||
|
...(cachedTosHash != null && {
|
||||||
|
initialData: { hash: cachedTosHash },
|
||||||
|
}),
|
||||||
|
select: (data) => data.hash,
|
||||||
|
})
|
||||||
|
const cachedPrivacyPolicyHash = localStorage.get('privacyPolicy')?.versionHash
|
||||||
|
const { data: privacyPolicyHash } = useSuspenseQuery({
|
||||||
|
...latestPrivacyPolicyQueryOptions,
|
||||||
|
...(cachedPrivacyPolicyHash != null && {
|
||||||
|
initialData: { hash: cachedPrivacyPolicyHash },
|
||||||
|
}),
|
||||||
|
select: (data) => data.hash,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLatest = tosHash === cachedTosHash && privacyPolicyHash === cachedPrivacyPolicyHash
|
||||||
|
const isAccepted = cachedTosHash != null
|
||||||
|
const shouldDisplay = !(isAccepted && isLatest)
|
||||||
|
|
||||||
|
const formSchema = Form.useFormSchema((schema) =>
|
||||||
|
schema.object({
|
||||||
|
// The user must agree to the ToS to proceed.
|
||||||
|
agreedToTos: schema
|
||||||
|
.boolean()
|
||||||
|
// The user must agree to the ToS to proceed.
|
||||||
|
.refine((value) => value, getText('licenseAgreementCheckboxError')),
|
||||||
|
agreedToPrivacyPolicy: schema
|
||||||
|
.boolean()
|
||||||
|
.refine((value) => value, getText('privacyPolicyCheckboxError')),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (shouldDisplay) {
|
||||||
|
// Note that this produces warnings about missing a `<Heading slot="title">`, even though
|
||||||
|
// all `ariaComponents.Dialog`s contain one. This is likely caused by Suspense discarding
|
||||||
|
// renders, and so it does not seem to be fixable.
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={getText('licenseAgreementTitle')}
|
||||||
|
isKeyboardDismissDisabled
|
||||||
|
isDismissable={false}
|
||||||
|
hideCloseButton
|
||||||
|
modalProps={{ defaultOpen: true }}
|
||||||
|
testId="agreements-modal"
|
||||||
|
id="agreements-modal"
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
schema={formSchema}
|
||||||
|
defaultValues={{
|
||||||
|
agreedToTos: tosHash === cachedTosHash,
|
||||||
|
agreedToPrivacyPolicy: privacyPolicyHash === cachedPrivacyPolicyHash,
|
||||||
|
}}
|
||||||
|
testId="agreements-form"
|
||||||
|
method="dialog"
|
||||||
|
onSubmit={() => {
|
||||||
|
localStorage.set('termsOfService', { versionHash: tosHash })
|
||||||
|
localStorage.set('privacyPolicy', { versionHash: privacyPolicyHash })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ register }) => (
|
||||||
|
<>
|
||||||
|
<Text>{getText('someAgreementsHaveBeenUpdated')}</Text>
|
||||||
|
|
||||||
|
<Form.Field name="agreedToTos">
|
||||||
|
{({ isInvalid }) => (
|
||||||
|
<>
|
||||||
|
<div className="flex w-full items-center gap-1">
|
||||||
|
<Input
|
||||||
|
type="checkbox"
|
||||||
|
className={twMerge(
|
||||||
|
'flex size-4 cursor-pointer overflow-clip rounded-lg border border-primary outline-primary focus-visible:outline focus-visible:outline-2',
|
||||||
|
isInvalid && 'border-red-700 text-red-500 outline-red-500',
|
||||||
|
)}
|
||||||
|
id={checkboxId}
|
||||||
|
data-testid="terms-of-service-checkbox"
|
||||||
|
{...omit(register('agreedToTos'), 'isInvalid')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label htmlFor={checkboxId}>
|
||||||
|
<Text>{getText('licenseAgreementCheckbox')}</Text>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="link" target="_blank" href="https://ensoanalytics.com/eula">
|
||||||
|
{getText('viewLicenseAgreement')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<Form.Field name="agreedToPrivacyPolicy">
|
||||||
|
{({ isInvalid }) => (
|
||||||
|
<>
|
||||||
|
<label className="flex w-full items-center gap-1">
|
||||||
|
<Input
|
||||||
|
type="checkbox"
|
||||||
|
className={twMerge(
|
||||||
|
'flex size-4 cursor-pointer overflow-clip rounded-lg border border-primary outline-primary focus-visible:outline focus-visible:outline-2',
|
||||||
|
isInvalid && 'border-red-700 text-red-500 outline-red-500',
|
||||||
|
)}
|
||||||
|
data-testid="privacy-policy-checkbox"
|
||||||
|
{...omit(register('agreedToPrivacyPolicy'), 'isInvalid')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text>{getText('privacyPolicyCheckbox')}</Text>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Button variant="link" target="_blank" href="https://ensoanalytics.com/privacy">
|
||||||
|
{getText('viewPrivacyPolicy')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<Form.FormError />
|
||||||
|
|
||||||
|
<Form.Submit fullWidth>{getText('accept')}</Form.Submit>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return <Outlet context={session} />
|
||||||
|
}
|
||||||
|
}
|
@ -1,171 +0,0 @@
|
|||||||
/** @file Modal for accepting the terms of service. */
|
|
||||||
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import * as reactQuery from '@tanstack/react-query'
|
|
||||||
import * as router from 'react-router'
|
|
||||||
import * as z from 'zod'
|
|
||||||
|
|
||||||
import * as authProvider from '#/providers/AuthProvider'
|
|
||||||
import * as localStorageProvider from '#/providers/LocalStorageProvider'
|
|
||||||
import * as textProvider from '#/providers/TextProvider'
|
|
||||||
|
|
||||||
import * as aria from '#/components/aria'
|
|
||||||
import * as ariaComponents from '#/components/AriaComponents'
|
|
||||||
|
|
||||||
import LocalStorage from '#/utilities/LocalStorage'
|
|
||||||
import * as object from '#/utilities/object'
|
|
||||||
import * as tailwindMerge from '#/utilities/tailwindMerge'
|
|
||||||
|
|
||||||
// =================
|
|
||||||
// === Constants ===
|
|
||||||
// =================
|
|
||||||
|
|
||||||
const TEN_MINUTES_MS = 600_000
|
|
||||||
const TERMS_OF_SERVICE_SCHEMA = z.object({ versionHash: z.string() })
|
|
||||||
|
|
||||||
// ============================
|
|
||||||
// === Global configuration ===
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
declare module '#/utilities/LocalStorage' {
|
|
||||||
/** Metadata containing the version hash of the terms of service that the user has accepted. */
|
|
||||||
interface LocalStorageData {
|
|
||||||
readonly termsOfService: z.infer<typeof TERMS_OF_SERVICE_SCHEMA> | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalStorage.registerKey('termsOfService', { schema: TERMS_OF_SERVICE_SCHEMA })
|
|
||||||
|
|
||||||
export const latestTermsOfService = reactQuery.queryOptions({
|
|
||||||
queryKey: ['termsOfService', 'currentVersion'],
|
|
||||||
queryFn: () =>
|
|
||||||
fetch(new URL('/eula.json', process.env.ENSO_CLOUD_ENSO_HOST))
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch terms of service')
|
|
||||||
} else {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
const schema = z.object({ hash: z.string() })
|
|
||||||
return schema.parse(data)
|
|
||||||
}),
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
refetchInterval: TEN_MINUTES_MS,
|
|
||||||
})
|
|
||||||
|
|
||||||
// ===========================
|
|
||||||
// === TermsOfServiceModal ===
|
|
||||||
// ===========================
|
|
||||||
|
|
||||||
/** Modal for accepting the terms of service. */
|
|
||||||
export function TermsOfServiceModal() {
|
|
||||||
const { getText } = textProvider.useText()
|
|
||||||
const { localStorage } = localStorageProvider.useLocalStorage()
|
|
||||||
const checkboxId = React.useId()
|
|
||||||
const { session } = authProvider.useAuth()
|
|
||||||
|
|
||||||
const localVersionHash = localStorage.get('termsOfService')?.versionHash
|
|
||||||
const { data: latestVersionHash } = reactQuery.useSuspenseQuery({
|
|
||||||
...latestTermsOfService,
|
|
||||||
// If the user has already accepted EULA, we don't need to
|
|
||||||
// block user interaction with the app while we fetch the latest version.
|
|
||||||
// We can use the local version hash as the initial data.
|
|
||||||
// and refetch in the background to check for updates.
|
|
||||||
...(localVersionHash != null && {
|
|
||||||
initialData: { hash: localVersionHash },
|
|
||||||
}),
|
|
||||||
select: (data) => data.hash,
|
|
||||||
})
|
|
||||||
|
|
||||||
const isLatest = latestVersionHash === localVersionHash
|
|
||||||
const isAccepted = localVersionHash != null
|
|
||||||
const shouldDisplay = !(isAccepted && isLatest)
|
|
||||||
|
|
||||||
const formSchema = ariaComponents.Form.useFormSchema((schema) =>
|
|
||||||
schema.object({
|
|
||||||
agree: schema
|
|
||||||
.boolean()
|
|
||||||
// we accept only true
|
|
||||||
.refine((value) => value, getText('licenseAgreementCheckboxError')),
|
|
||||||
hash: schema.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (shouldDisplay) {
|
|
||||||
// Note that this produces warnings about missing a `<Heading slot="title">`, even though
|
|
||||||
// all `ariaComponents.Dialog`s contain one. This is likely caused by Suspense discarding
|
|
||||||
// renders, and so it does not seem to be fixable.
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ariaComponents.Dialog
|
|
||||||
title={getText('licenseAgreementTitle')}
|
|
||||||
isKeyboardDismissDisabled
|
|
||||||
isDismissable={false}
|
|
||||||
hideCloseButton
|
|
||||||
modalProps={{ defaultOpen: true }}
|
|
||||||
testId="terms-of-service-modal"
|
|
||||||
id="terms-of-service-modal"
|
|
||||||
>
|
|
||||||
<ariaComponents.Form
|
|
||||||
schema={formSchema}
|
|
||||||
defaultValues={{ agree: false, hash: latestVersionHash }}
|
|
||||||
testId="terms-of-service-form"
|
|
||||||
method="dialog"
|
|
||||||
onSubmit={({ hash }) => {
|
|
||||||
localStorage.set('termsOfService', { versionHash: hash })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ register }) => (
|
|
||||||
<>
|
|
||||||
<ariaComponents.Form.Field name="agree">
|
|
||||||
{({ isInvalid }) => (
|
|
||||||
<>
|
|
||||||
<div className="flex w-full items-center gap-1">
|
|
||||||
<aria.Input
|
|
||||||
type="checkbox"
|
|
||||||
className={tailwindMerge.twMerge(
|
|
||||||
'flex size-4 cursor-pointer overflow-clip rounded-lg border border-primary outline-primary focus-visible:outline focus-visible:outline-2',
|
|
||||||
isInvalid && 'border-red-700 text-red-500 outline-red-500',
|
|
||||||
)}
|
|
||||||
id={checkboxId}
|
|
||||||
data-testid="terms-of-service-checkbox"
|
|
||||||
{...object.omit(register('agree'), 'isInvalid')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label htmlFor={checkboxId}>
|
|
||||||
<ariaComponents.Text>
|
|
||||||
{getText('licenseAgreementCheckbox')}
|
|
||||||
</ariaComponents.Text>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ariaComponents.Button
|
|
||||||
variant="link"
|
|
||||||
target="_blank"
|
|
||||||
href="https://enso.org/eula"
|
|
||||||
>
|
|
||||||
{getText('viewLicenseAgreement')}
|
|
||||||
</ariaComponents.Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ariaComponents.Form.Field>
|
|
||||||
|
|
||||||
<ariaComponents.Form.FormError />
|
|
||||||
|
|
||||||
<ariaComponents.Form.Submit fullWidth>
|
|
||||||
{getText('accept')}
|
|
||||||
</ariaComponents.Form.Submit>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ariaComponents.Form>
|
|
||||||
</ariaComponents.Dialog>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return <router.Outlet context={session} />
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,8 +9,13 @@ import AtIcon from '#/assets/at.svg'
|
|||||||
import CreateAccountIcon from '#/assets/create_account.svg'
|
import CreateAccountIcon from '#/assets/create_account.svg'
|
||||||
import GoBackIcon from '#/assets/go_back.svg'
|
import GoBackIcon from '#/assets/go_back.svg'
|
||||||
import LockIcon from '#/assets/lock.svg'
|
import LockIcon from '#/assets/lock.svg'
|
||||||
import { Form, Input, Password } from '#/components/AriaComponents'
|
import { Input as AriaInput } from '#/components/aria'
|
||||||
|
import { Button, Form, Input, Password, Text } from '#/components/AriaComponents'
|
||||||
import Link from '#/components/Link'
|
import Link from '#/components/Link'
|
||||||
|
import {
|
||||||
|
latestPrivacyPolicyQueryOptions,
|
||||||
|
latestTermsOfServiceQueryOptions,
|
||||||
|
} from '#/modals/AgreementsModal'
|
||||||
import AuthenticationPage from '#/pages/authentication/AuthenticationPage'
|
import AuthenticationPage from '#/pages/authentication/AuthenticationPage'
|
||||||
import { passwordWithPatternSchema } from '#/pages/authentication/schemas'
|
import { passwordWithPatternSchema } from '#/pages/authentication/schemas'
|
||||||
import { useAuth } from '#/providers/AuthProvider'
|
import { useAuth } from '#/providers/AuthProvider'
|
||||||
@ -18,7 +23,10 @@ import { useLocalBackend } from '#/providers/BackendProvider'
|
|||||||
import { useLocalStorage } from '#/providers/LocalStorageProvider'
|
import { useLocalStorage } from '#/providers/LocalStorageProvider'
|
||||||
import { type GetText, useText } from '#/providers/TextProvider'
|
import { type GetText, useText } from '#/providers/TextProvider'
|
||||||
import LocalStorage from '#/utilities/LocalStorage'
|
import LocalStorage from '#/utilities/LocalStorage'
|
||||||
|
import { twMerge } from '#/utilities/tailwindMerge'
|
||||||
import { PASSWORD_REGEX } from '#/utilities/validation'
|
import { PASSWORD_REGEX } from '#/utilities/validation'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { omit } from 'enso-common/src/utilities/data/object'
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// === Global configuration ===
|
// === Global configuration ===
|
||||||
@ -43,6 +51,10 @@ function createRegistrationFormSchema(getText: GetText) {
|
|||||||
email: z.string().email(getText('invalidEmailValidationError')),
|
email: z.string().email(getText('invalidEmailValidationError')),
|
||||||
password: passwordWithPatternSchema(getText),
|
password: passwordWithPatternSchema(getText),
|
||||||
confirmPassword: z.string(),
|
confirmPassword: z.string(),
|
||||||
|
agreedToTos: z.boolean().refine((value) => value, getText('licenseAgreementCheckboxError')),
|
||||||
|
agreedToPrivacyPolicy: z
|
||||||
|
.boolean()
|
||||||
|
.refine((value) => value, getText('privacyPolicyCheckboxError')),
|
||||||
})
|
})
|
||||||
.superRefine((object, context) => {
|
.superRefine((object, context) => {
|
||||||
if (PASSWORD_REGEX.test(object.password) && object.password !== object.confirmPassword) {
|
if (PASSWORD_REGEX.test(object.password) && object.password !== object.confirmPassword) {
|
||||||
@ -74,6 +86,27 @@ export default function Registration() {
|
|||||||
const redirectTo = query.get('redirect_to')
|
const redirectTo = query.get('redirect_to')
|
||||||
const [emailInput, setEmailInput] = useState(initialEmail ?? '')
|
const [emailInput, setEmailInput] = useState(initialEmail ?? '')
|
||||||
|
|
||||||
|
const cachedTosHash = localStorage.get('termsOfService')?.versionHash
|
||||||
|
const { data: tosHash } = useSuspenseQuery({
|
||||||
|
...latestTermsOfServiceQueryOptions,
|
||||||
|
// If the user has already accepted the EULA, we don't need to
|
||||||
|
// block user interaction with the app while we fetch the latest version.
|
||||||
|
// We can use the local version hash as the initial data.
|
||||||
|
// and refetch in the background to check for updates.
|
||||||
|
...(cachedTosHash != null && {
|
||||||
|
initialData: { hash: cachedTosHash },
|
||||||
|
}),
|
||||||
|
select: (data) => data.hash,
|
||||||
|
})
|
||||||
|
const cachedPrivacyPolicyHash = localStorage.get('privacyPolicy')?.versionHash
|
||||||
|
const { data: privacyPolicyHash } = useSuspenseQuery({
|
||||||
|
...latestPrivacyPolicyQueryOptions,
|
||||||
|
...(cachedPrivacyPolicyHash != null && {
|
||||||
|
initialData: { hash: cachedPrivacyPolicyHash },
|
||||||
|
}),
|
||||||
|
select: (data) => data.hash,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (redirectTo != null) {
|
if (redirectTo != null) {
|
||||||
localStorage.set('loginRedirect', redirectTo)
|
localStorage.set('loginRedirect', redirectTo)
|
||||||
@ -85,6 +118,7 @@ export default function Registration() {
|
|||||||
return (
|
return (
|
||||||
<AuthenticationPage
|
<AuthenticationPage
|
||||||
schema={createRegistrationFormSchema(getText)}
|
schema={createRegistrationFormSchema(getText)}
|
||||||
|
defaultValues={{ agreedToTos: false, agreedToPrivacyPolicy: false }}
|
||||||
title={getText('createANewAccount')}
|
title={getText('createANewAccount')}
|
||||||
supportsOffline={supportsOffline}
|
supportsOffline={supportsOffline}
|
||||||
footer={
|
footer={
|
||||||
@ -94,48 +128,104 @@ export default function Registration() {
|
|||||||
text={getText('alreadyHaveAnAccount')}
|
text={getText('alreadyHaveAnAccount')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onSubmit={({ email, password }) => signUp(email, password, organizationId)}
|
onSubmit={async ({ email, password }) => {
|
||||||
|
await signUp(email, password, organizationId)
|
||||||
|
localStorage.set('termsOfService', { versionHash: tosHash })
|
||||||
|
localStorage.set('privacyPolicy', { versionHash: privacyPolicyHash })
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
{({ register }) => (
|
||||||
autoFocus
|
<>
|
||||||
required
|
<Input
|
||||||
data-testid="email-input"
|
autoFocus
|
||||||
name="email"
|
required
|
||||||
label={getText('emailLabel')}
|
data-testid="email-input"
|
||||||
type="email"
|
name="email"
|
||||||
autoComplete="email"
|
label={getText('emailLabel')}
|
||||||
icon={AtIcon}
|
type="email"
|
||||||
placeholder={getText('emailPlaceholder')}
|
autoComplete="email"
|
||||||
defaultValue={initialEmail ?? undefined}
|
icon={AtIcon}
|
||||||
onChange={(event) => {
|
placeholder={getText('emailPlaceholder')}
|
||||||
setEmailInput(event.currentTarget.value)
|
defaultValue={initialEmail ?? undefined}
|
||||||
}}
|
onChange={(event) => {
|
||||||
/>
|
setEmailInput(event.currentTarget.value)
|
||||||
<Password
|
}}
|
||||||
required
|
/>
|
||||||
data-testid="password-input"
|
<Password
|
||||||
name="password"
|
required
|
||||||
label={getText('passwordLabel')}
|
data-testid="password-input"
|
||||||
autoComplete="new-password"
|
name="password"
|
||||||
icon={LockIcon}
|
label={getText('passwordLabel')}
|
||||||
placeholder={getText('passwordPlaceholder')}
|
autoComplete="new-password"
|
||||||
description={getText('passwordValidationMessage')}
|
icon={LockIcon}
|
||||||
/>
|
placeholder={getText('passwordPlaceholder')}
|
||||||
<Password
|
description={getText('passwordValidationMessage')}
|
||||||
required
|
/>
|
||||||
data-testid="confirm-password-input"
|
<Password
|
||||||
name="confirmPassword"
|
required
|
||||||
label={getText('confirmPasswordLabel')}
|
data-testid="confirm-password-input"
|
||||||
autoComplete="new-password"
|
name="confirmPassword"
|
||||||
icon={LockIcon}
|
label={getText('confirmPasswordLabel')}
|
||||||
placeholder={getText('confirmPasswordPlaceholder')}
|
autoComplete="new-password"
|
||||||
/>
|
icon={LockIcon}
|
||||||
|
placeholder={getText('confirmPasswordPlaceholder')}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Submit size="large" icon={CreateAccountIcon} className="w-full">
|
<Form.Field name="agreedToTos">
|
||||||
{getText('register')}
|
{({ isInvalid }) => (
|
||||||
</Form.Submit>
|
<>
|
||||||
|
<label className="flex w-full items-center gap-1">
|
||||||
|
<AriaInput
|
||||||
|
type="checkbox"
|
||||||
|
className={twMerge(
|
||||||
|
'flex size-4 cursor-pointer overflow-clip rounded-lg border border-primary outline-primary focus-visible:outline focus-visible:outline-2',
|
||||||
|
isInvalid && 'border-red-700 text-red-500 outline-red-500',
|
||||||
|
)}
|
||||||
|
data-testid="terms-of-service-checkbox"
|
||||||
|
{...omit(register('agreedToTos'), 'isInvalid')}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.FormError />
|
<Text>{getText('licenseAgreementCheckbox')}</Text>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Button variant="link" target="_blank" href="https://ensoanalytics.com/eula">
|
||||||
|
{getText('viewLicenseAgreement')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<Form.Field name="agreedToPrivacyPolicy">
|
||||||
|
{({ isInvalid }) => (
|
||||||
|
<>
|
||||||
|
<label className="flex w-full items-center gap-1">
|
||||||
|
<AriaInput
|
||||||
|
type="checkbox"
|
||||||
|
className={twMerge(
|
||||||
|
'flex size-4 cursor-pointer overflow-clip rounded-lg border border-primary outline-primary focus-visible:outline focus-visible:outline-2',
|
||||||
|
isInvalid && 'border-red-700 text-red-500 outline-red-500',
|
||||||
|
)}
|
||||||
|
data-testid="privacy-policy-checkbox"
|
||||||
|
{...omit(register('agreedToPrivacyPolicy'), 'isInvalid')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text>{getText('privacyPolicyCheckbox')}</Text>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Button variant="link" target="_blank" href="https://ensoanalytics.com/privacy">
|
||||||
|
{getText('viewPrivacyPolicy')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<Form.Submit size="large" icon={CreateAccountIcon} className="w-full">
|
||||||
|
{getText('register')}
|
||||||
|
</Form.Submit>
|
||||||
|
|
||||||
|
<Form.FormError />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</AuthenticationPage>
|
</AuthenticationPage>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -430,6 +430,7 @@
|
|||||||
"enableVersionCheckerDescription": "Show a dialog if the current version of the desktop app does not match the latest version.",
|
"enableVersionCheckerDescription": "Show a dialog if the current version of the desktop app does not match the latest version.",
|
||||||
"removeTheLocalDirectoryXFromFavorites": "remove the local folder '$0' from your favorites",
|
"removeTheLocalDirectoryXFromFavorites": "remove the local folder '$0' from your favorites",
|
||||||
"changeLocalRootDirectoryInSettings": "Change your root folder in Settings.",
|
"changeLocalRootDirectoryInSettings": "Change your root folder in Settings.",
|
||||||
|
"localStorage": "Local Storage",
|
||||||
"addLocalDirectory": "Add Folder",
|
"addLocalDirectory": "Add Folder",
|
||||||
"browseForNewLocalRootDirectory": "Browse for new Root Folder",
|
"browseForNewLocalRootDirectory": "Browse for new Root Folder",
|
||||||
"resetLocalRootDirectory": "Reset Root Folder",
|
"resetLocalRootDirectory": "Reset Root Folder",
|
||||||
@ -573,10 +574,14 @@
|
|||||||
|
|
||||||
"downgradeInfo": "Looking for downgrade options?",
|
"downgradeInfo": "Looking for downgrade options?",
|
||||||
|
|
||||||
|
"someAgreementsHaveBeenUpdated": "Some agreements have been updated. Please re-read and agree to continue.",
|
||||||
"licenseAgreementTitle": "Enso Terms of Service",
|
"licenseAgreementTitle": "Enso Terms of Service",
|
||||||
"licenseAgreementCheckbox": "I agree to the Enso Terms of Service",
|
"licenseAgreementCheckbox": "I agree to the Enso Terms of Service",
|
||||||
"licenseAgreementCheckboxError": "Please agree to the Enso Terms of Service",
|
"licenseAgreementCheckboxError": "Please agree to the Enso Terms of Service",
|
||||||
"viewLicenseAgreement": "View Terms of Service",
|
"viewLicenseAgreement": "View Terms of Service",
|
||||||
|
"privacyPolicyCheckbox": "I agree to the Privacy Policy",
|
||||||
|
"privacyPolicyCheckboxError": "Please agree to the Privacy Policy",
|
||||||
|
"viewPrivacyPolicy": "View Privacy Policy",
|
||||||
|
|
||||||
"metaModifier": "Meta",
|
"metaModifier": "Meta",
|
||||||
"shiftModifier": "Shift",
|
"shiftModifier": "Shift",
|
||||||
|
Loading…
Reference in New Issue
Block a user