Skip to content

feat(clerk-js,clerk-react,types): Signal transfer support #6614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/late-results-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal transfer support
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
17 changes: 17 additions & 0 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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({
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class State implements StateInterface {
}

if (payload.resource instanceof SignUp) {
this.signUpResourceSignal({ resource: payload.resource });
this.signUpErrorSignal({ error: payload.error });
}
};

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/experimental.ts
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure the hook is actually exported from hooks/useClerkSignal and add JSDoc at the source.

Add or verify JSDoc on the hook definition so consumers see guidance in IDEs (experimental, subject to change).


🏁 Script executed:

#!/bin/bash
# Confirm the named export exists and is correctly spelled.
rg -n "^export\s+\{\s*useSignInSignal\s*,\s*useSignUpSignal\s*\}" packages/react/src/hooks/useClerkSignal.ts
rg -n "export function useSignUpSignal|export const useSignUpSignal" packages/react/src/hooks/useClerkSignal.ts

Length of output: 263


🏁 Script executed:

#!/bin/bash
# Verify presence of useSignInSignal and its export
rg -n "useSignInSignal" packages/react/src/hooks/useClerkSignal.ts
rg -C3 -n "export function useSignInSignal" packages/react/src/hooks/useClerkSignal.ts

# Verify presence of useSignUpSignal export context and surrounding JSDoc
rg -C3 -n "export function useSignUpSignal" packages/react/src/hooks/useClerkSignal.ts
rg -C3 -n "/\*\*" packages/react/src/hooks/useClerkSignal.ts

Length of output: 589


Add JSDoc to exported hooks in useClerkSignal.ts

Both useSignInSignal (line 58) and useSignUpSignal (line 62) are correctly exported, but neither has preceding JSDoc comments. Please add descriptive JSDoc blocks above each function so IDEs surface usage details and mark these as experimental. For example:

• File: packages/react/src/hooks/useClerkSignal.ts
– Line 57–58 (before export function useSignInSignal):

/**
 * Returns a signal that tracks the sign-in process.
 *
 * @remarks
 * Experimental: subject to change.
 *
 * @returns A signal object for sign-in state.
 */
export function useSignInSignal() {  }

• File: packages/react/src/hooks/useClerkSignal.ts
– Line 61–62 (before export function useSignUpSignal):

/**
 * Returns a signal that tracks the sign-up process.
 *
 * @remarks
 * Experimental: subject to change.
 *
 * @returns A signal object for sign-up state.
 */
export function useSignUpSignal() {  }

This ensures consumers see the experimental-status notice and return type guidance in their IDEs.

🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts around lines 57–62, the exported
functions useSignInSignal and useSignUpSignal lack JSDoc comments; add
descriptive JSDoc blocks immediately above each export that briefly describe
what the hook returns, include an "@remarks Experimental: subject to change."
line, and an "@returns" annotation describing the returned signal object so IDEs
surface usage and the experimental status.


export type {
__experimental_CheckoutButtonProps as CheckoutButtonProps,
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/signIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/signUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;
}

Expand Down
Loading