Skip to content
Merged
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
22 changes: 13 additions & 9 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,24 @@ jobs:
- name: Install JS deps
run: pnpm install --frozen-lockfile

- name: Resolve API + gateway URLs
# The mobile shell defaults apiFetch to /api which only works when
# the web bundle is hosted on the same origin as the API. Capacitor
# serves from https://localhost, so without baking VITE_API_BASE
# into the build, every login attempt 404s and `start.challenge`
# blows up. Allow the build to fall back to the values committed
# in apps/mobile/.env.production if the repo vars are not set.
- name: Resolve URLs
# The bundled web build needs the API + gateway URLs at compile time
# (Vite inlines them). The Capacitor shell additionally needs
# TEMPEST_WEB_URL set so the native app loads the live web origin -
# WebAuthn / passkeys refuse to run from https://localhost. All
# three values fall back to apps/mobile/.env.production if the
# repo vars are not configured.
run: |
: "${VITE_API_BASE:=${{ vars.VITE_API_BASE }}}"
: "${VITE_GATEWAY_URL:=${{ vars.VITE_GATEWAY_URL }}}"
if [ -z "$VITE_API_BASE" ] && [ -f apps/mobile/.env.production ]; then
: "${TEMPEST_WEB_URL:=${{ vars.TEMPEST_WEB_URL }}}"
if [ -f apps/mobile/.env.production ]; then
set -a; . apps/mobile/.env.production; set +a
fi
echo "VITE_API_BASE=$VITE_API_BASE" >> "$GITHUB_ENV"
echo "VITE_GATEWAY_URL=$VITE_GATEWAY_URL" >> "$GITHUB_ENV"
echo "Building web bundle against API=$VITE_API_BASE GW=$VITE_GATEWAY_URL"
echo "TEMPEST_WEB_URL=$TEMPEST_WEB_URL" >> "$GITHUB_ENV"
echo "Building web bundle against API=$VITE_API_BASE GW=$VITE_GATEWAY_URL WEB=$TEMPEST_WEB_URL"

- name: Build web bundle
env:
Expand All @@ -77,6 +79,8 @@ jobs:
# project. We don't commit it; CI re-creates it every run so the
# build is always reproducible from the current Capacitor config.
working-directory: apps/mobile
env:
TEMPEST_WEB_URL: ${{ env.TEMPEST_WEB_URL }}
run: |
pnpm exec cap add android
pnpm exec cap sync android
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ jobs:
gem install cocoapods --no-document
pod --version

- name: Resolve API + gateway URLs
- name: Resolve URLs
run: |
: "${VITE_API_BASE:=${{ vars.VITE_API_BASE }}}"
: "${VITE_GATEWAY_URL:=${{ vars.VITE_GATEWAY_URL }}}"
if [ -z "$VITE_API_BASE" ] && [ -f apps/mobile/.env.production ]; then
: "${TEMPEST_WEB_URL:=${{ vars.TEMPEST_WEB_URL }}}"
if [ -f apps/mobile/.env.production ]; then
set -a; . apps/mobile/.env.production; set +a
fi
echo "VITE_API_BASE=$VITE_API_BASE" >> "$GITHUB_ENV"
echo "VITE_GATEWAY_URL=$VITE_GATEWAY_URL" >> "$GITHUB_ENV"
echo "Building web bundle against API=$VITE_API_BASE GW=$VITE_GATEWAY_URL"
echo "TEMPEST_WEB_URL=$TEMPEST_WEB_URL" >> "$GITHUB_ENV"
echo "Building web bundle against API=$VITE_API_BASE GW=$VITE_GATEWAY_URL WEB=$TEMPEST_WEB_URL"

- name: Build web bundle
env:
Expand All @@ -54,6 +56,8 @@ jobs:

- name: Add iOS platform
working-directory: apps/mobile
env:
TEMPEST_WEB_URL: ${{ env.TEMPEST_WEB_URL }}
run: |
pnpm exec cap add ios
pnpm exec cap sync ios
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
# Example (replace with the URLs printed by `railway up`):
# VITE_API_BASE=https://tempest-api-production-2899.up.railway.app
# VITE_GATEWAY_URL=wss://tempest-gateway-production-1213ce.up.railway.app/gateway
#
# TEMPEST_WEB_URL controls whether Capacitor loads the live deployment as
# the initial page (recommended; passkeys / WebAuthn work over the real
# HTTPS origin) or falls back to the bundled web/dist (faster first paint,
# but passkeys refuse to run on https://localhost).

VITE_API_BASE=https://tempest-api-production-2899.up.railway.app
VITE_GATEWAY_URL=wss://tempest-gateway-production-1213ce.up.railway.app/gateway
TEMPEST_WEB_URL=https://tempest-web-production-1213ce.up.railway.app
21 changes: 16 additions & 5 deletions apps/mobile/capacitor.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import type { CapacitorConfig } from "@capacitor/cli";

// Capacitor wraps the existing apps/web Vite build into native iOS / Android
// shells. The web bundle is built into apps/web/dist and we point Capacitor's
// `webDir` at that path; `pnpm --filter tempest-mobile build` handles the
// build + sync round trip.
// shells. There are two supported modes:
//
// * server.url set (recommended for now): the native app loads the live
// web deployment as its initial page. WebAuthn / passkeys, Service
// Workers, and other origin-locked APIs use the real HTTPS origin and
// work the same as a browser visit. Set TEMPEST_WEB_URL in the build
// env (CI repo var or apps/mobile/.env.production) to enable this.
//
// * server.url unset (offline-first): the app uses the bundled
// apps/web/dist as the initial page, served from https://localhost.
// Faster first paint, but WebAuthn refuses to run on the localhost
// origin so passkey login does not work. A native passkey bridge is
// needed to use this mode for auth - tracked as future work.
const liveWebUrl = process.env.TEMPEST_WEB_URL?.trim();

const config: CapacitorConfig = {
appId: "chat.tempest.app",
appName: "Tempest",
Expand All @@ -21,12 +33,11 @@ const config: CapacitorConfig = {
contentInset: "automatic",
limitsNavigationsToAppBoundDomains: false,
backgroundColor: "#0b0d12",
// Support iPad multitasking (split view, slide over). The web layout
// already adapts to the available width via the >=900px breakpoint.
scheme: "Tempest",
},
server: {
androidScheme: "https",
...(liveWebUrl ? { url: liveWebUrl, cleartext: false } : {}),
},
plugins: {
SplashScreen: {
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/auth/passkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,25 @@ function rewriteForGet(input: any): CredentialRequestOptions {
return { publicKey: o };
}

function ensureSupported(): void {
if (typeof navigator === "undefined" || !navigator.credentials || typeof PublicKeyCredential === "undefined") {
throw new Error(
"Passkeys aren't available in this webview. Open Tempest in a browser " +
"or update your in-app webview to the latest version (Android System WebView " +
"/ Safari) and try again.",
);
}
}

export async function createPasskey(challenge: any): Promise<unknown> {
ensureSupported();
const cred = (await navigator.credentials.create(rewriteForCreate(challenge))) as PublicKeyCredential | null;
if (!cred) throw new Error("passkey creation cancelled");
return serializeCredential(cred, "create");
}

export async function getPasskey(challenge: any): Promise<unknown> {
ensureSupported();
const cred = (await navigator.credentials.get(rewriteForGet(challenge))) as PublicKeyCredential | null;
if (!cred) throw new Error("passkey assertion cancelled");
return serializeCredential(cred, "get");
Expand Down
20 changes: 20 additions & 0 deletions docs/MOBILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ The CI workflow patches the generated Xcode project to keep iPad
support universal even if the Capacitor template ever regresses, and
forces `UIRequiresFullScreen=NO` so iPad multitasking works.

## Passkeys / WebAuthn on mobile

WebAuthn refuses to run on the synthetic `https://localhost` origin
Capacitor uses for the bundled webview. Two ways to handle this:

1. **`TEMPEST_WEB_URL` set (default).** The Capacitor app loads the
live web deployment as its initial page. Passkeys then use the real
HTTPS origin and Just Work, identical to a browser visit. Set the
value as a repo variable in GitHub Actions or in
`apps/mobile/.env.production`. The app keeps its native chrome
(no browser bar, native splash, status-bar styling); it just sources
its HTML from the live URL.
2. **`TEMPEST_WEB_URL` unset.** The app uses the bundled
`apps/web/dist`. Faster cold start, works offline, but passkey
login throws "passkeys aren't available in this webview" because
`navigator.credentials` refuses to operate on `localhost`. A
native passkey bridge (Android `CredentialManager`, iOS
`ASAuthorizationController`) is the path to fully offline-first
auth - tracked as future work.

## Signing

### Android release APK
Expand Down
Loading