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:
committed by
GitHub
parent
ae404d8366
commit
fa2fa9a15c
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './captcha-field';
|
||||
export * from './use-captcha';
|
||||
export * from './use-captcha';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user