From aa3524fc6a99073137d48987314af672460f9d69 Mon Sep 17 00:00:00 2001 From: DevCult Date: Tue, 16 Jun 2026 22:34:10 +0000 Subject: [PATCH] login: print a login link for single-device / browser-terminal use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pg login QR encodes a polkadotapp://pair?handshake=… deeplink. When the terminal runs on the same device as the Polkadot app (e.g. a browser terminal on a phone) you can't scan a QR on your own screen, so also print a link: - thread the raw deeplink (status.payload) out of connect() as result.link; - print it below the (unchanged) QR. When PG_LOGIN_LINK_BASE is set, print an https link that redirects to the deeplink instead — the form browser terminals (ttyd/xterm.js) reliably make tappable, since they don't linkify custom schemes and canvas text isn't selectable on iOS. Unset = raw deeplink. Additive; the QR rendering is unchanged. An official https universal link for pairing would let the CLI print a tappable link unconditionally and drop the env var. --- src/commands/login/index.ts | 12 ++++++++++++ src/utils/auth.ts | 13 ++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/commands/login/index.ts b/src/commands/login/index.ts index b94b5c0..959a34f 100644 --- a/src/commands/login/index.ts +++ b/src/commands/login/index.ts @@ -45,6 +45,18 @@ export const loginCommand = new Command("login") login = result.login; console.log(" Scan with the Polkadot mobile app to log in:\n"); console.log(result.qrCode); + // On the same device as the app (e.g. a browser terminal on a + // phone) you can't scan a QR on your own screen, so also print + // a link. Browser terminals (ttyd/xterm.js) only make http(s) + // tappable — not custom schemes — so when PG_LOGIN_LINK_BASE is + // set, print an https link that redirects to the deeplink; + // otherwise print the raw polkadotapp:// deeplink. + const linkBase = process.env.PG_LOGIN_LINK_BASE?.replace(/\/+$/, ""); + const openUrl = linkBase + ? `${linkBase}/?d=${encodeURIComponent(result.link)}` + : result.link; + console.log("\n or open this link to log in on this device:\n"); + console.log(` ${openUrl}\n`); } } catch (err) { const msg = errorMessage(err); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 3f3505b..6c9ed2f 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -197,7 +197,7 @@ function sessionLogoutAddress(session: UserSession): string { export type ConnectResult = | { kind: "existing"; address: string; addresses: SessionAddresses } - | { kind: "qr"; qrCode: string; login: LoginHandle }; + | { kind: "qr"; qrCode: string; link: string; login: LoginHandle }; export type LoginStatus = | { step: "waiting" } @@ -275,15 +275,18 @@ export async function connect(): Promise { // Wait for the QR payload (with timeout) try { - const qrCode = await Promise.race([ - new Promise((resolve) => { + const { qrCode, link } = await Promise.race([ + new Promise<{ qrCode: string; link: string }>((resolve) => { let done = false; let unsub: (() => void) | undefined; unsub = adapter.sso.pairingStatus.subscribe(async (status: PairingStatus) => { if (status.step === "pairing" && !done) { done = true; unsub?.(); - resolve(await renderQrCode(status.payload)); + // `status.payload` is the pairing deeplink + // (polkadotapp://pair?handshake=…); expose it raw so the + // command layer can offer a scan-free, single-device login. + resolve({ qrCode: await renderQrCode(status.payload), link: status.payload }); } }); }), @@ -302,7 +305,7 @@ export async function connect(): Promise { ), ]); - return { kind: "qr", qrCode, login: { adapter, authPromise } }; + return { kind: "qr", qrCode, link, login: { adapter, authPromise } }; } catch (err) { // Release the WebSocket so we don't leak on the error path. SDK's // destroy() returns Promise; `.catch()` swallows the