From afc00fb393c5db55b759aa4199d9ca2f9c7c3184 Mon Sep 17 00:00:00 2001 From: Simone <185146821+Lucenx9@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:55:07 +0200 Subject: [PATCH] fix(mobile): use HTTP for bare IP pairing --- .agents/skills/test-t3-mobile/SKILL.md | 2 +- .../src/features/connection/pairing.test.ts | 21 +++++++++++++++++++ .../mobile/src/features/connection/pairing.ts | 17 ++++++++++++++- docs/user/remote-access.md | 2 ++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.agents/skills/test-t3-mobile/SKILL.md b/.agents/skills/test-t3-mobile/SKILL.md index f3e3dcfd8ce..98c1c3b2022 100644 --- a/.agents/skills/test-t3-mobile/SKILL.md +++ b/.agents/skills/test-t3-mobile/SKILL.md @@ -66,7 +66,7 @@ Use these client origins: - Android Emulator: `http://10.0.2.2:` - Physical device: bind the backend to `0.0.0.0` and use the host's reachable LAN origin -Always enter the complete `http://` origin; the mobile host field otherwise assumes HTTPS. When testing web and mobile together, run `vp run dev --home-dir --host 127.0.0.1` instead and do not launch a second backend over the same base directory. +Enter the complete `http://` origin to make the test transport explicit. Bare IP addresses default to HTTP, while bare hostnames default to HTTPS. When testing web and mobile together, run `vp run dev --home-dir --host 127.0.0.1` instead and do not launch a second backend over the same base directory. ## Start or reuse Metro safely diff --git a/apps/mobile/src/features/connection/pairing.test.ts b/apps/mobile/src/features/connection/pairing.test.ts index 18b6c71a293..19392768479 100644 --- a/apps/mobile/src/features/connection/pairing.test.ts +++ b/apps/mobile/src/features/connection/pairing.test.ts @@ -1,11 +1,32 @@ import { describe, expect, it } from "vite-plus/test"; import { + buildPairingUrl, extractPairingUrlFromQrPayload, PairingQrPayloadEmptyError, parsePairingUrl, } from "./pairing"; +describe("buildPairingUrl", () => { + it("uses HTTP for a schemeless IP address", () => { + expect(buildPairingUrl("192.168.1.100:3773", "pairing-token")).toBe( + "http://192.168.1.100:3773/#token=pairing-token", + ); + }); + + it("keeps HTTPS as the default for a schemeless hostname", () => { + expect(buildPairingUrl("remote.example.com", "pairing-token")).toBe( + "https://remote.example.com/#token=pairing-token", + ); + }); + + it("preserves an explicit scheme for an IP address", () => { + expect(buildPairingUrl("https://192.168.1.100:3773", "pairing-token")).toBe( + "https://192.168.1.100:3773/#token=pairing-token", + ); + }); +}); + describe("extractPairingUrlFromQrPayload", () => { it("trims raw pairing urls from qr payloads", () => { expect( diff --git a/apps/mobile/src/features/connection/pairing.ts b/apps/mobile/src/features/connection/pairing.ts index 910efa7f256..569d00cbdd3 100644 --- a/apps/mobile/src/features/connection/pairing.ts +++ b/apps/mobile/src/features/connection/pairing.ts @@ -3,6 +3,21 @@ import * as Schema from "effect/Schema"; const MOBILE_PAIRING_URL_PARAM = "pairingUrl"; +function isIpLiteral(host: string): boolean { + try { + const hostname = new URL(`http://${host}`).hostname.replace(/^\[|\]$/g, ""); + if (hostname.includes(":")) return true; + + const octets = hostname.split("."); + return ( + octets.length === 4 && + octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255) + ); + } catch { + return false; + } +} + export class PairingQrPayloadEmptyError extends Schema.TaggedErrorClass()( "PairingQrPayloadEmptyError", {}, @@ -19,7 +34,7 @@ export function buildPairingUrl(host: string, code: string): string { if (!c) return h; try { - const url = new URL(h.includes("://") ? h : `https://${h}`); + const url = new URL(h.includes("://") ? h : `${isIpLiteral(h) ? "http" : "https"}://${h}`); url.hash = new URLSearchParams([["token", c]]).toString(); return url.toString(); } catch { diff --git a/docs/user/remote-access.md b/docs/user/remote-access.md index 3881c5be3a4..60cfc44d292 100644 --- a/docs/user/remote-access.md +++ b/docs/user/remote-access.md @@ -38,6 +38,8 @@ available. You can set another endpoint as the default from the expanded endpoin If the copied link points directly at `http://192.168.x.y:3773`, open it from a client that can reach that LAN address. If it points at `https://app.t3.codes/pair?...`, the hosted web app will save the environment and connect directly to the backend URL in the link. +In the mobile app's **Add Environment** form, a numeric IP address without a scheme uses HTTP. Include `https://` explicitly when the backend is served over HTTPS. + ### Tailscale Endpoints When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list.