Skip to content

Commit cd70481

Browse files
buggy324234claude
andcommitted
Add WhatsApp pairing code support as alternative to QR scanning
Adds --phone flag to `openclaw channels login` that uses Baileys' requestPairingCode() to generate an 8-digit code instead of a QR. Users enter the code in WhatsApp > Linked Devices > Link with phone number. This enables WhatsApp linking without a second device for QR scanning, which is critical for automated/headless deployment flows. Usage: openclaw channels login --channel whatsapp --phone +353830836798 Changes: - session.ts: Accept phoneNumber option, call requestPairingCode() when QR is generated and phone number is provided, fall back to QR on error - login.ts: Add phoneNumber parameter to loginWeb(), normalize phone format - channel.ts: Pass phoneNumber from auth login callback to loginWeb() - channels-cli.ts: Add --phone option to `channels login` command - channel-auth.ts: Add phoneNumber to ChannelAuthOptions, pass through - types.adapters.ts: Add phoneNumber to ChannelAuthAdapter login params Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 94c012b commit cd70481

6 files changed

Lines changed: 44 additions & 3 deletions

File tree

extensions/whatsapp/src/channel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,14 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
167167
},
168168
approvalCapability: whatsappApprovalAuth,
169169
auth: {
170-
login: async ({ cfg, accountId, runtime, verbose }) => {
170+
login: async ({ cfg, accountId, runtime, verbose, phoneNumber }) => {
171171
const resolvedAccountId =
172172
accountId?.trim() ||
173173
whatsappPlugin.config.defaultAccountId?.(cfg) ||
174174
DEFAULT_ACCOUNT_ID;
175175
await (
176176
await loadWhatsAppChannelRuntime()
177-
).loginWeb(Boolean(verbose), undefined, runtime, resolvedAccountId);
177+
).loginWeb(Boolean(verbose), undefined, runtime, resolvedAccountId, phoneNumber);
178178
},
179179
},
180180
lifecycle: {

extensions/whatsapp/src/login.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export async function loginWeb(
1515
waitForConnection?: typeof waitForWaConnection,
1616
runtime: RuntimeEnv = defaultRuntime,
1717
accountId?: string,
18+
phoneNumber?: string,
1819
) {
1920
const cfg = getRuntimeConfig();
2021
const account = resolveWhatsAppAccount({ cfg, accountId });
@@ -30,10 +31,19 @@ export async function loginWeb(
3031
runtime.error(`failed rendering WhatsApp QR: ${String(err)}`);
3132
});
3233
};
34+
const onPairingCode = (code: string) => {
35+
runtime.log(
36+
`Enter this pairing code in WhatsApp > Linked Devices > Link with phone number:\n\n ${code}\n`,
37+
);
38+
};
39+
// Normalize phone number: remove + prefix and non-digit characters
40+
const normalizedPhone = phoneNumber?.replace(/[^0-9]/g, "") || undefined;
3341
let sock = await createWaSocket(false, verbose, {
3442
authDir: account.authDir,
3543
...socketTiming,
3644
onQr,
45+
phoneNumber: normalizedPhone,
46+
onPairingCode,
3747
});
3848
logInfo("Waiting for WhatsApp connection...", runtime);
3949
try {

extensions/whatsapp/src/session.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export async function createWaSocket(
134134
opts: {
135135
authDir?: string;
136136
onQr?: (qr: string) => void;
137+
phoneNumber?: string;
138+
onPairingCode?: (code: string) => void;
137139
} & WhatsAppSocketTimingOptions = {},
138140
): Promise<ReturnType<typeof makeWASocket>> {
139141
const baseLogger = getChildLogger(
@@ -184,11 +186,35 @@ export async function createWaSocket(
184186
fetchAgent: fetchAgent as Agent | undefined,
185187
});
186188

189+
let pairingCodeRequested = false;
187190
sock.ev.on("creds.update", () => enqueueSaveCreds(authDir, saveCreds, sessionLogger));
188191
sock.ev.on("connection.update", async (update: Partial<import("baileys").ConnectionState>) => {
189192
try {
190193
const { connection, lastDisconnect, qr } = update;
191-
if (qr) {
194+
if (qr && opts.phoneNumber && !pairingCodeRequested && !state.creds.registered) {
195+
// Use pairing code instead of QR when phone number is provided
196+
pairingCodeRequested = true;
197+
try {
198+
const code = await sock.requestPairingCode(opts.phoneNumber);
199+
sessionLogger.info({ phoneNumber: opts.phoneNumber }, `Pairing code generated: ${code}`);
200+
opts.onPairingCode?.(code);
201+
if (printQr) {
202+
console.log(
203+
`Enter this pairing code in WhatsApp > Linked Devices > Link with phone number:\n\n ${code}\n`,
204+
);
205+
}
206+
} catch (err) {
207+
sessionLogger.error({ error: String(err) }, "Failed to request pairing code, falling back to QR");
208+
// Fall back to QR
209+
opts.onQr?.(qr);
210+
if (printQr) {
211+
console.log("Open the WhatsApp app, go to Linked Devices, then scan this QR:");
212+
void printTerminalQr(qr).catch((qrErr) => {
213+
sessionLogger.warn({ error: String(qrErr) }, "failed rendering WhatsApp QR");
214+
});
215+
}
216+
}
217+
} else if (qr) {
192218
opts.onQr?.(qr);
193219
if (printQr) {
194220
console.log("Open the WhatsApp app, go to Linked Devices, then scan this QR:");

src/channels/plugins/types.adapters.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ export type ChannelAuthAdapter = {
365365
runtime: RuntimeEnv;
366366
verbose?: boolean;
367367
channelInput?: string | null;
368+
phoneNumber?: string;
368369
}) => Promise<void>;
369370
};
370371

src/cli/channel-auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type ChannelAuthOptions = {
2323
channel?: string;
2424
account?: string;
2525
verbose?: boolean;
26+
phoneNumber?: string;
2627
};
2728

2829
type ChannelPlugin = NonNullable<ReturnType<typeof getChannelPlugin>>;
@@ -249,6 +250,7 @@ export async function runChannelLogin(
249250
runtime,
250251
verbose: Boolean(opts.verbose),
251252
channelInput,
253+
phoneNumber: opts.phoneNumber,
252254
});
253255
await reconcileGatewayRuntimeAfterLocalLogin({
254256
cfg,

src/cli/channels-cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export async function registerChannelsCli(
256256
.description("Link a channel account (if supported)")
257257
.option("--channel <channel>", "Channel alias (auto when only one is configured)")
258258
.option("--account <id>", "Account id (accountId)")
259+
.option("--phone <number>", "Phone number for pairing code login (WhatsApp only, e.g. +1234567890)")
259260
.option("--verbose", "Verbose connection logs", false)
260261
.action(async (opts) => {
261262
await runChannelsCommandWithDanger(async () => {
@@ -264,6 +265,7 @@ export async function registerChannelsCli(
264265
channel: opts.channel as string | undefined,
265266
account: opts.account as string | undefined,
266267
verbose: Boolean(opts.verbose),
268+
phoneNumber: opts.phone as string | undefined,
267269
},
268270
defaultRuntime,
269271
);

0 commit comments

Comments
 (0)