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

@@ -55,12 +55,18 @@ interface LinkAccountsListProps {
showEmailOption?: boolean;
enabled?: boolean;
redirectTo?: string;
onPasswordSet?: () => void;
onProviderLinked?: () => void;
}
export function LinkAccountsList(props: LinkAccountsListProps) {
const unlinkMutation = useUnlinkUserIdentity();
const linkMutation = useLinkIdentityWithProvider();
const pathname = usePathname();
const user = useUser();
const linkMutation = useLinkIdentityWithProvider({
redirectToPath: props.redirectTo,
});
const {
identities,
@@ -81,7 +87,6 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
? props.providers.filter((provider) => !isProviderConnected(provider))
: [];
const user = useUser();
const amr = user.data ? user.data.amr : [];
const isConnectedWithPassword = amr.some(
@@ -118,7 +123,10 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
* @param provider
*/
const handleLinkAccount = (provider: Provider) => {
const promise = linkMutation.mutateAsync(provider);
const promise = linkMutation.mutateAsync(provider).then((result) => {
props.onProviderLinked?.();
return result;
});
toast.promise(promise, {
loading: <Trans i18nKey={'account:linkingAccount'} />,
@@ -252,6 +260,7 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
<UpdatePasswordDialog
userEmail={userEmail}
redirectTo={props.redirectTo || '/home'}
onPasswordSet={props.onPasswordSet}
/>
</If>
@@ -358,12 +367,13 @@ function UpdateEmailDialog(props: { redirectTo: string }) {
function UpdatePasswordDialog(props: {
redirectTo: string;
userEmail: string;
onPasswordSet?: () => void;
}) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<DialogTrigger asChild data-test="open-password-dialog-trigger">
<Item variant="outline" role="button" className="hover:bg-muted/50">
<ItemMedia>
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
@@ -406,6 +416,7 @@ function UpdatePasswordDialog(props: {
email={props.userEmail}
onSuccess={() => {
setOpen(false);
props.onPasswordSet?.();
}}
/>
</Suspense>

View File

@@ -98,7 +98,7 @@ export const UpdatePasswordForm = ({
return (
<Form {...form}>
<form
data-test={'account-password-form'}
data-test="identity-form"
onSubmit={form.handleSubmit(updatePasswordCallback)}
>
<div className={'flex flex-col space-y-4'}>
@@ -178,7 +178,10 @@ export const UpdatePasswordForm = ({
</div>
<div>
<Button disabled={updateUserMutation.isPending}>
<Button
disabled={updateUserMutation.isPending}
data-test="identity-form-submit"
>
<Trans i18nKey={'account:updatePasswordSubmitLabel'} />
</Button>
</div>

View File

@@ -1,2 +1,2 @@
export * from './captcha-field';
export * from './use-captcha';
export * from './use-captcha';

View File

@@ -151,6 +151,21 @@ class AccountInvitationsDispatchService {
return url.href;
}
/**
* @name getAcceptInvitationLink
* @description Generates an internal link that validates invitation and generates auth token on-demand.
* This solves the 24-hour Supabase auth token expiry issue by generating fresh tokens when clicked.
* @param token - The invitation token to use
*/
getAcceptInvitationLink(token: string) {
const siteUrl = env.siteURL;
const url = new URL('/join/accept', siteUrl);
url.searchParams.set('invite_token', token);
return url.href;
}
/**
* @name sendEmail
* @description Sends an invitation email to the invited user

View File

@@ -7,7 +7,6 @@ import { z } from 'zod';
import { getLogger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import type { DeleteInvitationSchema } from '../../schema/delete-invitation.schema';
import type { InviteMembersSchema } from '../../schema/invite-members.schema';
@@ -344,71 +343,13 @@ class AccountInvitationsService {
}
const logger = await getLogger();
const adminClient = getSupabaseServerAdminClient();
const service = createAccountInvitationsDispatchService(this.client);
const results = await Promise.allSettled(
invitations.map(async (invitation) => {
const joinTeamLink = service.getInvitationLink(invitation.invite_token);
const authCallbackUrl = service.getAuthCallbackUrl(joinTeamLink);
const getEmailLinkType = async () => {
const user = await adminClient
.from('accounts')
.select('*')
.eq('email', invitation.email)
.single();
// if the user is not found, return the invite type
// this link allows the user to register to the platform
if (user.error || !user.data) {
return 'invite';
}
// if the user is found, return the email link type to sign in
return 'magiclink';
};
const emailLinkType = await getEmailLinkType();
// generate an invitation link with Supabase admin client
// use the "redirectTo" parameter to redirect the user to the invitation page after the link is clicked
const generateLinkResponse = await adminClient.auth.admin.generateLink({
email: invitation.email,
type: emailLinkType,
});
// if the link generation fails, throw an error
if (generateLinkResponse.error) {
logger.error(
{
...ctx,
error: generateLinkResponse.error,
},
'Failed to generate link',
);
throw generateLinkResponse.error;
}
// get the link from the response
const verifyLink = generateLinkResponse.data.properties?.action_link;
// extract token
const token = new URL(verifyLink).searchParams.get('token');
if (!token) {
// return error
throw new Error(
'Token in verify link from Supabase Auth was not found',
);
}
// add search params to be consumed by /auth/confirm route
authCallbackUrl.searchParams.set('token_hash', token);
authCallbackUrl.searchParams.set('type', emailLinkType);
const link = authCallbackUrl.href;
// Generate internal link that will validate and generate auth token on-demand
// This solves the 24-hour auth token expiry issue
const link = service.getAcceptInvitationLink(invitation.invite_token);
// send the invitation email
const data = await service.sendInvitationEmail({

View File

@@ -75,7 +75,7 @@ export class PromptsManager {
- Proper error handling with try/catch and typed error objects
- Clean, clear, well-designed code without obvious comments
**React & Next.js 15 Best Practices:**
**React & Next.js 16 Best Practices:**
- Functional components only with 'use client' directive for client components
- Encapsulate repeated blocks of code into reusable local components
- Avoid useEffect (code smell) - justify if absolutely necessary