Revert "Unify workspace dropdowns; Update layouts (#458)"
This reverts commit 4bc8448a1d.
This commit is contained in:
156
apps/web/app/identities/_components/identities-step-wrapper.tsx
Normal file
156
apps/web/app/identities/_components/identities-step-wrapper.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { LinkAccountsList } from '@kit/accounts/personal-account-settings';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
import { useUserIdentities } from '@kit/supabase/hooks/use-user-identities';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
interface IdentitiesStepWrapperProps {
|
||||
nextPath: string;
|
||||
showPasswordOption: boolean;
|
||||
showEmailOption: boolean;
|
||||
enableIdentityLinking: boolean;
|
||||
oAuthProviders: Provider[];
|
||||
requiresConfirmation: boolean;
|
||||
}
|
||||
|
||||
export function IdentitiesStepWrapper(props: IdentitiesStepWrapperProps) {
|
||||
const user = useUser();
|
||||
const { identities } = useUserIdentities();
|
||||
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [hasSetPassword, setHasSetPassword] = useState(false);
|
||||
const [hasLinkedProvider, setHasLinkedProvider] = useState(false);
|
||||
|
||||
const initialCountRef = useRef<number | null>(null);
|
||||
const initialHasPasswordRef = useRef<boolean | null>(null);
|
||||
|
||||
// Capture initial state once when data becomes available
|
||||
// Using refs to avoid re-renders and useEffect to avoid accessing refs during render
|
||||
useEffect(() => {
|
||||
if (initialCountRef.current === null && identities.length > 0) {
|
||||
const nonEmailIdentities = identities.filter(
|
||||
(identity) => identity.provider !== 'email',
|
||||
);
|
||||
|
||||
initialCountRef.current = nonEmailIdentities.length;
|
||||
}
|
||||
}, [identities]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialHasPasswordRef.current === null && user.data) {
|
||||
const amr = user.data.amr || [];
|
||||
|
||||
const hasPassword = amr.some(
|
||||
(item: { method: string }) => item.method === 'password',
|
||||
);
|
||||
|
||||
initialHasPasswordRef.current = hasPassword;
|
||||
}
|
||||
}, [user.data]);
|
||||
|
||||
const handleContinueClick = (e: React.MouseEvent) => {
|
||||
// Only show confirmation if password or oauth is enabled (requiresConfirmation)
|
||||
if (!props.requiresConfirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNonEmailIdentities = identities.filter(
|
||||
(identity) => identity.provider !== 'email',
|
||||
);
|
||||
|
||||
const hasAddedNewIdentity =
|
||||
currentNonEmailIdentities.length > (initialCountRef.current ?? 0);
|
||||
|
||||
// Check if password was added
|
||||
const amr = user.data?.amr || [];
|
||||
|
||||
const currentHasPassword = amr.some(
|
||||
(item: { method: string }) => item.method === 'password',
|
||||
);
|
||||
|
||||
const hasAddedPassword =
|
||||
currentHasPassword && !initialHasPasswordRef.current;
|
||||
|
||||
// If no new identity was added AND no password was set AND no provider linked, show confirmation dialog
|
||||
if (
|
||||
!hasAddedNewIdentity &&
|
||||
!hasAddedPassword &&
|
||||
!hasSetPassword &&
|
||||
!hasLinkedProvider
|
||||
) {
|
||||
e.preventDefault();
|
||||
setShowConfirmDialog(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
'animate-in fade-in slide-in-from-bottom-4 mx-auto flex w-full max-w-md flex-col space-y-4 duration-500'
|
||||
}
|
||||
data-test="join-step-two"
|
||||
>
|
||||
<LinkAccountsList
|
||||
providers={props.oAuthProviders}
|
||||
showPasswordOption={props.showPasswordOption}
|
||||
showEmailOption={props.showEmailOption}
|
||||
redirectTo={props.nextPath}
|
||||
enabled={props.enableIdentityLinking}
|
||||
onPasswordSet={() => setHasSetPassword(true)}
|
||||
onProviderLinked={() => setHasLinkedProvider(true)}
|
||||
/>
|
||||
|
||||
<Button asChild data-test="continue-button">
|
||||
<Link href={props.nextPath} onClick={handleContinueClick}>
|
||||
<Trans i18nKey={'common:continueKey'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<AlertDialogContent data-test="no-auth-method-dialog">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle data-test="no-auth-dialog-title">
|
||||
<Trans i18nKey={'auth:noIdentityLinkedTitle'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription data-test="no-auth-dialog-description">
|
||||
<Trans i18nKey={'auth:noIdentityLinkedDescription'} />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel data-test="no-auth-dialog-cancel">
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction asChild data-test="no-auth-dialog-continue">
|
||||
<Link href={props.nextPath}>
|
||||
<Trans i18nKey={'common:continueKey'} />
|
||||
</Link>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
126
apps/web/app/identities/page.tsx
Normal file
126
apps/web/app/identities/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
import { getSafeRedirectPath } from '@kit/shared/utils';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { IdentitiesStepWrapper } from './_components/identities-step-wrapper';
|
||||
|
||||
export const meta = async (): Promise<Metadata> => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: i18n.t('auth:setupAccount'),
|
||||
};
|
||||
};
|
||||
|
||||
type IdentitiesPageProps = {
|
||||
searchParams: Promise<{ next?: string }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @name IdentitiesPage
|
||||
* @description Displays linked accounts and available authentication methods.
|
||||
*/
|
||||
async function IdentitiesPage(props: IdentitiesPageProps) {
|
||||
const {
|
||||
nextPath,
|
||||
showPasswordOption,
|
||||
showEmailOption,
|
||||
oAuthProviders,
|
||||
enableIdentityLinking,
|
||||
requiresConfirmation,
|
||||
} = await fetchData(props);
|
||||
|
||||
return (
|
||||
<AuthLayoutShell
|
||||
Logo={AppLogo}
|
||||
contentClassName="max-w-md overflow-y-hidden"
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex max-h-[70vh] w-full flex-col items-center space-y-6 overflow-y-auto'
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-col items-center gap-1'}>
|
||||
<Heading
|
||||
level={4}
|
||||
className="text-center"
|
||||
data-test="identities-page-heading"
|
||||
>
|
||||
<Trans i18nKey={'auth:linkAccountToSignIn'} />
|
||||
</Heading>
|
||||
|
||||
<Heading
|
||||
level={6}
|
||||
className={'text-muted-foreground text-center text-sm'}
|
||||
data-test="identities-page-description"
|
||||
>
|
||||
<Trans i18nKey={'auth:linkAccountToSignInDescription'} />
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<IdentitiesStepWrapper
|
||||
nextPath={nextPath}
|
||||
showPasswordOption={showPasswordOption}
|
||||
showEmailOption={showEmailOption}
|
||||
oAuthProviders={oAuthProviders}
|
||||
enableIdentityLinking={enableIdentityLinking}
|
||||
requiresConfirmation={requiresConfirmation}
|
||||
/>
|
||||
</div>
|
||||
</AuthLayoutShell>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(IdentitiesPage);
|
||||
|
||||
async function fetchData(props: IdentitiesPageProps) {
|
||||
const searchParams = await props.searchParams;
|
||||
const client = getSupabaseServerClient();
|
||||
const auth = await requireUser(client);
|
||||
|
||||
// If not authenticated, redirect to sign in
|
||||
if (!auth.data) {
|
||||
throw redirect(pathsConfig.auth.signIn);
|
||||
}
|
||||
|
||||
// Get the next path from URL params (where to redirect after setup)
|
||||
const nextPath = getSafeRedirectPath(searchParams.next, pathsConfig.app.home);
|
||||
|
||||
// Available auth methods to add
|
||||
const showPasswordOption = authConfig.providers.password;
|
||||
|
||||
// Show email option if password, magic link, or OTP is enabled
|
||||
const showEmailOption =
|
||||
authConfig.providers.password ||
|
||||
authConfig.providers.magicLink ||
|
||||
authConfig.providers.otp;
|
||||
|
||||
const oAuthProviders = authConfig.providers.oAuth;
|
||||
const enableIdentityLinking = authConfig.enableIdentityLinking;
|
||||
|
||||
// Only require confirmation if password or oauth providers are enabled
|
||||
const requiresConfirmation =
|
||||
authConfig.providers.password || oAuthProviders.length > 0;
|
||||
|
||||
return {
|
||||
nextPath,
|
||||
showPasswordOption,
|
||||
showEmailOption,
|
||||
oAuthProviders,
|
||||
enableIdentityLinking,
|
||||
requiresConfirmation,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user