diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json
index 0259cb04543..4df4e48e59c 100644
--- a/oxlint-suppressions.json
+++ b/oxlint-suppressions.json
@@ -4171,11 +4171,6 @@
"count": 2
}
},
- "web/hooks/use-oauth.ts": {
- "typescript/no-explicit-any": {
- "count": 1
- }
- },
"web/hooks/use-pay.tsx": {
"eslint-react/set-state-in-effect": {
"count": 4
diff --git a/web/app/oauth-callback/__tests__/page.spec.tsx b/web/app/oauth-callback/__tests__/page.spec.tsx
new file mode 100644
index 00000000000..5a89f68855e
--- /dev/null
+++ b/web/app/oauth-callback/__tests__/page.spec.tsx
@@ -0,0 +1,100 @@
+import { render, screen } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createReactI18nextMock } from '@/test/i18n-mock'
+
+vi.mock('react-i18next', () =>
+ createReactI18nextMock({
+ 'oauth.callback.success': 'Authorization complete',
+ 'oauth.callback.successHint': 'You can now close this tab and return to the previous page.',
+ 'oauth.callback.error': 'Authorization failed',
+ 'oauth.callback.errorHint': 'Please return to the previous page and try again.',
+ 'login.signBtn': 'Sign in',
+ }),
+)
+
+vi.mock('@/hooks/use-oauth', () => ({
+ useOAuthCallback: vi.fn(),
+}))
+
+vi.mock('@/hooks/use-document-title', () => ({
+ default: vi.fn(),
+}))
+
+const { useOAuthCallback } = await import('@/hooks/use-oauth')
+const OAuthCallback = (await import('../page')).default
+
+const setOpener = (opener: Window | null) => {
+ Object.defineProperty(window, 'opener', { configurable: true, writable: true, value: opener })
+}
+
+describe('OAuthCallback page', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ afterEach(() => {
+ setOpener(null)
+ })
+
+ it('renders an empty
when opened as a popup (hasOpener=true) so the popup can close cleanly', () => {
+ setOpener({ postMessage: vi.fn() } as unknown as Window)
+ vi.mocked(useOAuthCallback).mockReturnValue({
+ hasOpener: true,
+ finished: false,
+ error: null,
+ errorDescription: null,
+ })
+
+ const { container } = render()
+ expect(container.firstChild).not.toBeNull()
+ expect(container.firstChild!.nodeName).toBe('DIV')
+ expect(container.firstChild!.childNodes.length).toBe(0)
+ })
+
+ it('renders a success message when opened in a new tab with a subscription_id (#39752)', () => {
+ setOpener(null)
+ vi.mocked(useOAuthCallback).mockReturnValue({
+ hasOpener: false,
+ finished: true,
+ error: null,
+ errorDescription: null,
+ })
+
+ render()
+ expect(screen.getByRole('main')).toBeInTheDocument()
+ expect(screen.getByText('Authorization complete')).toBeInTheDocument()
+ expect(
+ screen.getByText('You can now close this tab and return to the previous page.'),
+ ).toBeInTheDocument()
+ })
+
+ it('renders an error message with the provider description when the provider reported an error (#39752)', () => {
+ setOpener(null)
+ vi.mocked(useOAuthCallback).mockReturnValue({
+ hasOpener: false,
+ finished: true,
+ error: 'access_denied',
+ errorDescription: 'Please re-authorize the app.',
+ })
+
+ render()
+ expect(screen.getByText('Authorization failed')).toBeInTheDocument()
+ expect(screen.getByText('Please re-authorize the app.')).toBeInTheDocument()
+ })
+
+ it('falls back to a generic error hint when the provider did not supply an error_description', () => {
+ setOpener(null)
+ vi.mocked(useOAuthCallback).mockReturnValue({
+ hasOpener: false,
+ finished: true,
+ error: 'server_error',
+ errorDescription: null,
+ })
+
+ render()
+ expect(screen.getByText('Authorization failed')).toBeInTheDocument()
+ expect(
+ screen.getByText('Please return to the previous page and try again.'),
+ ).toBeInTheDocument()
+ })
+})
diff --git a/web/app/oauth-callback/page.tsx b/web/app/oauth-callback/page.tsx
index ad1e0204feb..096e892ee67 100644
--- a/web/app/oauth-callback/page.tsx
+++ b/web/app/oauth-callback/page.tsx
@@ -4,11 +4,32 @@ import useDocumentTitle from '@/hooks/use-document-title'
import { useOAuthCallback } from '@/hooks/use-oauth'
const OAuthCallback = () => {
- const { t } = useTranslation(['login'])
+ const { t } = useTranslation(['login', 'oauth'])
useDocumentTitle(t(($) => $.signBtn, { ns: 'login' }))
- useOAuthCallback()
+ const { hasOpener, finished, error, errorDescription } = useOAuthCallback()
- return
+ // When the page is opened as a popup by the console, the hook posts the
+ // OAuth result to the opener and calls window.close(). The page only
+ // needs to render a fallback message when the page was opened in a new
+ // tab (no `window.opener`); see issue #39752.
+ if (hasOpener || !finished) return
+
+ return (
+
+
+
+ {error
+ ? t(($) => $['callback.error'], { ns: 'oauth' })
+ : t(($) => $['callback.success'], { ns: 'oauth' })}
+
+
+ {error
+ ? errorDescription || t(($) => $['callback.errorHint'], { ns: 'oauth' })
+ : t(($) => $['callback.successHint'], { ns: 'oauth' })}
+
+
+
+ )
}
export default OAuthCallback
diff --git a/web/hooks/__tests__/use-oauth.spec.tsx b/web/hooks/__tests__/use-oauth.spec.tsx
new file mode 100644
index 00000000000..f3d6d0e0eed
--- /dev/null
+++ b/web/hooks/__tests__/use-oauth.spec.tsx
@@ -0,0 +1,126 @@
+import { renderHook } from '@testing-library/react'
+import { renderToString } from 'react-dom/server'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { useOAuthCallback } from '../use-oauth'
+
+const originalOpener = window.opener
+const originalClose = window.close
+
+const setOpener = (opener: Window | null) => {
+ Object.defineProperty(window, 'opener', { configurable: true, writable: true, value: opener })
+}
+
+describe('useOAuthCallback', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ afterEach(() => {
+ setOpener(originalOpener)
+ window.close = originalClose
+ })
+
+ it('posts a success message and closes the popup when window.opener is present and a subscription_id is in the query', () => {
+ const postMessage = vi.fn()
+ setOpener({ origin: 'https://console.example.test', postMessage } as unknown as Window)
+ const close = vi.fn()
+ window.close = close
+ window.history.replaceState({}, '', '/oauth-callback?subscription_id=sub-123')
+
+ const { result } = renderHook(() => useOAuthCallback())
+
+ expect(postMessage).toHaveBeenCalledWith(
+ { type: 'oauth_callback', success: true, subscriptionId: 'sub-123' },
+ 'https://console.example.test',
+ )
+ expect(close).toHaveBeenCalledTimes(1)
+ expect(result.current.hasOpener).toBe(true)
+ expect(result.current.finished).toBe(true)
+ })
+
+ it('posts an error message and closes the popup when window.opener is present and an error is in the query', () => {
+ const postMessage = vi.fn()
+ setOpener({ origin: 'https://console.example.test', postMessage } as unknown as Window)
+ const close = vi.fn()
+ window.close = close
+ window.history.replaceState(
+ {},
+ '',
+ '/oauth-callback?error=access_denied&error_description=User%20denied%20access',
+ )
+
+ renderHook(() => useOAuthCallback())
+
+ expect(postMessage).toHaveBeenCalledWith(
+ {
+ type: 'oauth_callback',
+ success: false,
+ error: 'access_denied',
+ errorDescription: 'User denied access',
+ },
+ 'https://console.example.test',
+ )
+ expect(close).toHaveBeenCalledTimes(1)
+ })
+
+ it('falls back to targetOrigin "*" when window.opener has no origin (e.g. file:// popup)', () => {
+ const postMessage = vi.fn()
+ setOpener({ postMessage } as unknown as Window)
+ const close = vi.fn()
+ window.close = close
+ window.history.replaceState({}, '', '/oauth-callback?subscription_id=sub-1')
+
+ renderHook(() => useOAuthCallback())
+
+ expect(postMessage).toHaveBeenCalledWith(expect.any(Object), '*')
+ })
+
+ it('exposes success state without posting a message when window.opener is missing (new-tab case) (#39752)', () => {
+ setOpener(null)
+ const close = vi.fn()
+ window.close = close
+ window.history.replaceState({}, '', '/oauth-callback?subscription_id=sub-tab-1')
+
+ const { result } = renderHook(() => useOAuthCallback())
+
+ // No opener, so we must NOT try to close (would close the only tab).
+ expect(close).not.toHaveBeenCalled()
+ // The page needs the state to render a meaningful message instead of an empty .
+ expect(result.current.hasOpener).toBe(false)
+ expect(result.current.finished).toBe(true)
+ expect(result.current.error).toBeNull()
+ expect(result.current.errorDescription).toBeNull()
+ })
+
+ it('exposes the provider error in state when window.opener is missing (#39752)', () => {
+ setOpener(null)
+ const close = vi.fn()
+ window.close = close
+ window.history.replaceState(
+ {},
+ '',
+ '/oauth-callback?error=access_denied&error_description=Please%20re-authorize',
+ )
+
+ const { result } = renderHook(() => useOAuthCallback())
+
+ expect(close).not.toHaveBeenCalled()
+ expect(result.current.hasOpener).toBe(false)
+ expect(result.current.finished).toBe(true)
+ expect(result.current.error).toBe('access_denied')
+ expect(result.current.errorDescription).toBe('Please re-authorize')
+ })
+
+ it('renders without browser globals on the server and waits for hydration', () => {
+ const ServerCallback = () => {
+ const state = useOAuthCallback()
+ return {state.finished ? 'finished' : 'waiting'}
+ }
+ vi.stubGlobal('window', undefined)
+ try {
+ expect(renderToString()).toContain('waiting')
+ } finally {
+ vi.unstubAllGlobals()
+ }
+ })
+})
diff --git a/web/hooks/use-oauth.ts b/web/hooks/use-oauth.ts
index d0cdc6d12b5..298261d85a7 100644
--- a/web/hooks/use-oauth.ts
+++ b/web/hooks/use-oauth.ts
@@ -1,51 +1,78 @@
'use client'
-import { useEffect } from 'react'
+import { useEffect, useSyncExternalStore } from 'react'
import { validateRedirectUrl } from '@/utils/urlValidation'
-export const useOAuthCallback = () => {
+export type OAuthCallbackState = {
+ /** True when the callback tab/popup was opened by another window via window.open. */
+ hasOpener: boolean
+ /** True after the callback has posted a message to the opener (or decided not to). */
+ finished: boolean
+ /** Provider-supplied error code, if any. */
+ error: string | null
+ /** Provider-supplied error description, if any. */
+ errorDescription: string | null
+}
+
+// OAuth redirects load a new page. The URL and opener do not change during
+// this page's lifetime, so a subscription is not needed after hydration.
+const subscribeToCallback = () => () => {}
+const getCallbackSnapshot = () => JSON.stringify([!!window.opener, window.location.search])
+const getServerCallbackSnapshot = () => null
+
+export const useOAuthCallback = (): OAuthCallbackState => {
+ const snapshot = useSyncExternalStore(
+ subscribeToCallback,
+ getCallbackSnapshot,
+ getServerCallbackSnapshot,
+ )
+ const [hasOpener, search] =
+ snapshot === null ? [false, ''] : (JSON.parse(snapshot) as [boolean, string])
+ const urlParams = new URLSearchParams(search)
+ const error = urlParams.get('error')
+ const errorDescription = urlParams.get('error_description')
+
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search)
const subscriptionId = urlParams.get('subscription_id')
const error = urlParams.get('error')
const errorDescription = urlParams.get('error_description')
+ const opener = window.opener
- if (window.opener) {
- // Use window.opener.origin instead of '*' for security
- const targetOrigin = window.opener?.origin || '*'
+ if (!opener) return
- if (subscriptionId) {
- window.opener.postMessage(
- {
- type: 'oauth_callback',
- success: true,
- subscriptionId,
- },
- targetOrigin,
- )
- } else if (error) {
- window.opener.postMessage(
- {
- type: 'oauth_callback',
- success: false,
- error,
- errorDescription,
- },
- targetOrigin,
- )
- } else {
- window.opener.postMessage(
- {
- type: 'oauth_callback',
- },
- targetOrigin,
- )
- }
- window.close()
+ // Use window.opener.origin instead of '*' for security.
+ const targetOrigin = opener.origin || '*'
+
+ if (subscriptionId) {
+ opener.postMessage({ type: 'oauth_callback', success: true, subscriptionId }, targetOrigin)
+ } else if (error) {
+ opener.postMessage(
+ { type: 'oauth_callback', success: false, error, errorDescription },
+ targetOrigin,
+ )
+ } else {
+ opener.postMessage({ type: 'oauth_callback' }, targetOrigin)
}
+ window.close()
}, [])
+
+ return {
+ hasOpener,
+ finished: snapshot !== null,
+ error,
+ errorDescription,
+ }
}
-export const openOAuthPopup = (url: string, callback: (data?: any) => void) => {
+type OAuthCallbackMessage = {
+ type: 'oauth_callback'
+ success?: boolean
+ subscriptionId?: string
+ error?: string
+ errorDescription?: string
+}
+
+export const openOAuthPopup = (url: string, callback: (data?: OAuthCallbackMessage) => void) => {
const width = 600
const height = 600
const left = window.screenX + (window.outerWidth - width) / 2
diff --git a/web/i18n/locales/ar-TN/oauth.json b/web/i18n/locales/ar-TN/oauth.json
index 7ac178c7e33..839398df86d 100644
--- a/web/i18n/locales/ar-TN/oauth.json
+++ b/web/i18n/locales/ar-TN/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "فشل التفويض",
+ "callback.errorHint": "يرجى العودة إلى الصفحة السابقة والمحاولة مرة أخرى.",
+ "callback.success": "اكتمل التفويض",
+ "callback.successHint": "يمكنك الآن إغلاق علامة التبويب هذه والعودة إلى الصفحة السابقة.",
"connect": "الاتصال بـ",
"continue": "متابعة",
"error.authAppInfoFetchFailed": "فشل جلب معلومات التطبيق للتفويض",
diff --git a/web/i18n/locales/az-AZ/oauth.json b/web/i18n/locales/az-AZ/oauth.json
index 6c7220ee335..8462b6f9869 100644
--- a/web/i18n/locales/az-AZ/oauth.json
+++ b/web/i18n/locales/az-AZ/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "İcazə vermək mümkün olmadı",
+ "callback.errorHint": "Əvvəlki səhifəyə qayıdıb yenidən cəhd edin.",
+ "callback.success": "İcazə verildi",
+ "callback.successHint": "İndi bu vərəqi bağlayıb əvvəlki səhifəyə qayıda bilərsiniz.",
"connect": "Qoşulma:",
"continue": "Davam et",
"error.authAppInfoFetchFailed": "İcazə vermək üçün tətbiq məlumatlarını əldə etmək mümkün olmadı",
diff --git a/web/i18n/locales/de-DE/oauth.json b/web/i18n/locales/de-DE/oauth.json
index 65b6471e8e6..eeaa70e3a5b 100644
--- a/web/i18n/locales/de-DE/oauth.json
+++ b/web/i18n/locales/de-DE/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Autorisierung fehlgeschlagen",
+ "callback.errorHint": "Bitte kehren Sie zur vorherigen Seite zurück und versuchen Sie es erneut.",
+ "callback.success": "Autorisierung abgeschlossen",
+ "callback.successHint": "Sie können diesen Tab jetzt schließen und zur vorherigen Seite zurückkehren.",
"connect": "Verbinden mit",
"continue": "Fortsetzen",
"error.authAppInfoFetchFailed": "Fehler beim Abrufen der App-Informationen für die Autorisierung",
diff --git a/web/i18n/locales/en-US/oauth.json b/web/i18n/locales/en-US/oauth.json
index 6383839b9f9..09701ccd714 100644
--- a/web/i18n/locales/en-US/oauth.json
+++ b/web/i18n/locales/en-US/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Authorization failed",
+ "callback.errorHint": "Please return to the previous page and try again.",
+ "callback.success": "Authorization complete",
+ "callback.successHint": "You can now close this tab and return to the previous page.",
"connect": "Connect to",
"continue": "Continue",
"error.authAppInfoFetchFailed": "Failed to fetch app info for authorization",
diff --git a/web/i18n/locales/es-ES/oauth.json b/web/i18n/locales/es-ES/oauth.json
index e54fc7fa4ac..e0f06dc1ac5 100644
--- a/web/i18n/locales/es-ES/oauth.json
+++ b/web/i18n/locales/es-ES/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "La autorización falló",
+ "callback.errorHint": "Vuelve a la página anterior e inténtalo de nuevo.",
+ "callback.success": "Autorización completada",
+ "callback.successHint": "Ya puedes cerrar esta pestaña y volver a la página anterior.",
"connect": "Conectar a",
"continue": "Continuar",
"error.authAppInfoFetchFailed": "No se pudo obtener la información de la aplicación para la autorización",
diff --git a/web/i18n/locales/fa-IR/oauth.json b/web/i18n/locales/fa-IR/oauth.json
index 1a0116d61cb..1fbcfe291e3 100644
--- a/web/i18n/locales/fa-IR/oauth.json
+++ b/web/i18n/locales/fa-IR/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "مجوزدهی ناموفق بود",
+ "callback.errorHint": "لطفاً به صفحه قبلی برگردید و دوباره تلاش کنید.",
+ "callback.success": "مجوزدهی تکمیل شد",
+ "callback.successHint": "اکنون میتوانید این برگه را ببندید و به صفحه قبلی بازگردید.",
"connect": "متصل به",
"continue": "ادامه دهید",
"error.authAppInfoFetchFailed": "عدم موفقیت در دریافت اطلاعات برنامه برای مجوز",
diff --git a/web/i18n/locales/fr-FR/oauth.json b/web/i18n/locales/fr-FR/oauth.json
index 7c82c92a831..482df6c7555 100644
--- a/web/i18n/locales/fr-FR/oauth.json
+++ b/web/i18n/locales/fr-FR/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Échec de l’autorisation",
+ "callback.errorHint": "Veuillez revenir à la page précédente et réessayer.",
+ "callback.success": "Autorisation terminée",
+ "callback.successHint": "Vous pouvez maintenant fermer cet onglet et revenir à la page précédente.",
"connect": "Se connecter à",
"continue": "Continuer",
"error.authAppInfoFetchFailed": "Échec de la récupération des informations de l'application pour l'autorisation",
diff --git a/web/i18n/locales/hi-IN/oauth.json b/web/i18n/locales/hi-IN/oauth.json
index b1a58fa0da9..0b99ae34457 100644
--- a/web/i18n/locales/hi-IN/oauth.json
+++ b/web/i18n/locales/hi-IN/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "प्राधिकरण विफल",
+ "callback.errorHint": "कृपया पिछले पृष्ठ पर लौटें और फिर से प्रयास करें।",
+ "callback.success": "प्राधिकरण पूरा हुआ",
+ "callback.successHint": "अब आप इस टैब को बंद करके पिछले पृष्ठ पर लौट सकते हैं।",
"connect": "संयोजित करें",
"continue": "जारी रखें",
"error.authAppInfoFetchFailed": "प्राधिकरण के लिए ऐप जानकारी प्राप्त करने में असफल हुआ",
diff --git a/web/i18n/locales/id-ID/oauth.json b/web/i18n/locales/id-ID/oauth.json
index 688c7873b55..163b9e6a911 100644
--- a/web/i18n/locales/id-ID/oauth.json
+++ b/web/i18n/locales/id-ID/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Otorisasi gagal",
+ "callback.errorHint": "Kembali ke halaman sebelumnya dan coba lagi.",
+ "callback.success": "Otorisasi selesai",
+ "callback.successHint": "Anda sekarang dapat menutup tab ini dan kembali ke halaman sebelumnya.",
"connect": "Hubungkan",
"continue": "Lanjut",
"error.authAppInfoFetchFailed": "Gagal mengambil info aplikasi untuk otorisasi",
diff --git a/web/i18n/locales/it-IT/oauth.json b/web/i18n/locales/it-IT/oauth.json
index 82b2c1d1f01..41da6019d1b 100644
--- a/web/i18n/locales/it-IT/oauth.json
+++ b/web/i18n/locales/it-IT/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Autorizzazione non riuscita",
+ "callback.errorHint": "Torna alla pagina precedente e riprova.",
+ "callback.success": "Autorizzazione completata",
+ "callback.successHint": "Ora puoi chiudere questa scheda e tornare alla pagina precedente.",
"connect": "Connetti a",
"continue": "Continua",
"error.authAppInfoFetchFailed": "Impossibile recuperare le informazioni sull'app per l'autorizzazione",
diff --git a/web/i18n/locales/ja-JP/oauth.json b/web/i18n/locales/ja-JP/oauth.json
index 14e2f0ddf48..970cb90d5f2 100644
--- a/web/i18n/locales/ja-JP/oauth.json
+++ b/web/i18n/locales/ja-JP/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "認証に失敗しました",
+ "callback.errorHint": "前のページに戻ってもう一度お試しください。",
+ "callback.success": "認証が完了しました",
+ "callback.successHint": "このタブを閉じて前のページに戻ることができます。",
"connect": "接続する",
"continue": "続行",
"error.authAppInfoFetchFailed": "認証のためのアプリ情報の取得に失敗しました",
diff --git a/web/i18n/locales/ko-KR/oauth.json b/web/i18n/locales/ko-KR/oauth.json
index af510c6014b..8c74ef9b0aa 100644
--- a/web/i18n/locales/ko-KR/oauth.json
+++ b/web/i18n/locales/ko-KR/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "권한 부여 실패",
+ "callback.errorHint": "이전 페이지로 돌아가 다시 시도해 주세요.",
+ "callback.success": "권한 부여 완료",
+ "callback.successHint": "이제 이 탭을 닫고 이전 페이지로 돌아갈 수 있습니다.",
"connect": "연결",
"continue": "계속",
"error.authAppInfoFetchFailed": "인증을 위한 앱 정보를 가져오지 못했습니다.",
diff --git a/web/i18n/locales/lo-LA/oauth.json b/web/i18n/locales/lo-LA/oauth.json
index d7aff0dac91..c876c46af11 100644
--- a/web/i18n/locales/lo-LA/oauth.json
+++ b/web/i18n/locales/lo-LA/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "ການອະນຸມັດລົ້ມເຫຼວ",
+ "callback.errorHint": "ກະລຸນາກັບໄປໜ້າກ່ອນ ແລະລອງອີກຄັ້ງ.",
+ "callback.success": "ການອະນຸມັດສຳເລັດແລ້ວ",
+ "callback.successHint": "ຕອນນີ້ທ່ານສາມາດປິດແທັບນີ້ ແລະກັບໄປໜ້າກ່ອນໄດ້.",
"connect": "ເຊື່ອມຕໍ່ກັບ",
"continue": "ດຳເນີນການຕໍ່",
"error.authAppInfoFetchFailed": "ບໍ່ສາມາດດຶງຂໍ້ມູນແອັບຯເພື່ອການອະນຸມັດໄດ້",
diff --git a/web/i18n/locales/nl-NL/oauth.json b/web/i18n/locales/nl-NL/oauth.json
index 6383839b9f9..5cfba8aa612 100644
--- a/web/i18n/locales/nl-NL/oauth.json
+++ b/web/i18n/locales/nl-NL/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Autorisatie mislukt",
+ "callback.errorHint": "Ga terug naar de vorige pagina en probeer het opnieuw.",
+ "callback.success": "Autorisatie voltooid",
+ "callback.successHint": "Je kunt dit tabblad nu sluiten en teruggaan naar de vorige pagina.",
"connect": "Connect to",
"continue": "Continue",
"error.authAppInfoFetchFailed": "Failed to fetch app info for authorization",
diff --git a/web/i18n/locales/pl-PL/oauth.json b/web/i18n/locales/pl-PL/oauth.json
index 71d8cef67ba..7dce4b6dd03 100644
--- a/web/i18n/locales/pl-PL/oauth.json
+++ b/web/i18n/locales/pl-PL/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Autoryzacja nie powiodła się",
+ "callback.errorHint": "Wróć do poprzedniej strony i spróbuj ponownie.",
+ "callback.success": "Autoryzacja zakończona",
+ "callback.successHint": "Możesz teraz zamknąć tę kartę i wrócić do poprzedniej strony.",
"connect": "Połącz z",
"continue": "Kontynuuj",
"error.authAppInfoFetchFailed": "Nie udało się pobrać informacji o aplikacji w celu autoryzacji",
diff --git a/web/i18n/locales/pt-BR/oauth.json b/web/i18n/locales/pt-BR/oauth.json
index 35ab3d43970..fed5471bb9b 100644
--- a/web/i18n/locales/pt-BR/oauth.json
+++ b/web/i18n/locales/pt-BR/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Falha na autorização",
+ "callback.errorHint": "Volte à página anterior e tente novamente.",
+ "callback.success": "Autorização concluída",
+ "callback.successHint": "Agora você pode fechar esta aba e voltar à página anterior.",
"connect": "Conectar a",
"continue": "Continue",
"error.authAppInfoFetchFailed": "Falha ao buscar informações do aplicativo para autorização",
diff --git a/web/i18n/locales/ro-RO/oauth.json b/web/i18n/locales/ro-RO/oauth.json
index 5bb5da3141c..5ff71ea653e 100644
--- a/web/i18n/locales/ro-RO/oauth.json
+++ b/web/i18n/locales/ro-RO/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Autorizarea a eșuat",
+ "callback.errorHint": "Reveniți la pagina anterioară și încercați din nou.",
+ "callback.success": "Autorizare finalizată",
+ "callback.successHint": "Acum puteți închide această filă și reveni la pagina anterioară.",
"connect": "Conectează la",
"continue": "Continuați",
"error.authAppInfoFetchFailed": "Nu s-au putut obține informațiile aplicației pentru autorizare",
diff --git a/web/i18n/locales/ru-RU/oauth.json b/web/i18n/locales/ru-RU/oauth.json
index ce742da416e..0ab11a6fdcf 100644
--- a/web/i18n/locales/ru-RU/oauth.json
+++ b/web/i18n/locales/ru-RU/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Авторизация не удалась",
+ "callback.errorHint": "Вернитесь на предыдущую страницу и попробуйте ещё раз.",
+ "callback.success": "Авторизация завершена",
+ "callback.successHint": "Теперь вы можете закрыть эту вкладку и вернуться на предыдущую страницу.",
"connect": "Подключиться к",
"continue": "Продолжить",
"error.authAppInfoFetchFailed": "Не удалось получить информацию об приложении для авторизации",
diff --git a/web/i18n/locales/sl-SI/oauth.json b/web/i18n/locales/sl-SI/oauth.json
index 0d6aec23930..e7964710a67 100644
--- a/web/i18n/locales/sl-SI/oauth.json
+++ b/web/i18n/locales/sl-SI/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Avtorizacija ni uspela",
+ "callback.errorHint": "Vrnite se na prejšnjo stran in poskusite znova.",
+ "callback.success": "Avtorizacija je zaključena",
+ "callback.successHint": "Zdaj lahko zaprete ta zavihek in se vrnete na prejšnjo stran.",
"connect": "Poveži se z",
"continue": "Nadaljuj",
"error.authAppInfoFetchFailed": "Pridobivanje informacij o aplikaciji za avtorizacijo ni uspelo",
diff --git a/web/i18n/locales/th-TH/oauth.json b/web/i18n/locales/th-TH/oauth.json
index 4b732876503..9822b6ec6ac 100644
--- a/web/i18n/locales/th-TH/oauth.json
+++ b/web/i18n/locales/th-TH/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "การอนุญาตล้มเหลว",
+ "callback.errorHint": "โปรดกลับไปยังหน้าก่อนหน้าแล้วลองอีกครั้ง",
+ "callback.success": "การอนุญาตเสร็จสมบูรณ์",
+ "callback.successHint": "ตอนนี้คุณสามารถปิดแท็บนี้และกลับไปยังหน้าก่อนหน้าได้",
"connect": "เชื่อมต่อกับ",
"continue": "ดำเนินต่อไป",
"error.authAppInfoFetchFailed": "ไม่สามารถดึงข้อมูลแอปเพื่อการอนุญาตได้",
diff --git a/web/i18n/locales/tr-TR/oauth.json b/web/i18n/locales/tr-TR/oauth.json
index 0a23e04a960..bb6cffcbe44 100644
--- a/web/i18n/locales/tr-TR/oauth.json
+++ b/web/i18n/locales/tr-TR/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Yetkilendirme başarısız",
+ "callback.errorHint": "Lütfen önceki sayfaya dönüp tekrar deneyin.",
+ "callback.success": "Yetkilendirme tamamlandı",
+ "callback.successHint": "Artık bu sekmeyi kapatıp önceki sayfaya dönebilirsiniz.",
"connect": "Bağlan",
"continue": "Devam et",
"error.authAppInfoFetchFailed": "Yetkilendirme için uygulama bilgisi alınamadı",
diff --git a/web/i18n/locales/uk-UA/oauth.json b/web/i18n/locales/uk-UA/oauth.json
index 28028304522..b3f078939bb 100644
--- a/web/i18n/locales/uk-UA/oauth.json
+++ b/web/i18n/locales/uk-UA/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Авторизація не вдалася",
+ "callback.errorHint": "Поверніться на попередню сторінку та спробуйте ще раз.",
+ "callback.success": "Авторизацію завершено",
+ "callback.successHint": "Тепер ви можете закрити цю вкладку та повернутися на попередню сторінку.",
"connect": "Підключитися до",
"continue": "Продовжувати",
"error.authAppInfoFetchFailed": "Не вдалося отримати інформацію про додаток для авторизації",
diff --git a/web/i18n/locales/vi-VN/oauth.json b/web/i18n/locales/vi-VN/oauth.json
index 88355688f28..535cf3710c4 100644
--- a/web/i18n/locales/vi-VN/oauth.json
+++ b/web/i18n/locales/vi-VN/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "Ủy quyền không thành công",
+ "callback.errorHint": "Vui lòng quay lại trang trước và thử lại.",
+ "callback.success": "Ủy quyền hoàn tất",
+ "callback.successHint": "Bây giờ bạn có thể đóng tab này và quay lại trang trước.",
"connect": "Kết nối với",
"continue": "Tiếp tục",
"error.authAppInfoFetchFailed": "Không thể lấy thông tin ứng dụng để xác thực",
diff --git a/web/i18n/locales/zh-Hans/oauth.json b/web/i18n/locales/zh-Hans/oauth.json
index 05b8a47aea9..0d39f9098db 100644
--- a/web/i18n/locales/zh-Hans/oauth.json
+++ b/web/i18n/locales/zh-Hans/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "授权失败",
+ "callback.errorHint": "请返回上一页并重试。",
+ "callback.success": "授权完成",
+ "callback.successHint": "现在可以关闭此标签页并返回上一页。",
"connect": "连接到",
"continue": "继续",
"error.authAppInfoFetchFailed": "获取待授权应用的信息失败",
diff --git a/web/i18n/locales/zh-Hant/oauth.json b/web/i18n/locales/zh-Hant/oauth.json
index 66675c77f44..61ce543cead 100644
--- a/web/i18n/locales/zh-Hant/oauth.json
+++ b/web/i18n/locales/zh-Hant/oauth.json
@@ -1,4 +1,8 @@
{
+ "callback.error": "授權失敗",
+ "callback.errorHint": "請返回上一頁並重試。",
+ "callback.success": "授權完成",
+ "callback.successHint": "現在可以關閉此分頁並返回上一頁。",
"connect": "連接到",
"continue": "繼續",
"error.authAppInfoFetchFailed": "無法獲取應用程式授權信息",
diff --git a/web/i18n/resources.generated.d.ts b/web/i18n/resources.generated.d.ts
index 86dd9c7b6fc..830ffe28045 100644
--- a/web/i18n/resources.generated.d.ts
+++ b/web/i18n/resources.generated.d.ts
@@ -3467,6 +3467,10 @@ export default interface Resources {
'toolsPage.toolPlugin': 'Tool Plugin'
}
oauth: {
+ 'callback.error': 'Authorization failed'
+ 'callback.errorHint': 'Please return to the previous page and try again.'
+ 'callback.success': 'Authorization complete'
+ 'callback.successHint': 'You can now close this tab and return to the previous page.'
connect: 'Connect to'
continue: 'Continue'
'error.authAppInfoFetchFailed': 'Failed to fetch app info for authorization'