From c271beb8166f21a0bdaf144373467892979273d5 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Tue, 4 Aug 2026 17:21:36 +0200 Subject: [PATCH 1/8] feat(onboarding): add confirmation screen before organization creation New signups with 0 organizations now land on /onboarding/confirm before entering the personalize/use-cases/project funnel. It surfaces the SSO provider and email just used (plus the previously used provider on this browser, when different) so users who clicked the wrong SSO button catch it before creating a duplicate "shadow" organization. Co-Authored-By: Claude Sonnet 5 --- apps/console/src/routeTree.gen.ts | 22 +++++++ .../_authenticated/onboarding/confirm.tsx | 37 +++++++++++ .../src/routes/login/auth0-callback.tsx | 10 ++- apps/console/src/routes/login/index.tsx | 4 +- libs/domains/onboarding/feature/src/index.ts | 2 + .../src/lib/step-confirm/provider-display.ts | 23 +++++++ .../src/lib/step-confirm/step-confirm.tsx | 61 +++++++++++++++++++ libs/shared/auth/src/lib/enum/auth.enum.ts | 2 + .../src/lib/sub-router/onboarding.router.ts | 1 + 9 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 apps/console/src/routes/_authenticated/onboarding/confirm.tsx create mode 100644 libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts create mode 100644 libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx diff --git a/apps/console/src/routeTree.gen.ts b/apps/console/src/routeTree.gen.ts index 50660620478..96fafa94a91 100644 --- a/apps/console/src/routeTree.gen.ts +++ b/apps/console/src/routeTree.gen.ts @@ -22,6 +22,7 @@ import { Route as AuthenticatedOnboardingUseCasesRouteImport } from './routes/_a import { Route as AuthenticatedOnboardingProjectRouteImport } from './routes/_authenticated/onboarding/project' import { Route as AuthenticatedOnboardingPlansRouteImport } from './routes/_authenticated/onboarding/plans' import { Route as AuthenticatedOnboardingPersonalizeRouteImport } from './routes/_authenticated/onboarding/personalize' +import { Route as AuthenticatedOnboardingConfirmRouteImport } from './routes/_authenticated/onboarding/confirm' import { Route as AuthenticatedOrganizationOrganizationIdRouteRouteImport } from './routes/_authenticated/organization/$organizationId/route' import { Route as AuthenticatedOrganizationOrganizationIdIndexRouteImport } from './routes/_authenticated/organization/$organizationId/index' import { Route as AuthenticatedOrganizationOrganizationIdOverviewRouteImport } from './routes/_authenticated/organization/$organizationId/overview' @@ -268,6 +269,12 @@ const AuthenticatedOnboardingPersonalizeRoute = path: '/onboarding/personalize', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedOnboardingConfirmRoute = + AuthenticatedOnboardingConfirmRouteImport.update({ + id: '/onboarding/confirm', + path: '/onboarding/confirm', + getParentRoute: () => AuthenticatedRoute, + } as any) const AuthenticatedOrganizationOrganizationIdRouteRoute = AuthenticatedOrganizationOrganizationIdRouteRouteImport.update({ id: '/$organizationId', @@ -1732,6 +1739,7 @@ export interface FileRoutesByFullPath { '/login/auth0-callback': typeof LoginAuth0CallbackRoute '/login': typeof LoginIndexRoute '/organization/$organizationId': typeof AuthenticatedOrganizationOrganizationIdRouteRouteWithChildren + '/onboarding/confirm': typeof AuthenticatedOnboardingConfirmRoute '/onboarding/personalize': typeof AuthenticatedOnboardingPersonalizeRoute '/onboarding/plans': typeof AuthenticatedOnboardingPlansRoute '/onboarding/project': typeof AuthenticatedOnboardingProjectRoute @@ -1916,6 +1924,7 @@ export interface FileRoutesByTo { '/preview-code': typeof AuthenticatedPreviewCodeRoute '/login/auth0-callback': typeof LoginAuth0CallbackRoute '/login': typeof LoginIndexRoute + '/onboarding/confirm': typeof AuthenticatedOnboardingConfirmRoute '/onboarding/personalize': typeof AuthenticatedOnboardingPersonalizeRoute '/onboarding/plans': typeof AuthenticatedOnboardingPlansRoute '/onboarding/project': typeof AuthenticatedOnboardingProjectRoute @@ -2085,6 +2094,7 @@ export interface FileRoutesById { '/login/auth0-callback': typeof LoginAuth0CallbackRoute '/login/': typeof LoginIndexRoute '/_authenticated/organization/$organizationId': typeof AuthenticatedOrganizationOrganizationIdRouteRouteWithChildren + '/_authenticated/onboarding/confirm': typeof AuthenticatedOnboardingConfirmRoute '/_authenticated/onboarding/personalize': typeof AuthenticatedOnboardingPersonalizeRoute '/_authenticated/onboarding/plans': typeof AuthenticatedOnboardingPlansRoute '/_authenticated/onboarding/project': typeof AuthenticatedOnboardingProjectRoute @@ -2273,6 +2283,7 @@ export interface FileRouteTypes { | '/login/auth0-callback' | '/login' | '/organization/$organizationId' + | '/onboarding/confirm' | '/onboarding/personalize' | '/onboarding/plans' | '/onboarding/project' @@ -2457,6 +2468,7 @@ export interface FileRouteTypes { | '/preview-code' | '/login/auth0-callback' | '/login' + | '/onboarding/confirm' | '/onboarding/personalize' | '/onboarding/plans' | '/onboarding/project' @@ -2625,6 +2637,7 @@ export interface FileRouteTypes { | '/login/auth0-callback' | '/login/' | '/_authenticated/organization/$organizationId' + | '/_authenticated/onboarding/confirm' | '/_authenticated/onboarding/personalize' | '/_authenticated/onboarding/plans' | '/_authenticated/onboarding/project' @@ -2904,6 +2917,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOnboardingPersonalizeRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/onboarding/confirm': { + id: '/_authenticated/onboarding/confirm' + path: '/onboarding/confirm' + fullPath: '/onboarding/confirm' + preLoaderRoute: typeof AuthenticatedOnboardingConfirmRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/organization/$organizationId': { id: '/_authenticated/organization/$organizationId' path: '/$organizationId' @@ -4897,6 +4917,7 @@ interface AuthenticatedRouteChildren { AuthenticatedOrganizationRouteRoute: typeof AuthenticatedOrganizationRouteRouteWithChildren AuthenticatedHelmDefaultValuesRoute: typeof AuthenticatedHelmDefaultValuesRoute AuthenticatedPreviewCodeRoute: typeof AuthenticatedPreviewCodeRoute + AuthenticatedOnboardingConfirmRoute: typeof AuthenticatedOnboardingConfirmRoute AuthenticatedOnboardingPersonalizeRoute: typeof AuthenticatedOnboardingPersonalizeRoute AuthenticatedOnboardingPlansRoute: typeof AuthenticatedOnboardingPlansRoute AuthenticatedOnboardingProjectRoute: typeof AuthenticatedOnboardingProjectRoute @@ -4909,6 +4930,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedOrganizationRouteRouteWithChildren, AuthenticatedHelmDefaultValuesRoute: AuthenticatedHelmDefaultValuesRoute, AuthenticatedPreviewCodeRoute: AuthenticatedPreviewCodeRoute, + AuthenticatedOnboardingConfirmRoute: AuthenticatedOnboardingConfirmRoute, AuthenticatedOnboardingPersonalizeRoute: AuthenticatedOnboardingPersonalizeRoute, AuthenticatedOnboardingPlansRoute: AuthenticatedOnboardingPlansRoute, diff --git a/apps/console/src/routes/_authenticated/onboarding/confirm.tsx b/apps/console/src/routes/_authenticated/onboarding/confirm.tsx new file mode 100644 index 00000000000..95b5a39c364 --- /dev/null +++ b/apps/console/src/routes/_authenticated/onboarding/confirm.tsx @@ -0,0 +1,37 @@ +import { useAuth0 } from '@auth0/auth0-react' +import { Navigate, createFileRoute, useNavigate } from '@tanstack/react-router' +import { StepConfirm, getProviderDisplay, getProviderKeyFromSub } from '@qovery/domains/onboarding/feature' +import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' +import { LAST_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' +import { getOnboardingEntryUrl } from '@qovery/shared/routes' +import { useDocumentTitle, useLocalStorage } from '@qovery/shared/util-hooks' + +export const Route = createFileRoute('/_authenticated/onboarding/confirm')({ + component: Confirm, +}) + +function Confirm() { + useDocumentTitle('Onboarding Confirm - Qovery') + const { isAuthenticated } = useAuth0() + const navigate = useNavigate() + const { user, authLogout } = useAuth() + const { data: userSignUp } = useUserSignUp() + const [lastUsedLogin] = useLocalStorage(LAST_USED_LOGIN_STORAGE_KEY, undefined) + + const provider = getProviderDisplay(getProviderKeyFromSub(user?.sub)) + const previousProvider = getProviderDisplay(lastUsedLogin) + + if (!isAuthenticated) { + return + } + + return ( + navigate({ href: getOnboardingEntryUrl(userSignUp) })} + onWrongAccount={() => authLogout()} + /> + ) +} diff --git a/apps/console/src/routes/login/auth0-callback.tsx b/apps/console/src/routes/login/auth0-callback.tsx index 984fe70a306..78ba33e6615 100644 --- a/apps/console/src/routes/login/auth0-callback.tsx +++ b/apps/console/src/routes/login/auth0-callback.tsx @@ -3,9 +3,8 @@ import { Navigate, createFileRoute, useNavigate } from '@tanstack/react-router' import axios from 'axios' import { useEffect } from 'react' import { useOrganizations } from '@qovery/domains/organizations/feature' -import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' import { useAuth } from '@qovery/shared/auth' -import { getOnboardingEntryUrl } from '@qovery/shared/routes' +import { ONBOARDING_CONFIRM_URL, ONBOARDING_URL } from '@qovery/shared/routes' import { LoadingScreen } from '@qovery/shared/ui' import { QOVERY_API } from '@qovery/shared/util-node-env' import { useAuthInterceptor } from '@qovery/shared/utils' @@ -38,7 +37,6 @@ function useRedirectIfLogged(connection?: string) { const { data: organizations = [], isFetched: isFetchedOrganizations } = useOrganizations({ enabled: isAuthenticated, }) - const { refetch: refetchUserSignUp } = useUserSignUp({ enabled: false }) useEffect(() => { if (connection && !isAuthenticated) { @@ -71,15 +69,15 @@ function useRedirectIfLogged(connection?: string) { navigate({ to: '/organization/$organizationId/overview', params: { organizationId: organizations[0]?.id } }) } } else { - const { data: userSignUp } = await refetchUserSignUp() - navigate({ href: getOnboardingEntryUrl(userSignUp) }) + // No organization yet: confirm this is really a new account before entering the onboarding funnel + navigate({ href: `${ONBOARDING_URL}${ONBOARDING_CONFIRM_URL}` }) } } if (isAuthenticated) { fetchData() } - }, [authLogin, connection, navigate, isAuthenticated, organizations, isFetchedOrganizations, refetchUserSignUp]) + }, [authLogin, connection, navigate, isAuthenticated, organizations, isFetchedOrganizations]) } function PageRedirectLogin() { diff --git a/apps/console/src/routes/login/index.tsx b/apps/console/src/routes/login/index.tsx index 55bf2a37bf3..a74a55ac112 100644 --- a/apps/console/src/routes/login/index.tsx +++ b/apps/console/src/routes/login/index.tsx @@ -4,13 +4,11 @@ import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useState } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' import { z } from 'zod' -import { AuthEnum, useAuth } from '@qovery/shared/auth' +import { AuthEnum, LAST_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' import { IconEnum } from '@qovery/shared/enums' import { Badge, Button, Icon, InputTextSmall, Link } from '@qovery/shared/ui' import { useLocalStorage } from '@qovery/shared/util-hooks' -const LAST_USED_LOGIN_STORAGE_KEY = 'lastUsedLogin' - const CUBIC_BEZIER_EASE = [0.65, 0.05, 0.36, 1] as const const SCREEN_STACK_MOVE_DURATION_S = 0.6 const SCREEN_STACK_HOLD_DURATION_S = 8 diff --git a/libs/domains/onboarding/feature/src/index.ts b/libs/domains/onboarding/feature/src/index.ts index 9e47ba614c9..5740bd95a1a 100644 --- a/libs/domains/onboarding/feature/src/index.ts +++ b/libs/domains/onboarding/feature/src/index.ts @@ -4,5 +4,7 @@ export * from './lib/hooks/use-invite-member/use-invite-member' export * from './lib/step-personalize/step-personalize' export * from './lib/step-personalize/personal-email-domains' export * from './lib/step-use-cases/step-use-cases' +export * from './lib/step-confirm/step-confirm' +export * from './lib/step-confirm/provider-display' export * from './lib/onboarding-project/onboarding-project' export * from './lib/onboarding-plans/onboarding-plans' diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts b/libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts new file mode 100644 index 00000000000..d3349f150e2 --- /dev/null +++ b/libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts @@ -0,0 +1,23 @@ +import { IconEnum } from '@qovery/shared/enums' + +export interface ProviderDisplay { + label: string + icon: IconEnum +} + +const PROVIDER_DISPLAY_BY_KEY: Record = { + github: { label: 'GitHub', icon: IconEnum.GITHUB }, + Gitlab: { label: 'GitLab', icon: IconEnum.GITLAB }, + 'google-oauth2': { label: 'Google', icon: IconEnum.GOOGLE }, + windowslive: { label: 'Microsoft', icon: IconEnum.MICROSOFT }, + bitbucket: { label: 'Bitbucket', icon: IconEnum.BITBUCKET }, +} + +export function getProviderKeyFromSub(sub?: string | null): string | undefined { + return sub?.split('|')[0] +} + +export function getProviderDisplay(providerKey?: string | null): ProviderDisplay | undefined { + if (!providerKey) return undefined + return PROVIDER_DISPLAY_BY_KEY[providerKey] +} diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx new file mode 100644 index 00000000000..8ca01711ab9 --- /dev/null +++ b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx @@ -0,0 +1,61 @@ +import { Button, Icon } from '@qovery/shared/ui' +import { type ProviderDisplay } from './provider-display' + +export interface StepConfirmProps { + provider?: ProviderDisplay + email?: string + previousProvider?: ProviderDisplay + onContinue: () => void + onWrongAccount: () => void +} + +export function StepConfirm({ provider, email, previousProvider, onContinue, onWrongAccount }: StepConfirmProps) { + const showPreviousProviderHint = previousProvider && previousProvider.label !== provider?.label + + return ( +
+
+
+ Qovery logo + +

Already have a Qovery account?

+

+ You're about to create a brand new organization. If you already have a Qovery account, sign in with the + right provider instead. +

+ + {provider && ( +
+ +
+ Signed in with {provider.label} + {email && {email}} +
+
+ )} + + {showPreviousProviderHint && ( +

+ Last time on this browser, you signed in with {previousProvider?.label}. +

+ )} + +
+ + +
+ +

+ You can invite your team and manage sign-in providers later. +

+
+
+
+ ) +} + +export default StepConfirm diff --git a/libs/shared/auth/src/lib/enum/auth.enum.ts b/libs/shared/auth/src/lib/enum/auth.enum.ts index 2ba42d292ed..29c9cefb163 100644 --- a/libs/shared/auth/src/lib/enum/auth.enum.ts +++ b/libs/shared/auth/src/lib/enum/auth.enum.ts @@ -5,3 +5,5 @@ export const enum AuthEnum { GOOGLE_SSO = 'google-oauth2', MICROSOFT = 'windowslive', } + +export const LAST_USED_LOGIN_STORAGE_KEY = 'lastUsedLogin' diff --git a/libs/shared/routes/src/lib/sub-router/onboarding.router.ts b/libs/shared/routes/src/lib/sub-router/onboarding.router.ts index 28e6e2862d1..3128325c512 100644 --- a/libs/shared/routes/src/lib/sub-router/onboarding.router.ts +++ b/libs/shared/routes/src/lib/sub-router/onboarding.router.ts @@ -1,4 +1,5 @@ export const ONBOARDING_URL = '/onboarding' +export const ONBOARDING_CONFIRM_URL = '/confirm' export const ONBOARDING_PERSONALIZE_URL = '/personalize' export const ONBOARDING_USE_CASES_URL = '/use-cases' export const ONBOARDING_PLANS_URL = '/plans' From 2142752bbf9053c48573b29678e184cc9cc36a19 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Tue, 4 Aug 2026 17:32:12 +0200 Subject: [PATCH 2/8] fix(onboarding): remove inaccurate sign-in provider claim from confirm screen copy There's no feature to link or manage additional sign-in providers on an existing account, so the reassurance microcopy shouldn't promise it. Co-Authored-By: Claude Sonnet 5 --- .../onboarding/feature/src/lib/step-confirm/step-confirm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx index 8ca01711ab9..516375b0ca2 100644 --- a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx +++ b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx @@ -50,7 +50,7 @@ export function StepConfirm({ provider, email, previousProvider, onContinue, onW

- You can invite your team and manage sign-in providers later. + You can invite your team later.

From df290d8ed49fb771b46a4fde8ae8a96b2e1d1182 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Wed, 5 Aug 2026 09:15:08 +0200 Subject: [PATCH 3/8] fix(onboarding): address review feedback on the confirm screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use Heading and Section instead of raw h1/div for the confirm screen title - Fold provider-display helpers into step-confirm.tsx instead of a separate file - Move data-fetching (auth0, user sign-up, local storage) into StepConfirm itself instead of threading props from the route - Add unit tests for StepConfirm - Only show the confirm screen when this browser previously signed in with a different provider than the one just used, instead of on every new signup — avoids adding friction for genuinely new users while still catching the wrong-SSO-button mistake, using the existing lastUsedLogin signal already captured on the login page (no backend change needed) - Fix yarn nx format:check failure flagged by CI Co-Authored-By: Claude Sonnet 5 --- .../_authenticated/onboarding/confirm.tsx | 26 +----- .../src/routes/login/auth0-callback.tsx | 42 +++++++-- apps/console/src/routes/login/index.tsx | 8 +- libs/domains/onboarding/feature/src/index.ts | 1 - .../src/lib/step-confirm/provider-display.ts | 23 ----- .../lib/step-confirm/step-confirm.spec.tsx | 86 +++++++++++++++++++ .../src/lib/step-confirm/step-confirm.tsx | 76 ++++++++++++---- libs/shared/auth/src/lib/enum/auth.enum.ts | 1 + 8 files changed, 188 insertions(+), 75 deletions(-) delete mode 100644 libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts create mode 100644 libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx diff --git a/apps/console/src/routes/_authenticated/onboarding/confirm.tsx b/apps/console/src/routes/_authenticated/onboarding/confirm.tsx index 95b5a39c364..744eaedd4c9 100644 --- a/apps/console/src/routes/_authenticated/onboarding/confirm.tsx +++ b/apps/console/src/routes/_authenticated/onboarding/confirm.tsx @@ -1,10 +1,7 @@ import { useAuth0 } from '@auth0/auth0-react' -import { Navigate, createFileRoute, useNavigate } from '@tanstack/react-router' -import { StepConfirm, getProviderDisplay, getProviderKeyFromSub } from '@qovery/domains/onboarding/feature' -import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' -import { LAST_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' -import { getOnboardingEntryUrl } from '@qovery/shared/routes' -import { useDocumentTitle, useLocalStorage } from '@qovery/shared/util-hooks' +import { Navigate, createFileRoute } from '@tanstack/react-router' +import { StepConfirm } from '@qovery/domains/onboarding/feature' +import { useDocumentTitle } from '@qovery/shared/util-hooks' export const Route = createFileRoute('/_authenticated/onboarding/confirm')({ component: Confirm, @@ -13,25 +10,10 @@ export const Route = createFileRoute('/_authenticated/onboarding/confirm')({ function Confirm() { useDocumentTitle('Onboarding Confirm - Qovery') const { isAuthenticated } = useAuth0() - const navigate = useNavigate() - const { user, authLogout } = useAuth() - const { data: userSignUp } = useUserSignUp() - const [lastUsedLogin] = useLocalStorage(LAST_USED_LOGIN_STORAGE_KEY, undefined) - - const provider = getProviderDisplay(getProviderKeyFromSub(user?.sub)) - const previousProvider = getProviderDisplay(lastUsedLogin) if (!isAuthenticated) { return } - return ( - navigate({ href: getOnboardingEntryUrl(userSignUp) })} - onWrongAccount={() => authLogout()} - /> - ) + return } diff --git a/apps/console/src/routes/login/auth0-callback.tsx b/apps/console/src/routes/login/auth0-callback.tsx index 78ba33e6615..28840473bbb 100644 --- a/apps/console/src/routes/login/auth0-callback.tsx +++ b/apps/console/src/routes/login/auth0-callback.tsx @@ -2,10 +2,13 @@ import { useAuth0 } from '@auth0/auth0-react' import { Navigate, createFileRoute, useNavigate } from '@tanstack/react-router' import axios from 'axios' import { useEffect } from 'react' +import { getProviderKeyFromSub } from '@qovery/domains/onboarding/feature' import { useOrganizations } from '@qovery/domains/organizations/feature' -import { useAuth } from '@qovery/shared/auth' -import { ONBOARDING_CONFIRM_URL, ONBOARDING_URL } from '@qovery/shared/routes' +import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' +import { PREVIOUS_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' +import { ONBOARDING_CONFIRM_URL, ONBOARDING_URL, getOnboardingEntryUrl } from '@qovery/shared/routes' import { LoadingScreen } from '@qovery/shared/ui' +import { useLocalStorage } from '@qovery/shared/util-hooks' import { QOVERY_API } from '@qovery/shared/util-node-env' import { useAuthInterceptor } from '@qovery/shared/utils' import { consumePendingReturnTo } from '../../auth/auth0' @@ -33,10 +36,12 @@ export const Route = createFileRoute('/login/auth0-callback')({ function useRedirectIfLogged(connection?: string) { const navigate = useNavigate() const { authLogin } = useAuth() - const { isAuthenticated } = useAuth0() + const { isAuthenticated, user } = useAuth0() const { data: organizations = [], isFetched: isFetchedOrganizations } = useOrganizations({ enabled: isAuthenticated, }) + const { data: userSignUp, isFetched: isFetchedUserSignUp } = useUserSignUp({ enabled: isAuthenticated }) + const [previousUsedLogin] = useLocalStorage(PREVIOUS_USED_LOGIN_STORAGE_KEY, undefined) useEffect(() => { if (connection && !isAuthenticated) { @@ -50,8 +55,8 @@ function useRedirectIfLogged(connection?: string) { return } - async function fetchData() { - if (!isFetchedOrganizations) { + function redirect() { + if (!isFetchedOrganizations || !isFetchedUserSignUp) { return } @@ -68,16 +73,35 @@ function useRedirectIfLogged(connection?: string) { } else { navigate({ to: '/organization/$organizationId/overview', params: { organizationId: organizations[0]?.id } }) } - } else { - // No organization yet: confirm this is really a new account before entering the onboarding funnel + return + } + + // No organization yet: only pause on the confirm screen if this browser was + // previously used to sign in with a different provider than the one just used + const provider = getProviderKeyFromSub(user?.sub) + if (previousUsedLogin && previousUsedLogin !== provider) { navigate({ href: `${ONBOARDING_URL}${ONBOARDING_CONFIRM_URL}` }) + return } + + navigate({ href: getOnboardingEntryUrl(userSignUp) }) } if (isAuthenticated) { - fetchData() + redirect() } - }, [authLogin, connection, navigate, isAuthenticated, organizations, isFetchedOrganizations]) + }, [ + authLogin, + connection, + navigate, + isAuthenticated, + organizations, + isFetchedOrganizations, + userSignUp, + isFetchedUserSignUp, + previousUsedLogin, + user, + ]) } function PageRedirectLogin() { diff --git a/apps/console/src/routes/login/index.tsx b/apps/console/src/routes/login/index.tsx index a74a55ac112..9a16057cca6 100644 --- a/apps/console/src/routes/login/index.tsx +++ b/apps/console/src/routes/login/index.tsx @@ -4,7 +4,7 @@ import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useState } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' import { z } from 'zod' -import { AuthEnum, LAST_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' +import { AuthEnum, LAST_USED_LOGIN_STORAGE_KEY, PREVIOUS_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' import { IconEnum } from '@qovery/shared/enums' import { Badge, Button, Icon, InputTextSmall, Link } from '@qovery/shared/ui' import { useLocalStorage } from '@qovery/shared/util-hooks' @@ -192,6 +192,7 @@ function RouteComponent() { const { auth0Error, setAuth0Error } = useAuth0Error() const [loading, setLoading] = useState<{ provider: string; active: boolean } | undefined>() const [lastUsedLogin, setLastUsedLogin] = useLocalStorage(LAST_USED_LOGIN_STORAGE_KEY, undefined) + const [, setPreviousUsedLogin] = useLocalStorage(PREVIOUS_USED_LOGIN_STORAGE_KEY, undefined) const [lastUsedLoginAtPageLoad] = useState(lastUsedLogin) const [testimonialIndex, setTestimonialIndex] = useState(0) @@ -234,6 +235,11 @@ function RouteComponent() { active: true, }) + // Stash the provider used before this click, so the onboarding confirm + // screen can tell a genuine first-time signup from a provider switch. + if (lastUsedLogin) { + setPreviousUsedLogin(lastUsedLogin) + } setLastUsedLogin(lastUsedProvider) try { diff --git a/libs/domains/onboarding/feature/src/index.ts b/libs/domains/onboarding/feature/src/index.ts index 5740bd95a1a..9c543e5923e 100644 --- a/libs/domains/onboarding/feature/src/index.ts +++ b/libs/domains/onboarding/feature/src/index.ts @@ -5,6 +5,5 @@ export * from './lib/step-personalize/step-personalize' export * from './lib/step-personalize/personal-email-domains' export * from './lib/step-use-cases/step-use-cases' export * from './lib/step-confirm/step-confirm' -export * from './lib/step-confirm/provider-display' export * from './lib/onboarding-project/onboarding-project' export * from './lib/onboarding-plans/onboarding-plans' diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts b/libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts deleted file mode 100644 index d3349f150e2..00000000000 --- a/libs/domains/onboarding/feature/src/lib/step-confirm/provider-display.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IconEnum } from '@qovery/shared/enums' - -export interface ProviderDisplay { - label: string - icon: IconEnum -} - -const PROVIDER_DISPLAY_BY_KEY: Record = { - github: { label: 'GitHub', icon: IconEnum.GITHUB }, - Gitlab: { label: 'GitLab', icon: IconEnum.GITLAB }, - 'google-oauth2': { label: 'Google', icon: IconEnum.GOOGLE }, - windowslive: { label: 'Microsoft', icon: IconEnum.MICROSOFT }, - bitbucket: { label: 'Bitbucket', icon: IconEnum.BITBUCKET }, -} - -export function getProviderKeyFromSub(sub?: string | null): string | undefined { - return sub?.split('|')[0] -} - -export function getProviderDisplay(providerKey?: string | null): ProviderDisplay | undefined { - if (!providerKey) return undefined - return PROVIDER_DISPLAY_BY_KEY[providerKey] -} diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx new file mode 100644 index 00000000000..05bc9032aaf --- /dev/null +++ b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx @@ -0,0 +1,86 @@ +import { type ReactNode } from 'react' +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import StepConfirm from './step-confirm' + +const mockedUsedNavigate = jest.fn() +const mockedAuthLogout = jest.fn() + +jest.mock('@tanstack/react-router', () => ({ + ...jest.requireActual('@tanstack/react-router'), + useNavigate: () => mockedUsedNavigate, +})) + +jest.mock('@auth0/auth0-react', () => ({ + ...jest.requireActual('@auth0/auth0-react'), + Auth0Provider: ({ children }: { children: ReactNode }) => children, + useAuth0: () => ({ + user: { sub: 'google-oauth2|123', email: 'jdan@qovery.com' }, + }), +})) + +jest.mock('@qovery/shared/auth', () => ({ + ...jest.requireActual('@qovery/shared/auth'), + useAuth: () => ({ authLogout: mockedAuthLogout }), +})) + +jest.mock('@qovery/domains/users-sign-up/feature', () => ({ + ...jest.requireActual('@qovery/domains/users-sign-up/feature'), + useUserSignUp: () => ({ data: undefined }), +})) + +jest.mock('@qovery/shared/util-hooks', () => ({ + ...jest.requireActual('@qovery/shared/util-hooks'), + useLocalStorage: jest.fn(), +})) + +const { useLocalStorage } = jest.requireMock('@qovery/shared/util-hooks') as { useLocalStorage: jest.Mock } + +describe('StepConfirm', () => { + beforeEach(() => { + mockedUsedNavigate.mockClear() + mockedAuthLogout.mockClear() + useLocalStorage.mockReturnValue([undefined, jest.fn()]) + }) + + it('should render successfully', () => { + const { baseElement } = renderWithProviders() + expect(baseElement).toBeTruthy() + }) + + it('should show the provider and email just used to sign in', () => { + renderWithProviders() + + expect(screen.getByText('Signed in with Google')).toBeInTheDocument() + expect(screen.getByText('jdan@qovery.com')).toBeInTheDocument() + }) + + it('should not show the previous provider hint when there is none', () => { + renderWithProviders() + + expect(screen.queryByText(/Last time on this browser/)).not.toBeInTheDocument() + }) + + it('should show the previous provider hint when it differs from the current one', () => { + useLocalStorage.mockReturnValue(['github', jest.fn()]) + + renderWithProviders() + + expect(screen.getByText('Last time on this browser, you signed in with GitHub.')).toBeInTheDocument() + }) + + it('should navigate into the onboarding funnel when continuing', async () => { + const { userEvent } = renderWithProviders() + + await userEvent.click(screen.getByRole('button', { name: 'Create my organization' })) + + expect(mockedUsedNavigate).toHaveBeenCalledWith({ href: '/onboarding/personalize' }) + }) + + it('should log the user out when switching account', async () => { + const { userEvent } = renderWithProviders() + + await userEvent.click(screen.getByRole('button', { name: 'Switch account' })) + + expect(mockedAuthLogout).toHaveBeenCalled() + }) +}) diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx index 516375b0ca2..97414636f53 100644 --- a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx +++ b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx @@ -1,24 +1,54 @@ -import { Button, Icon } from '@qovery/shared/ui' -import { type ProviderDisplay } from './provider-display' - -export interface StepConfirmProps { - provider?: ProviderDisplay - email?: string - previousProvider?: ProviderDisplay - onContinue: () => void - onWrongAccount: () => void +import { useAuth0 } from '@auth0/auth0-react' +import { useNavigate } from '@tanstack/react-router' +import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' +import { PREVIOUS_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' +import { IconEnum } from '@qovery/shared/enums' +import { getOnboardingEntryUrl } from '@qovery/shared/routes' +import { Button, Heading, Icon, Section } from '@qovery/shared/ui' +import { useLocalStorage } from '@qovery/shared/util-hooks' + +export interface ProviderDisplay { + label: string + icon: IconEnum +} + +const PROVIDER_DISPLAY_BY_KEY: Record = { + github: { label: 'GitHub', icon: IconEnum.GITHUB }, + Gitlab: { label: 'GitLab', icon: IconEnum.GITLAB }, + 'google-oauth2': { label: 'Google', icon: IconEnum.GOOGLE }, + windowslive: { label: 'Microsoft', icon: IconEnum.MICROSOFT }, + bitbucket: { label: 'Bitbucket', icon: IconEnum.BITBUCKET }, +} + +export function getProviderKeyFromSub(sub?: string | null): string | undefined { + return sub?.split('|')[0] } -export function StepConfirm({ provider, email, previousProvider, onContinue, onWrongAccount }: StepConfirmProps) { +export function getProviderDisplay(providerKey?: string | null): ProviderDisplay | undefined { + if (!providerKey) return undefined + return PROVIDER_DISPLAY_BY_KEY[providerKey] +} + +export function StepConfirm() { + const navigate = useNavigate() + const { user } = useAuth0() + const { authLogout } = useAuth() + const { data: userSignUp } = useUserSignUp() + const [previousUsedLogin] = useLocalStorage(PREVIOUS_USED_LOGIN_STORAGE_KEY, undefined) + + const provider = getProviderDisplay(getProviderKeyFromSub(user?.sub)) + const previousProvider = getProviderDisplay(previousUsedLogin) const showPreviousProviderHint = previousProvider && previousProvider.label !== provider?.label return ( -
+
Qovery logo -

Already have a Qovery account?

+ + Already have a Qovery account? +

You're about to create a brand new organization. If you already have a Qovery account, sign in with the right provider instead. @@ -29,7 +59,7 @@ export function StepConfirm({ provider, email, previousProvider, onContinue, onW

Signed in with {provider.label} - {email && {email}} + {user?.email && {user.email}}
)} @@ -41,20 +71,28 @@ export function StepConfirm({ provider, email, previousProvider, onContinue, onW )}
- -
-

- You can invite your team later. -

+

You can invite your team later.

- + ) } diff --git a/libs/shared/auth/src/lib/enum/auth.enum.ts b/libs/shared/auth/src/lib/enum/auth.enum.ts index 29c9cefb163..0177686f236 100644 --- a/libs/shared/auth/src/lib/enum/auth.enum.ts +++ b/libs/shared/auth/src/lib/enum/auth.enum.ts @@ -7,3 +7,4 @@ export const enum AuthEnum { } export const LAST_USED_LOGIN_STORAGE_KEY = 'lastUsedLogin' +export const PREVIOUS_USED_LOGIN_STORAGE_KEY = 'previousUsedLogin' From dda9c818410a78fbba412c68f5f0341fe3b6dfc6 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Wed, 5 Aug 2026 11:05:44 +0200 Subject: [PATCH 4/8] refactor(onboarding): fold the confirm screen into the personalize step wording Remove the dedicated /onboarding/confirm screen and its signal-based gating (only reached a subset of users on the same browser/device). Instead, revise the wording of the existing personalize step, seen by every new signup: - Breadcrumb and heading now say plainly that this creates a new organization - Subtitle tells users with an existing team to get an invitation instead - A new inline link lets users who signed up with the wrong provider log out and sign in again, right where the mistake is most likely to be noticed This reaches 100% of new signups without adding an extra screen or step, and drops the previousUsedLogin/localStorage tracking that only covered users staying on the same browser. Co-Authored-By: Claude Sonnet 5 --- apps/console/src/routeTree.gen.ts | 22 ----- .../_authenticated/onboarding/confirm.tsx | 19 ---- .../src/routes/login/auth0-callback.tsx | 19 +--- apps/console/src/routes/login/index.tsx | 8 +- libs/domains/onboarding/feature/src/index.ts | 1 - .../feature/src/lib/container/container.tsx | 2 +- .../lib/step-confirm/step-confirm.spec.tsx | 86 ---------------- .../src/lib/step-confirm/step-confirm.tsx | 99 ------------------- .../lib/step-personalize/step-personalize.tsx | 16 ++- libs/shared/auth/src/lib/enum/auth.enum.ts | 1 - .../src/lib/sub-router/onboarding.router.ts | 1 - 11 files changed, 18 insertions(+), 256 deletions(-) delete mode 100644 apps/console/src/routes/_authenticated/onboarding/confirm.tsx delete mode 100644 libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx delete mode 100644 libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx diff --git a/apps/console/src/routeTree.gen.ts b/apps/console/src/routeTree.gen.ts index 96fafa94a91..50660620478 100644 --- a/apps/console/src/routeTree.gen.ts +++ b/apps/console/src/routeTree.gen.ts @@ -22,7 +22,6 @@ import { Route as AuthenticatedOnboardingUseCasesRouteImport } from './routes/_a import { Route as AuthenticatedOnboardingProjectRouteImport } from './routes/_authenticated/onboarding/project' import { Route as AuthenticatedOnboardingPlansRouteImport } from './routes/_authenticated/onboarding/plans' import { Route as AuthenticatedOnboardingPersonalizeRouteImport } from './routes/_authenticated/onboarding/personalize' -import { Route as AuthenticatedOnboardingConfirmRouteImport } from './routes/_authenticated/onboarding/confirm' import { Route as AuthenticatedOrganizationOrganizationIdRouteRouteImport } from './routes/_authenticated/organization/$organizationId/route' import { Route as AuthenticatedOrganizationOrganizationIdIndexRouteImport } from './routes/_authenticated/organization/$organizationId/index' import { Route as AuthenticatedOrganizationOrganizationIdOverviewRouteImport } from './routes/_authenticated/organization/$organizationId/overview' @@ -269,12 +268,6 @@ const AuthenticatedOnboardingPersonalizeRoute = path: '/onboarding/personalize', getParentRoute: () => AuthenticatedRoute, } as any) -const AuthenticatedOnboardingConfirmRoute = - AuthenticatedOnboardingConfirmRouteImport.update({ - id: '/onboarding/confirm', - path: '/onboarding/confirm', - getParentRoute: () => AuthenticatedRoute, - } as any) const AuthenticatedOrganizationOrganizationIdRouteRoute = AuthenticatedOrganizationOrganizationIdRouteRouteImport.update({ id: '/$organizationId', @@ -1739,7 +1732,6 @@ export interface FileRoutesByFullPath { '/login/auth0-callback': typeof LoginAuth0CallbackRoute '/login': typeof LoginIndexRoute '/organization/$organizationId': typeof AuthenticatedOrganizationOrganizationIdRouteRouteWithChildren - '/onboarding/confirm': typeof AuthenticatedOnboardingConfirmRoute '/onboarding/personalize': typeof AuthenticatedOnboardingPersonalizeRoute '/onboarding/plans': typeof AuthenticatedOnboardingPlansRoute '/onboarding/project': typeof AuthenticatedOnboardingProjectRoute @@ -1924,7 +1916,6 @@ export interface FileRoutesByTo { '/preview-code': typeof AuthenticatedPreviewCodeRoute '/login/auth0-callback': typeof LoginAuth0CallbackRoute '/login': typeof LoginIndexRoute - '/onboarding/confirm': typeof AuthenticatedOnboardingConfirmRoute '/onboarding/personalize': typeof AuthenticatedOnboardingPersonalizeRoute '/onboarding/plans': typeof AuthenticatedOnboardingPlansRoute '/onboarding/project': typeof AuthenticatedOnboardingProjectRoute @@ -2094,7 +2085,6 @@ export interface FileRoutesById { '/login/auth0-callback': typeof LoginAuth0CallbackRoute '/login/': typeof LoginIndexRoute '/_authenticated/organization/$organizationId': typeof AuthenticatedOrganizationOrganizationIdRouteRouteWithChildren - '/_authenticated/onboarding/confirm': typeof AuthenticatedOnboardingConfirmRoute '/_authenticated/onboarding/personalize': typeof AuthenticatedOnboardingPersonalizeRoute '/_authenticated/onboarding/plans': typeof AuthenticatedOnboardingPlansRoute '/_authenticated/onboarding/project': typeof AuthenticatedOnboardingProjectRoute @@ -2283,7 +2273,6 @@ export interface FileRouteTypes { | '/login/auth0-callback' | '/login' | '/organization/$organizationId' - | '/onboarding/confirm' | '/onboarding/personalize' | '/onboarding/plans' | '/onboarding/project' @@ -2468,7 +2457,6 @@ export interface FileRouteTypes { | '/preview-code' | '/login/auth0-callback' | '/login' - | '/onboarding/confirm' | '/onboarding/personalize' | '/onboarding/plans' | '/onboarding/project' @@ -2637,7 +2625,6 @@ export interface FileRouteTypes { | '/login/auth0-callback' | '/login/' | '/_authenticated/organization/$organizationId' - | '/_authenticated/onboarding/confirm' | '/_authenticated/onboarding/personalize' | '/_authenticated/onboarding/plans' | '/_authenticated/onboarding/project' @@ -2917,13 +2904,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOnboardingPersonalizeRouteImport parentRoute: typeof AuthenticatedRoute } - '/_authenticated/onboarding/confirm': { - id: '/_authenticated/onboarding/confirm' - path: '/onboarding/confirm' - fullPath: '/onboarding/confirm' - preLoaderRoute: typeof AuthenticatedOnboardingConfirmRouteImport - parentRoute: typeof AuthenticatedRoute - } '/_authenticated/organization/$organizationId': { id: '/_authenticated/organization/$organizationId' path: '/$organizationId' @@ -4917,7 +4897,6 @@ interface AuthenticatedRouteChildren { AuthenticatedOrganizationRouteRoute: typeof AuthenticatedOrganizationRouteRouteWithChildren AuthenticatedHelmDefaultValuesRoute: typeof AuthenticatedHelmDefaultValuesRoute AuthenticatedPreviewCodeRoute: typeof AuthenticatedPreviewCodeRoute - AuthenticatedOnboardingConfirmRoute: typeof AuthenticatedOnboardingConfirmRoute AuthenticatedOnboardingPersonalizeRoute: typeof AuthenticatedOnboardingPersonalizeRoute AuthenticatedOnboardingPlansRoute: typeof AuthenticatedOnboardingPlansRoute AuthenticatedOnboardingProjectRoute: typeof AuthenticatedOnboardingProjectRoute @@ -4930,7 +4909,6 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedOrganizationRouteRouteWithChildren, AuthenticatedHelmDefaultValuesRoute: AuthenticatedHelmDefaultValuesRoute, AuthenticatedPreviewCodeRoute: AuthenticatedPreviewCodeRoute, - AuthenticatedOnboardingConfirmRoute: AuthenticatedOnboardingConfirmRoute, AuthenticatedOnboardingPersonalizeRoute: AuthenticatedOnboardingPersonalizeRoute, AuthenticatedOnboardingPlansRoute: AuthenticatedOnboardingPlansRoute, diff --git a/apps/console/src/routes/_authenticated/onboarding/confirm.tsx b/apps/console/src/routes/_authenticated/onboarding/confirm.tsx deleted file mode 100644 index 744eaedd4c9..00000000000 --- a/apps/console/src/routes/_authenticated/onboarding/confirm.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useAuth0 } from '@auth0/auth0-react' -import { Navigate, createFileRoute } from '@tanstack/react-router' -import { StepConfirm } from '@qovery/domains/onboarding/feature' -import { useDocumentTitle } from '@qovery/shared/util-hooks' - -export const Route = createFileRoute('/_authenticated/onboarding/confirm')({ - component: Confirm, -}) - -function Confirm() { - useDocumentTitle('Onboarding Confirm - Qovery') - const { isAuthenticated } = useAuth0() - - if (!isAuthenticated) { - return - } - - return -} diff --git a/apps/console/src/routes/login/auth0-callback.tsx b/apps/console/src/routes/login/auth0-callback.tsx index 28840473bbb..40c3639d59d 100644 --- a/apps/console/src/routes/login/auth0-callback.tsx +++ b/apps/console/src/routes/login/auth0-callback.tsx @@ -2,13 +2,11 @@ import { useAuth0 } from '@auth0/auth0-react' import { Navigate, createFileRoute, useNavigate } from '@tanstack/react-router' import axios from 'axios' import { useEffect } from 'react' -import { getProviderKeyFromSub } from '@qovery/domains/onboarding/feature' import { useOrganizations } from '@qovery/domains/organizations/feature' import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' -import { PREVIOUS_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' -import { ONBOARDING_CONFIRM_URL, ONBOARDING_URL, getOnboardingEntryUrl } from '@qovery/shared/routes' +import { useAuth } from '@qovery/shared/auth' +import { getOnboardingEntryUrl } from '@qovery/shared/routes' import { LoadingScreen } from '@qovery/shared/ui' -import { useLocalStorage } from '@qovery/shared/util-hooks' import { QOVERY_API } from '@qovery/shared/util-node-env' import { useAuthInterceptor } from '@qovery/shared/utils' import { consumePendingReturnTo } from '../../auth/auth0' @@ -36,12 +34,11 @@ export const Route = createFileRoute('/login/auth0-callback')({ function useRedirectIfLogged(connection?: string) { const navigate = useNavigate() const { authLogin } = useAuth() - const { isAuthenticated, user } = useAuth0() + const { isAuthenticated } = useAuth0() const { data: organizations = [], isFetched: isFetchedOrganizations } = useOrganizations({ enabled: isAuthenticated, }) const { data: userSignUp, isFetched: isFetchedUserSignUp } = useUserSignUp({ enabled: isAuthenticated }) - const [previousUsedLogin] = useLocalStorage(PREVIOUS_USED_LOGIN_STORAGE_KEY, undefined) useEffect(() => { if (connection && !isAuthenticated) { @@ -76,14 +73,6 @@ function useRedirectIfLogged(connection?: string) { return } - // No organization yet: only pause on the confirm screen if this browser was - // previously used to sign in with a different provider than the one just used - const provider = getProviderKeyFromSub(user?.sub) - if (previousUsedLogin && previousUsedLogin !== provider) { - navigate({ href: `${ONBOARDING_URL}${ONBOARDING_CONFIRM_URL}` }) - return - } - navigate({ href: getOnboardingEntryUrl(userSignUp) }) } @@ -99,8 +88,6 @@ function useRedirectIfLogged(connection?: string) { isFetchedOrganizations, userSignUp, isFetchedUserSignUp, - previousUsedLogin, - user, ]) } diff --git a/apps/console/src/routes/login/index.tsx b/apps/console/src/routes/login/index.tsx index 9a16057cca6..a74a55ac112 100644 --- a/apps/console/src/routes/login/index.tsx +++ b/apps/console/src/routes/login/index.tsx @@ -4,7 +4,7 @@ import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useState } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' import { z } from 'zod' -import { AuthEnum, LAST_USED_LOGIN_STORAGE_KEY, PREVIOUS_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' +import { AuthEnum, LAST_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' import { IconEnum } from '@qovery/shared/enums' import { Badge, Button, Icon, InputTextSmall, Link } from '@qovery/shared/ui' import { useLocalStorage } from '@qovery/shared/util-hooks' @@ -192,7 +192,6 @@ function RouteComponent() { const { auth0Error, setAuth0Error } = useAuth0Error() const [loading, setLoading] = useState<{ provider: string; active: boolean } | undefined>() const [lastUsedLogin, setLastUsedLogin] = useLocalStorage(LAST_USED_LOGIN_STORAGE_KEY, undefined) - const [, setPreviousUsedLogin] = useLocalStorage(PREVIOUS_USED_LOGIN_STORAGE_KEY, undefined) const [lastUsedLoginAtPageLoad] = useState(lastUsedLogin) const [testimonialIndex, setTestimonialIndex] = useState(0) @@ -235,11 +234,6 @@ function RouteComponent() { active: true, }) - // Stash the provider used before this click, so the onboarding confirm - // screen can tell a genuine first-time signup from a provider switch. - if (lastUsedLogin) { - setPreviousUsedLogin(lastUsedLogin) - } setLastUsedLogin(lastUsedProvider) try { diff --git a/libs/domains/onboarding/feature/src/index.ts b/libs/domains/onboarding/feature/src/index.ts index 9c543e5923e..9e47ba614c9 100644 --- a/libs/domains/onboarding/feature/src/index.ts +++ b/libs/domains/onboarding/feature/src/index.ts @@ -4,6 +4,5 @@ export * from './lib/hooks/use-invite-member/use-invite-member' export * from './lib/step-personalize/step-personalize' export * from './lib/step-personalize/personal-email-domains' export * from './lib/step-use-cases/step-use-cases' -export * from './lib/step-confirm/step-confirm' export * from './lib/onboarding-project/onboarding-project' export * from './lib/onboarding-plans/onboarding-plans' diff --git a/libs/domains/onboarding/feature/src/lib/container/container.tsx b/libs/domains/onboarding/feature/src/lib/container/container.tsx index 6fade1c0d01..bfcd2220fdc 100644 --- a/libs/domains/onboarding/feature/src/lib/container/container.tsx +++ b/libs/domains/onboarding/feature/src/lib/container/container.tsx @@ -47,7 +47,7 @@ export function Container(props: PropsWithChildren) { const titlesPerRoute = [ { path: '/personalize', - title: 'Just a few questions', + title: 'Create a new organization', }, { path: '/use-cases', diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx deleted file mode 100644 index 05bc9032aaf..00000000000 --- a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.spec.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { type ReactNode } from 'react' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import StepConfirm from './step-confirm' - -const mockedUsedNavigate = jest.fn() -const mockedAuthLogout = jest.fn() - -jest.mock('@tanstack/react-router', () => ({ - ...jest.requireActual('@tanstack/react-router'), - useNavigate: () => mockedUsedNavigate, -})) - -jest.mock('@auth0/auth0-react', () => ({ - ...jest.requireActual('@auth0/auth0-react'), - Auth0Provider: ({ children }: { children: ReactNode }) => children, - useAuth0: () => ({ - user: { sub: 'google-oauth2|123', email: 'jdan@qovery.com' }, - }), -})) - -jest.mock('@qovery/shared/auth', () => ({ - ...jest.requireActual('@qovery/shared/auth'), - useAuth: () => ({ authLogout: mockedAuthLogout }), -})) - -jest.mock('@qovery/domains/users-sign-up/feature', () => ({ - ...jest.requireActual('@qovery/domains/users-sign-up/feature'), - useUserSignUp: () => ({ data: undefined }), -})) - -jest.mock('@qovery/shared/util-hooks', () => ({ - ...jest.requireActual('@qovery/shared/util-hooks'), - useLocalStorage: jest.fn(), -})) - -const { useLocalStorage } = jest.requireMock('@qovery/shared/util-hooks') as { useLocalStorage: jest.Mock } - -describe('StepConfirm', () => { - beforeEach(() => { - mockedUsedNavigate.mockClear() - mockedAuthLogout.mockClear() - useLocalStorage.mockReturnValue([undefined, jest.fn()]) - }) - - it('should render successfully', () => { - const { baseElement } = renderWithProviders() - expect(baseElement).toBeTruthy() - }) - - it('should show the provider and email just used to sign in', () => { - renderWithProviders() - - expect(screen.getByText('Signed in with Google')).toBeInTheDocument() - expect(screen.getByText('jdan@qovery.com')).toBeInTheDocument() - }) - - it('should not show the previous provider hint when there is none', () => { - renderWithProviders() - - expect(screen.queryByText(/Last time on this browser/)).not.toBeInTheDocument() - }) - - it('should show the previous provider hint when it differs from the current one', () => { - useLocalStorage.mockReturnValue(['github', jest.fn()]) - - renderWithProviders() - - expect(screen.getByText('Last time on this browser, you signed in with GitHub.')).toBeInTheDocument() - }) - - it('should navigate into the onboarding funnel when continuing', async () => { - const { userEvent } = renderWithProviders() - - await userEvent.click(screen.getByRole('button', { name: 'Create my organization' })) - - expect(mockedUsedNavigate).toHaveBeenCalledWith({ href: '/onboarding/personalize' }) - }) - - it('should log the user out when switching account', async () => { - const { userEvent } = renderWithProviders() - - await userEvent.click(screen.getByRole('button', { name: 'Switch account' })) - - expect(mockedAuthLogout).toHaveBeenCalled() - }) -}) diff --git a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx b/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx deleted file mode 100644 index 97414636f53..00000000000 --- a/libs/domains/onboarding/feature/src/lib/step-confirm/step-confirm.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useAuth0 } from '@auth0/auth0-react' -import { useNavigate } from '@tanstack/react-router' -import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' -import { PREVIOUS_USED_LOGIN_STORAGE_KEY, useAuth } from '@qovery/shared/auth' -import { IconEnum } from '@qovery/shared/enums' -import { getOnboardingEntryUrl } from '@qovery/shared/routes' -import { Button, Heading, Icon, Section } from '@qovery/shared/ui' -import { useLocalStorage } from '@qovery/shared/util-hooks' - -export interface ProviderDisplay { - label: string - icon: IconEnum -} - -const PROVIDER_DISPLAY_BY_KEY: Record = { - github: { label: 'GitHub', icon: IconEnum.GITHUB }, - Gitlab: { label: 'GitLab', icon: IconEnum.GITLAB }, - 'google-oauth2': { label: 'Google', icon: IconEnum.GOOGLE }, - windowslive: { label: 'Microsoft', icon: IconEnum.MICROSOFT }, - bitbucket: { label: 'Bitbucket', icon: IconEnum.BITBUCKET }, -} - -export function getProviderKeyFromSub(sub?: string | null): string | undefined { - return sub?.split('|')[0] -} - -export function getProviderDisplay(providerKey?: string | null): ProviderDisplay | undefined { - if (!providerKey) return undefined - return PROVIDER_DISPLAY_BY_KEY[providerKey] -} - -export function StepConfirm() { - const navigate = useNavigate() - const { user } = useAuth0() - const { authLogout } = useAuth() - const { data: userSignUp } = useUserSignUp() - const [previousUsedLogin] = useLocalStorage(PREVIOUS_USED_LOGIN_STORAGE_KEY, undefined) - - const provider = getProviderDisplay(getProviderKeyFromSub(user?.sub)) - const previousProvider = getProviderDisplay(previousUsedLogin) - const showPreviousProviderHint = previousProvider && previousProvider.label !== provider?.label - - return ( -
-
-
- Qovery logo - - - Already have a Qovery account? - -

- You're about to create a brand new organization. If you already have a Qovery account, sign in with the - right provider instead. -

- - {provider && ( -
- -
- Signed in with {provider.label} - {user?.email && {user.email}} -
-
- )} - - {showPreviousProviderHint && ( -

- Last time on this browser, you signed in with {previousProvider?.label}. -

- )} - -
- - -
- -

You can invite your team later.

-
-
-
- ) -} - -export default StepConfirm diff --git a/libs/domains/onboarding/feature/src/lib/step-personalize/step-personalize.tsx b/libs/domains/onboarding/feature/src/lib/step-personalize/step-personalize.tsx index e3b3a306b35..f158ff4c172 100644 --- a/libs/domains/onboarding/feature/src/lib/step-personalize/step-personalize.tsx +++ b/libs/domains/onboarding/feature/src/lib/step-personalize/step-personalize.tsx @@ -12,8 +12,11 @@ export function StepPersonalize({ onSubmit, authLogout }: StepPersonalizeProps) return (
-

To tailor your experience

-

We need some information to proceed with your account creation.

+

Set up your new organization

+

+ You're starting a brand-new Qovery organization. If your team already has one, you need an invitation from them + instead. +

)} /> -
+

+ Already part of an organization on Qovery?{' '} + {' '} + if you signed up with a different provider last time. +

+