diff --git a/.changeset/quiet-devices-verify.md b/.changeset/quiet-devices-verify.md new file mode 100644 index 00000000000..f6d5550a5f2 --- /dev/null +++ b/.changeset/quiet-devices-verify.md @@ -0,0 +1,13 @@ +--- +'@clerk/astro': minor +'@clerk/clerk-js': minor +'@clerk/localizations': minor +'@clerk/nextjs': minor +'@clerk/nuxt': minor +'@clerk/react': minor +'@clerk/shared': minor +'@clerk/ui': minor +'@clerk/vue': minor +--- + +Add an authenticated OAuth device verification component and workflow hook for approving or denying OAuth Device Authorization Grant requests. diff --git a/packages/astro/src/astro-components/index.ts b/packages/astro/src/astro-components/index.ts index 0f02bca09ff..a9fe8255964 100644 --- a/packages/astro/src/astro-components/index.ts +++ b/packages/astro/src/astro-components/index.ts @@ -29,5 +29,6 @@ export { default as CreateOrganization } from './interactive/CreateOrganization. export { default as GoogleOneTap } from './interactive/GoogleOneTap.astro'; export { default as Waitlist } from './interactive/Waitlist.astro'; export { default as OAuthConsent } from './interactive/OAuthConsent.astro'; +export { default as OAuthDeviceVerification } from './interactive/OAuthDeviceVerification.astro'; export { default as PricingTable } from './interactive/PricingTable.astro'; export { default as APIKeys } from './interactive/APIKeys.astro'; diff --git a/packages/astro/src/astro-components/interactive/OAuthDeviceVerification.astro b/packages/astro/src/astro-components/interactive/OAuthDeviceVerification.astro new file mode 100644 index 00000000000..7177a3750d8 --- /dev/null +++ b/packages/astro/src/astro-components/interactive/OAuthDeviceVerification.astro @@ -0,0 +1,11 @@ +--- +import type { OAuthDeviceVerificationProps } from '@clerk/shared/types'; +type Props = OAuthDeviceVerificationProps; + +import InternalUIComponentRenderer from './InternalUIComponentRenderer.astro'; +--- + + diff --git a/packages/astro/src/internal/mount-clerk-astro-js-components.ts b/packages/astro/src/internal/mount-clerk-astro-js-components.ts index bb4807aacf1..4177de7698a 100644 --- a/packages/astro/src/internal/mount-clerk-astro-js-components.ts +++ b/packages/astro/src/internal/mount-clerk-astro-js-components.ts @@ -21,6 +21,7 @@ const mountAllClerkAstroJSComponents = () => { waitlist: 'mountWaitlist', 'pricing-table': 'mountPricingTable', 'api-keys': 'mountAPIKeys', + 'oauth-device-verification': '__internal_mountOAuthDeviceVerification', } as const satisfies Record; Object.entries(mountFns).forEach(([category, mountFn]) => { diff --git a/packages/astro/src/react/__tests__/uiComponents.test.tsx b/packages/astro/src/react/__tests__/uiComponents.test.tsx new file mode 100644 index 00000000000..cb227ecc918 --- /dev/null +++ b/packages/astro/src/react/__tests__/uiComponents.test.tsx @@ -0,0 +1,48 @@ +import { render } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { $clerk, $csrState } from '../../stores/internal'; +import { OAuthDeviceVerification } from '../uiComponents'; + +describe('', () => { + afterEach(() => { + $clerk.set(null); + $csrState.set({ + isLoaded: false, + client: undefined, + user: undefined, + session: undefined, + organization: undefined, + }); + }); + + it('updates the mounted component when its appearance changes', () => { + const mount = vi.fn(); + const unmount = vi.fn(); + const updateProps = vi.fn(); + $clerk.set({ + __internal_mountOAuthDeviceVerification: mount, + __internal_unmountOAuthDeviceVerification: unmount, + __internal_updateProps: updateProps, + } as any); + $csrState.set({ + isLoaded: true, + client: undefined, + user: undefined, + session: undefined, + organization: undefined, + }); + const firstAppearance = {}; + const secondAppearance = {}; + const { rerender } = render(); + + rerender(); + + expect(mount).toHaveBeenCalledOnce(); + expect(updateProps).toHaveBeenCalledOnce(); + expect(updateProps).toHaveBeenCalledWith({ + node: expect.any(HTMLDivElement), + props: { appearance: secondAppearance }, + }); + }); +}); diff --git a/packages/astro/src/react/uiComponents.tsx b/packages/astro/src/react/uiComponents.tsx index e8d420e82da..c964904f027 100644 --- a/packages/astro/src/react/uiComponents.tsx +++ b/packages/astro/src/react/uiComponents.tsx @@ -1,6 +1,7 @@ import type { GoogleOneTapProps, OAuthConsentProps, + OAuthDeviceVerificationProps, OrganizationListProps, OrganizationProfileProps, OrganizationSwitcherProps, @@ -207,3 +208,14 @@ export const OAuthConsent = withClerk(({ clerk, ...props }: WithClerkProp ); }, 'OAuthConsent'); + +export const OAuthDeviceVerification = withClerk(({ clerk, ...props }: WithClerkProp) => { + return ( + + ); +}, 'OAuthDeviceVerification'); diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts index c6c563a68da..54840bbe21b 100644 --- a/packages/astro/src/types.ts +++ b/packages/astro/src/types.ts @@ -119,4 +119,5 @@ export type InternalUIComponentId = | 'google-one-tap' | 'waitlist' | 'pricing-table' - | 'api-keys'; + | 'api-keys' + | 'oauth-device-verification'; diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 7bb1612e181..d957bf780e2 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -99,6 +99,7 @@ import type { LoadedClerk, NavigateOptions, OAuthApplicationNamespace, + OAuthDeviceVerificationProps, OAuthTransport, OrganizationListProps, OrganizationProfileProps, @@ -1545,6 +1546,36 @@ export class Clerk implements ClerkInterface { return this.unmountOAuthConsent(node); }; + public __internal_mountOAuthDeviceVerification = (node: HTMLDivElement, props?: OAuthDeviceVerificationProps) => { + if (noUserExists(this)) { + if (this.#instanceType === 'development') { + throw new ClerkRuntimeError(warnings.cannotRenderOAuthDeviceVerificationComponentWhenUserDoesNotExist, { + code: CANNOT_RENDER_USER_MISSING_ERROR_CODE, + }); + } + return; + } + + this.assertComponentsReady(this.#clerkUI); + const component = 'OAuthDeviceVerification'; + void this.#clerkUI + .then(ui => ui.ensureMounted({ preloadHint: component })) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'oauthDeviceVerification', + node, + props, + }), + ); + + this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); + }; + + public __internal_unmountOAuthDeviceVerification = (node: HTMLDivElement) => { + void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); + }; + /** * Mount an API keys component at the target element. * @param targetNode Target to mount the APIKeys component. diff --git a/packages/clerk-js/src/core/modules/oauthApplication/__tests__/OAuthApplication.test.ts b/packages/clerk-js/src/core/modules/oauthApplication/__tests__/OAuthApplication.test.ts index 8cc770ff8bb..5d46db59bde 100644 --- a/packages/clerk-js/src/core/modules/oauthApplication/__tests__/OAuthApplication.test.ts +++ b/packages/clerk-js/src/core/modules/oauthApplication/__tests__/OAuthApplication.test.ts @@ -1,5 +1,10 @@ import { ClerkAPIResponseError } from '@clerk/shared/error'; -import type { InstanceType, OAuthConsentInfoJSON } from '@clerk/shared/types'; +import type { + InstanceType, + OAuthConsentInfoJSON, + OAuthDeviceVerificationInfoJSON, + OAuthDeviceVerificationResultJSON, +} from '@clerk/shared/types'; import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; import { mockFetch } from '@/test/core-fixtures'; @@ -20,6 +25,34 @@ const consentPayload: OAuthConsentInfoJSON = { scopes: [{ scope: 'openid', description: 'OpenID', requires_consent: true }], }; +const deviceVerificationPayload: OAuthDeviceVerificationInfoJSON = { + oauth_application_name: 'TV App', + oauth_application_logo_url: null, + client_id: 'client_device', + scopes: [{ scope: 'profile', description: 'Your profile', requires_consent: true }], + status: 'pending', + expires_at: 1_800_000_000_000, +}; + +const deviceVerificationResultPayload: OAuthDeviceVerificationResultJSON = { + object: 'oauth_device_verification', + status: 'approved', +}; + +function setFapiClerk() { + BaseResource.clerk = { + getFapiClient: () => + createFapiClient({ + frontendApi: 'clerk.example.com', + getSessionId: () => undefined, + instanceType: 'development' as InstanceType, + }), + __internal_setCountry: vi.fn(), + handleUnauthenticated: vi.fn(), + __internal_handleUnauthenticatedDevBrowser: vi.fn(), + } as any; +} + describe('OAuthApplication', () => { let oauthApp: OAuthApplication; @@ -222,4 +255,150 @@ describe('OAuthApplication', () => { expect(result).toContain('__clerk_db_jwt=devjwt'); }); }); + + describe('lookupDeviceVerification', () => { + it.each([ + ['direct', deviceVerificationPayload], + ['enveloped', { response: deviceVerificationPayload }], + ])('maps a %s FAPI response', async (_shape, response) => { + const fetchSpy = vi.spyOn(BaseResource, '_fetch').mockResolvedValue(response as any); + BaseResource.clerk = {} as any; + + await expect(oauthApp.lookupDeviceVerification({ userCode: 'bcdf ghjk' })).resolves.toEqual({ + oauthApplicationName: 'TV App', + oauthApplicationLogoUrl: null, + clientId: 'client_device', + scopes: [{ scope: 'profile', description: 'Your profile', requiresConsent: true }], + status: 'pending', + expiresAt: 1_800_000_000_000, + }); + expect(fetchSpy).toHaveBeenCalledWith( + { + method: 'POST', + path: '/me/oauth/device/lookup', + body: { userCode: 'bcdf ghjk' }, + }, + { skipUpdateClient: true }, + ); + }); + + it('defaults missing scopes to an empty array', async () => { + vi.spyOn(BaseResource, '_fetch').mockResolvedValue({ + response: { ...deviceVerificationPayload, scopes: undefined }, + } as any); + BaseResource.clerk = {} as any; + + await expect(oauthApp.lookupDeviceVerification({ userCode: 'BCDF-GHJK' })).resolves.toMatchObject({ scopes: [] }); + }); + + it.each(['pending', 'approved', 'denied', 'consumed'] as const)('preserves the %s lookup status', async status => { + vi.spyOn(BaseResource, '_fetch').mockResolvedValue({ + response: { ...deviceVerificationPayload, status }, + } as any); + BaseResource.clerk = {} as any; + + await expect(oauthApp.lookupDeviceVerification({ userCode: 'BCDF-GHJK' })).resolves.toMatchObject({ status }); + }); + + it('sends one credentialed form request with a snake-case user code field', async () => { + mockFetch(true, 200, deviceVerificationPayload); + setFapiClerk(); + + await oauthApp.lookupDeviceVerification({ userCode: 'BCDF-GHJK' }); + + expect(global.fetch).toHaveBeenCalledOnce(); + const [url, init] = (global.fetch as Mock).mock.calls[0]; + expect(url.toString()).toContain('/v1/me/oauth/device/lookup'); + expect(init).toMatchObject({ credentials: 'include', method: 'POST', body: 'user_code=BCDF-GHJK' }); + }); + + it('throws a network error when _fetch returns null', async () => { + vi.spyOn(BaseResource, '_fetch').mockResolvedValue(null); + BaseResource.clerk = {} as any; + + await expect(oauthApp.lookupDeviceVerification({ userCode: 'BCDF-GHJK' })).rejects.toMatchObject({ + code: 'network_error', + }); + }); + + it('propagates FAPI errors without retrying', async () => { + const error = new ClerkAPIResponseError('Unknown device code', { + data: [{ code: 'resource_not_found', message: 'Unknown device code' }], + status: 404, + }); + const fetchSpy = vi.spyOn(BaseResource, '_fetch').mockRejectedValue(error); + BaseResource.clerk = {} as any; + + await expect(oauthApp.lookupDeviceVerification({ userCode: 'BCDF-GHJK' })).rejects.toBe(error); + expect(fetchSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('submitDeviceVerification', () => { + it.each([ + ['direct', deviceVerificationResultPayload], + ['enveloped', { response: deviceVerificationResultPayload }], + ])('posts a decision and maps a %s FAPI response', async (_shape, response) => { + const fetchSpy = vi.spyOn(BaseResource, '_fetch').mockResolvedValue(response as any); + BaseResource.clerk = {} as any; + + await expect( + oauthApp.submitDeviceVerification({ + userCode: 'BCDF-GHJK', + approved: true, + organizationId: 'org_123', + }), + ).resolves.toEqual(deviceVerificationResultPayload); + expect(fetchSpy).toHaveBeenCalledWith( + { + method: 'POST', + path: '/me/oauth/device', + body: { userCode: 'BCDF-GHJK', approved: true, organizationId: 'org_123' }, + }, + { skipUpdateClient: true }, + ); + }); + + it('omits organizationId through the FAPI form encoder when undefined', async () => { + const fetchSpy = vi.spyOn(BaseResource, '_fetch').mockResolvedValue(deviceVerificationResultPayload as any); + BaseResource.clerk = {} as any; + + await oauthApp.submitDeviceVerification({ userCode: 'BCDF-GHJK', approved: false }); + + expect(fetchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + body: { userCode: 'BCDF-GHJK', approved: false, organizationId: undefined }, + }), + { skipUpdateClient: true }, + ); + }); + + it('sends a snake-case decision form and omits an undefined organization', async () => { + mockFetch(true, 200, deviceVerificationResultPayload); + setFapiClerk(); + + await oauthApp.submitDeviceVerification({ userCode: 'BCDF-GHJK', approved: false }); + + expect(global.fetch).toHaveBeenCalledOnce(); + const [url, init] = (global.fetch as Mock).mock.calls[0]; + expect(url.toString()).toContain('/v1/me/oauth/device'); + expect(init).toMatchObject({ + credentials: 'include', + method: 'POST', + body: 'user_code=BCDF-GHJK&approved=false', + }); + }); + + it('propagates decision errors without retrying', async () => { + const error = new ClerkAPIResponseError('No longer pending', { + data: [{ code: 'bad_request', message: 'No longer pending' }], + status: 400, + }); + const fetchSpy = vi.spyOn(BaseResource, '_fetch').mockRejectedValue(error); + BaseResource.clerk = {} as any; + + await expect(oauthApp.submitDeviceVerification({ userCode: 'BCDF-GHJK', approved: true })).rejects.toBe(error); + expect(fetchSpy).toHaveBeenCalledOnce(); + }); + }); }); diff --git a/packages/clerk-js/src/core/modules/oauthApplication/index.ts b/packages/clerk-js/src/core/modules/oauthApplication/index.ts index 5907bb51979..edd313c7b80 100644 --- a/packages/clerk-js/src/core/modules/oauthApplication/index.ts +++ b/packages/clerk-js/src/core/modules/oauthApplication/index.ts @@ -1,9 +1,16 @@ import { ClerkRuntimeError } from '@clerk/shared/error'; import type { + ClerkResourceJSON, GetOAuthConsentInfoParams, + LookupOAuthDeviceVerificationParams, OAuthApplicationNamespace, OAuthConsentInfo, OAuthConsentInfoJSON, + OAuthDeviceVerificationInfo, + OAuthDeviceVerificationInfoJSON, + OAuthDeviceVerificationResult, + OAuthDeviceVerificationResultJSON, + SubmitOAuthDeviceVerificationParams, } from '@clerk/shared/types'; import { BaseResource } from '../../resources/internal'; @@ -45,6 +52,61 @@ export class OAuthApplication implements OAuthApplicationNamespace { }; } + async lookupDeviceVerification(params: LookupOAuthDeviceVerificationParams): Promise { + const json = await BaseResource._fetch( + { + method: 'POST', + path: '/me/oauth/device/lookup', + body: { userCode: params.userCode } as any, + }, + { skipUpdateClient: true }, + ); + + if (!json) { + throw new ClerkRuntimeError('Network request failed while offline', { code: 'network_error' }); + } + + const data = json.response ?? json; + return { + oauthApplicationName: data.oauth_application_name, + oauthApplicationLogoUrl: data.oauth_application_logo_url, + clientId: data.client_id, + scopes: + data.scopes?.map(scope => ({ + scope: scope.scope, + description: scope.description, + requiresConsent: scope.requires_consent, + })) ?? [], + status: data.status, + expiresAt: data.expires_at, + }; + } + + async submitDeviceVerification(params: SubmitOAuthDeviceVerificationParams): Promise { + const json = await BaseResource._fetch( + { + method: 'POST', + path: '/me/oauth/device', + body: { + userCode: params.userCode, + approved: params.approved, + organizationId: params.organizationId, + } as any, + }, + { skipUpdateClient: true }, + ); + + if (!json) { + throw new ClerkRuntimeError('Network request failed while offline', { code: 'network_error' }); + } + + const data = json.response ?? json; + return { + object: data.object, + status: data.status, + }; + } + buildConsentActionUrl({ clientId }: { clientId: string }): string { const url = BaseResource.fapiClient.buildUrl({ path: `/me/oauth/consent/${encodeURIComponent(clientId)}`, diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index d015635ca21..6c6b0d40347 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -952,6 +952,47 @@ export const arSA: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'أنشاء منظمة', action__invitationAccept: 'أنضمام', diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index 30c38f3dbaf..ea0b5488a6a 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -954,6 +954,47 @@ export const beBY: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Create organization', action__invitationAccept: 'Join', diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 95a43427b9d..286e93c319a 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -953,6 +953,47 @@ export const bgBG: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Създаване на организация', action__invitationAccept: 'Присъединяване', diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index 2a0065738a8..d5f8ab3a08c 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -959,6 +959,47 @@ export const bnIN: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'সংগঠন তৈরি করুন', action__invitationAccept: 'যোগ দিন', diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index 08430395624..e830eb1d243 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -959,6 +959,47 @@ export const caES: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Crea organització', action__invitationAccept: 'Uneix-te', diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index 80fd609523c..162b5b42b1c 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -957,6 +957,47 @@ export const csCZ: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Vytvořit organizaci', action__invitationAccept: 'Připojit se', diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index 9a7166d05cb..347842755bd 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -952,6 +952,47 @@ export const daDK: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Opret organisation', action__invitationAccept: 'Deltag', diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 5cbe85b07cd..9b28854e244 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -963,6 +963,47 @@ export const deDE: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Organisation erstellen', action__invitationAccept: 'Beitreten', diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 5765199ad34..2e7f03b6c9c 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -955,6 +955,47 @@ export const elGR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Δημιουργία οργανισμού', action__invitationAccept: 'Συμμετοχή', diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index 08f58fc229d..34a7803870b 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -952,6 +952,47 @@ export const enGB: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Create organisation', action__invitationAccept: 'Join', diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 2126dc5cbef..f13d1f147e6 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -978,6 +978,47 @@ export const enUS: LocalizationResource = { warning: 'Make sure that you trust {{applicationName}} ({{domainAction}}). You may be sharing sensitive data with this site or app.', }, + oauthDeviceVerification: { + action__tryAnotherCode: 'Enter another code', + confirmation: { + action__approve: 'Approve', + action__deny: 'Deny', + scopeListTitle: 'This will allow {{applicationName}} access to:', + subtitle: 'Confirm this request for {{identifier}}', + title: 'Allow {{applicationName}} to access your account?', + warning: 'Only approve this request if you started it on your other device.', + }, + error: { + expiredSubtitle: 'Start over on your device.', + expiredTitle: 'This code expired', + genericSubtitle: 'Check your connection and try again.', + genericTitle: 'We could not verify this code', + invalidCode: 'Enter a valid 8-character code.', + rateLimitedSubtitle: 'Wait before trying another code.', + rateLimitedTitle: 'Too many attempts', + unknownCode: "We couldn't find that code. Check it and try again.", + }, + start: { + action__continue: 'Continue', + subtitle: 'Enter the code shown on the device or app you want to authorize.', + title: 'Verify a device', + userCodeLabel: 'Device code', + }, + status: { + alreadyApprovedSubtitle: 'Return to your device to continue.', + alreadyApprovedTitle: "You've approved this", + alreadyDecidedSubtitle: 'It was decided elsewhere. Return to your device.', + alreadyDecidedTitle: 'This request was already completed', + alreadyDeniedSubtitle: 'Start over on your device if you want to try again.', + alreadyDeniedTitle: 'This request was denied', + approvedSubtitle: 'You approved this request. Return to your device to continue.', + approvedTitle: 'Device approved', + consumedSubtitle: 'Your device is authorized. You can close this window.', + consumedTitle: 'This code has already been used', + deniedSubtitle: 'You denied this request. Return to your device.', + deniedTitle: 'Access denied', + }, + }, organizationList: { action__createOrganization: 'Create organization', action__invitationAccept: 'Join', diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index 4045c9510d4..7d8c5c42db2 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -954,6 +954,47 @@ export const esCR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Crear organización', action__invitationAccept: 'Unirse', diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 1ea16637c51..bf430d147bd 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -959,6 +959,47 @@ export const esES: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Crear organización', action__invitationAccept: 'Unirse', diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index da89e3bd7c0..4dd7048c823 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -955,6 +955,47 @@ export const esMX: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Crear organización', action__invitationAccept: 'Unirse', diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 897ab08b4bf..ef7e529367a 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -954,6 +954,47 @@ export const esUY: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Crear organización', action__invitationAccept: 'Unirse', diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index f5cfd0adb38..a3260652238 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -958,6 +958,47 @@ export const faIR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'ایجاد سازمان', action__invitationAccept: 'بپیوندید', diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 7505bd2bf59..dacd9caf9a4 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -959,6 +959,47 @@ export const fiFI: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Luo organisaatio', action__invitationAccept: 'Liity', diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index cda07ac8ba0..adfc9d705cd 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -963,6 +963,47 @@ export const frFR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Créer une organisation', action__invitationAccept: 'Rejoindre', diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index f855917b9c4..0211707a632 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -951,6 +951,47 @@ export const heIL: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'צור ארגון', action__invitationAccept: 'הצטרף', diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index 1e3edf7fe46..9ea6d8a4a77 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -959,6 +959,47 @@ export const hiIN: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'संगठन बनाएँ', action__invitationAccept: 'शामिल हों', diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index 75de0ec3117..f8d00284c0a 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -959,6 +959,47 @@ export const hrHR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Kreiraj organizaciju', action__invitationAccept: 'Pridruži se', diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index 2a230e4d082..140e54e8f3a 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -960,6 +960,47 @@ export const huHU: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Szervezet létrehozása', action__invitationAccept: 'Csatlakozás', diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index e8cbda31019..09248a313a2 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -953,6 +953,47 @@ export const idID: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Buat organisasi', action__invitationAccept: 'Gabung', diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index b44378836d4..8ad2ae89b97 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -959,6 +959,47 @@ export const isIS: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Stofna samtök', action__invitationAccept: 'Ganga í', diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index c8190f1a768..891ba86b378 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -960,6 +960,47 @@ export const itIT: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Crea organizzazione', action__invitationAccept: 'Unisciti', diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index dc44d3d6020..9d78ce771bb 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -959,6 +959,47 @@ export const jaJP: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: '組織を作成する', action__invitationAccept: '参加する', diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index 77216c81646..064db249686 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -952,6 +952,47 @@ export const kkKZ: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Ұйым құру', action__invitationAccept: 'Қосылу', diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 6ea515cc5ce..452f58cc752 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -955,6 +955,47 @@ export const koKR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: '조직 만들기', action__invitationAccept: '참여', diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index ec1e61c0b66..84d85a3e9f3 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -953,6 +953,47 @@ export const mnMN: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Байгууллага үүсгэх', action__invitationAccept: 'Нэгдэх', diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index 479d5ffdc4c..29f1ce8c1e2 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -961,6 +961,47 @@ export const msMY: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Cipta organisasi', action__invitationAccept: 'Sertai', diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index 8658b2f3e90..c0a5c0d67e4 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -961,6 +961,47 @@ export const nbNO: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Lag organisasjon', action__invitationAccept: 'Bli med', diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index 2292775f4a6..b8b5079e655 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -952,6 +952,47 @@ export const nlBE: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Creëer organisatie', action__invitationAccept: 'Toetreden', diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index 02df1e48647..65224e71070 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -952,6 +952,47 @@ export const nlNL: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Creëer organisatie', action__invitationAccept: 'Toetreden', diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 97476b8d3b9..66e0b09ae66 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -953,6 +953,47 @@ export const plPL: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Utwórz organizację', action__invitationAccept: 'Dołącz', diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index eed608b6595..ba9e289f124 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -960,6 +960,47 @@ export const ptBR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Criar organização', action__invitationAccept: 'Participar', diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index de524ce7abe..b703bfa28c4 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -961,6 +961,47 @@ export const ptPT: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Criar organização', action__invitationAccept: 'Participar', diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index ebccd8b9e5b..9120f132875 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -960,6 +960,47 @@ export const roRO: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Creează organizație', action__invitationAccept: 'Alătură-te', diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index 5418ecdae52..1eb4abd7cd7 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -956,6 +956,47 @@ export const ruRU: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Создать организацию', action__invitationAccept: 'Присоединиться', diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index 5caf4769636..c7127dafac3 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -953,6 +953,47 @@ export const skSK: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Vytvoriť organizáciu', action__invitationAccept: 'Pripojiť sa', diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index 79368a7ba39..af61b012500 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -952,6 +952,47 @@ export const srRS: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Kreiraj organizaciju', action__invitationAccept: 'Pridruži se', diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 09c59f11542..dcd5d346b13 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -953,6 +953,47 @@ export const svSE: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Skapa organisation', action__invitationAccept: 'Gå med', diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index 6c3f11c130b..9c81cbae11a 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -961,6 +961,47 @@ export const taIN: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'நிறுவனத்தை உருவாக்கு', action__invitationAccept: 'சேரவும்', diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index a26ccf4ad52..c73672423bb 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -959,6 +959,47 @@ export const teIN: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'సంస్థను సృష్టించండి', action__invitationAccept: 'చేరండి', diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 6412a174a91..a545cb21f31 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -955,6 +955,47 @@ export const thTH: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'สร้างองค์กร', action__invitationAccept: 'เข้าร่วม', diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index 3a29d569336..5b4762ab1fc 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -952,6 +952,47 @@ export const trTR: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Organizasyon oluştur', action__invitationAccept: 'Katıl', diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index 9f30408df85..c88bba8dfa7 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -953,6 +953,47 @@ export const ukUA: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Create organization', action__invitationAccept: 'Join', diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index c7fcb2a1deb..159fb9731c7 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -959,6 +959,47 @@ export const viVN: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: 'Tạo tổ chức', action__invitationAccept: 'Tham gia', diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 067bad786d6..3b0f93e0b0b 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -950,6 +950,47 @@ export const zhCN: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: '创建组织', action__invitationAccept: '加入', diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index 395894de0c0..992cc87adab 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -953,6 +953,47 @@ export const zhTW: LocalizationResource = { viewFullUrl: undefined, warning: undefined, }, + oauthDeviceVerification: { + action__tryAnotherCode: undefined, + confirmation: { + action__approve: undefined, + action__deny: undefined, + scopeListTitle: undefined, + subtitle: undefined, + title: undefined, + warning: undefined, + }, + error: { + expiredSubtitle: undefined, + expiredTitle: undefined, + genericSubtitle: undefined, + genericTitle: undefined, + invalidCode: undefined, + rateLimitedSubtitle: undefined, + rateLimitedTitle: undefined, + unknownCode: undefined, + }, + start: { + action__continue: undefined, + subtitle: undefined, + title: undefined, + userCodeLabel: undefined, + }, + status: { + alreadyApprovedSubtitle: undefined, + alreadyApprovedTitle: undefined, + alreadyDecidedSubtitle: undefined, + alreadyDecidedTitle: undefined, + alreadyDeniedSubtitle: undefined, + alreadyDeniedTitle: undefined, + approvedSubtitle: undefined, + approvedTitle: undefined, + consumedSubtitle: undefined, + consumedTitle: undefined, + deniedSubtitle: undefined, + deniedTitle: undefined, + }, + }, organizationList: { action__createOrganization: '建立組織', action__invitationAccept: '加入', diff --git a/packages/nextjs/src/client-boundary/hooks.ts b/packages/nextjs/src/client-boundary/hooks.ts index 4527d8efc22..81be38b570d 100644 --- a/packages/nextjs/src/client-boundary/hooks.ts +++ b/packages/nextjs/src/client-boundary/hooks.ts @@ -5,6 +5,7 @@ export { useClerk, useEmailLink, useOAuthConsent, + useOAuthDeviceVerification, useOrganization, useOrganizationList, useOrganizationCreationDefaults, diff --git a/packages/nextjs/src/client-boundary/uiComponents.tsx b/packages/nextjs/src/client-boundary/uiComponents.tsx index 1c2982bd933..82e719046d6 100644 --- a/packages/nextjs/src/client-boundary/uiComponents.tsx +++ b/packages/nextjs/src/client-boundary/uiComponents.tsx @@ -18,6 +18,7 @@ export { HandleSSOCallback, InviteMembersButton, OAuthConsent, + OAuthDeviceVerification, OrganizationList, OrganizationSwitcher, PricingTable, diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index f8321ccb4ba..a90a2a975c6 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -27,6 +27,7 @@ export { GoogleOneTap, InviteMembersButton, OAuthConsent, + OAuthDeviceVerification, OrganizationList, OrganizationProfile, OrganizationSwitcher, @@ -55,6 +56,7 @@ export { useClerk, useEmailLink, useOAuthConsent, + useOAuthDeviceVerification, useOrganization, useOrganizationCreationDefaults, useOrganizationList, diff --git a/packages/nuxt/src/runtime/components/index.ts b/packages/nuxt/src/runtime/components/index.ts index 0d73def7d8c..9b533e29e01 100644 --- a/packages/nuxt/src/runtime/components/index.ts +++ b/packages/nuxt/src/runtime/components/index.ts @@ -6,6 +6,7 @@ export { OrganizationSwitcher, GoogleOneTap, OAuthConsent, + OAuthDeviceVerification, Waitlist, // Control components ClerkLoaded, diff --git a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap index fb0e0ea41c9..b186435cbc7 100644 --- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap @@ -28,6 +28,7 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "HandleSSOCallback", "InviteMembersButton", "OAuthConsent", + "OAuthDeviceVerification", "OrganizationList", "OrganizationProfile", "OrganizationSwitcher", @@ -64,6 +65,7 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "useClerk", "useEmailLink", "useOAuthConsent", + "useOAuthDeviceVerification", "useOrganization", "useOrganizationCreationDefaults", "useOrganizationList", diff --git a/packages/react/src/__tests__/isomorphicClerk.test.ts b/packages/react/src/__tests__/isomorphicClerk.test.ts index 331baf77157..43c1e8a6e84 100644 --- a/packages/react/src/__tests__/isomorphicClerk.test.ts +++ b/packages/react/src/__tests__/isomorphicClerk.test.ts @@ -256,6 +256,22 @@ describe('isomorphicClerk', () => { ).resolves.toBeUndefined(); }); + it('does nothing when clerk-js predates OAuth device verification mounting', () => { + const isomorphicClerk = new IsomorphicClerk({ publishableKey: 'pk_test_XXX' }); + const node = document.createElement('div'); + + isomorphicClerk.__internal_mountOAuthDeviceVerification(node); + + expect(() => + (isomorphicClerk as any).replayInterceptedInvocations({ + addListener: vi.fn(), + loaded: true, + } as unknown as BrowserClerk), + ).not.toThrow(); + expect(() => isomorphicClerk.__internal_mountOAuthDeviceVerification(node)).not.toThrow(); + expect(() => isomorphicClerk.__internal_unmountOAuthDeviceVerification(node)).not.toThrow(); + }); + it('calls __internal_handleResourceCallback immediately after clerk-js has loaded', async () => { const signInOrUp = {} as unknown as SignInResource; const params: HandleOAuthCallbackParams = { signInUrl: '/sign-in' }; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 8baf38bf307..c241edfb954 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -3,6 +3,7 @@ export { CreateOrganization, GoogleOneTap, OAuthConsent, + OAuthDeviceVerification, OrganizationList, OrganizationProfile, OrganizationSwitcher, diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index b8ab9bdddbb..0a101c93f6b 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -3,6 +3,7 @@ import type { APIKeysProps, CreateOrganizationProps, GoogleOneTapProps, + OAuthDeviceVerificationProps, OrganizationListProps, OrganizationProfileProps, OrganizationSwitcherProps, @@ -31,6 +32,7 @@ import { userProfileLinkRenderedError, userProfilePageRenderedError, } from '../errors/messages'; +import type { IsomorphicClerk } from '../isomorphicClerk'; import type { CustomPortalsRendererProps, MountProps, @@ -59,6 +61,8 @@ type FallbackProp = { fallback?: ReactNode; }; +type ClerkWithInternalUpdateProps = WithClerkProp['clerk'] & Pick; + type UserProfileExportType = typeof _UserProfile & { Page: typeof UserProfilePage; Link: typeof UserProfileLink; @@ -672,6 +676,48 @@ export const OAuthConsent = withClerk( { component: 'OAuthConsent', renderWhileLoading: true }, ); +/** + * Renders the OAuth device verification flow for a signed-in user to authorize a device or app. + * + * @example + * ### Add a device verification page + * + * ```tsx + * import { OAuthDeviceVerification } from '@clerk/react'; + * + * export default function DeviceVerificationPage() { + * return ; + * } + * ``` + */ +export const OAuthDeviceVerification = withClerk( + ({ clerk, component, fallback, ...props }: WithClerkProp) => { + const mountingStatus = useWaitForComponentMount(component); + const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; + + const rendererRootProps = { + ...(shouldShowFallback && fallback && { style: { display: 'none' } }), + }; + + return ( + <> + {shouldShowFallback && fallback} + {clerk.loaded && ( + void (clerk as ClerkWithInternalUpdateProps).__internal_updateProps(props)} + props={props} + rootProps={rendererRootProps} + /> + )} + + ); + }, + { component: 'OAuthDeviceVerification', renderWhileLoading: true }, +); + export const UserAvatar = withClerk( ({ clerk, component, fallback, ...props }: WithClerkProp) => { const mountingStatus = useWaitForComponentMount(component); diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index e33d87ea1c6..72d097a8af9 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -12,6 +12,7 @@ export { useReverification, useAPIKeys, useOAuthConsent, + useOAuthDeviceVerification, __experimental_useCheckout, __experimental_CheckoutProvider, __experimental_usePaymentElement, diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index c4c160c35cf..790e1ac099b 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -41,6 +41,7 @@ import type { LoadedClerk, OAuthApplicationNamespace, OAuthConsentProps, + OAuthDeviceVerificationProps, OrganizationListProps, OrganizationProfileProps, OrganizationResource, @@ -166,6 +167,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountAPIKeysNodes = new Map(); private premountConfigureSSONodes = new Map(); private premountOAuthConsentNodes = new Map(); + private premountOAuthDeviceVerificationNodes = new Map(); private premountTaskChooseOrganizationNodes = new Map(); private premountTaskResetPasswordNodes = new Map(); private premountTaskSetupMFANodes = new Map(); @@ -803,6 +805,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.__internal_mountOAuthConsent(node, props); }); + if (typeof clerkjs.__internal_mountOAuthDeviceVerification === 'function') { + this.premountOAuthDeviceVerificationNodes.forEach((props, node) => { + clerkjs.__internal_mountOAuthDeviceVerification(node, props); + }); + } + this.premountTaskChooseOrganizationNodes.forEach((props, node) => { clerkjs.mountTaskChooseOrganization(node, props); }); @@ -1403,6 +1411,23 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.__internal_unmountOAuthConsent(node); }; + __internal_mountOAuthDeviceVerification = (node: HTMLDivElement, props?: OAuthDeviceVerificationProps) => { + if (this.clerkjs && this.loaded) { + if (typeof this.clerkjs.__internal_mountOAuthDeviceVerification === 'function') { + this.clerkjs.__internal_mountOAuthDeviceVerification(node, props); + } + } else { + this.premountOAuthDeviceVerificationNodes.set(node, props); + } + }; + + __internal_unmountOAuthDeviceVerification = (node: HTMLDivElement) => { + if (this.clerkjs && this.loaded && typeof this.clerkjs.__internal_unmountOAuthDeviceVerification === 'function') { + this.clerkjs.__internal_unmountOAuthDeviceVerification(node); + } + this.premountOAuthDeviceVerificationNodes.delete(node); + }; + mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps): void => { if (this.clerkjs && this.loaded) { this.clerkjs.mountTaskChooseOrganization(node, props); diff --git a/packages/shared/src/internal/clerk-js/warnings.ts b/packages/shared/src/internal/clerk-js/warnings.ts index 4081f830339..51ea3a6a4bc 100644 --- a/packages/shared/src/internal/clerk-js/warnings.ts +++ b/packages/shared/src/internal/clerk-js/warnings.ts @@ -82,6 +82,8 @@ const warnings = { 'The component cannot be rendered when organization API keys are disabled. Since organization API keys are disabled, this is no-op.', cannotRenderOAuthConsentComponentWhenUserDoesNotExist: ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', + cannotRenderOAuthDeviceVerificationComponentWhenUserDoesNotExist: + ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', cannotRenderConfigureSSOComponentWhenUserDoesNotExist: ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', cannotRenderConfigureSSOComponentWhenDisabled: diff --git a/packages/shared/src/react/hooks/__tests__/useOAuthDeviceVerification.spec.tsx b/packages/shared/src/react/hooks/__tests__/useOAuthDeviceVerification.spec.tsx new file mode 100644 index 00000000000..04be0a56d23 --- /dev/null +++ b/packages/shared/src/react/hooks/__tests__/useOAuthDeviceVerification.spec.tsx @@ -0,0 +1,254 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { OAuthDeviceVerificationInfo, OAuthDeviceVerificationResult } from '@/types'; + +import { useOAuthDeviceVerification } from '../useOAuthDeviceVerification'; +import { createMockClerk } from './mocks/clerk'; +import { wrapper } from './wrapper'; + +const verificationInfo: OAuthDeviceVerificationInfo = { + oauthApplicationName: 'TV App', + oauthApplicationLogoUrl: null, + clientId: 'client_device', + scopes: [], + status: 'pending', + expiresAt: 1_800_000_000_000, +}; + +const approvedResult: OAuthDeviceVerificationResult = { object: 'oauth_device_verification', status: 'approved' }; +const lookupDeviceVerification = vi.fn(() => Promise.resolve(verificationInfo)); +const submitDeviceVerification = vi.fn<() => Promise>(() => + Promise.resolve(approvedResult), +); +const mockClerk = createMockClerk({ + oauthApplication: { lookupDeviceVerification, submitDeviceVerification }, +}); + +vi.mock('../../contexts', () => ({ + useAssertWrappedByClerkProvider: () => {}, + useClerkInstanceContext: () => mockClerk, +})); + +describe('useOAuthDeviceVerification', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockClerk.loaded = true; + mockClerk.oauthApplication = { lookupDeviceVerification, submitDeviceVerification }; + }); + + it('does not make a request until an action is called', () => { + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isSubmitting).toBe(false); + expect(lookupDeviceVerification).not.toHaveBeenCalled(); + expect(submitDeviceVerification).not.toHaveBeenCalled(); + }); + + it('looks up verification information once and stores it', async () => { + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + await act(() => result.current.lookup({ userCode: 'bcdf ghjk' })); + + expect(lookupDeviceVerification).toHaveBeenCalledOnce(); + expect(lookupDeviceVerification).toHaveBeenCalledWith({ userCode: 'BCDFGHJK' }); + expect(result.current.data).toEqual(verificationInfo); + expect(result.current.error).toBeNull(); + }); + + it('submits approve and deny decisions with the expected bodies', async () => { + submitDeviceVerification + .mockResolvedValueOnce(approvedResult) + .mockResolvedValueOnce({ object: 'oauth_device_verification', status: 'denied' }); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + await act(() => result.current.approve({ userCode: 'bcdf ghjk', organizationId: 'org_123' })); + expect(submitDeviceVerification).toHaveBeenLastCalledWith({ + userCode: 'BCDFGHJK', + organizationId: 'org_123', + approved: true, + }); + + await act(() => result.current.deny({ userCode: 'BCDF-GHJK' })); + expect(submitDeviceVerification).toHaveBeenLastCalledWith({ userCode: 'BCDFGHJK', approved: false }); + expect(result.current.result?.status).toBe('denied'); + }); + + it('coalesces concurrent lookups for equivalent normalized codes', async () => { + let resolveLookup!: (value: typeof verificationInfo) => void; + lookupDeviceVerification.mockImplementationOnce(() => new Promise(resolve => (resolveLookup = resolve))); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let first!: Promise; + let second!: Promise; + act(() => { + first = result.current.lookup({ userCode: 'BCDF-GHJK' }); + second = result.current.lookup({ userCode: 'bcdf ghjk' }); + }); + + expect(first).toBe(second); + expect(lookupDeviceVerification).toHaveBeenCalledOnce(); + resolveLookup(verificationInfo); + await act(() => first); + }); + + it('rejects a distinct lookup while another code is pending', async () => { + let resolveLookup!: (value: typeof verificationInfo) => void; + lookupDeviceVerification.mockImplementationOnce(() => new Promise(resolve => (resolveLookup = resolve))); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let first!: Promise; + act(() => { + first = result.current.lookup({ userCode: 'BCDF-GHJK' }); + }); + + const second = result.current.lookup({ userCode: 'JKLM-NPQR' }); + await expect(second).rejects.toMatchObject({ code: 'oauth_device_verification_lookup_in_progress' }); + expect(lookupDeviceVerification).toHaveBeenCalledOnce(); + expect(result.current.isLoading).toBe(true); + + resolveLookup(verificationInfo); + await act(() => first); + expect(result.current.data).toEqual(verificationInfo); + }); + + it('rejects a pre-load lookup without leaving loading state stuck and succeeds after Clerk loads', async () => { + mockClerk.loaded = false; + mockClerk.oauthApplication = undefined; + const { result, rerender } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let request!: Promise; + expect(() => { + request = result.current.lookup({ userCode: 'BCDF-GHJK' }); + }).not.toThrow(); + await expect(request).rejects.toMatchObject({ code: 'oauth_device_verification_not_ready' }); + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.result).toBeUndefined(); + expect(result.current.error).toBeNull(); + + mockClerk.loaded = true; + mockClerk.oauthApplication = { lookupDeviceVerification, submitDeviceVerification }; + rerender(); + await act(() => result.current.lookup({ userCode: 'BCDF-GHJK' })); + + expect(lookupDeviceVerification).toHaveBeenCalledOnce(); + expect(result.current.data).toEqual(verificationInfo); + }); + + it('rejects a pre-load decision without entering submitting state', async () => { + mockClerk.loaded = false; + mockClerk.oauthApplication = undefined; + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + const request = result.current.approve({ userCode: 'BCDF-GHJK' }); + await expect(request).rejects.toMatchObject({ code: 'oauth_device_verification_not_ready' }); + expect(result.current.isSubmitting).toBe(false); + expect(result.current.result).toBeUndefined(); + expect(result.current.error).toBeNull(); + expect(submitDeviceVerification).not.toHaveBeenCalled(); + }); + + it('coalesces concurrent equivalent decisions', async () => { + let resolveSubmit!: (value: OAuthDeviceVerificationResult) => void; + submitDeviceVerification.mockImplementationOnce(() => new Promise(resolve => (resolveSubmit = resolve))); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let first!: Promise; + let second!: Promise; + act(() => { + first = result.current.approve({ userCode: 'BCDF-GHJK', organizationId: 'org_123' }); + second = result.current.approve({ userCode: 'bcdf ghjk', organizationId: 'org_123' }); + }); + + expect(first).toBe(second); + expect(submitDeviceVerification).toHaveBeenCalledOnce(); + expect(submitDeviceVerification).toHaveBeenCalledWith({ + userCode: 'BCDFGHJK', + organizationId: 'org_123', + approved: true, + }); + resolveSubmit(approvedResult); + await act(() => first); + }); + + it('rejects a conflicting decision while another decision is pending', async () => { + let resolveSubmit!: (value: OAuthDeviceVerificationResult) => void; + submitDeviceVerification.mockImplementationOnce(() => new Promise(resolve => (resolveSubmit = resolve))); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let first!: Promise; + act(() => { + first = result.current.approve({ userCode: 'BCDF-GHJK' }); + }); + + const second = result.current.deny({ userCode: 'BCDF-GHJK' }); + await expect(second).rejects.toMatchObject({ code: 'oauth_device_verification_submission_in_progress' }); + expect(submitDeviceVerification).toHaveBeenCalledOnce(); + expect(result.current.isSubmitting).toBe(true); + + resolveSubmit(approvedResult); + await act(() => first); + expect(result.current.result).toEqual(approvedResult); + }); + + it('stores and rethrows an action error without retrying', async () => { + const error = new Error('lookup failed'); + lookupDeviceVerification.mockRejectedValueOnce(error); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let caught: unknown; + await act(async () => { + try { + await result.current.lookup({ userCode: 'BCDF-GHJK' }); + } catch (err) { + caught = err; + } + }); + + expect(caught).toBe(error); + expect(lookupDeviceVerification).toHaveBeenCalledOnce(); + expect(result.current.error).toBe(error); + }); + + it('reset clears state and ignores a pending response', async () => { + let resolveLookup!: (value: typeof verificationInfo) => void; + lookupDeviceVerification.mockImplementationOnce(() => new Promise(resolve => (resolveLookup = resolve))); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let request!: Promise; + act(() => { + request = result.current.lookup({ userCode: 'BCDF-GHJK' }); + }); + act(() => result.current.reset()); + resolveLookup(verificationInfo); + await act(() => request); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + expect(result.current.data).toBeUndefined(); + expect(result.current.result).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + + it('reset keeps a pending decision locked until its request settles', async () => { + let resolveSubmit!: (value: OAuthDeviceVerificationResult) => void; + submitDeviceVerification.mockImplementationOnce(() => new Promise(resolve => (resolveSubmit = resolve))); + const { result } = renderHook(() => useOAuthDeviceVerification(), { wrapper }); + + let first!: Promise; + act(() => { + first = result.current.approve({ userCode: 'BCDF-GHJK' }); + }); + act(() => result.current.reset()); + + const second = result.current.deny({ userCode: 'BCDF-GHJK' }); + await expect(second).rejects.toMatchObject({ code: 'oauth_device_verification_submission_in_progress' }); + expect(submitDeviceVerification).toHaveBeenCalledOnce(); + + resolveSubmit(approvedResult); + await act(() => first); + await act(() => result.current.deny({ userCode: 'BCDF-GHJK' })); + expect(submitDeviceVerification).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/shared/src/react/hooks/index.ts b/packages/shared/src/react/hooks/index.ts index c4816d3d19c..3c4aa2ab8d5 100644 --- a/packages/shared/src/react/hooks/index.ts +++ b/packages/shared/src/react/hooks/index.ts @@ -2,6 +2,8 @@ export { assertContextExists, createContextAndHook } from './createContextAndHoo export { useAPIKeys } from './useAPIKeys'; export { useOAuthConsent } from './useOAuthConsent'; export type { UseOAuthConsentParams, UseOAuthConsentReturn } from './useOAuthConsent.types'; +export { useOAuthDeviceVerification } from './useOAuthDeviceVerification'; +export type { UseOAuthDeviceVerificationReturn } from './useOAuthDeviceVerification.types'; export { useOrganization } from './useOrganization'; export { useOrganizationCreationDefaults } from './useOrganizationCreationDefaults'; export type { diff --git a/packages/shared/src/react/hooks/useOAuthDeviceVerification.tsx b/packages/shared/src/react/hooks/useOAuthDeviceVerification.tsx new file mode 100644 index 00000000000..7daecdb49cb --- /dev/null +++ b/packages/shared/src/react/hooks/useOAuthDeviceVerification.tsx @@ -0,0 +1,198 @@ +import { useCallback, useRef, useState } from 'react'; + +import type { ClerkAPIResponseError } from '../../errors/clerkApiResponseError'; +import { ClerkRuntimeError } from '../../errors/clerkRuntimeError'; +import { eventMethodCalled } from '../../telemetry/events/method-called'; +import type { + LookupOAuthDeviceVerificationParams, + OAuthDeviceVerificationInfo, + OAuthDeviceVerificationResult, + SubmitOAuthDeviceVerificationParams, +} from '../../types'; +import { useAssertWrappedByClerkProvider, useClerkInstanceContext } from '../contexts'; +import type { UseOAuthDeviceVerificationReturn } from './useOAuthDeviceVerification.types'; + +const HOOK_NAME = 'useOAuthDeviceVerification'; +type HookError = ClerkAPIResponseError | ClerkRuntimeError; +type DecisionParams = Omit; +type PendingLookup = { + key: string; + promise: Promise; +}; +type PendingSubmit = { + key: string; + promise: Promise; +}; + +const normalizeUserCode = (userCode: string) => userCode.toUpperCase().replace(/[-\p{White_Space}]/gu, ''); + +const createNotReadyError = () => + new ClerkRuntimeError('Clerk must be loaded before using OAuth device verification.', { + code: 'oauth_device_verification_not_ready', + }); + +const createLookupInProgressError = () => + new ClerkRuntimeError('An OAuth device verification lookup is already in progress for another code.', { + code: 'oauth_device_verification_lookup_in_progress', + }); + +const createSubmissionInProgressError = () => + new ClerkRuntimeError('Another OAuth device verification decision is already in progress.', { + code: 'oauth_device_verification_submission_in_progress', + }); + +/** + * Provides state and actions for building a custom OAuth device verification flow. + * + * @returns The current verification state and methods for looking up, approving, denying, or resetting a request. + * + * @example + * ### Look up a device authorization request + * + * ```tsx + * import { useOAuthDeviceVerification } from '@clerk/react'; + * + * function CustomDeviceVerification({ userCode }: { userCode: string }) { + * const { data, isLoading, lookup } = useOAuthDeviceVerification(); + * + * const handleLookup = async () => { + * await lookup({ userCode }); + * }; + * + * return ( + * <> + * + * {data &&

Authorize {data.oauthApplicationName}?

} + * + * ); + * } + * ``` + */ +export function useOAuthDeviceVerification(): UseOAuthDeviceVerificationReturn { + useAssertWrappedByClerkProvider(HOOK_NAME); + const clerk = useClerkInstanceContext(); + const [data, setData] = useState(); + const [result, setResult] = useState(); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const pendingLookup = useRef(null); + const pendingSubmit = useRef(null); + const generation = useRef(0); + + clerk.telemetry?.record(eventMethodCalled(HOOK_NAME)); + + const lookup = useCallback( + (params: LookupOAuthDeviceVerificationParams) => { + const oauthApplication = clerk.loaded ? clerk.oauthApplication : undefined; + if (!oauthApplication) { + return Promise.reject(createNotReadyError()); + } + + const key = normalizeUserCode(params.userCode); + if (pendingLookup.current) { + if (pendingLookup.current.key === key) { + return pendingLookup.current.promise; + } + return Promise.reject(createLookupInProgressError()); + } + + const requestGeneration = generation.current; + setError(null); + setData(undefined); + setResult(undefined); + setIsLoading(true); + + const request = oauthApplication + .lookupDeviceVerification({ ...params, userCode: key }) + .then(info => { + if (generation.current === requestGeneration) { + setData(info); + } + return info; + }) + .catch((err: HookError) => { + if (generation.current === requestGeneration) { + setError(err); + } + throw err; + }) + .finally(() => { + if (pendingLookup.current?.promise === request) { + pendingLookup.current = null; + } + if (generation.current === requestGeneration) { + setIsLoading(false); + } + }); + + pendingLookup.current = { key, promise: request }; + return request; + }, + [clerk], + ); + + const submit = useCallback( + (params: DecisionParams, approved: boolean) => { + const oauthApplication = clerk.loaded ? clerk.oauthApplication : undefined; + if (!oauthApplication) { + return Promise.reject(createNotReadyError()); + } + + const userCode = normalizeUserCode(params.userCode); + const key = JSON.stringify([userCode, params.organizationId ?? null, approved]); + if (pendingSubmit.current) { + if (pendingSubmit.current.key === key) { + return pendingSubmit.current.promise; + } + return Promise.reject(createSubmissionInProgressError()); + } + + const requestGeneration = generation.current; + setError(null); + setIsSubmitting(true); + + const request = oauthApplication + .submitDeviceVerification({ ...params, userCode, approved }) + .then(decision => { + if (generation.current === requestGeneration) { + setResult(decision); + } + return decision; + }) + .catch((err: HookError) => { + if (generation.current === requestGeneration) { + setError(err); + } + throw err; + }) + .finally(() => { + if (pendingSubmit.current?.promise === request) { + pendingSubmit.current = null; + } + if (generation.current === requestGeneration) { + setIsSubmitting(false); + } + }); + + pendingSubmit.current = { key, promise: request }; + return request; + }, + [clerk], + ); + + const approve = useCallback((params: DecisionParams) => submit(params, true), [submit]); + const deny = useCallback((params: LookupOAuthDeviceVerificationParams) => submit(params, false), [submit]); + + const reset = useCallback(() => { + generation.current += 1; + pendingLookup.current = null; + setData(undefined); + setResult(undefined); + setError(null); + setIsLoading(false); + setIsSubmitting(false); + }, []); + + return { data, result, error, isLoading, isSubmitting, lookup, approve, deny, reset }; +} diff --git a/packages/shared/src/react/hooks/useOAuthDeviceVerification.types.ts b/packages/shared/src/react/hooks/useOAuthDeviceVerification.types.ts new file mode 100644 index 00000000000..9b223a58caf --- /dev/null +++ b/packages/shared/src/react/hooks/useOAuthDeviceVerification.types.ts @@ -0,0 +1,22 @@ +import type { ClerkAPIResponseError } from '../../errors/clerkApiResponseError'; +import type { ClerkRuntimeError } from '../../errors/clerkRuntimeError'; +import type { + LookupOAuthDeviceVerificationParams, + OAuthDeviceVerificationInfo, + OAuthDeviceVerificationResult, + SubmitOAuthDeviceVerificationParams, +} from '../../types'; + +type DecisionParams = Omit; + +export type UseOAuthDeviceVerificationReturn = { + data: OAuthDeviceVerificationInfo | undefined; + result: OAuthDeviceVerificationResult | undefined; + error: ClerkAPIResponseError | ClerkRuntimeError | null; + isLoading: boolean; + isSubmitting: boolean; + lookup: (params: LookupOAuthDeviceVerificationParams) => Promise; + approve: (params: DecisionParams) => Promise; + deny: (params: LookupOAuthDeviceVerificationParams) => Promise; + reset: () => void; +}; diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 0f57591517d..bf5325b7e62 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -848,6 +848,23 @@ export interface Clerk { */ unmountOAuthConsent: (targetNode: HTMLDivElement) => void; + /** + * Mounts an OAuth device verification component at the target element. + * + * @param targetNode - Target node to mount the OAuth device verification component. + * @param props - OAuth device verification configuration parameters. + * @internal + */ + __internal_mountOAuthDeviceVerification: (targetNode: HTMLDivElement, props?: OAuthDeviceVerificationProps) => void; + + /** + * Unmounts an OAuth device verification component from the target element. + * If there is no component mounted at the target node, this is a noop. + * + * @internal + */ + __internal_unmountOAuthDeviceVerification: (targetNode: HTMLDivElement) => void; + /** * Mounts a TaskChooseOrganization component at the target element. * @@ -2770,6 +2787,13 @@ export type OAuthConsentProps = { onDeny?: () => void; }; +export type OAuthDeviceVerificationProps = { + /** + * Customization options to fully match the Clerk component to your own brand. + */ + appearance?: ClerkAppearanceTheme; +}; + /** @deprecated Use OAuthConsentProps instead. */ export type __internal_OAuthConsentProps = OAuthConsentProps; diff --git a/packages/shared/src/types/elementIds.ts b/packages/shared/src/types/elementIds.ts index 6975892a769..03a51460dc5 100644 --- a/packages/shared/src/types/elementIds.ts +++ b/packages/shared/src/types/elementIds.ts @@ -15,6 +15,7 @@ export type FieldId = | 'identifier' | 'username' | 'code' + | 'userCode' | 'role' | 'deleteConfirmation' | 'deleteOrganizationConfirmation' diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 82c326a2248..f1f6e9dd7bf 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1374,6 +1374,47 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue<'applicationName'>; }; }; + oauthDeviceVerification: { + start: { + title: LocalizationValue; + subtitle: LocalizationValue; + userCodeLabel: LocalizationValue; + action__continue: LocalizationValue; + }; + confirmation: { + title: LocalizationValue<'applicationName'>; + subtitle: LocalizationValue<'identifier'>; + scopeListTitle: LocalizationValue<'applicationName'>; + warning: LocalizationValue; + action__deny: LocalizationValue; + action__approve: LocalizationValue; + }; + status: { + approvedTitle: LocalizationValue; + approvedSubtitle: LocalizationValue; + alreadyApprovedTitle: LocalizationValue; + alreadyApprovedSubtitle: LocalizationValue; + alreadyDecidedTitle: LocalizationValue; + alreadyDecidedSubtitle: LocalizationValue; + deniedTitle: LocalizationValue; + deniedSubtitle: LocalizationValue; + alreadyDeniedTitle: LocalizationValue; + alreadyDeniedSubtitle: LocalizationValue; + consumedTitle: LocalizationValue; + consumedSubtitle: LocalizationValue; + }; + error: { + invalidCode: LocalizationValue; + unknownCode: LocalizationValue; + expiredTitle: LocalizationValue; + expiredSubtitle: LocalizationValue; + rateLimitedTitle: LocalizationValue; + rateLimitedSubtitle: LocalizationValue; + genericTitle: LocalizationValue; + genericSubtitle: LocalizationValue; + }; + action__tryAnotherCode: LocalizationValue; + }; unstable__errors: UnstableErrors; dates: { previous6Days: LocalizationValue<'date'>; diff --git a/packages/shared/src/types/oauthApplication.ts b/packages/shared/src/types/oauthApplication.ts index 3d633999e72..584c976bc65 100644 --- a/packages/shared/src/types/oauthApplication.ts +++ b/packages/shared/src/types/oauthApplication.ts @@ -9,6 +9,26 @@ export type OAuthConsentScopeJSON = { requires_consent: boolean; }; +/** + * @internal + */ +export type OAuthDeviceVerificationInfoJSON = { + oauth_application_name: string; + oauth_application_logo_url: string | null; + client_id: string; + scopes: OAuthConsentScopeJSON[]; + status: OAuthDeviceVerificationStatus; + expires_at: number; +}; + +/** + * @internal + */ +export type OAuthDeviceVerificationResultJSON = { + object: 'oauth_device_verification'; + status: Extract; +}; + /** * @internal */ @@ -81,6 +101,34 @@ export type OAuthConsentInfo = { scopes: OAuthConsentScope[]; }; +export type OAuthDeviceVerificationStatus = 'pending' | 'approved' | 'denied' | 'consumed'; + +/** + * A scope requested by an OAuth device authorization. + */ +export type OAuthDeviceVerificationScope = OAuthConsentScope; + +/** + * Information about an OAuth device authorization awaiting verification. + */ +export type OAuthDeviceVerificationInfo = { + oauthApplicationName: string; + oauthApplicationLogoUrl: string | null; + clientId: string; + scopes: OAuthDeviceVerificationScope[]; + status: OAuthDeviceVerificationStatus; + /** Expiration time as Unix milliseconds. */ + expiresAt: number; +}; + +/** + * The result of approving or denying an OAuth device authorization. + */ +export type OAuthDeviceVerificationResult = { + object: 'oauth_device_verification'; + status: Extract; +}; + export type GetOAuthConsentInfoParams = { /** The OAuth `client_id` from the authorize request. The hook is disabled when this value is empty or omitted. */ oauthClientId: string; @@ -90,6 +138,16 @@ export type GetOAuthConsentInfoParams = { redirectUri?: string; }; +export type LookupOAuthDeviceVerificationParams = { + userCode: string; +}; + +export type SubmitOAuthDeviceVerificationParams = { + userCode: string; + approved: boolean; + organizationId?: string; +}; + /** * Namespace exposed on `Clerk` for OAuth application / consent helpers. */ @@ -99,6 +157,16 @@ export interface OAuthApplicationNamespace { */ getConsentInfo: (params: GetOAuthConsentInfoParams) => Promise; + /** + * Looks up an OAuth device authorization by its human-readable user code. + */ + lookupDeviceVerification: (params: LookupOAuthDeviceVerificationParams) => Promise; + + /** + * Approves or denies an OAuth device authorization for the signed-in user. + */ + submitDeviceVerification: (params: SubmitOAuthDeviceVerificationParams) => Promise; + /** * Returns the URL to use as the `action` attribute of the consent form. * Includes `_clerk_session_id` and, in development, the dev browser JWT. diff --git a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap index ee11ac03e0f..bc8dc12e555 100644 --- a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap @@ -34,6 +34,7 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "HandleSSOCallback", "InviteMembersButton", "OAuthConsent", + "OAuthDeviceVerification", "OrganizationList", "OrganizationProfile", "OrganizationSwitcher", @@ -70,6 +71,7 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "useClerk", "useEmailLink", "useOAuthConsent", + "useOAuthDeviceVerification", "useOrganization", "useOrganizationCreationDefaults", "useOrganizationList", diff --git a/packages/ui/bundlewatch.config.json b/packages/ui/bundlewatch.config.json index 4206be9da18..e8ac59cbbfd 100644 --- a/packages/ui/bundlewatch.config.json +++ b/packages/ui/bundlewatch.config.json @@ -27,6 +27,7 @@ { "path": "./dist/subscriptionDetails*.js", "maxSize": "7KB" }, { "path": "./dist/apiKeys*.js", "maxSize": "6KB" }, { "path": "./dist/oauthConsent*.js", "maxSize": "7.5KB" }, + { "path": "./dist/oauthDeviceVerification*.js", "maxSize": "5.5KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "3KB" }, { "path": "./dist/op-billing-page*.js", "maxSize": "3KB" }, { "path": "./dist/up-plans-page*.js", "maxSize": "2.5KB" }, diff --git a/packages/ui/src/components/OAuthDeviceVerification/OAuthDeviceVerification.tsx b/packages/ui/src/components/OAuthDeviceVerification/OAuthDeviceVerification.tsx new file mode 100644 index 00000000000..bda29d436b3 --- /dev/null +++ b/packages/ui/src/components/OAuthDeviceVerification/OAuthDeviceVerification.tsx @@ -0,0 +1,523 @@ +import { isClerkAPIResponseError } from '@clerk/shared/error'; +import { useClerk, useOAuthDeviceVerification, useUser } from '@clerk/shared/react'; +import type { OAuthDeviceVerificationInfo } from '@clerk/shared/types'; +import type { FormEventHandler } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useEnvironment, withCoreUserGuard } from '@/ui/contexts'; +import { Button, Col, Flow, Grid, Image, localizationKeys, Text, useLocalizations } from '@/ui/customizables'; +import { ApplicationLogo } from '@/ui/elements/ApplicationLogo'; +import { Card } from '@/ui/elements/Card'; +import { withCardStateProvider } from '@/ui/elements/contexts'; +import { Form } from '@/ui/elements/Form'; +import { Header } from '@/ui/elements/Header'; +import { LoadingCardContainer } from '@/ui/elements/LoadingCard'; +import { Alert } from '@/ui/primitives'; +import { Route, Switch } from '@/ui/router'; +import { useFormControl } from '@/ui/utils/useFormControl'; + +import { + ListGroup, + ListGroupContent, + ListGroupHeader, + ListGroupHeaderTitle, + ListGroupItem, + ListGroupItemLabel, +} from '../OAuthConsent/ListGroup'; +import { + LogoGroup, + LogoGroupIcon, + LogoGroupItem, + LogoGroupItemContainer, + LogoGroupSeparator, +} from '../OAuthConsent/LogoGroup'; +import { OrgSelect } from '../OAuthConsent/OrgSelect'; +import { OAuthDeviceVerificationCodeInput } from './OAuthDeviceVerificationCodeInput'; +import { getOAuthDeviceUserCodeFromSearch, isValidOAuthDeviceUserCode, normalizeOAuthDeviceUserCode } from './utils'; + +const USER_ORG_READ_SCOPE = 'user:org:read'; +const OFFLINE_ACCESS_SCOPE = 'offline_access'; +const PRIVATE_METADATA_SCOPE = 'private_metadata'; + +type View = + | 'entry' + | 'loading' + | 'confirmation' + | 'approved' + | 'alreadyApproved' + | 'denied' + | 'alreadyDenied' + | 'consumed' + | 'expired' + | 'rateLimited' + | 'alreadyDecided' + | 'error'; + +function getErrorCode(error: unknown): string | undefined { + return isClerkAPIResponseError(error) ? error.errors?.[0]?.code : undefined; +} + +function OAuthDeviceVerificationInternal() { + const clerk = useClerk(); + const { user } = useUser(); + const { + displayConfig: { applicationName, logoImageUrl }, + organizationSettings, + } = useEnvironment(); + const { t } = useLocalizations(); + const verification = useOAuthDeviceVerification(); + const [view, setView] = useState('entry'); + const [selectedOrg, setSelectedOrg] = useState(null); + const [submittingDecision, setSubmittingDecision] = useState<'approve' | 'deny' | null>(null); + const handledPrefill = useRef(false); + const decisionInProgress = useRef(false); + + const codeControl = useFormControl('userCode', '', { + type: 'text', + label: localizationKeys('oauthDeviceVerification.start.userCodeLabel'), + transformer: normalizeOAuthDeviceUserCode, + isRequired: true, + }); + + const showLookupError = useCallback( + (error: unknown) => { + switch (getErrorCode(error)) { + case 'resource_not_found': + setView('entry'); + codeControl.setError(t(localizationKeys('oauthDeviceVerification.error.unknownCode'))); + break; + case 'oauth_device_code_expired': + setView('expired'); + break; + case 'too_many_requests': + setView('rateLimited'); + break; + default: + setView('error'); + } + }, + [codeControl, t], + ); + + const showLookupResult = useCallback((info: OAuthDeviceVerificationInfo) => { + switch (info.status) { + case 'pending': + setView('confirmation'); + break; + case 'approved': + setView('alreadyApproved'); + break; + case 'denied': + setView('alreadyDenied'); + break; + case 'consumed': + setView('consumed'); + break; + default: + setView('error'); + } + }, []); + + const lookup = useCallback( + async (userCode: string) => { + codeControl.clearFeedback(); + setView('loading'); + try { + const info = await verification.lookup({ userCode }); + showLookupResult(info); + } catch (error) { + showLookupError(error); + } + }, + [codeControl, showLookupError, showLookupResult, verification], + ); + + useEffect(() => { + if (handledPrefill.current) { + return; + } + handledPrefill.current = true; + + const userCode = getOAuthDeviceUserCodeFromSearch(); + if (!userCode) { + return; + } + + codeControl.setValue(normalizeOAuthDeviceUserCode(userCode)); + if (!isValidOAuthDeviceUserCode(userCode)) { + codeControl.setError(t(localizationKeys('oauthDeviceVerification.error.invalidCode'))); + return; + } + + void lookup(normalizeOAuthDeviceUserCode(userCode)); + }, [codeControl, lookup, t]); + + const handleLookup: FormEventHandler = event => { + event.preventDefault(); + const userCode = normalizeOAuthDeviceUserCode(codeControl.value); + if (!isValidOAuthDeviceUserCode(userCode)) { + codeControl.setError(t(localizationKeys('oauthDeviceVerification.error.invalidCode'))); + return; + } + void lookup(userCode); + }; + + const data = verification.data; + const hasOrgReadScope = data?.scopes.some(scope => scope.scope === USER_ORG_READ_SCOPE) ?? false; + const orgSelectionEnabled = hasOrgReadScope && organizationSettings.enabled; + const orgOptions = orgSelectionEnabled + ? (user?.organizationMemberships ?? []).map(membership => ({ + value: membership.organization.id, + label: membership.organization.name, + logoUrl: membership.organization.imageUrl, + })) + : []; + const lastActiveOrgId = clerk.session?.lastActiveOrganizationId; + const defaultOrg = orgOptions.find(option => option.value === lastActiveOrgId)?.value ?? orgOptions[0]?.value ?? null; + const effectiveOrg = selectedOrg ?? defaultOrg; + + const showDecisionError = (error: unknown) => { + switch (getErrorCode(error)) { + case 'oauth_device_code_expired': + setView('expired'); + break; + case 'too_many_requests': + setView('rateLimited'); + break; + case 'bad_request': + setView('alreadyDecided'); + break; + default: + setView('error'); + } + }; + + const handleApprove = async () => { + if (decisionInProgress.current) { + return; + } + decisionInProgress.current = true; + const userCode = normalizeOAuthDeviceUserCode(codeControl.value); + setSubmittingDecision('approve'); + try { + await verification.approve({ userCode, organizationId: effectiveOrg ?? undefined }); + setView('approved'); + } catch (error) { + showDecisionError(error); + } finally { + decisionInProgress.current = false; + setSubmittingDecision(null); + } + }; + + const handleDeny = async () => { + if (decisionInProgress.current) { + return; + } + decisionInProgress.current = true; + const userCode = normalizeOAuthDeviceUserCode(codeControl.value); + setSubmittingDecision('deny'); + try { + await verification.deny({ userCode }); + setView('denied'); + } catch (error) { + showDecisionError(error); + } finally { + decisionInProgress.current = false; + setSubmittingDecision(null); + } + }; + + const reset = () => { + decisionInProgress.current = false; + verification.reset(); + codeControl.setValue(''); + codeControl.clearFeedback(); + setSelectedOrg(null); + setView('entry'); + }; + + if (view === 'loading') { + return ( + + + + + + + ); + } + + if (view === 'entry') { + return ( + + + + + + + + + + + + + + + + + + ); + } + + if (view === 'confirmation' && data) { + const primaryIdentifier = user?.primaryEmailAddress?.emailAddress || user?.primaryPhoneNumber?.phoneNumber || ''; + const displayedScopes = data.scopes + .filter(scope => scope.scope !== OFFLINE_ACCESS_SCOPE) + .map(scope => ({ + ...scope, + description: + scope.scope === PRIVATE_METADATA_SCOPE + ? t( + localizationKeys('oauthConsent.scopeList.privateMetadata', { + applicationName, + }), + ) + : scope.description, + })); + const hasOfflineAccess = data.scopes.some(scope => scope.scope === OFFLINE_ACCESS_SCOPE); + return ( + + + + + + + + {orgSelectionEnabled && orgOptions.length > 0 && effectiveOrg && ( + + )} + + + + + + {displayedScopes.map(scope => ( + + {scope.description || scope.scope} + + ))} + + + + + + +