Skip to content
Merged
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/thin-gorillas-deliver.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
---

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
32 changes: 27 additions & 5 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -57,6 +59,7 @@ import type {
UserButtonProps,
UserProfileProps,
UserResource,
Web3Provider,
} from '@clerk/types';

import type { MountComponentRenderer } from '../ui/Components';
Expand All @@ -69,7 +72,10 @@ import {
createPageLifecycle,
disabledOrganizationsFeature,
errorThrower,
generateSignatureWithCoinbase,
generateSignatureWithMetamask,
getClerkQueryParam,
getWeb3Identifier,
hasExternalAccountSignUpError,
ignoreEventValue,
inActiveBrowserTab,
Expand Down Expand Up @@ -1333,25 +1339,41 @@ export class Clerk implements ClerkInterface {
}) as Promise<SignInResource | SignUpResource>;
};

public authenticateWithMetamask = async ({
public authenticateWithMetamask = async (props: AuthenticateWithMetamaskParams = {}): Promise<void> => {
await this.authenticateWithWeb3({ ...props, strategy: 'web3_metamask_signature' });
};

public authenticateWithCoinbase = async (props: AuthenticateWithCoinbaseParams = {}): Promise<void> => {
await this.authenticateWithWeb3({ ...props, strategy: 'web3_coinbase_signature' });
};

public authenticateWithWeb3 = async ({
redirectUrl,
signUpContinueUrl,
customNavigate,
unsafeMetadata,
}: AuthenticateWithMetamaskParams = {}): Promise<void> => {
strategy,
}: ClerkAuthenticateWithWeb3Params): Promise<void> => {
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 &&
Expand Down
41 changes: 33 additions & 8 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -223,16 +233,16 @@ export class SignIn extends BaseResource implements SignInResource {
};

public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise<SignInResource> => {
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');
Expand All @@ -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,
});
};

Expand All @@ -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<SignInResource> => {
const identifier = await getCoinbaseIdentifier();
return this.authenticateWithWeb3({
identifier,
generateSignature: generateSignatureWithCoinbase,
strategy: 'web3_coinbase_signature',
});
};

Expand Down Expand Up @@ -326,7 +351,7 @@ export class SignIn extends BaseResource implements SignInResource {
validatePassword: ReturnType<typeof createValidatePassword> = (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);
}
Expand Down
50 changes: 38 additions & 12 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type {
PrepareEmailAddressVerificationParams,
PreparePhoneNumberVerificationParams,
PrepareVerificationParams,
SignUpAuthenticateWithMetamaskParams,
PrepareWeb3WalletVerificationParams,
SignUpAuthenticateWithWeb3Params,
SignUpCreateParams,
SignUpField,
SignUpIdentificationField,
Expand All @@ -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';
Expand Down Expand Up @@ -170,38 +179,55 @@ export class SignUp extends BaseResource implements SignUpResource {
return this.attemptVerification({ ...params, strategy: 'phone_code' });
};

prepareWeb3WalletVerification = (): Promise<SignUpResource> => {
return this.prepareVerification({ strategy: 'web3_metamask_signature' });
prepareWeb3WalletVerification = (params?: PrepareWeb3WalletVerificationParams): Promise<SignUpResource> => {
return this.prepareVerification({ strategy: 'web3_metamask_signature', ...params });
};

attemptWeb3WalletVerification = async (params: AttemptWeb3WalletVerificationParams): Promise<SignUpResource> => {
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<SignUpResource> => {
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<SignUpResource> => {
public authenticateWithMetamask = async (params?: SignUpAuthenticateWithWeb3Params): Promise<SignUpResource> => {
const identifier = await getMetamaskIdentifier();
return this.authenticateWithWeb3({
identifier,
generateSignature: generateSignatureWithMetamask,
unsafeMetadata: params?.unsafeMetadata,
strategy: 'web3_metamask_signature',
});
};

public authenticateWithCoinbase = async (params?: SignUpAuthenticateWithWeb3Params): Promise<SignUpResource> => {
const identifier = await getCoinbaseIdentifier();
return this.authenticateWithWeb3({
identifier,
generateSignature: generateSignatureWithCoinbase,
unsafeMetadata: params?.unsafeMetadata,
strategy: 'web3_coinbase_signature',
});
};

Expand Down Expand Up @@ -245,7 +271,7 @@ export class SignUp extends BaseResource implements SignUpResource {
validatePassword: ReturnType<typeof createValidatePassword> = (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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}}
Expand Down
8 changes: 8 additions & 0 deletions packages/clerk-js/src/utils/web3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export async function getMetamaskIdentifier(): Promise<string> {
return await getWeb3Identifier({ provider: 'metamask' });
}

export async function getCoinbaseIdentifier(): Promise<string> {
return await getWeb3Identifier({ provider: 'coinbase' });
}

type GenerateSignatureParams = {
identifier: string;
nonce: string;
Expand All @@ -53,3 +57,7 @@ type GenerateSignatureParams = {
export async function generateSignatureWithMetamask({ identifier, nonce }: GenerateSignatureParams): Promise<string> {
return await generateWeb3Signature({ identifier, nonce, provider: 'metamask' });
}

export async function generateSignatureWithCoinbase({ identifier, nonce }: GenerateSignatureParams): Promise<string> {
return await generateWeb3Signature({ identifier, nonce, provider: 'coinbase' });
}
Loading