From aadf3ba66fc8994cb0067b60587d387c0fde712d Mon Sep 17 00:00:00 2001 From: Haris Chaniotakis Date: Fri, 9 Aug 2024 13:09:42 +0300 Subject: [PATCH 1/3] wip --- packages/clerk-js/src/core/clerk.ts | 13 ++- .../clerk-js/src/core/resources/SignIn.ts | 26 ++++-- .../clerk-js/src/core/resources/SignUp.ts | 34 ++++++-- packages/clerk-js/src/ui/common/constants.ts | 4 + .../components/SignIn/SignInSocialButtons.tsx | 3 +- .../ui/components/UserProfile/Web3Form.tsx | 2 +- packages/clerk-js/src/utils/web3.ts | 85 +++++++++++++++++-- packages/types/src/clerk.ts | 2 + packages/types/src/signIn.ts | 2 + packages/types/src/signUp.ts | 3 +- packages/types/src/web3.ts | 8 +- packages/types/src/web3Wallet.ts | 2 + 12 files changed, 155 insertions(+), 29 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 4b0ca388918..041ad9dbdcd 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1326,6 +1326,7 @@ export class Clerk implements ClerkInterface { signUpContinueUrl, customNavigate, unsafeMetadata, + strategy, }: AuthenticateWithMetamaskParams = {}): Promise => { if (!this.client || !this.environment) { return; @@ -1336,10 +1337,18 @@ export class Clerk implements ClerkInterface { let signInOrSignUp: SignInResource | SignUpResource; try { - signInOrSignUp = await this.client.signIn.authenticateWithMetamask(); + if (strategy === 'web3_metamask_signature') { + signInOrSignUp = await this.client.signIn.authenticateWithMetamask(); + } else { + signInOrSignUp = await this.client.signIn.authenticateWithCoinbase(); + } } catch (err) { if (isError(err, ERROR_CODES.FORM_IDENTIFIER_NOT_FOUND)) { - signInOrSignUp = await this.client.signUp.authenticateWithMetamask({ unsafeMetadata }); + if (strategy === 'web3_metamask_signature') { + signInOrSignUp = await this.client.signUp.authenticateWithMetamask({ unsafeMetadata }); + } else { + signInOrSignUp = await this.client.signUp.authenticateWithCoinbase({ unsafeMetadata }); + } if ( signUpContinueUrl && diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 77562861556..8e1eee73c42 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -31,7 +31,13 @@ import type { Web3SignatureFactor, } from '@clerk/types'; -import { generateSignatureWithMetamask, getMetamaskIdentifier, windowNavigate } from '../../utils'; +import { + generateSignatureWithCoinbase, + generateSignatureWithMetamask, + getCoinbaseIdentifier, + getMetamaskIdentifier, + windowNavigate, +} from '../../utils'; import { ClerkWebAuthnError, convertJSONToPublicKeyRequestOptions, @@ -223,16 +229,14 @@ export class SignIn extends BaseResource implements SignInResource { }; public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { - const { identifier, generateSignature } = params || {}; + const { identifier, generateSignature, strategy } = params || {}; if (!(typeof generateSignature === 'function')) { clerkMissingOptionError('generateSignature'); } await this.create({ identifier }); - const web3FirstFactor = this.supportedFirstFactors.find( - f => f.strategy === 'web3_metamask_signature', - ) as Web3SignatureFactor; + const web3FirstFactor = this.supportedFirstFactors.find(f => f.strategy === strategy) as Web3SignatureFactor; if (!web3FirstFactor) { clerkVerifyWeb3WalletCalledBeforeCreate('SignIn'); @@ -248,7 +252,7 @@ export class SignIn extends BaseResource implements SignInResource { return this.attemptFirstFactor({ signature, - strategy: 'web3_metamask_signature', + strategy, }); }; @@ -257,6 +261,16 @@ export class SignIn extends BaseResource implements SignInResource { return this.authenticateWithWeb3({ identifier, generateSignature: generateSignatureWithMetamask, + strategy: 'web3_metamask_signature', + }); + }; + + public authenticateWithCoinbase = async (): Promise => { + const identifier = await getCoinbaseIdentifier(); + return this.authenticateWithWeb3({ + identifier, + generateSignature: generateSignatureWithCoinbase, + strategy: 'web3_coinbase_signature', }); }; diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 6b610699b28..602a8bbb444 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -19,9 +19,16 @@ import type { SignUpStatus, SignUpUpdateParams, StartEmailLinkFlowParams, + Web3Strategy, } from '@clerk/types'; -import { generateSignatureWithMetamask, getMetamaskIdentifier, windowNavigate } from '../../utils'; +import { + generateSignatureWithCoinbase, + generateSignatureWithMetamask, + getCoinbaseIdentifier, + getMetamaskIdentifier, + windowNavigate, +} from '../../utils'; import { getCaptchaToken, retrieveCaptchaInfo } from '../../utils/captcha'; import { createValidatePassword } from '../../utils/passwords/password'; import { normalizeUnsafeMetadata } from '../../utils/resourceParams'; @@ -170,22 +177,22 @@ export class SignUp extends BaseResource implements SignUpResource { return this.attemptVerification({ ...params, strategy: 'phone_code' }); }; - prepareWeb3WalletVerification = (): Promise => { - return this.prepareVerification({ strategy: 'web3_metamask_signature' }); + prepareWeb3WalletVerification = (strategy: Web3Strategy): Promise => { + return this.prepareVerification({ strategy }); }; attemptWeb3WalletVerification = async (params: AttemptWeb3WalletVerificationParams): Promise => { - const { signature } = params; - return this.attemptVerification({ signature, strategy: 'web3_metamask_signature' }); + const { signature, strategy } = params; + return this.attemptVerification({ signature, strategy }); }; public authenticateWithWeb3 = async ( params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, ): Promise => { - const { generateSignature, identifier, unsafeMetadata } = params || {}; + const { generateSignature, identifier, strategy, unsafeMetadata } = params || {}; const web3Wallet = identifier || this.web3wallet!; await this.create({ web3Wallet, unsafeMetadata }); - await this.prepareWeb3WalletVerification(); + await this.prepareWeb3WalletVerification(strategy); const { nonce } = this.verifications.web3Wallet; if (!nonce) { @@ -193,7 +200,7 @@ export class SignUp extends BaseResource implements SignUpResource { } const signature = await generateSignature({ identifier, nonce }); - return this.attemptWeb3WalletVerification({ signature }); + return this.attemptWeb3WalletVerification({ signature, strategy }); }; public authenticateWithMetamask = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { @@ -201,6 +208,17 @@ export class SignUp extends BaseResource implements SignUpResource { return this.authenticateWithWeb3({ identifier, generateSignature: generateSignatureWithMetamask, + strategy: 'web3_metamask_signature', + unsafeMetadata: params?.unsafeMetadata, + }); + }; + + public authenticateWithCoinbase = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { + const identifier = await getCoinbaseIdentifier(); + return this.authenticateWithWeb3({ + identifier, + generateSignature: generateSignatureWithCoinbase, + strategy: 'web3_coinbase_signature', unsafeMetadata: params?.unsafeMetadata, }); }; diff --git a/packages/clerk-js/src/ui/common/constants.ts b/packages/clerk-js/src/ui/common/constants.ts index f14f0e8af8f..3f9ff3eb4c9 100644 --- a/packages/clerk-js/src/ui/common/constants.ts +++ b/packages/clerk-js/src/ui/common/constants.ts @@ -92,6 +92,10 @@ export const WEB3_PROVIDERS: Web3Providers = Object.freeze({ id: 'metamask', name: 'MetaMask', }, + coinbase: { + id: 'coinbase', + name: 'Coinbase Wallet' + } }); export function getWeb3ProviderData(name: Web3Provider): Web3ProviderData | undefined | null { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 2ba1b6aae35..bce09e9ede8 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -28,12 +28,13 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { .authenticateWithRedirect({ strategy, redirectUrl, redirectUrlComplete }) .catch(err => handleError(err, [], card.setError)); }} - web3Callback={() => { + web3Callback={strategy => { return clerk .authenticateWithMetamask({ customNavigate: navigate, redirectUrl: redirectUrlComplete, signUpContinueUrl: ctx.signUpContinueUrl, + strategy, }) .catch(err => handleError(err, [], card.setError)); }} diff --git a/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx b/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx index b2f8509e40d..29b1d4d4102 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx @@ -32,7 +32,7 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(() => { web3Wallet = await web3Wallet.prepareVerification({ strategy: 'web3_metamask_signature' }); const nonce = web3Wallet.verification.nonce as string; const signature = await generateSignatureWithMetamask({ identifier, nonce }); - await web3Wallet.attemptVerification({ signature }); + await web3Wallet.attemptVerification({ signature, strategy }); card.setIdle(); } catch (err) { card.setIdle(); diff --git a/packages/clerk-js/src/utils/web3.ts b/packages/clerk-js/src/utils/web3.ts index 464582497d9..5fecd1bba6c 100644 --- a/packages/clerk-js/src/utils/web3.ts +++ b/packages/clerk-js/src/utils/web3.ts @@ -1,15 +1,13 @@ import { toHex } from './hex'; export async function getMetamaskIdentifier(): Promise { - // @ts-ignore - if (!global.ethereum) { - // Do nothing when ethereum doesn't exist. We might revise this in the future - // to offer an Install Metamask prompt to our users. + const ethereum = getMetamaskProvider(); + if (!ethereum) { return ''; } // @ts-ignore - const identifiers = await global.ethereum.request({ + const identifiers = await ethereum.request({ method: 'eth_requestAccounts', }); @@ -22,18 +20,87 @@ export type GenerateSignatureParams = { }; export async function generateSignatureWithMetamask({ identifier, nonce }: GenerateSignatureParams): Promise { + const ethereum = getMetamaskProvider(); + if (!ethereum) { + return ''; + } + // @ts-ignore - if (!global.ethereum) { - // Do nothing when ethereum doesn't exist. We might revise this in the future - // to offer an Install Metamask prompt to our users. + const signature: string = await ethereum.request({ + method: 'personal_sign', + params: [`0x${toHex(nonce)}`, identifier], + }); + + return signature; +} + +export async function getCoinbaseIdentifier(): Promise { + const ethereum = getCoinbaseProvider(); + if (!ethereum) { return ''; } // @ts-ignore - const signature: string = await global.ethereum.request({ + const identifiers = await ethereum.request({ + method: 'eth_requestAccounts', + }); + + return (identifiers && identifiers[0]) || ''; +} + +export async function generateSignatureWithCoinbase({ identifier, nonce }: GenerateSignatureParams): Promise { + const ethereum = getCoinbaseProvider(); + if (!ethereum) { + return ''; + } + + // @ts-ignore + const signature: string = await ethereum.request({ method: 'personal_sign', params: [`0x${toHex(nonce)}`, identifier], }); return signature; } + +function getMetamaskProvider(): any { + // @ts-ignore + if (!global.ethereum) { + return undefined; + } + + // @ts-ignore + if (global.ethereum.providers) { + // @ts-ignore + return global.ethereum.providers[1]; + } + + // @ts-ignore + if (!global.ethereum.isMetaMask) { + return undefined; + } + + // @ts-ignore + return global.ethereum; +} + +function getCoinbaseProvider(): any { + // @ts-ignore + if (!global.ethereum) { + return undefined; + } + + // @ts-ignore + if (global.ethereum.providers) { + // @ts-ignore + return global.ethereum.providers[0]; + } + + // @ts-ignore + if (!global.ethereum.isCoinbaseWallet) { + return undefined; + } + + // @ts-ignore + return global.ethereum; +} diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index b7478735c78..f78479e62c2 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -36,6 +36,7 @@ import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; import type { UserResource } from './user'; import type { Autocomplete, DeepPartial, DeepSnakeToCamel } from './utils'; +import { Web3Strategy } from './strategies'; export type SDKMetadata = { name: string; @@ -1117,6 +1118,7 @@ export interface AuthenticateWithMetamaskParams { redirectUrl?: string; signUpContinueUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; + strategy?: Web3Strategy; } export interface AuthenticateWithGoogleOneTapParams { diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index ddd6323b396..a1bffa9c046 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -98,6 +98,8 @@ export interface SignInResource extends ClerkResource { authenticateWithMetamask: () => Promise; + authenticateWithCoinbase: () => Promise; + authenticateWithPasskey: (params?: AuthenticateWithPasskeyParams) => Promise; createEmailLinkFlow: () => CreateEmailLinkFlowReturn; diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index 8dc35b83394..93b6173eab2 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -72,7 +72,7 @@ export interface SignUpResource extends ClerkResource { attemptPhoneNumberVerification: (params: AttemptPhoneNumberVerificationParams) => Promise; - prepareWeb3WalletVerification: () => Promise; + prepareWeb3WalletVerification: (strategy: Web3Strategy) => Promise; attemptWeb3WalletVerification: (params: AttemptWeb3WalletVerificationParams) => Promise; @@ -89,6 +89,7 @@ export interface SignUpResource extends ClerkResource { ) => Promise; authenticateWithMetamask: (params?: SignUpAuthenticateWithMetamaskParams) => Promise; + authenticateWithCoinbase: (params?: SignUpAuthenticateWithMetamaskParams) => Promise; } export type SignUpStatus = 'missing_requirements' | 'complete' | 'abandoned'; diff --git a/packages/types/src/web3.ts b/packages/types/src/web3.ts index 2e9ced36066..de9c71bcb30 100644 --- a/packages/types/src/web3.ts +++ b/packages/types/src/web3.ts @@ -7,8 +7,9 @@ export interface Web3ProviderData { } export type MetamaskWeb3Provider = 'metamask'; +export type CoinbaseWeb3Provider = 'coinbase'; -export type Web3Provider = MetamaskWeb3Provider; +export type Web3Provider = MetamaskWeb3Provider | CoinbaseWeb3Provider; export const WEB3_PROVIDERS: Web3ProviderData[] = [ { @@ -16,6 +17,11 @@ export const WEB3_PROVIDERS: Web3ProviderData[] = [ strategy: 'web3_metamask_signature', name: 'MetaMask', }, + { + provider: 'coinbase', + strategy: 'web3_coinbase_signature', + name: 'Coinbase Wallet', + }, ]; interface getWeb3ProviderDataProps { diff --git a/packages/types/src/web3Wallet.ts b/packages/types/src/web3Wallet.ts index 2087a325644..0043f2f4bbb 100644 --- a/packages/types/src/web3Wallet.ts +++ b/packages/types/src/web3Wallet.ts @@ -8,6 +8,7 @@ export type PrepareWeb3WalletVerificationParams = { export type AttemptWeb3WalletVerificationParams = { signature: string; + strategy: Web3Strategy; }; export interface Web3WalletResource extends ClerkResource { @@ -26,6 +27,7 @@ export type GenerateSignature = (opts: GenerateSignatureParams) => Promise Date: Tue, 20 Aug 2024 16:51:00 +0300 Subject: [PATCH 2/3] feat(clerk-js): Update signIn and signUp authenticate with web3 functions --- .../clerk-js/src/core/resources/SignIn.ts | 39 +++++++++---------- .../clerk-js/src/core/resources/SignUp.ts | 36 ++++++++--------- .../components/SignUp/SignUpSocialButtons.tsx | 3 +- packages/clerk-js/src/utils/web3.ts | 37 ++++++++++++++++++ packages/types/src/web3Wallet.ts | 2 +- 5 files changed, 75 insertions(+), 42 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 8e1eee73c42..791e72c63df 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -31,13 +31,7 @@ import type { Web3SignatureFactor, } from '@clerk/types'; -import { - generateSignatureWithCoinbase, - generateSignatureWithMetamask, - getCoinbaseIdentifier, - getMetamaskIdentifier, - windowNavigate, -} from '../../utils'; +import { generateSignatureWithWeb3, getWeb3Identifier, windowNavigate } from '../../utils'; import { ClerkWebAuthnError, convertJSONToPublicKeyRequestOptions, @@ -113,6 +107,9 @@ export class SignIn extends BaseResource implements SignInResource { case 'web3_metamask_signature': config = { web3WalletId: factor.web3WalletId } as Web3SignatureConfig; break; + case 'web3_coinbase_signature': + config = { web3WalletId: factor.web3WalletId } as Web3SignatureConfig; + break; case 'reset_password_phone_code': config = { phoneNumberId: factor.phoneNumberId } as ResetPasswordPhoneCodeFactorConfig; break; @@ -229,8 +226,10 @@ export class SignIn extends BaseResource implements SignInResource { }; public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { - const { identifier, generateSignature, strategy } = params || {}; - if (!(typeof generateSignature === 'function')) { + const { identifier, provider } = params || {}; + const strategy = `web3_${provider}_signature`; + + if (!(typeof generateSignatureWithWeb3 === 'function')) { clerkMissingOptionError('generateSignature'); } @@ -245,9 +244,10 @@ export class SignIn extends BaseResource implements SignInResource { await this.prepareFirstFactor(web3FirstFactor); const { nonce } = this.firstFactorVerification; - const signature = await generateSignature({ + const signature = await generateSignatureWithWeb3({ identifier: this.identifier!, nonce: nonce!, + provider: provider, }); return this.attemptFirstFactor({ @@ -256,22 +256,21 @@ export class SignIn extends BaseResource implements SignInResource { }); }; - public authenticateWithMetamask = async (): Promise => { - const identifier = await getMetamaskIdentifier(); + public authenticateWeb3Provider = async (provider: 'metamask' | 'coinbase'): Promise => { + const identifier = await getWeb3Identifier(provider); + return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithMetamask, - strategy: 'web3_metamask_signature', + provider: provider, }); }; + public authenticateWithMetamask = async (): Promise => { + return this.authenticateWeb3Provider('metamask'); + }; + public authenticateWithCoinbase = async (): Promise => { - const identifier = await getCoinbaseIdentifier(); - return this.authenticateWithWeb3({ - identifier, - generateSignature: generateSignatureWithCoinbase, - strategy: 'web3_coinbase_signature', - }); + return this.authenticateWeb3Provider('coinbase'); }; public authenticateWithPasskey = async (params?: AuthenticateWithPasskeyParams): Promise => { diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 602a8bbb444..511900642f6 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -17,18 +17,13 @@ import type { SignUpJSON, SignUpResource, SignUpStatus, + SignUpUnsafeMetadata, SignUpUpdateParams, StartEmailLinkFlowParams, Web3Strategy, } from '@clerk/types'; -import { - generateSignatureWithCoinbase, - generateSignatureWithMetamask, - getCoinbaseIdentifier, - getMetamaskIdentifier, - windowNavigate, -} from '../../utils'; +import { generateSignatureWithWeb3, getWeb3Identifier, windowNavigate } from '../../utils'; import { getCaptchaToken, retrieveCaptchaInfo } from '../../utils/captcha'; import { createValidatePassword } from '../../utils/passwords/password'; import { normalizeUnsafeMetadata } from '../../utils/resourceParams'; @@ -189,7 +184,8 @@ export class SignUp extends BaseResource implements SignUpResource { public authenticateWithWeb3 = async ( params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, ): Promise => { - const { generateSignature, identifier, strategy, unsafeMetadata } = params || {}; + const { identifier, unsafeMetadata, provider } = params || {}; + const strategy = `web3_${provider}_signature`; const web3Wallet = identifier || this.web3wallet!; await this.create({ web3Wallet, unsafeMetadata }); await this.prepareWeb3WalletVerification(strategy); @@ -199,28 +195,28 @@ export class SignUp extends BaseResource implements SignUpResource { clerkVerifyWeb3WalletCalledBeforeCreate('SignUp'); } - const signature = await generateSignature({ identifier, nonce }); + const signature = await generateSignatureWithWeb3({ identifier, nonce, provider }); + return this.attemptWeb3WalletVerification({ signature, strategy }); }; - public authenticateWithMetamask = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { - const identifier = await getMetamaskIdentifier(); + public authenticateWeb3Provider = async ( + provider: 'metamask' | 'coinbase', + params?: SignUpAuthenticateWithMetamaskParams, + ): Promise => { + const identifier = await getWeb3Identifier(provider); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithMetamask, - strategy: 'web3_metamask_signature', unsafeMetadata: params?.unsafeMetadata, + provider, }); }; + public authenticateWithMetamask = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { + return this.authenticateWeb3Provider('metamask', params); + }; public authenticateWithCoinbase = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { - const identifier = await getCoinbaseIdentifier(); - return this.authenticateWithWeb3({ - identifier, - generateSignature: generateSignatureWithCoinbase, - strategy: 'web3_coinbase_signature', - unsafeMetadata: params?.unsafeMetadata, - }); + return this.authenticateWeb3Provider('coinbase', params); }; public authenticateWithRedirect = async ({ diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 4dd84f36039..38fadd01202 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -38,13 +38,14 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) }) .catch(err => handleError(err, [], card.setError)); }} - web3Callback={() => { + web3Callback={strategy => { return clerk .authenticateWithMetamask({ customNavigate: navigate, redirectUrl: redirectUrlComplete, signUpContinueUrl: 'continue', unsafeMetadata: ctx.unsafeMetadata, + strategy: strategy, }) .catch(err => handleError(err, [], card.setError)); }} diff --git a/packages/clerk-js/src/utils/web3.ts b/packages/clerk-js/src/utils/web3.ts index 5fecd1bba6c..914f0ae3a0e 100644 --- a/packages/clerk-js/src/utils/web3.ts +++ b/packages/clerk-js/src/utils/web3.ts @@ -1,5 +1,21 @@ import { toHex } from './hex'; +export async function getWeb3Identifier(provider: string): Promise { + const ethereum = + (provider === 'metamask' && getMetamaskProvider()) || (provider === 'coinbase' && getCoinbaseProvider()); + + if (!ethereum) { + return ''; + } + + // @ts-ignore + const identifiers = await ethereum.request({ + method: 'eth_requestAccounts', + }); + + return (identifiers && identifiers[0]) || ''; +} + export async function getMetamaskIdentifier(): Promise { const ethereum = getMetamaskProvider(); if (!ethereum) { @@ -17,6 +33,7 @@ export async function getMetamaskIdentifier(): Promise { export type GenerateSignatureParams = { identifier: string; nonce: string; + provider: 'metamask' | 'coinbase'; }; export async function generateSignatureWithMetamask({ identifier, nonce }: GenerateSignatureParams): Promise { @@ -33,6 +50,26 @@ export async function generateSignatureWithMetamask({ identifier, nonce }: Gener return signature; } +export async function generateSignatureWithWeb3({ + identifier, + nonce, + provider, +}: GenerateSignatureParams): Promise { + const ethereum = + (provider === 'metamask' && getMetamaskProvider()) || (provider === 'coinbase' && getCoinbaseProvider()); + + if (!ethereum) { + return ''; + } + + // @ts-ignore + const signature: string = await ethereum.request({ + method: 'personal_sign', + params: [`0x${toHex(nonce)}`, identifier], + }); + + return signature; +} export async function getCoinbaseIdentifier(): Promise { const ethereum = getCoinbaseProvider(); diff --git a/packages/types/src/web3Wallet.ts b/packages/types/src/web3Wallet.ts index 0043f2f4bbb..ed54c119d77 100644 --- a/packages/types/src/web3Wallet.ts +++ b/packages/types/src/web3Wallet.ts @@ -27,7 +27,7 @@ export type GenerateSignature = (opts: GenerateSignatureParams) => Promise Date: Thu, 22 Aug 2024 15:59:43 +0300 Subject: [PATCH 3/3] feat(clerk-js,types,react): Create authenticateWithWeb3 function --- packages/clerk-js/src/core/clerk.ts | 24 +++++++++---------- .../clerk-js/src/core/resources/SignIn.ts | 2 +- .../clerk-js/src/core/resources/SignUp.ts | 16 ++++++------- .../components/SignIn/SignInSocialButtons.tsx | 2 +- .../components/SignUp/SignUpSocialButtons.tsx | 2 +- packages/clerk-js/src/utils/web3.ts | 2 +- .../machines/sign-in/start.machine.ts | 3 +++ .../machines/sign-in/verification.machine.ts | 11 +++++++++ .../machines/sign-up/start.machine.ts | 3 +++ .../elements/src/react/common/loading.tsx | 4 ++-- .../hooks/use-third-party-provider.hook.ts | 3 +++ .../src/react/utils/map-scope-to-strategy.ts | 7 ++++-- packages/react/src/isomorphicClerk.ts | 12 +++++++++- packages/types/src/clerk.ts | 15 +++++++++++- packages/types/src/signUp.ts | 16 +++++++++---- packages/types/src/web3Wallet.ts | 4 ++-- 16 files changed, 88 insertions(+), 38 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 041ad9dbdcd..3ee14bd9cc7 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -19,6 +19,7 @@ import type { ActiveSessionResource, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, + AuthenticateWithWeb3Params, Clerk as ClerkInterface, ClerkAPIError, ClerkOptions, @@ -1321,34 +1322,31 @@ export class Clerk implements ClerkInterface { }) as Promise; }; - public authenticateWithMetamask = async ({ + public authenticateWithMetamask = async (props: AuthenticateWithMetamaskParams = {}): Promise => { + await this.authenticateWithWeb3({ ...props, strategy: 'web3_metamask_signature' }); + }; + + public authenticateWithWeb3 = async ({ redirectUrl, signUpContinueUrl, customNavigate, unsafeMetadata, strategy, - }: AuthenticateWithMetamaskParams = {}): Promise => { + }: AuthenticateWithWeb3Params = {}): Promise => { if (!this.client || !this.environment) { return; } - + const provider = + (strategy === 'web3_metamask_signature' && 'metamask') || (strategy === 'web3_coinbase_signature' && 'coinbase'); const navigate = (to: string) => customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); let signInOrSignUp: SignInResource | SignUpResource; try { - if (strategy === 'web3_metamask_signature') { - signInOrSignUp = await this.client.signIn.authenticateWithMetamask(); - } else { - signInOrSignUp = await this.client.signIn.authenticateWithCoinbase(); - } + signInOrSignUp = await this.client.signIn.authenticateWeb3Provider(provider); } catch (err) { if (isError(err, ERROR_CODES.FORM_IDENTIFIER_NOT_FOUND)) { - if (strategy === 'web3_metamask_signature') { - signInOrSignUp = await this.client.signUp.authenticateWithMetamask({ unsafeMetadata }); - } else { - signInOrSignUp = await this.client.signUp.authenticateWithCoinbase({ unsafeMetadata }); - } + signInOrSignUp = await this.client.signUp.authenticateWeb3Provider({ unsafeMetadata, provider }); if ( signUpContinueUrl && diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 791e72c63df..383128b04da 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -227,7 +227,7 @@ export class SignIn extends BaseResource implements SignInResource { public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { const { identifier, provider } = params || {}; - const strategy = `web3_${provider}_signature`; + const strategy = provider ? `web3_${provider}_signature` : 'web3_metamask_signature'; if (!(typeof generateSignatureWithWeb3 === 'function')) { clerkMissingOptionError('generateSignature'); diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 511900642f6..7e26d489880 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -10,7 +10,8 @@ import type { PrepareEmailAddressVerificationParams, PreparePhoneNumberVerificationParams, PrepareVerificationParams, - SignUpAuthenticateWithMetamaskParams, + PrepareWeb3WalletVerificationParams, + SignUpAuthenticateWithWeb3Params, SignUpCreateParams, SignUpField, SignUpIdentificationField, @@ -20,7 +21,6 @@ import type { SignUpUnsafeMetadata, SignUpUpdateParams, StartEmailLinkFlowParams, - Web3Strategy, } from '@clerk/types'; import { generateSignatureWithWeb3, getWeb3Identifier, windowNavigate } from '../../utils'; @@ -172,8 +172,8 @@ export class SignUp extends BaseResource implements SignUpResource { return this.attemptVerification({ ...params, strategy: 'phone_code' }); }; - prepareWeb3WalletVerification = (strategy: Web3Strategy): Promise => { - return this.prepareVerification({ strategy }); + prepareWeb3WalletVerification = (params?: PrepareWeb3WalletVerificationParams): Promise => { + return this.prepareVerification(params || { strategy: 'web3_metamask_signature' }); }; attemptWeb3WalletVerification = async (params: AttemptWeb3WalletVerificationParams): Promise => { @@ -185,7 +185,7 @@ export class SignUp extends BaseResource implements SignUpResource { params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, ): Promise => { const { identifier, unsafeMetadata, provider } = params || {}; - const strategy = `web3_${provider}_signature`; + const strategy = provider ? `web3_${provider}_signature` : 'web3_metamask_signature'; const web3Wallet = identifier || this.web3wallet!; await this.create({ web3Wallet, unsafeMetadata }); await this.prepareWeb3WalletVerification(strategy); @@ -202,7 +202,7 @@ export class SignUp extends BaseResource implements SignUpResource { public authenticateWeb3Provider = async ( provider: 'metamask' | 'coinbase', - params?: SignUpAuthenticateWithMetamaskParams, + params?: SignUpAuthenticateWithWeb3Params, ): Promise => { const identifier = await getWeb3Identifier(provider); return this.authenticateWithWeb3({ @@ -211,11 +211,11 @@ export class SignUp extends BaseResource implements SignUpResource { provider, }); }; - public authenticateWithMetamask = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { + public authenticateWithMetamask = async (params?: SignUpAuthenticateWithWeb3Params): Promise => { return this.authenticateWeb3Provider('metamask', params); }; - public authenticateWithCoinbase = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { + public authenticateWithCoinbase = async (params?: SignUpAuthenticateWithWeb3Params): Promise => { return this.authenticateWeb3Provider('coinbase', params); }; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index bce09e9ede8..fb9e6c3c9bf 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx @@ -30,7 +30,7 @@ export const SignInSocialButtons = React.memo((props: SocialButtonsProps) => { }} web3Callback={strategy => { return clerk - .authenticateWithMetamask({ + .authenticateWithWeb3({ customNavigate: navigate, redirectUrl: redirectUrlComplete, signUpContinueUrl: ctx.signUpContinueUrl, diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 38fadd01202..9a8cb98a839 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -40,7 +40,7 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) }} web3Callback={strategy => { return clerk - .authenticateWithMetamask({ + .authenticateWithWeb3({ customNavigate: navigate, redirectUrl: redirectUrlComplete, signUpContinueUrl: 'continue', diff --git a/packages/clerk-js/src/utils/web3.ts b/packages/clerk-js/src/utils/web3.ts index 914f0ae3a0e..10ab09274d7 100644 --- a/packages/clerk-js/src/utils/web3.ts +++ b/packages/clerk-js/src/utils/web3.ts @@ -33,7 +33,7 @@ export async function getMetamaskIdentifier(): Promise { export type GenerateSignatureParams = { identifier: string; nonce: string; - provider: 'metamask' | 'coinbase'; + provider?: 'metamask' | 'coinbase'; }; export async function generateSignatureWithMetamask({ identifier, nonce }: GenerateSignatureParams): Promise { diff --git a/packages/elements/src/internals/machines/sign-in/start.machine.ts b/packages/elements/src/internals/machines/sign-in/start.machine.ts index 0569abecc0b..03ab9ca605f 100644 --- a/packages/elements/src/internals/machines/sign-in/start.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/start.machine.ts @@ -29,6 +29,9 @@ export const SignInStartMachine = setup({ if (strategy === 'web3_metamask_signature') { return parent.getSnapshot().context.clerk.client.signIn.authenticateWithMetamask(); } + if (strategy === 'web3_coinbase_signature') { + return parent.getSnapshot().context.clerk.client.signIn.authenticateWithCoinbase(); + } throw new ClerkElementsRuntimeError(`Unsupported Web3 strategy: ${strategy}`); }, ), diff --git a/packages/elements/src/internals/machines/sign-in/verification.machine.ts b/packages/elements/src/internals/machines/sign-in/verification.machine.ts index 259390fa14b..600f86bc351 100644 --- a/packages/elements/src/internals/machines/sign-in/verification.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/verification.machine.ts @@ -439,6 +439,17 @@ export const SignInFirstFactorMachine = SignInVerificationMachine.provide({ break; } + case 'web3_coinbase_signature': { + const signature = fields.get('signature')?.value as string | undefined; + assertIsDefined(signature, 'Web3 Coinbase signature'); + + attemptParams = { + strategy, + signature, + } satisfies Web3Attempt; + + break; + } default: throw new ClerkElementsRuntimeError(`Invalid strategy: ${strategy}`); } diff --git a/packages/elements/src/internals/machines/sign-up/start.machine.ts b/packages/elements/src/internals/machines/sign-up/start.machine.ts index 27cd3cab926..ee79be96561 100644 --- a/packages/elements/src/internals/machines/sign-up/start.machine.ts +++ b/packages/elements/src/internals/machines/sign-up/start.machine.ts @@ -35,6 +35,9 @@ export const SignUpStartMachine = setup({ if (strategy === 'web3_metamask_signature') { return parent.getSnapshot().context.clerk.client.signUp.authenticateWithMetamask(); } + if (strategy === 'web3_coinbase_signature') { + return parent.getSnapshot().context.clerk.client.signUp.authenticateWithCoinbase(); + } throw new ClerkElementsRuntimeError(`Unsupported Web3 strategy: ${strategy}`); }, ), diff --git a/packages/elements/src/react/common/loading.tsx b/packages/elements/src/react/common/loading.tsx index a0a4d55626a..99847ed62f0 100644 --- a/packages/elements/src/react/common/loading.tsx +++ b/packages/elements/src/react/common/loading.tsx @@ -1,6 +1,6 @@ import { useClerk } from '@clerk/clerk-react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; -import type { OAuthProvider, SamlStrategy } from '@clerk/types'; +import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types'; import { useSelector } from '@xstate/react'; import * as React from 'react'; @@ -15,7 +15,7 @@ import type { TSignUpStep } from '~/react/sign-up/step'; import { SIGN_UP_STEPS } from '~/react/sign-up/step'; import { isProviderStrategyScope, mapScopeToStrategy } from '~/react/utils/map-scope-to-strategy'; -type Strategy = OAuthProvider | SamlStrategy | 'metamask'; +type Strategy = OAuthProvider | SamlStrategy | Web3Provider; type LoadingScope = 'global' | `step:${T}` | `provider:${Strategy}` | undefined; type LoadingProps = { diff --git a/packages/elements/src/react/hooks/use-third-party-provider.hook.ts b/packages/elements/src/react/hooks/use-third-party-provider.hook.ts index d0f31b41d84..5addd558afe 100644 --- a/packages/elements/src/react/hooks/use-third-party-provider.hook.ts +++ b/packages/elements/src/react/hooks/use-third-party-provider.hook.ts @@ -66,6 +66,9 @@ export const useThirdPartyProvider = < if (provider === 'metamask') { return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_metamask_signature' }); } + if (provider === 'coinbase') { + return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_coinbase_signature' }); + } return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy: `oauth_${provider}` }); }, diff --git a/packages/elements/src/react/utils/map-scope-to-strategy.ts b/packages/elements/src/react/utils/map-scope-to-strategy.ts index 8bac8fbc43a..5d1b298b18f 100644 --- a/packages/elements/src/react/utils/map-scope-to-strategy.ts +++ b/packages/elements/src/react/utils/map-scope-to-strategy.ts @@ -1,6 +1,6 @@ -import type { OAuthProvider, SamlStrategy, SignInStrategy } from '@clerk/types'; +import type { OAuthProvider, SamlStrategy, SignInStrategy, Web3Provider } from '@clerk/types'; -type Strategy = OAuthProvider | SamlStrategy | 'metamask'; +type Strategy = OAuthProvider | SamlStrategy | Web3Provider; export function isProviderStrategyScope(value: string): value is Strategy { return value.startsWith('provider:'); @@ -10,6 +10,9 @@ export function mapScopeToStrategy(scope: T): if (scope === 'provider:metamask') { return 'web3_metamask_signature'; } + if (scope === 'provider:coinbase') { + return 'web3_coinbase_signature'; + } if (scope === 'provider:saml') { return 'saml'; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 09071059c7f..50a89e8306d 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -5,6 +5,7 @@ import type { ActiveSessionResource, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, + AuthenticateWithWeb3Params, Clerk, ClientResource, CreateOrganizationParams, @@ -90,6 +91,7 @@ type IsomorphicLoadedClerk = Without< | 'handleGoogleOneTapCallback' | 'handleUnauthenticated' | 'authenticateWithMetamask' + | 'authenticateWithWeb3' | 'authenticateWithGoogleOneTap' | 'createOrganization' | 'getOrganization' @@ -109,6 +111,7 @@ type IsomorphicLoadedClerk = Without< handleUnauthenticated: () => void; // TODO: Align Promise unknown authenticateWithMetamask: (params: AuthenticateWithMetamaskParams) => Promise; + authenticateWithWeb3: (params: AuthenticateWithMetamaskParams) => Promise; authenticateWithGoogleOneTap: ( params: AuthenticateWithGoogleOneTapParams, ) => Promise; @@ -993,7 +996,14 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.premountMethodCalls.set('authenticateWithMetamask', callback); } }; - + authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { + const callback = () => this.clerkjs?.authenticateWithWeb3(params); + if (this.clerkjs && this.#loaded) { + return callback() as Promise; + } else { + this.premountMethodCalls.set('authenticateWithWeb3', callback); + } + }; authenticateWithGoogleOneTap = async ( params: AuthenticateWithGoogleOneTapParams, ): Promise => { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index f78479e62c2..ceb07ac75d1 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -34,9 +34,9 @@ import type { import type { ActiveSessionResource } from './session'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; +import type { Web3Strategy } from './strategies'; import type { UserResource } from './user'; import type { Autocomplete, DeepPartial, DeepSnakeToCamel } from './utils'; -import { Web3Strategy } from './strategies'; export type SDKMetadata = { name: string; @@ -479,6 +479,11 @@ export interface Clerk { */ authenticateWithMetamask: (params?: AuthenticateWithMetamaskParams) => Promise; + /** + * Authenticates user using their Web3 Wallet browser extension + */ + authenticateWithWeb3: (params?: AuthenticateWithWeb3Params) => Promise; + /** * Authenticates user using a Google token generated from Google identity services. */ @@ -1121,6 +1126,14 @@ export interface AuthenticateWithMetamaskParams { strategy?: Web3Strategy; } +export interface AuthenticateWithWeb3Params { + customNavigate?: (to: string) => Promise; + redirectUrl?: string; + signUpContinueUrl?: string; + unsafeMetadata?: SignUpUnsafeMetadata; + strategy?: Web3Strategy; +} + export interface AuthenticateWithGoogleOneTapParams { token: string; } diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index 93b6173eab2..49078272017 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -23,7 +23,11 @@ import type { } from './strategies'; import type { SnakeToCamel } from './utils'; import type { CreateEmailLinkFlowReturn, StartEmailLinkFlowParams, VerificationResource } from './verification'; -import type { AttemptWeb3WalletVerificationParams, AuthenticateWithWeb3Params } from './web3Wallet'; +import type { + AttemptWeb3WalletVerificationParams, + AuthenticateWithWeb3Params, + PrepareWeb3WalletVerificationParams, +} from './web3Wallet'; declare global { /** @@ -72,7 +76,7 @@ export interface SignUpResource extends ClerkResource { attemptPhoneNumberVerification: (params: AttemptPhoneNumberVerificationParams) => Promise; - prepareWeb3WalletVerification: (strategy: Web3Strategy) => Promise; + prepareWeb3WalletVerification: (params?: PrepareWeb3WalletVerificationParams) => Promise; attemptWeb3WalletVerification: (params: AttemptWeb3WalletVerificationParams) => Promise; @@ -88,8 +92,8 @@ export interface SignUpResource extends ClerkResource { params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, ) => Promise; - authenticateWithMetamask: (params?: SignUpAuthenticateWithMetamaskParams) => Promise; - authenticateWithCoinbase: (params?: SignUpAuthenticateWithMetamaskParams) => Promise; + authenticateWithMetamask: (params?: SignUpAuthenticateWithWeb3Params) => Promise; + authenticateWithCoinbase: (params?: SignUpAuthenticateWithWeb3Params) => Promise; } export type SignUpStatus = 'missing_requirements' | 'complete' | 'abandoned'; @@ -162,7 +166,9 @@ export type SignUpCreateParams = Partial< export type SignUpUpdateParams = SignUpCreateParams; -export type SignUpAuthenticateWithMetamaskParams = { +export type SignUpAuthenticateWithMetamaskParams = SignUpAuthenticateWithWeb3Params; + +export type SignUpAuthenticateWithWeb3Params = { unsafeMetadata?: SignUpUnsafeMetadata; }; diff --git a/packages/types/src/web3Wallet.ts b/packages/types/src/web3Wallet.ts index ed54c119d77..9297113d26b 100644 --- a/packages/types/src/web3Wallet.ts +++ b/packages/types/src/web3Wallet.ts @@ -8,7 +8,7 @@ export type PrepareWeb3WalletVerificationParams = { export type AttemptWeb3WalletVerificationParams = { signature: string; - strategy: Web3Strategy; + strategy?: Web3Strategy; }; export interface Web3WalletResource extends ClerkResource { @@ -27,7 +27,7 @@ export type GenerateSignature = (opts: GenerateSignatureParams) => Promise