From 47a12fbf79daad283191db7ef76242b90a3c9ff0 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:19:34 +0100 Subject: [PATCH] feat: add support for CLIENT_VERSION_UNSUPPORTED error messages --- src/app/i18n/locales/de.json | 8 ++- src/app/i18n/locales/en.json | 8 ++- src/app/i18n/locales/es.json | 8 ++- src/app/i18n/locales/fr.json | 8 ++- src/app/i18n/locales/it.json | 8 ++- src/app/i18n/locales/pt.json | 8 ++- src/app/i18n/locales/ru.json | 8 ++- src/app/i18n/locales/tw.json | 8 ++- src/app/i18n/locales/zh.json | 8 ++- .../Login/UniversalLinkErrorView.test.tsx | 68 +++++++++++++++++++ src/views/Login/UniversalLinkErrorView.tsx | 21 +++++- 11 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 src/views/Login/UniversalLinkErrorView.test.tsx diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index a4a1ad24b8..4c6572711f 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 4fd6022116..335dddeae9 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 784e67f7d9..134a8b66e6 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 8269024d53..e445a3090a 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 5d5555d93b..c76cd20a05 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 629e0d8fa1..7f000a72f6 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 ddaeb15709..183c4f6eea 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 bad2eff6a7..193fa6ea41 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 dabffa2af5..3711a5922b 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')}