|
| 1 | +import { inBrowser } from '@clerk/shared/browser'; |
1 | 2 | import { ClerkWebAuthnError } from '@clerk/shared/error'; |
2 | 3 | import { Poller } from '@clerk/shared/poller'; |
3 | 4 | import { deepCamelToSnake, deepSnakeToCamel } from '@clerk/shared/underscore'; |
@@ -31,6 +32,7 @@ import type { |
31 | 32 | SignInFutureCreateParams, |
32 | 33 | SignInFutureEmailCodeSendParams, |
33 | 34 | SignInFutureEmailCodeVerifyParams, |
| 35 | + SignInFutureEmailLinkSendParams, |
34 | 36 | SignInFutureFinalizeParams, |
35 | 37 | SignInFutureMFAPhoneCodeVerifyParams, |
36 | 38 | SignInFuturePasswordParams, |
@@ -61,6 +63,7 @@ import { |
61 | 63 | generateSignatureWithMetamask, |
62 | 64 | generateSignatureWithOKXWallet, |
63 | 65 | getBaseIdentifier, |
| 66 | + getClerkQueryParam, |
64 | 67 | getCoinbaseWalletIdentifier, |
65 | 68 | getMetamaskIdentifier, |
66 | 69 | getOKXWalletIdentifier, |
@@ -140,6 +143,14 @@ export class SignIn extends BaseResource implements SignInResource { |
140 | 143 | */ |
141 | 144 | __internal_basePost = this._basePost.bind(this); |
142 | 145 |
|
| 146 | + /** |
| 147 | + * @internal Only used for internal purposes, and is not intended to be used directly. |
| 148 | + * |
| 149 | + * This property is used to provide access to underlying Client methods to `SignInFuture`, which wraps an instance |
| 150 | + * of `SignIn`. |
| 151 | + */ |
| 152 | + __internal_baseGet = this._baseGet.bind(this); |
| 153 | + |
143 | 154 | constructor(data: SignInJSON | SignInJSONSnapshot | null = null) { |
144 | 155 | super(); |
145 | 156 | this.fromJSON(data); |
@@ -539,6 +550,34 @@ class SignInFuture implements SignInFutureResource { |
539 | 550 | verifyCode: this.verifyEmailCode.bind(this), |
540 | 551 | }; |
541 | 552 |
|
| 553 | + emailLink = { |
| 554 | + sendLink: this.sendEmailLink.bind(this), |
| 555 | + waitForVerification: this.waitForEmailLinkVerification.bind(this), |
| 556 | + get verification() { |
| 557 | + if (!inBrowser()) { |
| 558 | + return null; |
| 559 | + } |
| 560 | + |
| 561 | + const status = getClerkQueryParam('__clerk_status') as 'verified' | 'expired' | 'failed' | 'client_mismatch'; |
| 562 | + const createdSessionId = getClerkQueryParam('__clerk_created_session'); |
| 563 | + |
| 564 | + if (!status || !createdSessionId) { |
| 565 | + return null; |
| 566 | + } |
| 567 | + |
| 568 | + const verifiedFromTheSameClient = |
| 569 | + status === 'verified' && |
| 570 | + typeof SignIn.clerk.client !== 'undefined' && |
| 571 | + SignIn.clerk.client.sessions.some(s => s.id === createdSessionId); |
| 572 | + |
| 573 | + return { |
| 574 | + status, |
| 575 | + createdSessionId, |
| 576 | + verifiedFromTheSameClient, |
| 577 | + }; |
| 578 | + }, |
| 579 | + }; |
| 580 | + |
542 | 581 | resetPasswordEmailCode = { |
543 | 582 | sendCode: this.sendResetPasswordEmailCode.bind(this), |
544 | 583 | verifyCode: this.verifyResetPasswordEmailCode.bind(this), |
@@ -583,6 +622,10 @@ class SignInFuture implements SignInFutureResource { |
583 | 622 | return undefined; |
584 | 623 | } |
585 | 624 |
|
| 625 | + get firstFactorVerification() { |
| 626 | + return this.resource.firstFactorVerification; |
| 627 | + } |
| 628 | + |
586 | 629 | async sendResetPasswordEmailCode(): Promise<{ error: unknown }> { |
587 | 630 | return runAsyncResourceTask(this.resource, async () => { |
588 | 631 | if (!this.resource.id) { |
@@ -680,6 +723,56 @@ class SignInFuture implements SignInFutureResource { |
680 | 723 | }); |
681 | 724 | } |
682 | 725 |
|
| 726 | + async sendEmailLink(params: SignInFutureEmailLinkSendParams): Promise<{ error: unknown }> { |
| 727 | + const { email, verificationUrl } = params; |
| 728 | + return runAsyncResourceTask(this.resource, async () => { |
| 729 | + if (!this.resource.id) { |
| 730 | + await this.create({ identifier: email }); |
| 731 | + } |
| 732 | + |
| 733 | + const emailLinkFactor = this.resource.supportedFirstFactors?.find(f => f.strategy === 'email_link'); |
| 734 | + |
| 735 | + if (!emailLinkFactor) { |
| 736 | + throw new Error('Email link factor not found'); |
| 737 | + } |
| 738 | + |
| 739 | + const { emailAddressId } = emailLinkFactor; |
| 740 | + |
| 741 | + let absoluteVerificationUrl = verificationUrl; |
| 742 | + try { |
| 743 | + new URL(verificationUrl); |
| 744 | + } catch { |
| 745 | + absoluteVerificationUrl = window.location.origin + verificationUrl; |
| 746 | + } |
| 747 | + |
| 748 | + await this.resource.__internal_basePost({ |
| 749 | + body: { emailAddressId, redirectUrl: absoluteVerificationUrl, strategy: 'email_link' }, |
| 750 | + action: 'prepare_first_factor', |
| 751 | + }); |
| 752 | + }); |
| 753 | + } |
| 754 | + |
| 755 | + async waitForEmailLinkVerification(): Promise<{ error: unknown }> { |
| 756 | + return runAsyncResourceTask(this.resource, async () => { |
| 757 | + const { run, stop } = Poller(); |
| 758 | + await new Promise((resolve, reject) => { |
| 759 | + void run(async () => { |
| 760 | + try { |
| 761 | + const res = await this.resource.__internal_baseGet(); |
| 762 | + const status = res.firstFactorVerification.status; |
| 763 | + if (status === 'verified' || status === 'expired') { |
| 764 | + stop(); |
| 765 | + resolve(res); |
| 766 | + } |
| 767 | + } catch (err) { |
| 768 | + stop(); |
| 769 | + reject(err); |
| 770 | + } |
| 771 | + }); |
| 772 | + }); |
| 773 | + }); |
| 774 | + } |
| 775 | + |
683 | 776 | async sendPhoneCode(params: SignInFuturePhoneCodeSendParams): Promise<{ error: unknown }> { |
684 | 777 | const { phoneNumber, channel = 'sms' } = params; |
685 | 778 | return runAsyncResourceTask(this.resource, async () => { |
@@ -787,6 +880,9 @@ class SignInFuture implements SignInFutureResource { |
787 | 880 | throw new Error('Cannot finalize sign-in without a created session.'); |
788 | 881 | } |
789 | 882 |
|
| 883 | + // Reload the client to prevent an issue where the created session is not picked up. |
| 884 | + await SignIn.clerk.client?.reload(); |
| 885 | + |
790 | 886 | await SignIn.clerk.setActive({ session: this.resource.createdSessionId, navigate }); |
791 | 887 | }); |
792 | 888 | } |
|
0 commit comments