From 559f1ca0ad31e48380d82949e13f395f0e6e5a1b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 13 Nov 2024 16:27:57 +0200 Subject: [PATCH 01/15] WIP --- .../src/react/hooks/useReverification.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/react/hooks/useReverification.ts b/packages/shared/src/react/hooks/useReverification.ts index 091ded297bc..e8c23bc76f9 100644 --- a/packages/shared/src/react/hooks/useReverification.ts +++ b/packages/shared/src/react/hooks/useReverification.ts @@ -28,7 +28,13 @@ async function resolveResult( }); } -function createReverificationHandler(params: { onOpenModal: Clerk['__experimental_openUserVerification'] }) { +type UseReverificationOptions = { + onCancel?: (() => void) | 'throw'; +}; + +function createReverificationHandler( + params: { onOpenModal: Clerk['__experimental_openUserVerification'] } & UseReverificationOptions, +) { function assertReverification Promise>(fetcher: Fetcher): Fetcher { return (async (...args) => { let result = await resolveResult(fetcher(...args)); @@ -56,10 +62,20 @@ function createReverificationHandler(params: { onOpenModal: Clerk['__experimenta }, }); - /** - * Wait until the promise from above have been resolved or rejected - */ - await resolvers.promise; + try { + /** + * Wait until the promise from above have been resolved or rejected + */ + await resolvers.promise; + } catch (e) { + if (params.onCancel === 'throw') { + throw e; + } + params?.onCancel?.(); + + // Is this even right ? + return null; + } /** * After the promise resolved successfully try the original request one more time @@ -74,16 +90,21 @@ function createReverificationHandler(params: { onOpenModal: Clerk['__experimenta return assertReverification; } -function __experimental_useReverification Promise>(fetcher: Fetcher): readonly [Fetcher] { +function __experimental_useReverification Promise>( + fetcher: Fetcher, + options?: UseReverificationOptions, +): readonly [Fetcher] { const { __experimental_openUserVerification } = useClerk(); const fetcherRef = useRef(fetcher); + const optionsRef = useRef(options); const handleReverification = useMemo(() => { const handler = createReverificationHandler({ onOpenModal: __experimental_openUserVerification, + onCancel: optionsRef.current?.onCancel, })(fetcherRef.current); return [handler] as const; - }, [__experimental_openUserVerification, fetcherRef.current]); + }, [__experimental_openUserVerification, fetcherRef.current, optionsRef.current]); // Keep fetcher ref in sync useSafeLayoutEffect(() => { From 285b49cc472c70b6b1b4ec0b19e25d257b08c645 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 14 Nov 2024 20:32:02 +0200 Subject: [PATCH 02/15] update types for `useReverification` --- .../src/react/hooks/useReverification.ts | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/shared/src/react/hooks/useReverification.ts b/packages/shared/src/react/hooks/useReverification.ts index e8c23bc76f9..2464464fca9 100644 --- a/packages/shared/src/react/hooks/useReverification.ts +++ b/packages/shared/src/react/hooks/useReverification.ts @@ -2,7 +2,7 @@ import type { Clerk } from '@clerk/types'; import { useMemo, useRef } from 'react'; import { __experimental_isReverificationHint, __experimental_reverificationMismatch } from '../../authorization-errors'; -import { ClerkRuntimeError, isClerkAPIResponseError } from '../../error'; +import { ClerkRuntimeError, isClerkAPIResponseError, isClerkRuntimeError } from '../../error'; import { createDeferredPromise } from '../../utils/createDeferredPromise'; import { useClerk } from './useClerk'; import { useSafeLayoutEffect } from './useSafeLayoutEffect'; @@ -28,14 +28,21 @@ async function resolveResult( }); } +type ExcludeClerkError = T extends { clerk_error: any } ? (P extends { throwOnCancel: true } ? never : null) : T; + type UseReverificationOptions = { - onCancel?: (() => void) | 'throw'; + onCancel?: () => void; + throwOnCancel?: boolean; }; function createReverificationHandler( params: { onOpenModal: Clerk['__experimental_openUserVerification'] } & UseReverificationOptions, ) { - function assertReverification Promise>(fetcher: Fetcher): Fetcher { + function assertReverification Promise>( + fetcher: Fetcher, + ): ( + ...args: Parameters + ) => Promise>, Parameters[1]>> { return (async (...args) => { let result = await resolveResult(fetcher(...args)); @@ -68,12 +75,14 @@ function createReverificationHandler( */ await resolvers.promise; } catch (e) { - if (params.onCancel === 'throw') { + if (params.onCancel) { + params.onCancel(); + } + + if (isClerkRuntimeError(e) && e.code === 'reverification_cancelled' && params.throwOnCancel) { throw e; } - params?.onCancel?.(); - // Is this even right ? return null; } @@ -90,10 +99,13 @@ function createReverificationHandler( return assertReverification; } -function __experimental_useReverification Promise>( +function __experimental_useReverification< + Fetcher extends (...args: any[]) => Promise, + O extends UseReverificationOptions, +>( fetcher: Fetcher, - options?: UseReverificationOptions, -): readonly [Fetcher] { + options?: O, +): readonly [(...args: Parameters) => Promise>, O>>] { const { __experimental_openUserVerification } = useClerk(); const fetcherRef = useRef(fetcher); const optionsRef = useRef(options); From f0493d0ec19db6307bf9dd5f5eb057e65c7022d1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 14 Nov 2024 20:34:40 +0200 Subject: [PATCH 03/15] add changeset --- .changeset/silent-bears-worry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silent-bears-worry.md diff --git a/.changeset/silent-bears-worry.md b/.changeset/silent-bears-worry.md new file mode 100644 index 00000000000..9af54068800 --- /dev/null +++ b/.changeset/silent-bears-worry.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': minor +--- + +Change `useReverification` to handle error in a callback, but still allow an error to be thrown via options. From 13edbbfb28182810dc6f41332e27f9deafc2d503 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 13:20:15 +0200 Subject: [PATCH 04/15] useReverication respects reverification error metadata --- packages/shared/src/authorization.ts | 12 +++++++----- packages/shared/src/react/hooks/useReverification.ts | 6 +++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/authorization.ts b/packages/shared/src/authorization.ts index 888e73c6ae8..489067bbfb6 100644 --- a/packages/shared/src/authorization.ts +++ b/packages/shared/src/authorization.ts @@ -94,11 +94,11 @@ const validateReverificationConfig = (config: __experimental_ReverificationConfi return config; }; - if (typeof config === 'string' && isValidVerificationType(config)) { - return convertConfigToObject.bind(null, config); - } + const isValidStringValue = typeof config === 'string' && isValidVerificationType(config); + const isValidObjectValue = + typeof config === 'object' && isValidLevel(config.level) && isValidMaxAge(config.afterMinutes); - if (typeof config === 'object' && isValidLevel(config.level) && isValidMaxAge(config.afterMinutes)) { + if (isValidStringValue || isValidObjectValue) { return convertConfigToObject.bind(null, config); } @@ -145,7 +145,7 @@ const checkStepUpAuthorization: CheckStepUpAuthorization = (params, { __experime * The returned function authorizes if both checks pass, or if at least one passes * when the other is indeterminate. Fails if userId is missing. */ -export const createCheckAuthorization = (options: AuthorizationOptions): CheckAuthorizationWithCustomPermissions => { +const createCheckAuthorization = (options: AuthorizationOptions): CheckAuthorizationWithCustomPermissions => { return (params): boolean => { if (!options.userId) { return false; @@ -161,3 +161,5 @@ export const createCheckAuthorization = (options: AuthorizationOptions): CheckAu return [orgAuthorization, stepUpAuthorization].every(a => a === true); }; }; + +export { createCheckAuthorization, validateReverificationConfig }; diff --git a/packages/shared/src/react/hooks/useReverification.ts b/packages/shared/src/react/hooks/useReverification.ts index 2464464fca9..2518024bd06 100644 --- a/packages/shared/src/react/hooks/useReverification.ts +++ b/packages/shared/src/react/hooks/useReverification.ts @@ -1,6 +1,7 @@ import type { Clerk } from '@clerk/types'; import { useMemo, useRef } from 'react'; +import { validateReverificationConfig } from '../../authorization'; import { __experimental_isReverificationHint, __experimental_reverificationMismatch } from '../../authorization-errors'; import { ClerkRuntimeError, isClerkAPIResponseError, isClerkRuntimeError } from '../../error'; import { createDeferredPromise } from '../../utils/createDeferredPromise'; @@ -52,11 +53,14 @@ function createReverificationHandler( */ const resolvers = createDeferredPromise(); + const isValidMetadata = validateReverificationConfig(result.clerk_error.metadata.reverification); + /** * On success resolve the pending promise * On cancel reject the pending promise */ params.onOpenModal?.({ + level: isValidMetadata ? isValidMetadata().level : undefined, afterVerification() { resolvers.resolve(true); }, @@ -113,7 +117,7 @@ function __experimental_useReverification< const handleReverification = useMemo(() => { const handler = createReverificationHandler({ onOpenModal: __experimental_openUserVerification, - onCancel: optionsRef.current?.onCancel, + ...options, })(fetcherRef.current); return [handler] as const; }, [__experimental_openUserVerification, fetcherRef.current, optionsRef.current]); From 052b59e5ca17f29dbf00b480090c2a967d6857ac Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 13:20:57 +0200 Subject: [PATCH 05/15] improve error message formatting --- packages/shared/src/error.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 957148dfdbd..d093a6d8969 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -155,12 +155,16 @@ export class ClerkRuntimeError extends Error { code: string; constructor(message: string, { code }: { code: string }) { - super(message); + const prefix = '🔒 Clerk:'; + const regex = new RegExp(prefix.replace(' ', '\\s*'), 'i'); + const sanitized = message.replace(regex, ''); + const _message = `${prefix}\n${sanitized.trim()}\n\n(Code: "${code}")\n`; + super(_message); Object.setPrototypeOf(this, ClerkRuntimeError.prototype); this.code = code; - this.message = message; + this.message = _message; this.clerkRuntimeError = true; } From a074eb2d8681dedd6049c4bc7b1ff59fd839e0bd Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 17:56:33 +0200 Subject: [PATCH 06/15] wip testing --- .../hooks/__tests__/useReverification.test.ts | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/react/src/hooks/__tests__/useReverification.test.ts diff --git a/packages/react/src/hooks/__tests__/useReverification.test.ts b/packages/react/src/hooks/__tests__/useReverification.test.ts new file mode 100644 index 00000000000..7a08c44bf86 --- /dev/null +++ b/packages/react/src/hooks/__tests__/useReverification.test.ts @@ -0,0 +1,121 @@ +import { __experimental_useReverification as useReverification } from '@clerk/shared/react'; +import { expectTypeOf } from 'expect-type'; +// type HasFunction = Exclude['has'], undefined>; +// type ParamsOfHas = Parameters[0]; + +describe('useReverification type tests', () => { + const fetcher = async (key: string, options: { id: string }) => { + return { + key, + options, + }; + }; + + const [verifiedFetcher] = useReverification(fetcher); + + describe('has', () => { + it('allow pass through types', () => { + expectTypeOf({} as const).toMatchTypeOf(); + }); + + it('has({randomKey}) is not allowed', () => { + expectTypeOf({ + randomKey: '', + }).not.toMatchTypeOf(); + }); + + it('has({role: string}) is allowed', () => { + expectTypeOf({ role: 'org:admin' }).toMatchTypeOf(); + }); + + it('has({role: string, permission: string}) is NOT allowed', () => { + expectTypeOf({ role: 'org:admin', permission: 'some-perm' }).not.toMatchTypeOf(); + }); + + it('has with role and assurance is allowed', () => { + expectTypeOf({ + role: 'org:admin', + __experimental_reverification: { + level: 'first_factor', + afterMinutes: 10, + }, + } as const).toMatchTypeOf(); + }); + + it('has with permission and reverification is allowed', () => { + expectTypeOf({ + permission: 'org:edit:posts', + __experimental_reverification: { + level: 'first_factor', + afterMinutes: 10, + }, + } as const).toMatchTypeOf(); + }); + + it('has({reverification: {level, maxAge}}) is allowed', () => { + expectTypeOf({ + __experimental_reverification: { + level: 'first_factor', + afterMinutes: 10, + }, + } as const).toMatchTypeOf(); + }); + + it('reverification with other values as maxAge should throw', () => { + expectTypeOf({ + __experimental_reverification: { + level: 'first_factor', + afterMinutes: '10', + }, + } as const).not.toMatchTypeOf(); + }); + + it('veryStrict reverification is allowed', () => { + expectTypeOf({ + __experimental_reverification: 'strict_mfa', + } as const).toMatchTypeOf(); + }); + + it('strict reverification is allowed', () => { + expectTypeOf({ + __experimental_reverification: 'strict', + } as const).toMatchTypeOf(); + }); + + it('moderate reverification is allowed', () => { + expectTypeOf({ + __experimental_reverification: 'moderate', + } as const).toMatchTypeOf(); + }); + + it('lax reverification is allowed', () => { + expectTypeOf({ + __experimental_reverification: 'lax', + } as const).toMatchTypeOf(); + }); + + it('random reverification is not allowed', () => { + expectTypeOf({ + __experimental_reverification: 'random', + } as const).not.toMatchTypeOf(); + }); + + it('reverification with other strings as level should throw', () => { + expectTypeOf({ + __experimental_reverification: { + level: 'some-factor', + afterMinutes: 10, + }, + } as const).not.toMatchTypeOf(); + }); + + it('reverification with number as level should throw', () => { + expectTypeOf({ + __experimental_reverification: { + level: 2, + afterMinutes: 10, + }, + } as const).not.toMatchTypeOf(); + }); + }); +}); From cec0186064cd99310a20566f51450316d6a9dfd9 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 18:23:53 +0200 Subject: [PATCH 07/15] Add Type tests --- .../hooks/__tests__/useReverification.test.ts | 155 ++++++------------ 1 file changed, 47 insertions(+), 108 deletions(-) diff --git a/packages/react/src/hooks/__tests__/useReverification.test.ts b/packages/react/src/hooks/__tests__/useReverification.test.ts index 7a08c44bf86..284c23c1c8c 100644 --- a/packages/react/src/hooks/__tests__/useReverification.test.ts +++ b/packages/react/src/hooks/__tests__/useReverification.test.ts @@ -1,121 +1,60 @@ -import { __experimental_useReverification as useReverification } from '@clerk/shared/react'; +import { __experimental_reverificationMismatch } from '@clerk/shared/authorization-errors'; +import type { __experimental_useReverification as useReverification } from '@clerk/shared/react'; import { expectTypeOf } from 'expect-type'; -// type HasFunction = Exclude['has'], undefined>; -// type ParamsOfHas = Parameters[0]; -describe('useReverification type tests', () => { - const fetcher = async (key: string, options: { id: string }) => { - return { - key, - options, - }; - }; - - const [verifiedFetcher] = useReverification(fetcher); - - describe('has', () => { - it('allow pass through types', () => { - expectTypeOf({} as const).toMatchTypeOf(); - }); - - it('has({randomKey}) is not allowed', () => { - expectTypeOf({ - randomKey: '', - }).not.toMatchTypeOf(); - }); - - it('has({role: string}) is allowed', () => { - expectTypeOf({ role: 'org:admin' }).toMatchTypeOf(); - }); - - it('has({role: string, permission: string}) is NOT allowed', () => { - expectTypeOf({ role: 'org:admin', permission: 'some-perm' }).not.toMatchTypeOf(); - }); +type ExcludeClerkError = T extends { clerk_error: any } ? never : T; - it('has with role and assurance is allowed', () => { - expectTypeOf({ - role: 'org:admin', - __experimental_reverification: { - level: 'first_factor', - afterMinutes: 10, - }, - } as const).toMatchTypeOf(); - }); - - it('has with permission and reverification is allowed', () => { - expectTypeOf({ - permission: 'org:edit:posts', - __experimental_reverification: { - level: 'first_factor', - afterMinutes: 10, - }, - } as const).toMatchTypeOf(); - }); - - it('has({reverification: {level, maxAge}}) is allowed', () => { - expectTypeOf({ - __experimental_reverification: { - level: 'first_factor', - afterMinutes: 10, - }, - } as const).toMatchTypeOf(); - }); +const fetcher = async (key: string, options: { id: string }) => { + return { + key, + options, + }; +}; - it('reverification with other values as maxAge should throw', () => { - expectTypeOf({ - __experimental_reverification: { - level: 'first_factor', - afterMinutes: '10', - }, - } as const).not.toMatchTypeOf(); - }); +const fetcherWithHelper = async (key: string, options: { id: string }) => { + if (key == 'a') { + return __experimental_reverificationMismatch(); + } - it('veryStrict reverification is allowed', () => { - expectTypeOf({ - __experimental_reverification: 'strict_mfa', - } as const).toMatchTypeOf(); - }); + return { + key, + options, + }; +}; - it('strict reverification is allowed', () => { - expectTypeOf({ - __experimental_reverification: 'strict', - } as const).toMatchTypeOf(); - }); +type Fetcher = typeof fetcherWithHelper; - it('moderate reverification is allowed', () => { - expectTypeOf({ - __experimental_reverification: 'moderate', - } as const).toMatchTypeOf(); - }); +describe('useReverification type tests', () => { + it('allow pass through types', () => { + type UseReverificationWithFetcher = typeof useReverification; + type VerifiedFetcher = ReturnType[0]; + expectTypeOf(fetcher).toEqualTypeOf(); + }); - it('lax reverification is allowed', () => { - expectTypeOf({ - __experimental_reverification: 'lax', - } as const).toMatchTypeOf(); - }); + it('returned callback with clerk error excluded and possible null in case of cancelled flow', () => { + type UseReverificationWithFetcherHelper = typeof useReverification; + type VerifiedFetcherHelper = ReturnType[0]; - it('random reverification is not allowed', () => { - expectTypeOf({ - __experimental_reverification: 'random', - } as const).not.toMatchTypeOf(); - }); + expectTypeOf(fetcherWithHelper).not.toEqualTypeOf(); - it('reverification with other strings as level should throw', () => { - expectTypeOf({ - __experimental_reverification: { - level: 'some-factor', - afterMinutes: 10, - }, - } as const).not.toMatchTypeOf(); - }); + expectTypeOf>().toEqualTypeOf>(); + expectTypeOf>().not.toEqualTypeOf>(); + expectTypeOf>> | null>().toEqualTypeOf< + Awaited> + >(); + }); - it('reverification with number as level should throw', () => { - expectTypeOf({ - __experimental_reverification: { - level: 2, - afterMinutes: 10, - }, - } as const).not.toMatchTypeOf(); - }); + it('returned callback with clerk error excluded but without null since we throw', () => { + type UseReverificationWithFetcherHelperThrow = typeof useReverification< + typeof fetcherWithHelper, + { + throwOnCancel: true; + } + >; + type VerifiedFetcherHelperThrow = ReturnType[0]; + expectTypeOf(fetcherWithHelper).not.toEqualTypeOf(); + expectTypeOf>>>().toEqualTypeOf< + Awaited> + >(); }); }); From e9cc386bdfc4c327e9730d4d6e7db53d416dd06d Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 19:11:46 +0200 Subject: [PATCH 08/15] address pr feedback --- .../src/react/hooks/useReverification.ts | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/shared/src/react/hooks/useReverification.ts b/packages/shared/src/react/hooks/useReverification.ts index 2518024bd06..f9138edf06b 100644 --- a/packages/shared/src/react/hooks/useReverification.ts +++ b/packages/shared/src/react/hooks/useReverification.ts @@ -36,9 +36,11 @@ type UseReverificationOptions = { throwOnCancel?: boolean; }; -function createReverificationHandler( - params: { onOpenModal: Clerk['__experimental_openUserVerification'] } & UseReverificationOptions, -) { +type CreateReverificationHandlerParams = UseReverificationOptions & { + openUIComponent: Clerk['__experimental_openUserVerification']; +}; + +function createReverificationHandler(params: CreateReverificationHandlerParams) { function assertReverification Promise>( fetcher: Fetcher, ): ( @@ -59,7 +61,7 @@ function createReverificationHandler( * On success resolve the pending promise * On cancel reject the pending promise */ - params.onOpenModal?.({ + params.openUIComponent?.({ level: isValidMetadata ? isValidMetadata().level : undefined, afterVerification() { resolvers.resolve(true); @@ -103,28 +105,49 @@ function createReverificationHandler( return assertReverification; } +type UseReverificationResult< + Fetcher extends (...args: any[]) => Promise, + Options extends UseReverificationOptions, +> = readonly [(...args: Parameters) => Promise>, Options>>]; + +/** + * Receives a fetcher async function and returned an enhanced fetcher that automatically handles the reverification flow + * by displaying a prebuilt UI component when the request from the fetcher fails with a reverification error response. + * + * While the UI component is displayed the promise is still pending. + * On success: the original request is retried one more time. + * On error: + * (1) by default the fetcher will return `null` and the `onCancel` callback will be executed. + * (2) when `throwOnCancel: true` instead of returning null, the returned fetcher will throw a `ClerkRuntimeError`. + * + * @example + * A simple example: + * + * function Hello() { + * const [fetchBalance] = useReverification(()=> fetch('/transfer-balance',{method:"POST"})); + * return + * } + */ function __experimental_useReverification< Fetcher extends (...args: any[]) => Promise, - O extends UseReverificationOptions, ->( - fetcher: Fetcher, - options?: O, -): readonly [(...args: Parameters) => Promise>, O>>] { + Options extends UseReverificationOptions, +>(fetcher: Fetcher, options?: Options): UseReverificationResult { const { __experimental_openUserVerification } = useClerk(); const fetcherRef = useRef(fetcher); const optionsRef = useRef(options); const handleReverification = useMemo(() => { const handler = createReverificationHandler({ - onOpenModal: __experimental_openUserVerification, - ...options, + openUIComponent: __experimental_openUserVerification, + ...optionsRef.current, })(fetcherRef.current); return [handler] as const; - }, [__experimental_openUserVerification, fetcherRef.current, optionsRef.current]); + }, [__experimental_openUserVerification]); - // Keep fetcher ref in sync + // Keep fetcher and options ref in sync useSafeLayoutEffect(() => { fetcherRef.current = fetcher; + optionsRef.current = options; }); return handleReverification; From 86d41ce444cf1303c5726bbc5779573027caf69b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 19:41:28 +0200 Subject: [PATCH 09/15] throw on ts error when running unit tests --- packages/shared/jest.config.js | 2 +- packages/shared/src/__tests__/deprecated.test.ts | 9 ++++++--- packages/shared/src/__tests__/telemetry.test.ts | 9 ++++++--- packages/shared/src/telemetry/throttler.ts | 1 + packages/shared/tsconfig.test.json | 6 +++--- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js index fe05710e000..446c1da9ae6 100644 --- a/packages/shared/jest.config.js +++ b/packages/shared/jest.config.js @@ -16,7 +16,7 @@ const config = { moduleDirectories: ['node_modules', '/src'], transform: { - '^.+\\.m?tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json', diagnostics: false }], + '^.+\\.m?tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json' }], }, globals: { diff --git a/packages/shared/src/__tests__/deprecated.test.ts b/packages/shared/src/__tests__/deprecated.test.ts index cc9876bc947..2ae1296a985 100644 --- a/packages/shared/src/__tests__/deprecated.test.ts +++ b/packages/shared/src/__tests__/deprecated.test.ts @@ -9,9 +9,10 @@ import { deprecated, deprecatedObjectProperty, deprecatedProperty } from '../dep import { isProductionEnvironment, isTestEnvironment } from '../utils/runtimeEnvironment'; describe('deprecated(fnName, warning)', () => { - let consoleWarnSpy; + let consoleWarnSpy = jest.fn(); beforeEach(() => { + // @ts-ignore consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); }); afterEach(() => { @@ -179,9 +180,10 @@ describe('deprecated(fnName, warning)', () => { }); describe('deprecatedProperty(cls, propName, warning, isStatic = false)', () => { - let consoleWarnSpy; + let consoleWarnSpy = jest.fn(); beforeEach(() => { + // @ts-ignore consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); }); afterEach(() => { @@ -312,9 +314,10 @@ describe('deprecatedProperty(cls, propName, warning, isStatic = false)', () => { }); describe('deprecatedObjectProperty(obj, propName, warning)', () => { - let consoleWarnSpy; + let consoleWarnSpy = jest.fn(); beforeEach(() => { + // @ts-ignore consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); }); afterEach(() => { diff --git a/packages/shared/src/__tests__/telemetry.test.ts b/packages/shared/src/__tests__/telemetry.test.ts index a0bb77b0c62..1e202ed6ce3 100644 --- a/packages/shared/src/__tests__/telemetry.test.ts +++ b/packages/shared/src/__tests__/telemetry.test.ts @@ -1,20 +1,23 @@ import 'cross-fetch/polyfill'; +import type { TelemetryEvent } from '@clerk/types'; +// @ts-ignore import assert from 'assert'; import { TelemetryCollector } from '../telemetry'; -import type { TelemetryEvent } from '../telemetry/types'; jest.useFakeTimers(); const TEST_PK = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; describe('TelemetryCollector', () => { - let windowSpy; - let fetchSpy; + let windowSpy = jest.fn(); + let fetchSpy = jest.fn(); beforeEach(() => { + // @ts-ignore fetchSpy = jest.spyOn(global, 'fetch'); + // @ts-ignore windowSpy = jest.spyOn(window, 'window', 'get'); }); diff --git a/packages/shared/src/telemetry/throttler.ts b/packages/shared/src/telemetry/throttler.ts index fd3a698e755..e5a66dc893c 100644 --- a/packages/shared/src/telemetry/throttler.ts +++ b/packages/shared/src/telemetry/throttler.ts @@ -59,6 +59,7 @@ export class TelemetryEventThrottler { ...rest, }) .sort() + // @ts-ignore .map(key => sanitizedEvent[key]), ); } diff --git a/packages/shared/tsconfig.test.json b/packages/shared/tsconfig.test.json index fe4e59695b5..f4b63c535bb 100644 --- a/packages/shared/tsconfig.test.json +++ b/packages/shared/tsconfig.test.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "sourceMap": true + "sourceMap": true, + "noImplicitAny": false }, - "include": ["src/**/*"], - "exclude": ["node_modules"] + "include": ["src/**/*", "global.d.ts"] } From 8cbf50a7e617f3ebf2a64e965526aaa1a9a5d5de Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 19:42:39 +0200 Subject: [PATCH 10/15] move useReverification.test.ts to shared --- .../src/react}/__tests__/useReverification.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/{react/src/hooks => shared/src/react}/__tests__/useReverification.test.ts (100%) diff --git a/packages/react/src/hooks/__tests__/useReverification.test.ts b/packages/shared/src/react/__tests__/useReverification.test.ts similarity index 100% rename from packages/react/src/hooks/__tests__/useReverification.test.ts rename to packages/shared/src/react/__tests__/useReverification.test.ts From ea37357e999f5050aa6c5b27be0a8749f30fc5c7 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 20:16:14 +0200 Subject: [PATCH 11/15] revert --- packages/shared/src/__tests__/deprecated.test.ts | 3 +-- packages/shared/src/__tests__/telemetry.test.ts | 6 ++---- packages/shared/src/telemetry/throttler.ts | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/__tests__/deprecated.test.ts b/packages/shared/src/__tests__/deprecated.test.ts index 2ae1296a985..116770fca1d 100644 --- a/packages/shared/src/__tests__/deprecated.test.ts +++ b/packages/shared/src/__tests__/deprecated.test.ts @@ -9,10 +9,9 @@ import { deprecated, deprecatedObjectProperty, deprecatedProperty } from '../dep import { isProductionEnvironment, isTestEnvironment } from '../utils/runtimeEnvironment'; describe('deprecated(fnName, warning)', () => { - let consoleWarnSpy = jest.fn(); + let consoleWarnSpy; beforeEach(() => { - // @ts-ignore consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); }); afterEach(() => { diff --git a/packages/shared/src/__tests__/telemetry.test.ts b/packages/shared/src/__tests__/telemetry.test.ts index 7128941fbff..e1bf0884216 100644 --- a/packages/shared/src/__tests__/telemetry.test.ts +++ b/packages/shared/src/__tests__/telemetry.test.ts @@ -11,13 +11,11 @@ jest.useFakeTimers(); const TEST_PK = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; describe('TelemetryCollector', () => { - let windowSpy = jest.fn(); - let fetchSpy = jest.fn(); + let windowSpy; + let fetchSpy; beforeEach(() => { - // @ts-ignore fetchSpy = jest.spyOn(global, 'fetch'); - // @ts-ignore windowSpy = jest.spyOn(window, 'window', 'get'); }); diff --git a/packages/shared/src/telemetry/throttler.ts b/packages/shared/src/telemetry/throttler.ts index e5a66dc893c..fd3a698e755 100644 --- a/packages/shared/src/telemetry/throttler.ts +++ b/packages/shared/src/telemetry/throttler.ts @@ -59,7 +59,6 @@ export class TelemetryEventThrottler { ...rest, }) .sort() - // @ts-ignore .map(key => sanitizedEvent[key]), ); } From ceb2c462b1070364ed2405bf21d768697e04a278 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 15 Nov 2024 21:01:00 +0200 Subject: [PATCH 12/15] update imports --- .../shared/src/react/__tests__/useReverification.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/react/__tests__/useReverification.test.ts b/packages/shared/src/react/__tests__/useReverification.test.ts index 284c23c1c8c..113ea3ee518 100644 --- a/packages/shared/src/react/__tests__/useReverification.test.ts +++ b/packages/shared/src/react/__tests__/useReverification.test.ts @@ -1,7 +1,8 @@ -import { __experimental_reverificationMismatch } from '@clerk/shared/authorization-errors'; -import type { __experimental_useReverification as useReverification } from '@clerk/shared/react'; import { expectTypeOf } from 'expect-type'; +import { __experimental_reverificationError } from '../../authorization-errors'; +import type { __experimental_useReverification as useReverification } from '../hooks/useReverification'; + type ExcludeClerkError = T extends { clerk_error: any } ? never : T; const fetcher = async (key: string, options: { id: string }) => { @@ -13,7 +14,7 @@ const fetcher = async (key: string, options: { id: string }) => { const fetcherWithHelper = async (key: string, options: { id: string }) => { if (key == 'a') { - return __experimental_reverificationMismatch(); + return __experimental_reverificationError(); } return { From 6fb56d50d75f864d3a6f9cb0380d7d8daf18a267 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 18 Nov 2024 12:14:00 +0200 Subject: [PATCH 13/15] Add refs to deps of useMemo This allows for new references to be used, when something else is causing the re-render --- packages/shared/src/react/hooks/useReverification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/react/hooks/useReverification.ts b/packages/shared/src/react/hooks/useReverification.ts index 5db5eec6b11..9b3ddb80089 100644 --- a/packages/shared/src/react/hooks/useReverification.ts +++ b/packages/shared/src/react/hooks/useReverification.ts @@ -142,7 +142,7 @@ function __experimental_useReverification< ...optionsRef.current, })(fetcherRef.current); return [handler] as const; - }, [__experimental_openUserVerification]); + }, [__experimental_openUserVerification, fetcherRef.current, optionsRef.current]); // Keep fetcher and options ref in sync useSafeLayoutEffect(() => { From f2b5466d3930004aadaf60ea308eb0015787e56d Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 18 Nov 2024 12:16:25 +0200 Subject: [PATCH 14/15] Gracefully handle retuning undefined --- .../UserProfile/AddAuthenticatorApp.tsx | 7 +-- .../UserProfile/ConnectedAccountsMenu.tsx | 6 +-- .../UserProfile/ConnectedAccountsSection.tsx | 14 +++--- .../components/UserProfile/DeleteUserForm.tsx | 8 +--- .../ui/components/UserProfile/EmailForm.tsx | 7 +-- .../UserProfile/MfaBackupCodeCreateForm.tsx | 7 +-- .../components/UserProfile/PasskeySection.tsx | 7 +-- .../components/UserProfile/PasswordForm.tsx | 25 +++++------ .../ui/components/UserProfile/PhoneForm.tsx | 13 +++--- .../components/UserProfile/UsernameForm.tsx | 7 +-- .../src/react/hooks/useReverification.ts | 43 +++++++++---------- 11 files changed, 49 insertions(+), 95 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/AddAuthenticatorApp.tsx b/packages/clerk-js/src/ui/components/UserProfile/AddAuthenticatorApp.tsx index 997916cee4f..3393e31e998 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AddAuthenticatorApp.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AddAuthenticatorApp.tsx @@ -28,12 +28,7 @@ export const AddAuthenticatorApp = withCardStateProvider((props: AddAuthenticato const { title, onSuccess, onReset } = props; const { user } = useUser(); const card = useCardState(); - const [createTOTP] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - return user.createTOTP(); - }); + const [createTOTP] = useReverification(() => user?.createTOTP()); const { close } = useActionContext(); const [totp, setTOTP] = React.useState(undefined); const [displayFormat, setDisplayFormat] = React.useState('qr'); diff --git a/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsMenu.tsx b/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsMenu.tsx index fbb2aab9743..9845cf6bf1d 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsMenu.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsMenu.tsx @@ -20,17 +20,13 @@ const ConnectMenuButton = (props: { strategy: OAuthStrategy }) => { const isModal = mode === 'modal'; const [createExternalAccount] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - const socialProvider = strategy.replace('oauth_', '') as OAuthProvider; const redirectUrl = isModal ? appendModalState({ url: window.location.href, componentName, socialProvider: socialProvider }) : window.location.href; const additionalScopes = additionalOAuthScopes ? additionalOAuthScopes[socialProvider] : []; - return user.createExternalAccount({ + return user?.createExternalAccount({ strategy, redirectUrl, additionalScopes, diff --git a/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx index 75335459de3..03e81a2e809 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx @@ -98,17 +98,13 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => }) : window.location.href; - const [createExternalAccount] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - - return user.createExternalAccount({ - strategy: account.verification!.strategy as OAuthStrategy, + const [createExternalAccount] = useReverification(() => + user?.createExternalAccount({ + strategy: 'wowow' as OAuthStrategy, redirectUrl, additionalScopes, - }); - }); + }), + ); if (!user) { return null; diff --git a/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx index 25fc2999198..edbe4b82441 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/DeleteUserForm.tsx @@ -16,13 +16,7 @@ export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps) const { t } = useLocalizations(); const { otherSessions } = useMultipleSessions({ user }); const { setActive } = useClerk(); - const [deleteUserWithReverification] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - - return user.delete(); - }); + const [deleteUserWithReverification] = useReverification(() => user?.delete()); const confirmationField = useFormControl('deleteConfirmation', '', { type: 'text', diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx index deeb86b8daf..4c7e0bf44e1 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx @@ -22,12 +22,7 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => { const environment = useEnvironment(); const preferEmailLinks = emailLinksEnabledForInstance(environment); - const [createEmailAddress] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - return user.createEmailAddress({ email: emailField.value }); - }); + const [createEmailAddress] = useReverification(() => user?.createEmailAddress({ email: emailField.value })); const emailAddressRef = React.useRef(user?.emailAddresses.find(a => a.id === id)); const wizard = useWizard({ diff --git a/packages/clerk-js/src/ui/components/UserProfile/MfaBackupCodeCreateForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/MfaBackupCodeCreateForm.tsx index 0dd61ccb359..4f9cac0c069 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/MfaBackupCodeCreateForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/MfaBackupCodeCreateForm.tsx @@ -19,12 +19,7 @@ export const MfaBackupCodeCreateForm = withCardStateProvider((props: MfaBackupCo const { onSuccess } = props; const { user } = useUser(); const card = useCardState(); - const [createBackupCode] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - return user.createBackupCode(); - }); + const [createBackupCode] = useReverification(() => user?.createBackupCode()); const [backupCode, setBackupCode] = React.useState(undefined); React.useEffect(() => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/PasskeySection.tsx b/packages/clerk-js/src/ui/components/UserProfile/PasskeySection.tsx index a01eb98e0e6..4059a396a1b 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/PasskeySection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/PasskeySection.tsx @@ -191,12 +191,7 @@ const AddPasskeyButton = () => { const card = useCardState(); const { isSatellite } = useClerk(); const { user } = useUser(); - const [createPasskey] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - return user.createPasskey(); - }); + const [createPasskey] = useReverification(() => user?.createPasskey()); const handleCreatePasskey = async () => { if (!user) { diff --git a/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx index 07f6fb7e2ae..8b9b22a5fc2 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/PasswordForm.tsx @@ -1,4 +1,5 @@ import { __experimental_useReverification as useReverification, useSession, useUser } from '@clerk/shared/react'; +import type { UserResource } from '@clerk/types'; import { useRef } from 'react'; import { useEnvironment } from '../../contexts'; @@ -36,19 +37,9 @@ type PasswordFormProps = FormProps; export const PasswordForm = withCardStateProvider((props: PasswordFormProps) => { const { onSuccess, onReset } = props; const { user } = useUser(); - const [updatePasswordWithReverification] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - - const opts = { - newPassword: passwordField.value, - signOutOfOtherSessions: sessionsField.checked, - currentPassword: user.passwordEnabled ? currentPasswordField.value : undefined, - } satisfies Parameters[0]; - - return user.updatePassword(opts); - }); + const [updatePasswordWithReverification] = useReverification( + (user: UserResource, opts: Parameters) => user.updatePassword(...opts), + ); if (!user) { return null; @@ -124,7 +115,13 @@ export const PasswordForm = withCardStateProvider((props: PasswordFormProps) => text: generateSuccessPageText(user.passwordEnabled, !!sessionsField.checked), }; - await updatePasswordWithReverification(); + const opts = { + newPassword: passwordField.value, + signOutOfOtherSessions: sessionsField.checked, + currentPassword: user.passwordEnabled ? currentPasswordField.value : undefined, + } satisfies Parameters[0]; + + await updatePasswordWithReverification(user, [opts]); onSuccess(); } catch (e) { handleError(e, [currentPasswordField, passwordField, confirmField], card.setError); diff --git a/packages/clerk-js/src/ui/components/UserProfile/PhoneForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/PhoneForm.tsx index 0627e33dafd..6b1baeb9ef9 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/PhoneForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/PhoneForm.tsx @@ -1,5 +1,5 @@ import { __experimental_useReverification as useReverification, useUser } from '@clerk/shared/react'; -import type { PhoneNumberResource } from '@clerk/types'; +import type { PhoneNumberResource, UserResource } from '@clerk/types'; import React from 'react'; import { useWizard, Wizard } from '../../common'; @@ -49,12 +49,9 @@ export const AddPhone = (props: AddPhoneProps) => { const { title, onSuccess, onReset, onUseExistingNumberClick, resourceRef } = props; const card = useCardState(); const { user } = useUser(); - const [createPhoneNumber] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - return user.createPhoneNumber({ phoneNumber: phoneField.value }); - }); + const [createPhoneNumber] = useReverification( + (user: UserResource, opt: Parameters[0]) => user.createPhoneNumber(opt), + ); const phoneField = useFormControl('phoneNumber', '', { type: 'tel', @@ -70,7 +67,7 @@ export const AddPhone = (props: AddPhoneProps) => { if (!user) { return; } - return createPhoneNumber() + return createPhoneNumber(user, { phoneNumber: phoneField.value }) .then(res => { resourceRef.current = res; onSuccess(); diff --git a/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx b/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx index 44c0023b462..fc4876ded12 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UsernameForm.tsx @@ -12,12 +12,7 @@ export const UsernameForm = withCardStateProvider((props: UsernameFormProps) => const { onSuccess, onReset } = props; const { user } = useUser(); - const [updateUsername] = useReverification(() => { - if (!user) { - return Promise.resolve(undefined); - } - return user.update({ username: usernameField.value }); - }); + const [updateUsername] = useReverification(() => user?.update({ username: usernameField.value })); const { userSettings } = useEnvironment(); const card = useCardState(); diff --git a/packages/shared/src/react/hooks/useReverification.ts b/packages/shared/src/react/hooks/useReverification.ts index 9b3ddb80089..7efb0d844ec 100644 --- a/packages/shared/src/react/hooks/useReverification.ts +++ b/packages/shared/src/react/hooks/useReverification.ts @@ -9,24 +9,23 @@ import { useClerk } from './useClerk'; import { useSafeLayoutEffect } from './useSafeLayoutEffect'; async function resolveResult( - result: Promise, + result: Promise | T, ): Promise> { - return result - .then(r => { - if (r instanceof Response) { - return r.json(); - } - return r; - }) - .catch(e => { - // Treat fapi assurance as an assurance hint - if (isClerkAPIResponseError(e) && e.errors.find(({ code }) => code == 'session_step_up_verification_required')) { - return __experimental_reverificationError(); - } - - // rethrow - throw e; - }); + try { + const r = await result; + if (r instanceof Response) { + return r.json(); + } + return r; + } catch (e) { + // Treat fapi assurance as an assurance hint + if (isClerkAPIResponseError(e) && e.errors.find(({ code }) => code == 'session_step_up_verification_required')) { + return __experimental_reverificationError(); + } + + // rethrow + throw e; + } } type ExcludeClerkError = T extends { clerk_error: any } ? (P extends { throwOnCancel: true } ? never : null) : T; @@ -41,12 +40,12 @@ type CreateReverificationHandlerParams = UseReverificationOptions & { }; function createReverificationHandler(params: CreateReverificationHandlerParams) { - function assertReverification Promise>( + function assertReverification Promise | undefined>( fetcher: Fetcher, ): ( ...args: Parameters ) => Promise>, Parameters[1]>> { - return (async (...args) => { + return (async (...args: Parameters) => { let result = await resolveResult(fetcher(...args)); if (__experimental_isReverificationHint(result)) { @@ -99,14 +98,14 @@ function createReverificationHandler(params: CreateReverificationHandlerParams) } return result; - }) as Fetcher; + }) as ExcludeClerkError>, Parameters[1]>; } return assertReverification; } type UseReverificationResult< - Fetcher extends (...args: any[]) => Promise, + Fetcher extends (...args: any[]) => Promise | undefined, Options extends UseReverificationOptions, > = readonly [(...args: Parameters) => Promise>, Options>>]; @@ -129,7 +128,7 @@ type UseReverificationResult< * } */ function __experimental_useReverification< - Fetcher extends (...args: any[]) => Promise, + Fetcher extends (...args: any[]) => Promise | undefined, Options extends UseReverificationOptions, >(fetcher: Fetcher, options?: Options): UseReverificationResult { const { __experimental_openUserVerification } = useClerk(); From 33a4a0244c2088b069e16337977e39d72a6801f8 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 18 Nov 2024 12:29:22 +0200 Subject: [PATCH 15/15] revert change by mistake --- .../src/ui/components/UserProfile/ConnectedAccountsSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx index 03e81a2e809..ab61bcd6878 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx @@ -100,7 +100,7 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => const [createExternalAccount] = useReverification(() => user?.createExternalAccount({ - strategy: 'wowow' as OAuthStrategy, + strategy: account.verification!.strategy as OAuthStrategy, redirectUrl, additionalScopes, }),