Skip to content

Commit 2ed539c

Browse files
authored
feat(clerk-js,clerk-react,types): Signal transfer support (#6614)
1 parent 9036427 commit 2ed539c

File tree

9 files changed

+102
-3
lines changed

9 files changed

+102
-3
lines changed

.changeset/late-results-melt.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/clerk-react': minor
4+
'@clerk/types': minor
5+
---
6+
7+
[Experimental] Signal transfer support

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"files": [
33
{ "path": "./dist/clerk.js", "maxSize": "629KB" },
44
{ "path": "./dist/clerk.browser.js", "maxSize": "78KB" },
5-
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "119KB" },
5+
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "120KB" },
66
{ "path": "./dist/clerk.headless*.js", "maxSize": "61KB" },
77
{ "path": "./dist/ui-common*.js", "maxSize": "117.1KB" },
88
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },

packages/clerk-js/src/core/resources/SignIn.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,22 @@ class SignInFuture implements SignInFutureResource {
505505
return this.resource.supportedFirstFactors ?? [];
506506
}
507507

508+
get isTransferable() {
509+
return this.resource.firstFactorVerification.status === 'transferable';
510+
}
511+
512+
get existingSession() {
513+
if (
514+
this.resource.firstFactorVerification.status === 'failed' &&
515+
this.resource.firstFactorVerification.error?.code === 'identifier_already_signed_in' &&
516+
this.resource.firstFactorVerification.error?.meta?.sessionId
517+
) {
518+
return { sessionId: this.resource.firstFactorVerification.error?.meta?.sessionId };
519+
}
520+
521+
return undefined;
522+
}
523+
508524
async sendResetPasswordEmailCode(): Promise<{ error: unknown }> {
509525
return runAsyncResourceTask(this.resource, async () => {
510526
if (!this.resource.id) {
@@ -556,6 +572,7 @@ class SignInFuture implements SignInFutureResource {
556572
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
557573
redirectUrl?: string;
558574
actionCompleteRedirectUrl?: string;
575+
transfer?: boolean;
559576
}): Promise<{ error: unknown }> {
560577
return runAsyncResourceTask(this.resource, async () => {
561578
await this.resource.__internal_basePost({

packages/clerk-js/src/core/resources/SignUp.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,27 @@ class SignUpFuture implements SignUpFutureResource {
488488
return this.resource.unverifiedFields;
489489
}
490490

491+
get isTransferable() {
492+
// TODO: we can likely remove the error code check as the status should be sufficient
493+
return (
494+
this.resource.verifications.externalAccount.status === 'transferable' &&
495+
this.resource.verifications.externalAccount.error?.code === 'external_account_exists'
496+
);
497+
}
498+
499+
get existingSession() {
500+
if (
501+
(this.resource.verifications.externalAccount.status === 'failed' ||
502+
this.resource.verifications.externalAccount.status === 'unverified') &&
503+
this.resource.verifications.externalAccount.error?.code === 'identifier_already_signed_in' &&
504+
this.resource.verifications.externalAccount.error?.meta?.sessionId
505+
) {
506+
return { sessionId: this.resource.verifications.externalAccount.error?.meta?.sessionId };
507+
}
508+
509+
return undefined;
510+
}
511+
491512
private async getCaptchaToken(): Promise<{
492513
captchaToken?: string;
493514
captchaWidgetType?: CaptchaWidgetType;
@@ -503,6 +524,16 @@ class SignUpFuture implements SignUpFutureResource {
503524
return { captchaToken, captchaWidgetType, captchaError };
504525
}
505526

527+
async create({ transfer }: { transfer?: boolean }): Promise<{ error: unknown }> {
528+
return runAsyncResourceTask(this.resource, async () => {
529+
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken();
530+
await this.resource.__internal_basePost({
531+
path: this.resource.pathRoot,
532+
body: { transfer, captchaToken, captchaWidgetType, captchaError },
533+
});
534+
});
535+
}
536+
506537
async password({ emailAddress, password }: { emailAddress: string; password: string }): Promise<{ error: unknown }> {
507538
return runAsyncResourceTask(this.resource, async () => {
508539
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken();
@@ -539,6 +570,39 @@ class SignUpFuture implements SignUpFutureResource {
539570
});
540571
}
541572

573+
async sso({
574+
strategy,
575+
redirectUrl,
576+
redirectUrlComplete,
577+
}: {
578+
strategy: string;
579+
redirectUrl: string;
580+
redirectUrlComplete: string;
581+
}): Promise<{ error: unknown }> {
582+
return runAsyncResourceTask(this.resource, async () => {
583+
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken();
584+
await this.resource.__internal_basePost({
585+
path: this.resource.pathRoot,
586+
body: {
587+
strategy,
588+
redirectUrl: SignUp.clerk.buildUrlWithAuth(redirectUrl),
589+
redirectUrlComplete,
590+
captchaToken,
591+
captchaWidgetType,
592+
captchaError,
593+
},
594+
});
595+
596+
const { status, externalVerificationRedirectURL } = this.resource.verifications.externalAccount;
597+
598+
if (status === 'unverified' && !!externalVerificationRedirectURL) {
599+
windowNavigate(externalVerificationRedirectURL);
600+
} else {
601+
clerkInvalidFAPIResponse(status, SignUp.fapiClient.buildEmailAddress('support'));
602+
}
603+
});
604+
}
605+
542606
async finalize({ navigate }: { navigate?: SetActiveNavigate } = {}): Promise<{ error: unknown }> {
543607
return runAsyncResourceTask(this.resource, async () => {
544608
if (!this.resource.createdSessionId) {

packages/clerk-js/src/core/state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class State implements StateInterface {
4242
}
4343

4444
if (payload.resource instanceof SignUp) {
45-
this.signUpResourceSignal({ resource: payload.resource });
45+
this.signUpErrorSignal({ error: payload.error });
4646
}
4747
};
4848

packages/react/src/experimental.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export { CheckoutButton } from './components/CheckoutButton';
22
export { PlanDetailsButton } from './components/PlanDetailsButton';
33
export { SubscriptionDetailsButton } from './components/SubscriptionDetailsButton';
4-
export { useSignInSignal } from './hooks/useClerkSignal';
4+
export { useSignInSignal, useSignUpSignal } from './hooks/useClerkSignal';
55

66
export type {
77
__experimental_CheckoutButtonProps as CheckoutButtonProps,

packages/react/src/stateProxy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class StateProxy implements State {
4343
signIn: {
4444
status: 'needs_identifier' as const,
4545
availableStrategies: [],
46+
isTransferable: false,
4647

4748
create: this.gateMethod(target, 'create'),
4849
password: this.gateMethod(target, 'password'),
@@ -68,7 +69,10 @@ export class StateProxy implements State {
6869
signUp: {
6970
status: 'missing_requirements' as const,
7071
unverifiedFields: [],
72+
isTransferable: false,
7173

74+
create: this.gateMethod(target, 'create'),
75+
sso: this.gateMethod(target, 'sso'),
7276
password: this.gateMethod(target, 'password'),
7377
finalize: this.gateMethod(target, 'finalize'),
7478

packages/types/src/signIn.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,14 @@ export interface SignInResource extends ClerkResource {
134134
export interface SignInFutureResource {
135135
availableStrategies: SignInFirstFactor[];
136136
status: SignInStatus | null;
137+
isTransferable: boolean;
138+
existingSession?: { sessionId: string };
137139
create: (params: {
138140
identifier?: string;
139141
strategy?: OAuthStrategy | 'saml' | 'enterprise_sso';
140142
redirectUrl?: string;
141143
actionCompleteRedirectUrl?: string;
144+
transfer?: boolean;
142145
}) => Promise<{ error: unknown }>;
143146
password: (params: { identifier?: string; password: string }) => Promise<{ error: unknown }>;
144147
emailCode: {

packages/types/src/signUp.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,15 @@ export interface SignUpResource extends ClerkResource {
127127
export interface SignUpFutureResource {
128128
status: SignUpStatus | null;
129129
unverifiedFields: SignUpIdentificationField[];
130+
isTransferable: boolean;
131+
existingSession?: { sessionId: string };
132+
create: (params: { transfer?: boolean }) => Promise<{ error: unknown }>;
130133
verifications: {
131134
sendEmailCode: () => Promise<{ error: unknown }>;
132135
verifyEmailCode: (params: { code: string }) => Promise<{ error: unknown }>;
133136
};
134137
password: (params: { emailAddress: string; password: string }) => Promise<{ error: unknown }>;
138+
sso: (params: { strategy: string; redirectUrl: string; redirectUrlComplete: string }) => Promise<{ error: unknown }>;
135139
finalize: (params?: { navigate?: SetActiveNavigate }) => Promise<{ error: unknown }>;
136140
}
137141

0 commit comments

Comments
 (0)