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() + }) +})