diff --git a/.changeset/late-results-melt.md b/.changeset/late-results-melt.md new file mode 100644 index 00000000000..05120d6b387 --- /dev/null +++ b/.changeset/late-results-melt.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +[Experimental] Signal transfer support diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 043117dcec0..0aa61486504 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "./dist/clerk.js", "maxSize": "629KB" }, { "path": "./dist/clerk.browser.js", "maxSize": "78KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "119KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "120KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "61KB" }, { "path": "./dist/ui-common*.js", "maxSize": "117.1KB" }, { "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" }, diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 707b2b97261..1a23be65626 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -505,6 +505,22 @@ class SignInFuture implements SignInFutureResource { return this.resource.supportedFirstFactors ?? []; } + get isTransferable() { + return this.resource.firstFactorVerification.status === 'transferable'; + } + + get existingSession() { + if ( + this.resource.firstFactorVerification.status === 'failed' && + this.resource.firstFactorVerification.error?.code === 'identifier_already_signed_in' && + this.resource.firstFactorVerification.error?.meta?.sessionId + ) { + return { sessionId: this.resource.firstFactorVerification.error?.meta?.sessionId }; + } + + return undefined; + } + async sendResetPasswordEmailCode(): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { if (!this.resource.id) { @@ -556,6 +572,7 @@ class SignInFuture implements SignInFutureResource { strategy?: OAuthStrategy | 'saml' | 'enterprise_sso'; redirectUrl?: string; actionCompleteRedirectUrl?: string; + transfer?: boolean; }): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { await this.resource.__internal_basePost({ diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 8f9aee5b12e..4fd87d9c9ee 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -488,6 +488,26 @@ class SignUpFuture implements SignUpFutureResource { return this.resource.unverifiedFields; } + get isTransferable() { + return ( + this.resource.verifications.externalAccount.status === 'transferable' && + this.resource.verifications.externalAccount.error?.code === 'external_account_exists' + ); + } + + get existingSession() { + if ( + (this.resource.verifications.externalAccount.status === 'failed' || + this.resource.verifications.externalAccount.status === 'unverified') && + this.resource.verifications.externalAccount.error?.code === 'identifier_already_signed_in' && + this.resource.verifications.externalAccount.error?.meta?.sessionId + ) { + return { sessionId: this.resource.verifications.externalAccount.error?.meta?.sessionId }; + } + + return undefined; + } + private async getCaptchaToken(): Promise<{ captchaToken?: string; captchaWidgetType?: CaptchaWidgetType; @@ -503,6 +523,16 @@ class SignUpFuture implements SignUpFutureResource { return { captchaToken, captchaWidgetType, captchaError }; } + async create({ transfer }: { transfer?: boolean }): Promise<{ error: unknown }> { + return runAsyncResourceTask(this.resource, async () => { + const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(); + await this.resource.__internal_basePost({ + path: this.resource.pathRoot, + body: { transfer, captchaToken, captchaWidgetType, captchaError }, + }); + }); + } + async password({ emailAddress, password }: { emailAddress: string; password: string }): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(); @@ -539,6 +569,39 @@ class SignUpFuture implements SignUpFutureResource { }); } + async sso({ + strategy, + redirectUrl, + redirectUrlComplete, + }: { + strategy: string; + redirectUrl: string; + redirectUrlComplete: string; + }): Promise<{ error: unknown }> { + return runAsyncResourceTask(this.resource, async () => { + const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(); + await this.resource.__internal_basePost({ + path: this.resource.pathRoot, + body: { + strategy, + redirectUrl: SignUp.clerk.buildUrlWithAuth(redirectUrl), + redirectUrlComplete, + captchaToken, + captchaWidgetType, + captchaError, + }, + }); + + const { status, externalVerificationRedirectURL } = this.resource.verifications.externalAccount; + + if (status === 'unverified' && !!externalVerificationRedirectURL) { + windowNavigate(externalVerificationRedirectURL); + } else { + clerkInvalidFAPIResponse(status, SignUp.fapiClient.buildEmailAddress('support')); + } + }); + } + async finalize({ navigate }: { navigate?: SetActiveNavigate }): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { if (!this.resource.createdSessionId) { diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index a78586b920c..5746ed17d98 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -42,7 +42,7 @@ export class State implements StateInterface { } if (payload.resource instanceof SignUp) { - this.signUpResourceSignal({ resource: payload.resource }); + this.signUpErrorSignal({ error: payload.error }); } }; diff --git a/packages/react/src/experimental.ts b/packages/react/src/experimental.ts index 73b6ff81985..4926de9ffcb 100644 --- a/packages/react/src/experimental.ts +++ b/packages/react/src/experimental.ts @@ -1,7 +1,7 @@ export { CheckoutButton } from './components/CheckoutButton'; export { PlanDetailsButton } from './components/PlanDetailsButton'; export { SubscriptionDetailsButton } from './components/SubscriptionDetailsButton'; -export { useSignInSignal } from './hooks/useClerkSignal'; +export { useSignInSignal, useSignUpSignal } from './hooks/useClerkSignal'; export type { __experimental_CheckoutButtonProps as CheckoutButtonProps, diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index f1f7ff23920..63069becb48 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -129,11 +129,14 @@ export interface SignInResource extends ClerkResource { export interface SignInFutureResource { availableStrategies: SignInFirstFactor[]; status: SignInStatus | null; + isTransferable: boolean; + existingSession?: { sessionId: string }; create: (params: { identifier?: string; strategy?: OAuthStrategy | 'saml' | 'enterprise_sso'; redirectUrl?: string; actionCompleteRedirectUrl?: string; + transfer?: boolean; }) => Promise<{ error: unknown }>; password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>; emailCode: { diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index eded9a32f1d..7a5b08c6800 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -122,11 +122,15 @@ export interface SignUpResource extends ClerkResource { export interface SignUpFutureResource { status: SignUpStatus | null; unverifiedFields: SignUpIdentificationField[]; + isTransferable: boolean; + existingSession?: { sessionId: string }; + create: (params: { transfer?: boolean }) => Promise<{ error: unknown }>; verifications: { sendEmailCode: () => Promise<{ error: unknown }>; verifyEmailCode: (params: { code: string }) => Promise<{ error: unknown }>; }; password: (params: { emailAddress: string; password: string }) => Promise<{ error: unknown }>; + sso: (params: { strategy: string; redirectUrl: string; redirectUrlComplete: string }) => Promise<{ error: unknown }>; finalize: (params: { navigate?: SetActiveNavigate }) => Promise<{ error: unknown }>; }