diff --git a/.changeset/thin-gorillas-deliver.md b/.changeset/thin-gorillas-deliver.md new file mode 100644 index 0000000000..cd44a5ac12 --- /dev/null +++ b/.changeset/thin-gorillas-deliver.md @@ -0,0 +1,7 @@ +--- +"@clerk/clerk-js": minor +"@clerk/clerk-react": minor +"@clerk/types": minor +--- + +Add support for Coinbase Wallet strategy during sign in/up flows. Users can now authenticate using their Coinbase Wallet browser extension in the same way as MetaMask diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 71d2ee505a..aec54a17ab 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -18,10 +18,12 @@ import { logger } from '@clerk/shared/logger'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import type { ActiveSessionResource, + AuthenticateWithCoinbaseParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, Clerk as ClerkInterface, ClerkAPIError, + ClerkAuthenticateWithWeb3Params, ClerkOptions, ClientResource, CreateOrganizationParams, @@ -57,6 +59,7 @@ import type { UserButtonProps, UserProfileProps, UserResource, + Web3Provider, } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; @@ -69,7 +72,10 @@ import { createPageLifecycle, disabledOrganizationsFeature, errorThrower, + generateSignatureWithCoinbase, + generateSignatureWithMetamask, getClerkQueryParam, + getWeb3Identifier, hasExternalAccountSignUpError, ignoreEventValue, inActiveBrowserTab, @@ -1333,25 +1339,41 @@ 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 authenticateWithCoinbase = async (props: AuthenticateWithCoinbaseParams = {}): Promise => { + await this.authenticateWithWeb3({ ...props, strategy: 'web3_coinbase_signature' }); + }; + + public authenticateWithWeb3 = async ({ redirectUrl, signUpContinueUrl, customNavigate, unsafeMetadata, - }: AuthenticateWithMetamaskParams = {}): Promise => { + strategy, + }: ClerkAuthenticateWithWeb3Params): Promise => { if (!this.client || !this.environment) { return; } - + const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; + const identifier = await getWeb3Identifier({ provider }); + const generateSignature = provider === 'metamask' ? generateSignatureWithMetamask : generateSignatureWithCoinbase; const navigate = (to: string) => customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to); let signInOrSignUp: SignInResource | SignUpResource; try { - signInOrSignUp = await this.client.signIn.authenticateWithMetamask(); + signInOrSignUp = await this.client.signIn.authenticateWithWeb3({ identifier, generateSignature, strategy }); } catch (err) { if (isError(err, ERROR_CODES.FORM_IDENTIFIER_NOT_FOUND)) { - signInOrSignUp = await this.client.signUp.authenticateWithMetamask({ unsafeMetadata }); + signInOrSignUp = await this.client.signUp.authenticateWithWeb3({ + identifier, + generateSignature, + unsafeMetadata, + strategy, + }); if ( signUpContinueUrl && diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 93d66052fd..9caacda49e 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -27,11 +27,18 @@ import type { SignInStartEmailLinkFlowParams, SignInStatus, VerificationResource, + Web3Provider, Web3SignatureConfig, Web3SignatureFactor, } from '@clerk/types'; -import { generateSignatureWithMetamask, getMetamaskIdentifier, windowNavigate } from '../../utils'; +import { + generateSignatureWithCoinbase, + generateSignatureWithMetamask, + getCoinbaseIdentifier, + getMetamaskIdentifier, + windowNavigate, +} from '../../utils'; import { ClerkWebAuthnError, convertJSONToPublicKeyRequestOptions, @@ -107,6 +114,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; @@ -223,16 +233,16 @@ export class SignIn extends BaseResource implements SignInResource { }; public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { - const { identifier, generateSignature } = params || {}; + const { identifier, generateSignature, strategy = 'web3_metamask_signature' } = params || {}; + const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; + 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'); @@ -241,14 +251,19 @@ export class SignIn extends BaseResource implements SignInResource { await this.prepareFirstFactor(web3FirstFactor); const { nonce } = this.firstFactorVerification; + if (!nonce) { + clerkVerifyWeb3WalletCalledBeforeCreate('SignIn'); + } + const signature = await generateSignature({ identifier: this.identifier!, - nonce: nonce!, + nonce: nonce, + provider, }); return this.attemptFirstFactor({ signature, - strategy: 'web3_metamask_signature', + strategy, }); }; @@ -257,6 +272,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', }); }; @@ -326,7 +351,7 @@ export class SignIn extends BaseResource implements SignInResource { validatePassword: ReturnType = (password, cb) => { if (SignIn.clerk.__unstable__environment?.userSettings.passwordSettings) { return createValidatePassword({ - ...(SignIn.clerk.__unstable__environment?.userSettings.passwordSettings as any), + ...SignIn.clerk.__unstable__environment?.userSettings.passwordSettings, validatePassword: true, })(password, cb); } diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 6b610699b2..0c77f0549f 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, @@ -19,14 +20,22 @@ import type { SignUpStatus, SignUpUpdateParams, StartEmailLinkFlowParams, + Web3Provider, } 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'; import { clerkInvalidFAPIResponse, + clerkMissingOptionError, clerkVerifyEmailAddressCalledBeforeCreate, clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; @@ -170,38 +179,55 @@ export class SignUp extends BaseResource implements SignUpResource { return this.attemptVerification({ ...params, strategy: 'phone_code' }); }; - prepareWeb3WalletVerification = (): Promise => { - return this.prepareVerification({ strategy: 'web3_metamask_signature' }); + prepareWeb3WalletVerification = (params?: PrepareWeb3WalletVerificationParams): Promise => { + return this.prepareVerification({ strategy: 'web3_metamask_signature', ...params }); }; attemptWeb3WalletVerification = async (params: AttemptWeb3WalletVerificationParams): Promise => { - const { signature } = params; - return this.attemptVerification({ signature, strategy: 'web3_metamask_signature' }); + const { signature, strategy = 'web3_metamask_signature' } = params; + return this.attemptVerification({ signature, strategy }); }; public authenticateWithWeb3 = async ( params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, ): Promise => { - const { generateSignature, identifier, unsafeMetadata } = params || {}; + const { generateSignature, identifier, unsafeMetadata, strategy = 'web3_metamask_signature' } = params || {}; + const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; + + if (!(typeof generateSignature === 'function')) { + clerkMissingOptionError('generateSignature'); + } + const web3Wallet = identifier || this.web3wallet!; await this.create({ web3Wallet, unsafeMetadata }); - await this.prepareWeb3WalletVerification(); + await this.prepareWeb3WalletVerification({ strategy }); const { nonce } = this.verifications.web3Wallet; if (!nonce) { clerkVerifyWeb3WalletCalledBeforeCreate('SignUp'); } - const signature = await generateSignature({ identifier, nonce }); - return this.attemptWeb3WalletVerification({ signature }); + const signature = await generateSignature({ identifier, nonce, provider }); + return this.attemptWeb3WalletVerification({ signature, strategy }); }; - public authenticateWithMetamask = async (params?: SignUpAuthenticateWithMetamaskParams): Promise => { + public authenticateWithMetamask = async (params?: SignUpAuthenticateWithWeb3Params): Promise => { const identifier = await getMetamaskIdentifier(); return this.authenticateWithWeb3({ identifier, generateSignature: generateSignatureWithMetamask, unsafeMetadata: params?.unsafeMetadata, + strategy: 'web3_metamask_signature', + }); + }; + + public authenticateWithCoinbase = async (params?: SignUpAuthenticateWithWeb3Params): Promise => { + const identifier = await getCoinbaseIdentifier(); + return this.authenticateWithWeb3({ + identifier, + generateSignature: generateSignatureWithCoinbase, + unsafeMetadata: params?.unsafeMetadata, + strategy: 'web3_coinbase_signature', }); }; @@ -245,7 +271,7 @@ export class SignUp extends BaseResource implements SignUpResource { validatePassword: ReturnType = (password, cb) => { if (SignUp.clerk.__unstable__environment?.userSettings.passwordSettings) { return createValidatePassword({ - ...(SignUp.clerk.__unstable__environment?.userSettings.passwordSettings as any), + ...SignUp.clerk.__unstable__environment?.userSettings.passwordSettings, validatePassword: true, })(password, cb); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSocialButtons.tsx index 2ba1b6aae3..fb9e6c3c9b 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({ + .authenticateWithWeb3({ customNavigate: navigate, redirectUrl: redirectUrlComplete, signUpContinueUrl: ctx.signUpContinueUrl, + strategy, }) .catch(err => handleError(err, [], card.setError)); }} diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 4dd84f3603..5c600cc5af 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({ + .authenticateWithWeb3({ customNavigate: navigate, redirectUrl: redirectUrlComplete, signUpContinueUrl: 'continue', unsafeMetadata: ctx.unsafeMetadata, + 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 8c5a839a30..3dd3f6ade2 100644 --- a/packages/clerk-js/src/utils/web3.ts +++ b/packages/clerk-js/src/utils/web3.ts @@ -45,6 +45,10 @@ export async function getMetamaskIdentifier(): Promise { return await getWeb3Identifier({ provider: 'metamask' }); } +export async function getCoinbaseIdentifier(): Promise { + return await getWeb3Identifier({ provider: 'coinbase' }); +} + type GenerateSignatureParams = { identifier: string; nonce: string; @@ -53,3 +57,7 @@ type GenerateSignatureParams = { export async function generateSignatureWithMetamask({ identifier, nonce }: GenerateSignatureParams): Promise { return await generateWeb3Signature({ identifier, nonce, provider: 'metamask' }); } + +export async function generateSignatureWithCoinbase({ identifier, nonce }: GenerateSignatureParams): Promise { + return await generateWeb3Signature({ identifier, nonce, provider: 'coinbase' }); +} diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 9c9fad481b..af17158d5c 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -4,9 +4,11 @@ import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript'; import type { TelemetryCollector } from '@clerk/shared/telemetry'; import type { ActiveSessionResource, + AuthenticateWithCoinbaseParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, Clerk, + ClerkAuthenticateWithWeb3Params, ClientResource, CreateOrganizationParams, CreateOrganizationProps, @@ -91,6 +93,8 @@ type IsomorphicLoadedClerk = Without< | 'handleGoogleOneTapCallback' | 'handleUnauthenticated' | 'authenticateWithMetamask' + | 'authenticateWithCoinbase' + | 'authenticateWithWeb3' | 'authenticateWithGoogleOneTap' | 'createOrganization' | 'getOrganization' @@ -110,6 +114,8 @@ type IsomorphicLoadedClerk = Without< handleUnauthenticated: () => void; // TODO: Align Promise unknown authenticateWithMetamask: (params: AuthenticateWithMetamaskParams) => Promise; + authenticateWithCoinbase: (params: AuthenticateWithCoinbaseParams) => Promise; + authenticateWithWeb3: (params: ClerkAuthenticateWithWeb3Params) => Promise; authenticateWithGoogleOneTap: ( params: AuthenticateWithGoogleOneTapParams, ) => Promise; @@ -1011,6 +1017,24 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + authenticateWithCoinbase = async (params: AuthenticateWithCoinbaseParams): Promise => { + const callback = () => this.clerkjs?.authenticateWithCoinbase(params); + if (this.clerkjs && this.#loaded) { + return callback() as Promise; + } else { + this.premountMethodCalls.set('authenticateWithCoinbase', callback); + } + }; + + authenticateWithWeb3 = async (params: ClerkAuthenticateWithWeb3Params): 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 2b03f2b673..867f37755e 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -34,6 +34,7 @@ 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'; @@ -478,6 +479,16 @@ export interface Clerk { */ authenticateWithMetamask: (params?: AuthenticateWithMetamaskParams) => Promise; + /** + * Authenticates user using their Coinbase Wallet browser extension + */ + authenticateWithCoinbase: (params?: AuthenticateWithCoinbaseParams) => Promise; + + /** + * Authenticates user using their Web3 Wallet browser extension + */ + authenticateWithWeb3: (params: ClerkAuthenticateWithWeb3Params) => Promise; + /** * Authenticates user using a Google token generated from Google identity services. */ @@ -1115,6 +1126,14 @@ export interface CreateOrganizationParams { slug?: string; } +export interface ClerkAuthenticateWithWeb3Params { + customNavigate?: (to: string) => Promise; + redirectUrl?: string; + signUpContinueUrl?: string; + unsafeMetadata?: SignUpUnsafeMetadata; + strategy: Web3Strategy; +} + export interface AuthenticateWithMetamaskParams { customNavigate?: (to: string) => Promise; redirectUrl?: string; @@ -1122,6 +1141,13 @@ export interface AuthenticateWithMetamaskParams { unsafeMetadata?: SignUpUnsafeMetadata; } +export interface AuthenticateWithCoinbaseParams { + customNavigate?: (to: string) => Promise; + redirectUrl?: string; + signUpContinueUrl?: string; + unsafeMetadata?: SignUpUnsafeMetadata; +} + export interface AuthenticateWithGoogleOneTapParams { token: string; } diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index 39a6026d3a..ed8824d8ec 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 8dc35b8339..601c214916 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: () => Promise; + prepareWeb3WalletVerification: (params?: PrepareWeb3WalletVerificationParams) => Promise; attemptWeb3WalletVerification: (params: AttemptWeb3WalletVerificationParams) => Promise; @@ -88,7 +92,8 @@ export interface SignUpResource extends ClerkResource { params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, ) => Promise; - authenticateWithMetamask: (params?: SignUpAuthenticateWithMetamaskParams) => Promise; + authenticateWithMetamask: (params?: SignUpAuthenticateWithWeb3Params) => Promise; + authenticateWithCoinbase: (params?: SignUpAuthenticateWithWeb3Params) => Promise; } export type SignUpStatus = 'missing_requirements' | 'complete' | 'abandoned'; @@ -161,7 +166,12 @@ export type SignUpCreateParams = Partial< export type SignUpUpdateParams = SignUpCreateParams; -export type SignUpAuthenticateWithMetamaskParams = { +/** + * @deprecated use `SignUpAuthenticateWithWeb3Params` instead + */ +export type SignUpAuthenticateWithMetamaskParams = SignUpAuthenticateWithWeb3Params; + +export type SignUpAuthenticateWithWeb3Params = { unsafeMetadata?: SignUpUnsafeMetadata; }; diff --git a/packages/types/src/web3Wallet.ts b/packages/types/src/web3Wallet.ts index 2087a32564..56308e14ca 100644 --- a/packages/types/src/web3Wallet.ts +++ b/packages/types/src/web3Wallet.ts @@ -1,6 +1,7 @@ import type { ClerkResource } from './resource'; import type { Web3Strategy } from './strategies'; import type { VerificationResource } from './verification'; +import type { Web3Provider } from './web3'; export type PrepareWeb3WalletVerificationParams = { strategy: Web3Strategy; @@ -8,6 +9,7 @@ export type PrepareWeb3WalletVerificationParams = { export type AttemptWeb3WalletVerificationParams = { signature: string; + strategy?: Web3Strategy; }; export interface Web3WalletResource extends ClerkResource { @@ -26,9 +28,11 @@ export type GenerateSignature = (opts: GenerateSignatureParams) => Promise