From be211c22ed584ed34d3926e90b800c26f6e472d6 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 24 Sep 2026 12:48:12 +0200 Subject: [PATCH] fix(native): keep the iOS offline copy inside the app loadSimulatedRequest went to the server URL without its trailing slash, and Capacitor keeps a navigation in the WebView only when it starts with the bridge's serverURL, slash included. The offline start page would have opened in Safari instead. Also covers the platform helpers and the hand-off API module. --- client/ios/App/App/TrekShellPlugin.swift | 5 +++- client/src/api/nativeAuth.test.ts | 19 ++++++++++++++ client/src/native/platform.test.ts | 33 ++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 client/src/api/nativeAuth.test.ts create mode 100644 client/src/native/platform.test.ts diff --git a/client/ios/App/App/TrekShellPlugin.swift b/client/ios/App/App/TrekShellPlugin.swift index c946e5474..0414b3598 100644 --- a/client/ios/App/App/TrekShellPlugin.swift +++ b/client/ios/App/App/TrekShellPlugin.swift @@ -121,7 +121,10 @@ public class TrekShellPlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPr } @objc func openOfflineCopy(_ call: CAPPluginCall) { - guard let raw = TrekServerStore.url, let url = URL(string: raw), + // With the trailing slash, like the bridge's serverURL: Capacitor only + // keeps a navigation in the app when it starts with that exact string, + // and hands anything else to Safari. + guard let raw = TrekServerStore.url, let url = URL(string: raw.hasSuffix("/") ? raw : raw + "/"), let html = try? Data(contentsOf: TrekServerStore.offlineCopyURL) else { call.resolve(["opened": false]) return diff --git a/client/src/api/nativeAuth.test.ts b/client/src/api/nativeAuth.test.ts new file mode 100644 index 000000000..70c00c19c --- /dev/null +++ b/client/src/api/nativeAuth.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect, vi } from 'vitest' +import { nativeAuthApi } from './nativeAuth' +import { apiClient } from './client' + +vi.mock('./client', () => ({ apiClient: { post: vi.fn() } })) + +describe('nativeAuthApi', () => { + it('mints a hand-off code for a challenge', async () => { + vi.mocked(apiClient.post).mockResolvedValue({ data: { code: 'c1' } }) + await expect(nativeAuthApi.handoff('ab'.repeat(32))).resolves.toEqual({ code: 'c1' }) + expect(apiClient.post).toHaveBeenCalledWith('/auth/native/handoff', { challenge: 'ab'.repeat(32) }) + }) + + it('redeems the code with its verifier', async () => { + vi.mocked(apiClient.post).mockResolvedValue({ data: { ok: true } }) + await expect(nativeAuthApi.exchange({ code: 'c1', verifier: 'v'.repeat(64) })).resolves.toBeUndefined() + expect(apiClient.post).toHaveBeenCalledWith('/auth/native/exchange', { code: 'c1', verifier: 'v'.repeat(64) }) + }) +}) diff --git a/client/src/native/platform.test.ts b/client/src/native/platform.test.ts new file mode 100644 index 000000000..1647f13f0 --- /dev/null +++ b/client/src/native/platform.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Capacitor } from '@capacitor/core' +import { isNativeApp, isNativeIos, nativeServerHost } from './platform' + +vi.mock('@capacitor/core', () => ({ Capacitor: { isNativePlatform: vi.fn(), getPlatform: vi.fn() } })) + +beforeEach(() => { + vi.mocked(Capacitor.isNativePlatform).mockReturnValue(true) + vi.mocked(Capacitor.getPlatform).mockReturnValue('ios') +}) + +describe('platform', () => { + it('tells the app from a browser', () => { + expect(isNativeApp()).toBe(true) + vi.mocked(Capacitor.isNativePlatform).mockReturnValue(false) + expect(isNativeApp()).toBe(false) + }) + + it('narrows to iOS', () => { + expect(isNativeIos()).toBe(true) + vi.mocked(Capacitor.getPlatform).mockReturnValue('android') + expect(isNativeIos()).toBe(false) + vi.mocked(Capacitor.getPlatform).mockReturnValue('ios') + vi.mocked(Capacitor.isNativePlatform).mockReturnValue(false) + expect(isNativeIos()).toBe(false) + }) + + it('names the server the WebView sits on, only inside the app', () => { + expect(nativeServerHost()).toBe(window.location.host) + vi.mocked(Capacitor.isNativePlatform).mockReturnValue(false) + expect(nativeServerHost()).toBeNull() + }) +})