Skip to content
Draft
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
12 changes: 12 additions & 0 deletions src/commands/login/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 8 additions & 5 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -275,15 +275,18 @@ export async function connect(): Promise<ConnectResult> {

// Wait for the QR payload (with timeout)
try {
const qrCode = await Promise.race([
new Promise<string>((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 });
}
});
}),
Expand All @@ -302,7 +305,7 @@ export async function connect(): Promise<ConnectResult> {
),
]);

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<void>; `.catch()` swallows the
Expand Down