chore: improve invitation flow, update project dependencies and documentation for Next.js 16 (#408)

* chore: update project dependencies and documentation for Next.js 16

- Upgraded Next.js from version 15 to 16 across various documentation files and components.
- Updated references to Next.js 16 in AGENTS.md and CLAUDE.md for consistency.
- Incremented application version to 2.21.0 in package.json.
- Refactored identity setup components to improve user experience and added confirmation dialogs for authentication methods.
- Enhanced invitation flow with new logic for handling user redirection and token generation.

* refactor: streamline invitation flow in e2e tests

- Simplified the invitation flow test by using a predefined email instead of generating a random one.
- Removed unnecessary steps such as clearing cookies and reloading the page before user sign-up.
- Enhanced clarity by eliminating commented-out code related to identity verification and user membership checks.

* refactor: improve code readability in IdentitiesPage and UpdatePasswordForm components

- Enhanced formatting of JSX elements in IdentitiesPage and UpdatePasswordForm for better readability.
- Adjusted indentation and line breaks to maintain consistent coding style across components.

* refactor: enhance LinkAccountsList component with user redirection logic

- Updated the LinkAccountsList component to include a redirectToPath option in the useLinkIdentityWithProvider hook for improved user experience.
- Removed redundant user hook declaration to streamline the code structure.

* refactor: update account setup logic in JoinTeamAccountPage

- Introduced a check for email-only authentication support to streamline account setup requirements.
- Adjusted the conditions for determining if a new account should set up additional authentication methods, enhancing user experience for new users.
This commit is contained in:
Giancarlo Buomprisco
2025-11-05 11:39:08 +07:00
committed by GitHub
parent ae404d8366
commit fa2fa9a15c
23 changed files with 1005 additions and 154 deletions

View 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>
</>
);
}

View File

@@ -1,15 +1,10 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import type { Provider } from '@supabase/supabase-js';
import { LinkAccountsList } from '@kit/accounts/personal-account-settings';
import { AuthLayoutShell } from '@kit/auth/shared';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
@@ -19,6 +14,8 @@ 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();
@@ -42,6 +39,7 @@ async function IdentitiesPage(props: IdentitiesPageProps) {
showEmailOption,
oAuthProviders,
enableIdentityLinking,
requiresConfirmation,
} = await fetchData(props);
return (
@@ -55,24 +53,30 @@ async function IdentitiesPage(props: IdentitiesPageProps) {
}
>
<div className={'flex flex-col items-center gap-1'}>
<Heading level={4} className="text-center">
<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>
<IdentitiesStep
<IdentitiesStepWrapper
nextPath={nextPath}
showPasswordOption={showPasswordOption}
showEmailOption={showEmailOption}
oAuthProviders={oAuthProviders}
enableIdentityLinking={enableIdentityLinking}
requiresConfirmation={requiresConfirmation}
/>
</div>
</AuthLayoutShell>
@@ -81,42 +85,6 @@ async function IdentitiesPage(props: IdentitiesPageProps) {
export default withI18n(IdentitiesPage);
/**
* @name IdentitiesStep
* @description Displays linked accounts and available authentication methods.
* LinkAccountsList component handles all authentication options including OAuth and Email/Password.
*/
function IdentitiesStep(props: {
nextPath: string;
showPasswordOption: boolean;
showEmailOption: boolean;
enableIdentityLinking: boolean;
oAuthProviders: Provider[];
}) {
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}
/>
<Button asChild data-test="skip-identities-button">
<Link href={props.nextPath}>
<Trans i18nKey={'common:continueKey'} />
</Link>
</Button>
</div>
);
}
async function fetchData(props: IdentitiesPageProps) {
const searchParams = await props.searchParams;
const client = getSupabaseServerClient();
@@ -142,11 +110,16 @@ async function fetchData(props: IdentitiesPageProps) {
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,
};
}