Refactor join page and update packages

The join page's redirect functionality has been replaced with permanentRedirect. Adjustments were also made for handling conditional authentication and user account verification. Package upgrades were performed and the i18n client now checks if an instance is already initialized. The process tends to deliver performance benefits and enhancing the code readability.
This commit is contained in:
giancarlo
2024-04-14 13:48:50 +08:00
parent fd890ea09d
commit 91bee99857
13 changed files with 446 additions and 422 deletions

View File

@@ -48,8 +48,10 @@ export class AuthPageObject {
await this.page.click('button[type="submit"]'); await this.page.click('button[type="submit"]');
} }
async visitConfirmEmailLink(email: string, params?: { async visitConfirmEmailLink(email: string, params: {
deleteAfter: boolean deleteAfter: boolean
} = {
deleteAfter: true
}) { }) {
return expect(async() => { return expect(async() => {
const res = await this.mailbox.visitMailbox(email, params); const res = await this.mailbox.visitMailbox(email, params);

View File

@@ -36,9 +36,9 @@ test.describe('Invitations', () => {
// sign out and sign in with the first email // sign out and sign in with the first email
await invitations.auth.signOut(); await invitations.auth.signOut();
await invitations.auth.visitConfirmEmailLink(invites[0]!.email, { await invitations.auth.visitConfirmEmailLink(invites[0]!.email);
deleteAfter: true
}); console.log(`Signing up with ${firstEmail}`);
await invitations.auth.signUp({ await invitations.auth.signUp({
email: firstEmail, email: firstEmail,
@@ -48,6 +48,8 @@ test.describe('Invitations', () => {
await invitations.auth.visitConfirmEmailLink(firstEmail); await invitations.auth.visitConfirmEmailLink(firstEmail);
console.log(`Accepting invitation as ${firstEmail}`);
await invitations.acceptInvitation(); await invitations.acceptInvitation();
await invitations.teamAccounts.openAccountsSelector(); await invitations.teamAccounts.openAccountsSelector();

View File

@@ -1,5 +1,3 @@
import { GlobalLoader } from '@kit/ui/global-loader'; import { GlobalLoader } from '@kit/ui/global-loader';
import { withI18n } from '~/lib/i18n/with-i18n'; export default GlobalLoader;
export default withI18n(GlobalLoader);

View File

@@ -24,6 +24,11 @@ interface Props {
}; };
} }
const paths = {
callback: pathsConfig.auth.callback,
appHome: pathsConfig.app.home,
};
function SignUpPage({ searchParams }: Props) { function SignUpPage({ searchParams }: Props) {
const inviteToken = searchParams.invite_token; const inviteToken = searchParams.invite_token;
@@ -36,10 +41,7 @@ function SignUpPage({ searchParams }: Props) {
<SignUpMethodsContainer <SignUpMethodsContainer
providers={authConfig.providers} providers={authConfig.providers}
inviteToken={inviteToken} inviteToken={inviteToken}
paths={{ paths={paths}
callback: pathsConfig.auth.callback,
appHome: pathsConfig.app.home,
}}
/> />
<div className={'justify-centers flex'}> <div className={'justify-centers flex'}>

View File

@@ -1,5 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { notFound, redirect } from 'next/navigation'; import { notFound, permanentRedirect } from 'next/navigation';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
@@ -18,7 +18,7 @@ import { JoinTeamService } from './_lib/server/join-team.service';
interface Context { interface Context {
searchParams: { searchParams: {
invite_token: string; invite_token?: string;
}; };
} }
@@ -45,7 +45,9 @@ async function JoinTeamAccountPage({ searchParams }: Context) {
// redirect to the sign up page with the invite token // redirect to the sign up page with the invite token
// so that they will get back to this page after signing up // so that they will get back to this page after signing up
if (auth.error ?? !auth.data) { if (auth.error ?? !auth.data) {
redirect(pathsConfig.auth.signUp + '?invite_token=' + token); const path = `${pathsConfig.auth.signUp}?invite_token=${token}`;
permanentRedirect(path);
} }
const service = new JoinTeamService(); const service = new JoinTeamService();
@@ -53,16 +55,16 @@ async function JoinTeamAccountPage({ searchParams }: Context) {
// the user is logged in, we can now check if the token is valid // the user is logged in, we can now check if the token is valid
const invitation = await service.getInviteDataFromInviteToken(token); const invitation = await service.getInviteDataFromInviteToken(token);
// the invitation is not found or expired
if (!invitation) { if (!invitation) {
return <InviteNotFoundOrExpired />; return <InviteNotFoundOrExpired />;
} }
// we need to verify the user isn't already in the account // we need to verify the user isn't already in the account
const isInAccount = await service.isCurrentUserAlreadyInAccount( const isSignedInUserPartOfAccount =
invitation.account.id, await service.isCurrentUserAlreadyInAccount(invitation.account.id);
);
if (isInAccount) { if (isSignedInUserPartOfAccount) {
const { getLogger } = await import('@kit/shared/logger'); const { getLogger } = await import('@kit/shared/logger');
const logger = await getLogger(); const logger = await getLogger();
@@ -76,12 +78,12 @@ async function JoinTeamAccountPage({ searchParams }: Context) {
); );
// if the user is already in the account redirect to the home page // if the user is already in the account redirect to the home page
redirect(pathsConfig.app.home); permanentRedirect(pathsConfig.app.home);
} }
// if the user decides to sign in with a different account // if the user decides to sign in with a different account
// we redirect them to the sign in page with the invite token // we redirect them to the sign in page with the invite token
const signOutNext = pathsConfig.auth.signIn + '?invite_token=' + token; const signOutNext = `${pathsConfig.auth.signIn}?invite_token=${token}`;
// once the user accepts the invitation, we redirect them to the account home page // once the user accepts the invitation, we redirect them to the account home page
const accountHome = pathsConfig.app.accountHome.replace( const accountHome = pathsConfig.app.accountHome.replace(

View File

@@ -1,3 +0,0 @@
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;

View File

@@ -39,7 +39,7 @@
"@manypkg/cli": "^0.21.3", "@manypkg/cli": "^0.21.3",
"@turbo/gen": "^1.13.2", "@turbo/gen": "^1.13.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"pnpm": "^8.15.6", "pnpm": "^8.15.7",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"turbo": "^1.13.2", "turbo": "^1.13.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",

View File

@@ -3,6 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import resourcesToBackend from 'i18next-resources-to-backend'; import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
let clientInstance: i18n | null = null;
/** /**
* Initialize the i18n instance on the client. * Initialize the i18n instance on the client.
* @param settings - the i18n settings * @param settings - the i18n settings
@@ -12,6 +14,10 @@ export function initializeI18nClient(
settings: InitOptions, settings: InitOptions,
resolver: (lang: string, namespace: string) => Promise<object>, resolver: (lang: string, namespace: string) => Promise<object>,
): Promise<i18n> { ): Promise<i18n> {
if (clientInstance?.isInitialized) {
return Promise.resolve(clientInstance);
}
return new Promise<i18n>((resolve, reject) => { return new Promise<i18n>((resolve, reject) => {
void i18next void i18next
.use(initReactI18next) .use(initReactI18next)
@@ -40,7 +46,9 @@ export function initializeI18nClient(
return reject(err); return reject(err);
} }
resolve(i18next); clientInstance = i18next;
resolve(clientInstance);
}, },
); );
}); });

View File

@@ -14,6 +14,10 @@ export async function initializeServerI18n(
) { ) {
const i18nInstance = createInstance(); const i18nInstance = createInstance();
if (i18nInstance.isInitialized) {
return i18nInstance;
}
await i18nInstance await i18nInstance
.use(initReactI18next) .use(initReactI18next)
.use( .use(

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import { If } from './if';
import { LoadingOverlay } from './loading-overlay'; import { LoadingOverlay } from './loading-overlay';
import { TopLoadingBarIndicator } from './top-loading-bar-indicator'; import { TopLoadingBarIndicator } from './top-loading-bar-indicator';
import { Trans } from './trans'; import { Trans } from './trans';
@@ -8,21 +9,29 @@ export function GlobalLoader({
children, children,
displayLogo = false, displayLogo = false,
fullPage = false, fullPage = false,
displaySpinner = true,
displayTopLoadingBar = true,
}: React.PropsWithChildren<{ }: React.PropsWithChildren<{
displayLogo?: boolean; displayLogo?: boolean;
fullPage?: boolean; fullPage?: boolean;
displaySpinner?: boolean;
displayTopLoadingBar?: boolean;
}>) { }>) {
const Text = children ?? <Trans i18nKey={'common:loading'} />; const Text = children ?? <Trans i18nKey={'common:loading'} />;
return ( return (
<> <>
<If condition={displayTopLoadingBar}>
<TopLoadingBarIndicator /> <TopLoadingBarIndicator />
</If>
<If condition={displaySpinner}>
<div className={'flex flex-1 flex-col items-center justify-center py-48'}> <div className={'flex flex-1 flex-col items-center justify-center py-48'}>
<LoadingOverlay displayLogo={displayLogo} fullPage={fullPage}> <LoadingOverlay displayLogo={displayLogo} fullPage={fullPage}>
{Text} {Text}
</LoadingOverlay> </LoadingOverlay>
</div> </div>
</If>
</> </>
); );
} }

View File

@@ -1,6 +1,6 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { cn } from '../utils/cn'; import { cn } from '../utils';
import Spinner from './spinner'; import Spinner from './spinner';
export function LoadingOverlay({ export function LoadingOverlay({

780
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff