Skip to content

Commit 38b0cd0

Browse files
committed
refactor: update per review comments
1 parent 5fd8e44 commit 38b0cd0

File tree

20 files changed

+130
-66
lines changed

20 files changed

+130
-66
lines changed

packages/experience/src/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ const App = () => {
6262
element={<SocialSignInWebCallback />}
6363
/>
6464
<Route path="direct/:method/:target?" element={<DirectSignIn />} />
65-
<Route path="token/:token" element={<OneTimeToken />} />
6665
<Route element={<AppLayout />}>
66+
<Route path="one-time-token/:token" element={<OneTimeToken />} />
6767
<Route
6868
path="unknown-session"
6969
element={<ErrorPage message="error.invalid_session" />}
@@ -154,7 +154,7 @@ const App = () => {
154154
path={experience.routes.resetPassword}
155155
element={<ResetPasswordLanding />}
156156
/>
157-
<Route path="*" element={<ErrorPage title="description.not_found" />} />
157+
<Route path="*" element={<ErrorPage />} />
158158
</Route>
159159
</Route>
160160
</Routes>

packages/experience/src/apis/experience/one-time-token.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import api from '../api';
55
import { experienceApiRoutes, type VerificationResponse } from './const';
66
import { initInteraction } from './interaction';
77

8-
export const verifyOneTimeToken = async (payload: OneTimeTokenVerificationVerifyPayload) => {
8+
export const registerWithOneTimeToken = async (payload: OneTimeTokenVerificationVerifyPayload) => {
99
await initInteraction(InteractionEvent.Register);
1010

1111
return api

packages/experience/src/pages/OneTimeToken/index.tsx

+60-58
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
1-
import { AgreeToTermsPolicy, SignInIdentifier } from '@logto/schemas';
1+
import { AgreeToTermsPolicy, SignInIdentifier, type RequestErrorBody } from '@logto/schemas';
2+
import { condString } from '@silverhand/essentials';
23
import { useCallback, useEffect, useState } from 'react';
34
import { useParams } from 'react-router-dom';
45

56
import {
67
identifyAndSubmitInteraction,
78
signInWithVerifiedIdentifier,
8-
verifyOneTimeToken,
9+
registerWithOneTimeToken,
910
} from '@/apis/experience';
1011
import LoadingLayer from '@/components/LoadingLayer';
1112
import useApi from '@/hooks/use-api';
1213
import useErrorHandler from '@/hooks/use-error-handler';
13-
import useFallbackRoute from '@/hooks/use-fallback-route';
1414
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
1515
import useLoginHint from '@/hooks/use-login-hint';
1616
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
1717
import useTerms from '@/hooks/use-terms';
1818

1919
import ErrorPage from '../ErrorPage';
20-
import SwitchAccount from '../SwitchAccount';
2120

2221
const OneTimeToken = () => {
2322
const { token } = useParams();
24-
const fallback = useFallbackRoute();
2523
const email = useLoginHint();
26-
const [mismatchedAccount, setMismatchedAccount] = useState<string>();
27-
const [oneTimeTokenError, setOneTimeTokenError] = useState<unknown>();
24+
const [oneTimeTokenError, setOneTimeTokenError] = useState<RequestErrorBody | boolean>();
2825

29-
const asyncRegisterWithOneTimeToken = useApi(identifyAndSubmitInteraction);
26+
const asyncIdentifyUserAndSubmit = useApi(identifyAndSubmitInteraction);
3027
const asyncSignInWithVerifiedIdentifier = useApi(signInWithVerifiedIdentifier);
31-
const asyncVerifyOneTimeToken = useApi(verifyOneTimeToken);
28+
const asyncRegisterWithOneTimeToken = useApi(registerWithOneTimeToken);
3229

3330
const { termsValidation, agreeToTermsPolicy } = useTerms();
3431
const handleError = useErrorHandler();
3532
const redirectTo = useGlobalRedirectTo();
3633
const preSignInErrorHandler = usePreSignInErrorHandler();
3734

35+
/**
36+
* Update interaction event to `SignIn`, and then identify user and submit.
37+
*/
3838
const signInWithOneTimeToken = useCallback(
3939
async (verificationId: string) => {
4040
const [error, result] = await asyncSignInWithVerifiedIdentifier(verificationId);
@@ -51,15 +51,20 @@ const OneTimeToken = () => {
5151
[preSignInErrorHandler, asyncSignInWithVerifiedIdentifier, handleError, redirectTo]
5252
);
5353

54-
const registerWithOneTimeToken = useCallback(
54+
/**
55+
* Always try to submit the one-time token interaction with `Register` event first.
56+
* If the email already exists, call the `signInWithOneTimeToken` function instead.
57+
*/
58+
const submit = useCallback(
5559
async (verificationId: string) => {
56-
const [error, result] = await asyncRegisterWithOneTimeToken({ verificationId });
60+
const [error, result] = await asyncIdentifyUserAndSubmit({ verificationId });
5761

5862
if (error) {
5963
await handleError(error, {
6064
'user.email_already_in_use': async () => {
6165
await signInWithOneTimeToken(verificationId);
6266
},
67+
...preSignInErrorHandler,
6368
});
6469
return;
6570
}
@@ -68,70 +73,67 @@ const OneTimeToken = () => {
6873
await redirectTo(result.redirectTo);
6974
}
7075
},
71-
[asyncRegisterWithOneTimeToken, handleError, redirectTo, signInWithOneTimeToken]
76+
[
77+
preSignInErrorHandler,
78+
asyncIdentifyUserAndSubmit,
79+
handleError,
80+
redirectTo,
81+
signInWithOneTimeToken,
82+
]
7283
);
7384

7485
useEffect(() => {
7586
(async () => {
76-
if (token && email) {
77-
/**
78-
* Check if the user has agreed to the terms and privacy policy before navigating to the 3rd-party social sign-in page
79-
* when the policy is set to `Manual`
80-
*/
81-
if (agreeToTermsPolicy === AgreeToTermsPolicy.Manual && !(await termsValidation())) {
82-
return;
83-
}
84-
const [error, result] = await asyncVerifyOneTimeToken({
85-
token,
86-
identifier: { type: SignInIdentifier.Email, value: email },
87-
});
87+
if (!token || !email) {
88+
setOneTimeTokenError(true);
89+
return;
90+
}
91+
92+
/**
93+
* Check if the user has agreed to the terms and privacy policy before navigating to the 3rd-party social sign-in page
94+
* when the policy is set to `Manual`
95+
*/
96+
if (agreeToTermsPolicy === AgreeToTermsPolicy.Manual && !(await termsValidation())) {
97+
return;
98+
}
8899

89-
if (error) {
90-
await handleError(error, {
91-
'one_time_token.email_mismatch': () => {
92-
setMismatchedAccount(email);
93-
},
94-
'one_time_token.token_expired': () => {
95-
setOneTimeTokenError(error);
96-
},
97-
'one_time_token.token_consumed': () => {
98-
setOneTimeTokenError(error);
99-
},
100-
'one_time_token.token_revoked': () => {
101-
setOneTimeTokenError(error);
102-
},
103-
'one_time_token.token_not_found': () => {
104-
setOneTimeTokenError(error);
105-
},
106-
});
107-
return;
108-
}
109-
110-
if (!result?.verificationId) {
111-
return;
112-
}
113-
await registerWithOneTimeToken(result.verificationId);
100+
const [error, result] = await asyncRegisterWithOneTimeToken({
101+
token,
102+
identifier: { type: SignInIdentifier.Email, value: email },
103+
});
104+
105+
if (error) {
106+
await handleError(error, {
107+
global: (error: RequestErrorBody) => {
108+
setOneTimeTokenError(error);
109+
},
110+
});
111+
return;
114112
}
115113

116-
window.location.replace('/' + fallback);
114+
if (!result?.verificationId) {
115+
return;
116+
}
117+
await submit(result.verificationId);
117118
})();
118119
}, [
119120
agreeToTermsPolicy,
120121
email,
121-
fallback,
122122
token,
123-
asyncVerifyOneTimeToken,
123+
asyncRegisterWithOneTimeToken,
124124
handleError,
125-
registerWithOneTimeToken,
126125
termsValidation,
126+
submit,
127127
]);
128128

129-
if (mismatchedAccount) {
130-
return <SwitchAccount account={mismatchedAccount} />;
131-
}
132-
133129
if (oneTimeTokenError) {
134-
return <ErrorPage title="error.invalid_link" message="error.invalid_link_description" />;
130+
return (
131+
<ErrorPage
132+
title="error.invalid_link"
133+
message="error.invalid_link_description"
134+
rawMessage={condString(typeof oneTimeTokenError !== 'boolean' && oneTimeTokenError.message)}
135+
/>
136+
);
135137
}
136138

137139
return <LoadingLayer />;

packages/experience/src/utils/search-parameters.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export const searchKeys = Object.freeze({
1313
appId: 'app_id',
1414
} satisfies Record<SearchKeysCamelCase, string>);
1515

16+
/**
17+
* The one-time token used as verification method.
18+
* Example usage: Magic link
19+
*/
20+
export const oneTimeTokenSearchKey = 'one_time_token';
21+
1622
export const handleSearchParametersData = () => {
1723
const { search } = window.location;
1824

@@ -34,9 +40,14 @@ export const handleSearchParametersData = () => {
3440
}
3541
}
3642

37-
window.history.replaceState(
38-
{},
39-
'',
40-
window.location.pathname + condString(parameters.size > 0 && `?${parameters.toString()}`)
41-
);
43+
const conditionalParamString = condString(parameters.size > 0 && `?${parameters.toString()}`);
44+
45+
// Check one-time token existence, and redirect to the `/one-time-token` route.
46+
const oneTimeToken = parameters.get(oneTimeTokenSearchKey);
47+
if (oneTimeToken) {
48+
window.history.replaceState({}, '', '/one-time-token/' + oneTimeToken + conditionalParamString);
49+
return;
50+
}
51+
52+
window.history.replaceState({}, '', window.location.pathname + conditionalParamString);
4253
};

packages/phrases-experience/src/locales/ar/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const error = {
1919
timeout: 'انتهت مهلة الطلب. يرجى المحاولة مرة أخرى لاحقًا.',
2020
password_rejected,
2121
sso_not_enabled: 'تسجيل الدخول الموحد غير ممكّن لحساب البريد الإلكتروني هذا.',
22+
invalid_link: 'رابط غير صالح',
23+
invalid_link_description: 'ربما يكون رمز الدخول المؤقت قد انتهى أو لم يعد صالحًا.',
24+
something_went_wrong: 'حدث خطأ ما.',
2225
};
2326

2427
export default Object.freeze(error);

packages/phrases-experience/src/locales/de/error/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const error = {
1919
timeout: 'Zeitüberschreitung. Bitte melde dich erneut an.',
2020
password_rejected,
2121
sso_not_enabled: 'Single Sign-On ist für dieses E-Mail-Konto nicht aktiviert.',
22+
invalid_link: 'Ungültiger Link',
23+
invalid_link_description:
24+
'Dein einmaliger Token ist möglicherweise abgelaufen oder nicht mehr gültig.',
25+
something_went_wrong: 'Etwas ist schiefgegangen.',
2226
};
2327

2428
export default Object.freeze(error);

packages/phrases-experience/src/locales/en/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const error = {
1919
timeout: 'Request timeout. Please try again later.',
2020
password_rejected,
2121
sso_not_enabled: 'Single Sign-On is not enabled for this email account.',
22+
invalid_link: 'Invalid link',
23+
invalid_link_description: 'Your one-time token may have expired or is no longer valid.',
24+
something_went_wrong: 'Something went wrong.',
2225
};
2326

2427
export default Object.freeze(error);

packages/phrases-experience/src/locales/es/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const error = {
2121
password_rejected,
2222
sso_not_enabled:
2323
'El inicio de sesión único no está habilitado para esta cuenta de correo electrónico.',
24+
invalid_link: 'Enlace no válido',
25+
invalid_link_description: 'Tu token de un solo uso puede haber expirado o ya no ser válido.',
26+
something_went_wrong: 'Algo salió mal.',
2427
};
2528

2629
export default Object.freeze(error);

packages/phrases-experience/src/locales/fr/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const error = {
2121
timeout: "Délai d'attente de la requête dépassé. Veuillez réessayer plus tard.",
2222
password_rejected,
2323
sso_not_enabled: "La authentification unique n'est pas activée pour ce compte de messagerie.",
24+
invalid_link: 'Lien invalide',
25+
invalid_link_description: "Votre jeton à usage unique a peut-être expiré ou n'est plus valide.",
26+
something_went_wrong: 'Quelque chose a mal tourné.',
2427
};
2528

2629
export default Object.freeze(error);

packages/phrases-experience/src/locales/it/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const error = {
1919
timeout: 'Timeout della richiesta. Si prega di riprovare più tardi.',
2020
password_rejected,
2121
sso_not_enabled: 'Single sign-on non abilitato per questo account email.',
22+
invalid_link: 'Link invalido',
23+
invalid_link_description: 'Il tuo token monouso potrebbe essere scaduto o non è più valido.',
24+
something_went_wrong: 'Qualcosa è andato storto.',
2225
};
2326

2427
export default Object.freeze(error);

packages/phrases-experience/src/locales/ja/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const error = {
2020
timeout: 'リクエストタイムアウト。後でもう一度お試しください。',
2121
password_rejected,
2222
sso_not_enabled: 'このメールアカウントではシングルサインオンが有効になっていません。',
23+
invalid_link: '無効なリンク',
24+
invalid_link_description: 'ワンタイムトークンの有効期限が切れているか、無効になっています。',
25+
something_went_wrong: '問題が発生しました。',
2326
};
2427

2528
export default Object.freeze(error);

packages/phrases-experience/src/locales/ko/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const error = {
1919
timeout: '요청 시간이 초과되었어요. 잠시 후에 다시 시도해 주세요.',
2020
password_rejected,
2121
sso_not_enabled: '이 이메일 계정에 대해 단일 로그인이 활성화되지 않았어요.',
22+
invalid_link: '유효하지 않은 링크',
23+
invalid_link_description: '일회성 토큰이 만료되었거나 더 이상 유효하지 않을 수 있어요.',
24+
something_went_wrong: '문제가 발생했어요.',
2225
};
2326

2427
export default Object.freeze(error);

packages/phrases-experience/src/locales/pl-pl/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const error = {
2020
timeout: 'Czas żądania upłynął. Proszę spróbuj ponownie później.',
2121
password_rejected,
2222
sso_not_enabled: 'Pojedyncze logowanie nie jest włączony dla tego konta e-mail.',
23+
invalid_link: 'Nieprawidłowy link',
24+
invalid_link_description: 'Twój jednorazowy token mógł wygasnąć lub nie jest już ważny.',
25+
something_went_wrong: 'Coś poszło nie tak.',
2326
};
2427

2528
export default Object.freeze(error);

packages/phrases-experience/src/locales/pt-br/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const error = {
1919
timeout: 'Tempo limite excedido. Por favor, tente novamente mais tarde.',
2020
password_rejected,
2121
sso_not_enabled: 'O Single Sign-On não está habilitado para esta conta de e-mail.',
22+
invalid_link: 'Link inválido',
23+
invalid_link_description: 'Seu token de uso único pode ter expirado ou não é mais válido.',
24+
something_went_wrong: 'Algo deu errado.',
2225
};
2326

2427
export default Object.freeze(error);

packages/phrases-experience/src/locales/pt-pt/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const error = {
2020
timeout: 'Tempo limite de sessão. Volte e faça login novamente.',
2121
password_rejected,
2222
sso_not_enabled: 'O Single Sign-On não está habilitado para esta conta de e-mail.',
23+
invalid_link: 'Link inválido',
24+
invalid_link_description: 'O teu token de uso único pode ter expirado ou já não ser válido.',
25+
something_went_wrong: 'Algo correu mal.',
2326
};
2427

2528
export default Object.freeze(error);

packages/phrases-experience/src/locales/ru/error/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const error = {
2020
timeout: 'Время ожидания истекло. Пожалуйста, повторите попытку позднее.',
2121
password_rejected,
2222
sso_not_enabled: 'Односторонняя авторизация не включена для этого аккаунта электронной почты.',
23+
invalid_link: 'Неверная ссылка',
24+
invalid_link_description:
25+
'Ваш одноразовый токен мог истечь или больше не является действительным.',
26+
something_went_wrong: 'Что-то пошло не так.',
2327
};
2428

2529
export default Object.freeze(error);

packages/phrases-experience/src/locales/tr-tr/error/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const error = {
1919
timeout: 'Oturum zaman aşımına uğradı. Lütfen geri dönüp tekrar giriş yapınız.',
2020
password_rejected,
2121
sso_not_enabled: 'Bu e-posta hesabı için tek oturum açma etkin değil.',
22+
invalid_link: 'Geçersiz bağlantı',
23+
invalid_link_description:
24+
'Tek kullanımlık belirtecin süresi dolmuş olabilir veya artık geçerli değil.',
25+
something_went_wrong: 'Bir şeyler yanlış gitti.',
2226
};
2327

2428
export default Object.freeze(error);

packages/phrases-experience/src/locales/zh-cn/error/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const error = {
1919
timeout: '请求超时,请稍后重试。',
2020
password_rejected,
2121
sso_not_enabled: '此邮箱账户未启用单点登录。',
22+
invalid_link: '无效的链接',
23+
invalid_link_description: '你的一次性令牌可能已过期或不再有效。',
24+
something_went_wrong: '出现错误。',
2225
};
2326

2427
export default Object.freeze(error);

0 commit comments

Comments
 (0)