diff --git a/.changeset/whatsapp-signer.md b/.changeset/whatsapp-signer.md new file mode 100644 index 000000000..b5424838e --- /dev/null +++ b/.changeset/whatsapp-signer.md @@ -0,0 +1,5 @@ +--- +"@crossmint/wallets-sdk": minor +--- + +Add support for WhatsApp non-custodial signers diff --git a/packages/client/react-base/src/hooks/useSignerAuth.ts b/packages/client/react-base/src/hooks/useSignerAuth.ts index c1aaca1d5..258c0ebd9 100644 --- a/packages/client/react-base/src/hooks/useSignerAuth.ts +++ b/packages/client/react-base/src/hooks/useSignerAuth.ts @@ -5,7 +5,7 @@ const throwNotAvailable = (functionName: string) => () => { throw new Error(`${functionName} is not available. Make sure you're using an email or phone signer wallet.`); }; -export type SignerType = "email" | "phone"; +export type SignerType = "email" | "phone" | "whatsapp"; export type DialogStep = "initial" | "otp"; export interface SignerAuthState { @@ -128,7 +128,7 @@ export function useSignerAuth(): SignerAuthState & SignerAuthHandlers { const onAuthRequired: Callbacks["onAuthRequired"] = useCallback( ( - signerType: "email" | "phone", + signerType: "email" | "phone" | "whatsapp", signerLocator: string, needsAuth: boolean, sendMessageWithOtp: () => Promise, @@ -136,7 +136,7 @@ export function useSignerAuth(): SignerAuthState & SignerAuthHandlers { reject: () => void ) => { const signerValue = signerLocator.split(":")[1]; - if (signerType === "phone" && signerValue != null) { + if ((signerType === "phone" || signerType === "whatsapp") && signerValue != null) { setPhoneSignerDialogOpen(needsAuth); setActiveAuthPhone(needsAuth ? signerValue : undefined); sendPhoneOtpRef.current = sendMessageWithOtp; diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 21d935538..68ba4257f 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -234,7 +234,7 @@ export function CrossmintWalletBaseProvider({ const wrappedOnAuthRequired = useCallback( async ( - signerType: "email" | "phone", + signerType: "email" | "phone" | "whatsapp", signerLocator: string, needsAuth: boolean, sendOtp: () => Promise, @@ -255,7 +255,7 @@ export function CrossmintWalletBaseProvider({ const initializeWebViewIfNeeded = useCallback( async (signer: SignerConfigForChain) => { - if (signer.type === "email" || signer.type === "phone") { + if (signer.type === "email" || signer.type === "phone" || signer.type === "whatsapp") { await initializeWebView?.(); } }, diff --git a/packages/wallets/src/index.ts b/packages/wallets/src/index.ts index e0739cfe3..3db8dcd0a 100644 --- a/packages/wallets/src/index.ts +++ b/packages/wallets/src/index.ts @@ -59,6 +59,7 @@ export type { SignerAdapter, EmailSignerConfig, PhoneSignerConfig, + WhatsappSignerConfig, ExternalWalletSignerConfig, ExternalWalletRegistrationConfig, EvmExternalWalletSignerConfig, @@ -68,6 +69,7 @@ export type { SignerLocator, EmailSignerLocator, PhoneSignerLocator, + WhatsappSignerLocator, PasskeySignerLocator, DeviceSignerLocator, DeviceSignerConfig, diff --git a/packages/wallets/src/openapi.json b/packages/wallets/src/openapi.json index 4c7032232..2d355a1cd 100644 --- a/packages/wallets/src/openapi.json +++ b/packages/wallets/src/openapi.json @@ -7693,7 +7693,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -7960,7 +7960,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -8224,7 +8224,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -8953,7 +8953,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -9106,7 +9106,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -9322,7 +9322,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -9475,7 +9475,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -9773,7 +9773,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -9942,7 +9942,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -10286,7 +10286,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -10590,7 +10590,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -10772,7 +10772,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -11237,7 +11237,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -11494,7 +11494,10 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": [ + "phone", + "whatsapp" + ] }, "phone": { "type": "string" @@ -11702,7 +11705,10 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": [ + "phone", + "whatsapp" + ] }, "phone": { "type": "string" @@ -12026,7 +12032,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -12351,7 +12357,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -12533,7 +12539,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -15350,7 +15356,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -15731,7 +15737,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -15895,7 +15901,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -16058,7 +16064,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -16236,7 +16242,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -16553,7 +16559,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -16836,7 +16842,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -17014,7 +17020,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -17603,7 +17609,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -17781,7 +17787,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -18201,7 +18207,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -18351,7 +18357,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -18574,7 +18580,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -18752,7 +18758,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -19172,7 +19178,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -19360,7 +19366,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -19538,7 +19544,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -20140,7 +20146,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -20318,7 +20324,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -20760,7 +20766,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -21136,7 +21142,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -21300,7 +21306,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -21463,7 +21469,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -21641,7 +21647,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -21947,7 +21953,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -22230,7 +22236,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -22408,7 +22414,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -22989,7 +22995,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -23167,7 +23173,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -23579,7 +23585,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -23729,7 +23735,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -23952,7 +23958,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -24130,7 +24136,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -24542,7 +24548,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -24730,7 +24736,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -24908,7 +24914,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -25502,7 +25508,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -25680,7 +25686,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string" @@ -29257,7 +29263,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -29467,7 +29473,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -29701,7 +29707,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -29913,7 +29919,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -30252,7 +30258,8 @@ "type": { "type": "string", "enum": [ - "phone" + "phone", + "whatsapp" ] }, "phone": { @@ -30475,7 +30482,8 @@ "type": { "type": "string", "enum": [ - "phone" + "phone", + "whatsapp" ] }, "phone": { @@ -31031,7 +31039,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -31276,7 +31284,7 @@ "type": { "type": "string", "description": "Identifier for phone signer type", - "enum": ["phone"] + "enum": ["phone", "whatsapp"] }, "phone": { "type": "string", @@ -31636,7 +31644,8 @@ "type": { "type": "string", "enum": [ - "phone" + "phone", + "whatsapp" ] }, "phone": { @@ -31859,7 +31868,8 @@ "type": { "type": "string", "enum": [ - "phone" + "phone", + "whatsapp" ] }, "phone": { diff --git a/packages/wallets/src/signers/descriptors/descriptors.test.ts b/packages/wallets/src/signers/descriptors/descriptors.test.ts index c4148e7de..82ecc535e 100644 --- a/packages/wallets/src/signers/descriptors/descriptors.test.ts +++ b/packages/wallets/src/signers/descriptors/descriptors.test.ts @@ -44,6 +44,7 @@ function makeCtx( it.each<[name: string, type: string, config: Config, message: string]>([ ["email missing email", "email", cfg({ type: "email" }), "Email signer requires an email address"], ["phone missing phone", "phone", cfg({ type: "phone" }), "Phone signer requires a phone number"], + ["whatsapp missing phone", "whatsapp", cfg({ type: "whatsapp" }), "Whatsapp signer requires a phone number"], [ "ext missing address", "external-wallet", @@ -63,6 +64,7 @@ it.each<[name: string, type: string, config: Config, message: string]>([ it.each<[type: string, config: Config]>([ ["email", cfg({ type: "email", email: "a@b.com" })], ["phone", cfg({ type: "phone", phone: "+15551234" })], + ["whatsapp", cfg({ type: "whatsapp", phone: "+15551234" })], ["external-wallet", cfg({ type: "external-wallet", address: "0xabc", onSign: vi.fn() })], ["api-key", cfg({ type: "api-key" })], ["passkey", cfg({ type: "passkey" })], @@ -72,9 +74,10 @@ it.each<[type: string, config: Config]>([ expect(() => getSignerDescriptor(type as never).validateConfig(config as never)).not.toThrow(); }); -it.each<[type: "email" | "phone", field: "email" | "phone", value: string]>([ +it.each<[type: "email" | "phone" | "whatsapp", field: "email" | "phone", value: string]>([ ["email", "email", "a@b.com"], ["phone", "phone", "+15551234"], + ["whatsapp", "phone", "+15551234"], ])("buildInternalConfig: %s threads crossmint/clientTEEConnection/onAuthRequired from ctx", (type, field, value) => { const result = getSignerDescriptor(type).buildInternalConfig({ type, [field]: value } as never, makeCtx()); expect(result).toEqual({ @@ -139,7 +142,7 @@ it.each<[name: string, config: Config, expected: object]>([ expect(getSignerDescriptor("passkey").buildInternalConfig(config as never, makeCtx())).toMatchObject(expected); }); -it.each<[type: string]>([["email"], ["phone"], ["passkey"], ["api-key"]])( +it.each<[type: string]>([["email"], ["phone"], ["whatsapp"], ["passkey"], ["api-key"]])( "canAutoAssemble: %s is always true", (type) => { expect(getSignerDescriptor(type as never).canAutoAssemble({ type } as never, makeCtx())).toBe(true); @@ -192,6 +195,7 @@ it("addSignerPayload: device without publicKey returns locator", () => { it.each<[type: string, config: Config, expected: string]>([ ["email", cfg({ type: "email", email: "a@b.com" }), "email:a@b.com"], ["phone", cfg({ type: "phone", phone: "+15551234" }), "phone:+15551234"], + ["whatsapp", cfg({ type: "whatsapp", phone: "+15551234" }), "whatsapp:+15551234"], ["api-key", cfg({ type: "api-key" }), "api-key"], ["external-wallet", cfg({ type: "external-wallet", address: "0xabc", onSign: vi.fn() }), "external-wallet:0xabc"], ])("addSignerPayload: %s returns locator", (type, config, expected) => { @@ -279,6 +283,7 @@ it.each<[name: string, type: string, config: Config, recovery: Config, expected: it.each([ ["email", true], ["phone", true], + ["whatsapp", true], ["api-key", true], ["device", true], ["external-wallet", true], @@ -295,7 +300,7 @@ it.each([ expect(getSignerDescriptor(type).signerUnavailableReason()).toMatch(pattern); }); -it.each(["email", "phone", "api-key", "device", "passkey"] as const)( +it.each(["email", "phone", "whatsapp", "api-key", "device", "passkey"] as const)( "signerUnavailableReason: %s returns null so require() applies the generic fallback", (type) => { expect(getSignerDescriptor(type).signerUnavailableReason()).toBeNull(); diff --git a/packages/wallets/src/signers/descriptors/index.ts b/packages/wallets/src/signers/descriptors/index.ts index 926bef16c..69495ecb9 100644 --- a/packages/wallets/src/signers/descriptors/index.ts +++ b/packages/wallets/src/signers/descriptors/index.ts @@ -7,6 +7,7 @@ import { externalWalletSignerDescriptor } from "./external-wallet"; import { passkeySignerDescriptor } from "./passkey"; import { phoneSignerDescriptor } from "./phone"; import { serverSignerDescriptor } from "./server"; +import { whatsappSignerDescriptor } from "./whatsapp"; import type { SignerDescriptor } from "./types"; export type { SignerDescriptor, SignerDescriptorContext } from "./types"; @@ -17,6 +18,8 @@ export function getSignerDescriptor(type: SignerDescriptor["typ return emailSignerDescriptor as SignerDescriptor; case "phone": return phoneSignerDescriptor as SignerDescriptor; + case "whatsapp": + return whatsappSignerDescriptor as SignerDescriptor; case "passkey": return passkeySignerDescriptor as SignerDescriptor; case "device": diff --git a/packages/wallets/src/signers/descriptors/types.ts b/packages/wallets/src/signers/descriptors/types.ts index 1872cfac1..c67ea7c80 100644 --- a/packages/wallets/src/signers/descriptors/types.ts +++ b/packages/wallets/src/signers/descriptors/types.ts @@ -25,7 +25,7 @@ export interface SignerDescriptorContext { } export interface SignerDescriptor { - readonly type: "email" | "phone" | "passkey" | "device" | "api-key" | "server" | "external-wallet"; + readonly type: "email" | "phone" | "whatsapp" | "passkey" | "device" | "api-key" | "server" | "external-wallet"; validateConfig(config: SignerConfigForChain): void; buildInternalConfig( config: SignerConfigForChain | ApiSourcedServerSignerConfig, diff --git a/packages/wallets/src/signers/descriptors/whatsapp.ts b/packages/wallets/src/signers/descriptors/whatsapp.ts new file mode 100644 index 000000000..a7ffc80c8 --- /dev/null +++ b/packages/wallets/src/signers/descriptors/whatsapp.ts @@ -0,0 +1,47 @@ +import type { RegisterSignerParams } from "../../api"; +import type { Chain } from "../../chains/chains"; +import { getSignerLocator } from "../../utils/signer-locator"; +import type { + InternalSignerConfig, + SignerConfigForChain, + RecoverySignerConfigForChain, + SignerLocator, + WhatsappSignerConfig, +} from "../types"; +import type { SignerDescriptor, SignerDescriptorContext } from "./types"; + +export const whatsappSignerDescriptor: SignerDescriptor = { + type: "whatsapp", + validateConfig(config: SignerConfigForChain): void { + if (!("phone" in config) || config.phone == null) { + throw new Error("Whatsapp signer requires a phone number"); + } + }, + buildInternalConfig( + config: SignerConfigForChain, + ctx: SignerDescriptorContext + ): InternalSignerConfig { + const whatsappConfig = config as WhatsappSignerConfig; + return { + type: "whatsapp", + phone: whatsappConfig.phone, + locator: `whatsapp:${whatsappConfig.phone}` as SignerLocator, + address: ctx.walletAddress, + crossmint: ctx.crossmint, + clientTEEConnection: ctx.clientTEEConnection, + resetSignerFrame: ctx.resetSignerFrame, + onAuthRequired: ctx.onAuthRequired, + } as InternalSignerConfig; + }, + canAutoAssemble(): boolean { + return true; + }, + addSignerPayload(config: SignerConfigForChain): RegisterSignerParams["signer"] { + return getSignerLocator(config); + }, + matchesRecovery(config: SignerConfigForChain, recovery: RecoverySignerConfigForChain): boolean { + return getSignerLocator(config) === getSignerLocator(recovery as SignerConfigForChain); + }, + adoptsRecoveryConfigOnMatch: true, + signerUnavailableReason: () => null, +}; diff --git a/packages/wallets/src/signers/index.ts b/packages/wallets/src/signers/index.ts index d92b9091e..3f68a2b67 100644 --- a/packages/wallets/src/signers/index.ts +++ b/packages/wallets/src/signers/index.ts @@ -20,6 +20,7 @@ export function assembleSigner( switch (config.type) { case "email": case "phone": + case "whatsapp": if (chain === "solana") { return new SolanaNonCustodialSigner(config); } diff --git a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts index 2e0945e20..b3a3fabe7 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts @@ -1,11 +1,11 @@ -import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; +import type { EmailInternalSignerConfig, PhoneInternalSignerConfig, WhatsappInternalSignerConfig } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; import { PersonalMessage } from "ox"; import { isHex, toHex, type Hex } from "viem"; import { walletsLogger } from "../../logger"; export class EVMNonCustodialSigner extends NonCustodialSigner { - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { + constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig | WhatsappInternalSignerConfig) { super(config); } diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index d249d22c4..d57a7e261 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -6,6 +6,8 @@ import type { PhoneInternalSignerConfig, PhoneSignerLocator, SignerAdapter, + WhatsappInternalSignerConfig, + WhatsappSignerLocator, } from "../types"; import { AuthRejectedError, @@ -20,7 +22,7 @@ import type { SignerOutputEvent } from "@crossmint/client-signers"; import { walletsLogger } from "../../logger"; export abstract class NonCustodialSigner implements SignerAdapter { - public readonly type: "email" | "phone"; + public readonly type: "email" | "phone" | "whatsapp"; private _needsAuth = true; private _authPromise: { promise: Promise; @@ -30,7 +32,9 @@ export abstract class NonCustodialSigner implements SignerAdapter { private _initializationPromise: Promise | null = null; private _onboardingConnectionGeneration: number | null = null; - constructor(protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { + constructor( + protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig | WhatsappInternalSignerConfig + ) { // Only initialize the signer if running client-side if (typeof window !== "undefined") { this._initializationPromise = this.initialize(); @@ -38,7 +42,7 @@ export abstract class NonCustodialSigner implements SignerAdapter { this.type = this.config.type; } - locator(): EmailSignerLocator | PhoneSignerLocator { + locator(): EmailSignerLocator | PhoneSignerLocator | WhatsappSignerLocator { return this.config.locator; } @@ -288,7 +292,10 @@ export abstract class NonCustodialSigner implements SignerAdapter { if (this.config.type === "email") { return `email:${this.config.email}`; } - return `phone:${this.config.phone}`; + if (this.config.type === "phone") { + return `phone:${this.config.phone}`; + } + return `whatsapp:${this.config.phone}`; } private async verifyOtp(encryptedOtp: string) { diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 0f1f27256..95c14f6c0 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -1,11 +1,11 @@ import { VersionedTransaction } from "@solana/web3.js"; import base58 from "bs58"; -import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; +import type { EmailInternalSignerConfig, PhoneInternalSignerConfig, WhatsappInternalSignerConfig } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; import { walletsLogger } from "../../logger"; export class SolanaNonCustodialSigner extends NonCustodialSigner { - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { + constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig | WhatsappInternalSignerConfig) { super(config); } diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index 9b0856576..6e6a8efeb 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,9 +1,9 @@ -import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; +import type { EmailInternalSignerConfig, PhoneInternalSignerConfig, WhatsappInternalSignerConfig } from "../types"; import { DEFAULT_EVENT_OPTIONS, NonCustodialSigner } from "./ncs-signer"; import { walletsLogger } from "../../logger"; export class StellarNonCustodialSigner extends NonCustodialSigner { - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { + constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig | WhatsappInternalSignerConfig) { super(config); } diff --git a/packages/wallets/src/signers/types.ts b/packages/wallets/src/signers/types.ts index 479656f35..377a1824c 100644 --- a/packages/wallets/src/signers/types.ts +++ b/packages/wallets/src/signers/types.ts @@ -90,7 +90,16 @@ export type PhoneSignerConfig = { locator?: string; }; -export type NonCustodialSignerType = PhoneSignerConfig["type"] | EmailSignerConfig["type"]; +export type WhatsappSignerConfig = { + type: "whatsapp"; + phone?: string; + locator?: string; +}; + +export type NonCustodialSignerType = + | PhoneSignerConfig["type"] + | WhatsappSignerConfig["type"] + | EmailSignerConfig["type"]; export type ExternalWalletSignerConfigForChain = C extends SolanaChain ? SolanaExternalWalletSignerConfig @@ -158,6 +167,12 @@ export type PhoneInternalSignerConfig = PhoneSignerConfig & onAuthRequired?: Callbacks["onAuthRequired"]; }; +export type WhatsappInternalSignerConfig = WhatsappSignerConfig & + Omit & { + locator: WhatsappSignerLocator; + onAuthRequired?: Callbacks["onAuthRequired"]; + }; + export type DeviceInternalSignerConfig = { type: "device"; locator?: DeviceSignerLocator; @@ -189,6 +204,7 @@ export type ServerInternalSignerConfig = { export type InternalSignerConfig = | EmailInternalSignerConfig | PhoneInternalSignerConfig + | WhatsappInternalSignerConfig | PasskeyInternalSignerConfig | ApiKeyInternalSignerConfig | ExternalWalletInternalSignerConfig @@ -225,16 +241,23 @@ export type DeviceSignerConfig = { }; export type SignerConfigForChain = C extends SolanaChain - ? EmailSignerConfig | PhoneSignerConfig | BaseSignerConfig | DeviceSignerConfig + ? EmailSignerConfig | PhoneSignerConfig | WhatsappSignerConfig | BaseSignerConfig | DeviceSignerConfig : C extends StellarChain - ? EmailSignerConfig | PhoneSignerConfig | BaseSignerConfig | DeviceSignerConfig - : EmailSignerConfig | PhoneSignerConfig | PasskeySignerConfig | BaseSignerConfig | DeviceSignerConfig; + ? EmailSignerConfig | PhoneSignerConfig | WhatsappSignerConfig | BaseSignerConfig | DeviceSignerConfig + : + | EmailSignerConfig + | PhoneSignerConfig + | WhatsappSignerConfig + | PasskeySignerConfig + | BaseSignerConfig + | DeviceSignerConfig; //////////////////////////////////////////////////////////// // Signer locator types //////////////////////////////////////////////////////////// export type EmailSignerLocator = `email:${string}`; export type PhoneSignerLocator = `phone:${string}`; +export type WhatsappSignerLocator = `whatsapp:${string}`; export type PasskeySignerLocator = `passkey:${string}`; export type DeviceSignerLocator = `device:${string}`; export type ExternalWalletSignerLocator = `external-wallet:${string}`; @@ -244,6 +267,7 @@ export type ServerSignerLocator = `server:${string}`; export type SignerLocator = | EmailSignerLocator | PhoneSignerLocator + | WhatsappSignerLocator | PasskeySignerLocator | DeviceSignerLocator | ExternalWalletSignerLocator @@ -256,6 +280,7 @@ export type SignerLocator = type SignResultMap = { email: BaseSignResult; phone: BaseSignResult; + whatsapp: BaseSignResult; "api-key": BaseSignResult; "external-wallet": BaseSignResult; server: BaseSignResult; diff --git a/packages/wallets/src/utils/signer-locator.test.ts b/packages/wallets/src/utils/signer-locator.test.ts index 8e49d0351..b7f4a2021 100644 --- a/packages/wallets/src/utils/signer-locator.test.ts +++ b/packages/wallets/src/utils/signer-locator.test.ts @@ -56,6 +56,11 @@ describe("getSignerLocator", () => { expect(getSignerLocator(signer)).toBe("phone:+1234567890"); }); + it("returns whatsapp locator", () => { + const signer = { type: "whatsapp", phone: "+1234567890" } as SignerConfigForChain; + expect(getSignerLocator(signer)).toBe("whatsapp:+1234567890"); + }); + it("returns passkey locator", () => { const signer = { type: "passkey", id: "cred-123" } as SignerConfigForChain; expect(getSignerLocator(signer)).toBe("passkey:cred-123"); diff --git a/packages/wallets/src/utils/signer-locator.ts b/packages/wallets/src/utils/signer-locator.ts index 8fba3a49c..27c16e2a5 100644 --- a/packages/wallets/src/utils/signer-locator.ts +++ b/packages/wallets/src/utils/signer-locator.ts @@ -19,6 +19,9 @@ export function getSignerLocator( if (signer.type === "phone" && signer.phone) { return `phone:${signer.phone}`; } + if (signer.type === "whatsapp" && signer.phone) { + return `whatsapp:${signer.phone}`; + } if (signer.type === "passkey" && "id" in signer) { return `passkey:${signer.id}`; } diff --git a/packages/wallets/src/utils/signer-mapping.ts b/packages/wallets/src/utils/signer-mapping.ts index e4d24ffb3..842191616 100644 --- a/packages/wallets/src/utils/signer-mapping.ts +++ b/packages/wallets/src/utils/signer-mapping.ts @@ -42,6 +42,13 @@ export function extractSignerBase(apiSigner: APISigner): SignerBase { address: apiSigner.address, locator: apiSigner.locator, }; + case "whatsapp": + return { + type: "whatsapp", + phone: apiSigner.phone, + address: apiSigner.address, + locator: apiSigner.locator, + }; case "device": return { type: "device", diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index 9bd7d83bd..955bfc975 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -176,6 +176,14 @@ export type Signer = status: SignerStatus; scopes?: Scope[]; } + | { + type: "whatsapp"; + phone: string; + address: string; + locator: string; + status: SignerStatus; + scopes?: Scope[]; + } | { type: "device"; publicKey: { x: string; y: string }; @@ -200,7 +208,7 @@ export type Callbacks = { onWalletCreationStart?: () => Promise; onTransactionStart?: () => Promise; onAuthRequired?: ( - signerType: "email" | "phone", + signerType: "email" | "phone" | "whatsapp", signerLocator: string, needsAuth: boolean, sendOtp: () => Promise, diff --git a/packages/wallets/src/wallets/wallet.test.ts b/packages/wallets/src/wallets/wallet.test.ts index ca02529df..b056c99ce 100644 --- a/packages/wallets/src/wallets/wallet.test.ts +++ b/packages/wallets/src/wallets/wallet.test.ts @@ -1902,6 +1902,26 @@ describe("Wallet - useSigner()", () => { expect(mockApiClient.getSigner).not.toHaveBeenCalled(); }); + it("accepts the recovery signer (whatsapp) without calling getSigner", async () => { + mockApiClient = createMockApiClient(); + const wallet = new Wallet( + { + chain: "base-sepolia" as const, + address: "0x1234567890123456789012345678901234567890", + recovery: { type: "whatsapp", phone: "+1234567890" } as any, + }, + mockApiClient as unknown as ApiClient + ); + vi.spyOn(wallet, "signers").mockResolvedValue([]); + + await wallet.useSigner({ type: "whatsapp", phone: "+1234567890" } as any); + + expect(wallet.signer).toBeDefined(); + expect(wallet.signer?.type).toBe("whatsapp"); + expect(wallet.signer?.status).toBe("active"); + expect(mockApiClient.getSigner).not.toHaveBeenCalled(); + }); + it("accepts recovery external-wallet signer without calling getSigner", async () => { mockApiClient = createMockApiClient(); const wallet = new Wallet(