feat(native): the app needs TREK 5.1.3 on the server and checks a remembered one once after the update

The fixes that keep the app's sign-in in the app live in the server's UI,
so an updated app on a 5.1.2 server still ended in the browser. The server now
reports nativeApi 2 and the app refuses anything below it, naming 5.1.3.

A server the app remembers from before is not loaded unchecked: the first
launch after the update shows the address screen filled in with it, says why
and checks it right away. A server that is new enough loads again with its
session; an older one gets the update hint.
This commit is contained in:
Maurice
2026-11-10 09:17:26 +01:00
parent b124e1bc9c
commit 5cbe4e2fa3
39 changed files with 217 additions and 53 deletions
+14 -6
View File
@@ -3,7 +3,7 @@ import { candidateUrls, probeDemo, probeServer, type ProbeResponse } from './ser
const features = (
url: string,
data: unknown = { bookingImport: false, aiParsing: false, nativeApp: true }
data: unknown = { bookingImport: false, aiParsing: false, nativeApp: true, nativeApi: 2 }
): ProbeResponse => ({ status: 200, data, url });
describe('candidateUrls', () => {
@@ -26,13 +26,21 @@ describe('candidateUrls', () => {
describe('probeServer', () => {
it('accepts a server at the minimum native API version or above', async () => {
const data = { bookingImport: false, nativeApp: true, nativeApi: 1 };
const get = vi.fn().mockResolvedValue(features('https://trek.example.com/api/health/features', data));
expect(await probeServer('https://trek.example.com', get)).toEqual({ ok: true, url: 'https://trek.example.com' });
for (const nativeApi of [2, 3]) {
const data = { bookingImport: false, nativeApp: true, nativeApi };
const get = vi.fn().mockResolvedValue(features('https://trek.example.com/api/health/features', data));
expect(await probeServer('https://trek.example.com', get)).toEqual({ ok: true, url: 'https://trek.example.com' });
}
});
it('calls a server below the minimum native API version too old', async () => {
const data = { bookingImport: false, nativeApp: true, nativeApi: 0 };
const data = { bookingImport: false, nativeApp: true, nativeApi: 1 };
const get = vi.fn().mockResolvedValue(features('https://trek.example.com/api/health/features', data));
expect(await probeServer('https://trek.example.com', get)).toEqual({ ok: false, reason: 'tooOld' });
});
it('calls a server that speaks to the app but reports no native API version too old', async () => {
const data = { bookingImport: false, nativeApp: true };
const get = vi.fn().mockResolvedValue(features('https://trek.example.com/api/health/features', data));
expect(await probeServer('https://trek.example.com', get)).toEqual({ ok: false, reason: 'tooOld' });
});
@@ -173,7 +181,7 @@ describe('probeServer behind mutual TLS', () => {
});
it('connects where the proxy asked and TREK answered, with or without a certificate', async () => {
const trek = { bookingImport: false, nativeApp: true, nativeApi: 1 };
const trek = { bookingImport: false, nativeApp: true, nativeApi: 2 };
for (const sent of [true, false]) {
const get = vi.fn().mockResolvedValue(asked(200, sent, trek));
expect(await probeServer('https://trek.example.com', get)).toEqual({ ok: true, url: 'https://trek.example.com' });
+6 -4
View File
@@ -43,10 +43,12 @@ const FEATURES_PATH = '/api/health/features';
/**
* The oldest server API this app can work with (`nativeApi` in the features
* response). A server from before the field existed answers without it and is
* accepted while the minimum is 1.
* response). 2 is TREK 5.1.3, the first server whose sign-in from the app does
* not end in the browser. A server without the field is older still. Raising it
* goes with SERVER_CHECK in TrekServerStore (iOS and Android), so a server the
* app remembers from before is checked against the new minimum once.
*/
const MIN_NATIVE_API = 1;
export const MIN_NATIVE_API = 2;
function normalize(raw: string): string | null {
let parsed: URL;
@@ -80,7 +82,7 @@ function judge(response: ProbeResponse): ProbeResult {
if (response.status !== 200 || !isRecord(response.data)) return { ok: false, reason: 'notTrek' };
if (response.data.nativeApp === true) {
const api = response.data.nativeApi;
if (typeof api === 'number' && api < MIN_NATIVE_API) return { ok: false, reason: 'tooOld' };
if (typeof api !== 'number' || api < MIN_NATIVE_API) return { ok: false, reason: 'tooOld' };
// Follow a redirect (http to https, a canonical host) so the app loads the
// address the server actually answers on.
const base = response.url.endsWith(FEATURES_PATH) ? response.url.slice(0, -FEATURES_PATH.length) : null;
+6 -1
View File
@@ -13,7 +13,12 @@ import type { NativeCapabilities } from './capabilities';
* worker.
*/
export interface TrekShellPlugin {
getServer(): Promise<{ url: string | null }>;
/**
* `previousUrl`: the server this app remembered before an update asked for a
* newer one (SERVER_CHECK in TrekServerStore). Set only while no server is,
* so the address screen can check it again, pre-filled.
*/
getServer(): Promise<{ url: string | null; previousUrl?: string | null }>;
/**
* Remembers the server and loads it. The current page is gone afterwards.
* `signInHosts` are the hosts of a proxy's sign-in page in front of it