From f92aea80a7fdb5b636a078fd30ca18dd8343a9f2 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 6 Nov 2026 13:08:39 +0100 Subject: [PATCH] feat(native): a capability handshake between the app binary and the server's UI The UI ships with the server and the binary through the stores, so the two drift. TrekShell.getCapabilities now reports the binary's shell API, version and native features by name (iOS and Android), and the UI turns each native feature on only where the binary has it; a binary from before the handshake counts as shell API 1 with none. The other way round, the server names the oldest binary its UI works with (minNativeShellApi), and an app below it is asked to update with a link to its store. --- .../com/liketrek/trek/TrekShellPlugin.java | 31 ++++++ client/ios/App/App/TrekShellPlugin.swift | 19 ++++ client/src/App.tsx | 2 + .../Layout/NativeUpdateNotice.test.tsx | 46 ++++++++ .../components/Layout/NativeUpdateNotice.tsx | 54 ++++++++++ client/src/native/capabilities.test.ts | 77 +++++++++++++ client/src/native/capabilities.ts | 102 ++++++++++++++++++ client/src/native/nativeApp.test.ts | 7 ++ client/src/native/nativeApp.ts | 11 ++ client/src/native/trekShell.ts | 3 + server/src/nest/health/features.controller.ts | 4 + .../unit/nest/features.controller.test.ts | 6 ++ shared/src/i18n/ar/native.ts | 3 + shared/src/i18n/az/native.ts | 3 + shared/src/i18n/br/native.ts | 3 + shared/src/i18n/ca/native.ts | 3 + shared/src/i18n/cs/native.ts | 3 + shared/src/i18n/de/native.ts | 3 + shared/src/i18n/en/native.ts | 3 + shared/src/i18n/es/native.ts | 3 + shared/src/i18n/et/native.ts | 3 + shared/src/i18n/fr/native.ts | 3 + shared/src/i18n/gr/native.ts | 3 + shared/src/i18n/he/native.ts | 3 + shared/src/i18n/hu/native.ts | 3 + shared/src/i18n/id/native.ts | 3 + shared/src/i18n/it/native.ts | 3 + shared/src/i18n/ja/native.ts | 3 + shared/src/i18n/ko/native.ts | 3 + shared/src/i18n/nl/native.ts | 3 + shared/src/i18n/pl/native.ts | 3 + shared/src/i18n/ru/native.ts | 3 + shared/src/i18n/sk/native.ts | 3 + shared/src/i18n/sv/native.ts | 3 + shared/src/i18n/th/native.ts | 3 + shared/src/i18n/tr/native.ts | 3 + shared/src/i18n/uk/native.ts | 3 + shared/src/i18n/vi/native.ts | 3 + shared/src/i18n/zh-TW/native.ts | 3 + shared/src/i18n/zh/native.ts | 3 + 40 files changed, 446 insertions(+) create mode 100644 client/src/components/Layout/NativeUpdateNotice.test.tsx create mode 100644 client/src/components/Layout/NativeUpdateNotice.tsx create mode 100644 client/src/native/capabilities.test.ts create mode 100644 client/src/native/capabilities.ts diff --git a/client/android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java b/client/android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java index d59271a03..a12975fcd 100644 --- a/client/android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java +++ b/client/android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java @@ -24,6 +24,37 @@ public class TrekShellPlugin extends Plugin { private boolean authBrowserOpen; private final Handler handler = new Handler(Looper.getMainLooper()); + /** + * What this binary offers the server's UI, raised with every native feature + * an older binary lacks. The UI asks before it calls (native/capabilities.ts), + * and a server can require a minimum (minNativeShellApi in /api/health/features). + */ + static final int SHELL_API = 2; + /** The native features by name, so the UI can turn each on by itself. */ + static final String[] FEATURES = { "capabilities" }; + + @PluginMethod + public void getCapabilities(PluginCall call) { + JSObject result = new JSObject(); + result.put("shellApi", SHELL_API); + result.put("platform", "android"); + String version = ""; + String build = ""; + try { + android.content.pm.PackageInfo info = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0); + version = info.versionName == null ? "" : info.versionName; + build = String.valueOf(androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(info)); + } catch (android.content.pm.PackageManager.NameNotFoundException ignored) { + // Our own package is always there; the fields simply stay empty. + } + result.put("appVersion", version); + result.put("build", build); + com.getcapacitor.JSArray features = new com.getcapacitor.JSArray(); + for (String feature : FEATURES) features.put(feature); + result.put("features", features); + call.resolve(result); + } + @PluginMethod public void getServer(PluginCall call) { String url = TrekServerStore.getUrl(getContext()); diff --git a/client/ios/App/App/TrekShellPlugin.swift b/client/ios/App/App/TrekShellPlugin.swift index 0414b3598..b5d944939 100644 --- a/client/ios/App/App/TrekShellPlugin.swift +++ b/client/ios/App/App/TrekShellPlugin.swift @@ -16,8 +16,27 @@ public class TrekShellPlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPr CAPPluginMethod(name: "authenticate", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "refreshOfflineCopy", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "openOfflineCopy", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getCapabilities", returnType: CAPPluginReturnPromise), ] + /// What this binary offers the server's UI, raised with every native feature + /// an older binary lacks. The UI asks before it calls (native/capabilities.ts), + /// and a server can require a minimum (minNativeShellApi in /api/health/features). + static let shellApi = 2 + /// The native features by name, so the UI can turn each on by itself. + static let features: [String] = ["capabilities"] + + @objc func getCapabilities(_ call: CAPPluginCall) { + let info = Bundle.main.infoDictionary + call.resolve([ + "shellApi": TrekShellPlugin.shellApi, + "platform": "ios", + "appVersion": info?["CFBundleShortVersionString"] as? String ?? "", + "build": info?["CFBundleVersion"] as? String ?? "", + "features": TrekShellPlugin.features, + ]) + } + private var authSession: ASWebAuthenticationSession? @objc func getServer(_ call: CAPPluginCall) { diff --git a/client/src/App.tsx b/client/src/App.tsx index 39500bbd7..7699d0c4c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -32,6 +32,7 @@ import { useRoadtripPreferencesSync } from './hooks/useRoadtripPreferencesSync' import { registerSyncTriggers, unregisterSyncTriggers } from './sync/syncTriggers' import OfflineBanner from './components/Layout/OfflineBanner' import NewVersionNotice from './components/Layout/NewVersionNotice' +import NativeUpdateNotice from './components/Layout/NativeUpdateNotice' import { SystemNoticeHost } from './components/SystemNotices/SystemNoticeHost.js' import HelpPanel from './components/Help/HelpPanel' // Notice action registrations (side-effect imports): @@ -442,6 +443,7 @@ export default function App() { )} + {/* One boundary for all route chunks, above so it stays mounted across navigations. react-router runs location updates inside a transition, so a mounted boundary keeps the current page on screen instead of flashing diff --git a/client/src/components/Layout/NativeUpdateNotice.test.tsx b/client/src/components/Layout/NativeUpdateNotice.test.tsx new file mode 100644 index 000000000..0aca7480a --- /dev/null +++ b/client/src/components/Layout/NativeUpdateNotice.test.tsx @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { act, fireEvent, render, screen } from '../../../tests/helpers/render'; +import { _resetNativeCapabilities, checkAppUpdate, loadNativeCapabilities } from '../../native/capabilities'; +import { isNativeApp } from '../../native/platform'; +import { TrekShell } from '../../native/trekShell'; +import NativeUpdateNotice from './NativeUpdateNotice'; + +vi.mock('../../native/platform', () => ({ isNativeApp: vi.fn(), isNativeIos: vi.fn(() => false) })); +vi.mock('../../native/trekShell', () => ({ + TrekShell: { getCapabilities: vi.fn() }, + isUnimplemented: () => false, +})); + +beforeEach(() => { + _resetNativeCapabilities(); + vi.mocked(isNativeApp).mockReturnValue(true); + vi.mocked(TrekShell.getCapabilities).mockResolvedValue({ + shellApi: 2, + platform: 'android', + appVersion: '5.2.0', + build: '3', + features: [], + } as never); +}); + +describe('NativeUpdateNotice', () => { + it('stays away while the binary is new enough', async () => { + render(); + await act(async () => { + await checkAppUpdate(async () => ({ minNativeShellApi: 2 })); + }); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + + it('leads to the store when the server needs a newer binary, and can be dismissed', async () => { + render(); + await act(async () => { + await loadNativeCapabilities(); + await checkAppUpdate(async () => ({ minNativeShellApi: 3 })); + }); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', 'https://play.google.com/store/apps/details?id=com.liketrek.trek'); + fireEvent.click(screen.getByRole('button')); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); +}); diff --git a/client/src/components/Layout/NativeUpdateNotice.tsx b/client/src/components/Layout/NativeUpdateNotice.tsx new file mode 100644 index 000000000..d23a8129e --- /dev/null +++ b/client/src/components/Layout/NativeUpdateNotice.tsx @@ -0,0 +1,54 @@ +/** + * NativeUpdateNotice: this server's UI needs a newer app binary than the one + * installed (native/capabilities.ts, minNativeShellApi). The UI updates with + * the server, the binary only through the store, so the two can drift; the + * notice says so and leads to the store instead of leaving native calls to + * fail one by one. Rendered once in App, inside the app only. + */ +import { Download, X } from 'lucide-react'; +import React, { useState } from 'react'; +import { useTranslation } from '../../i18n'; +import { useNativeCapabilities } from '../../native/capabilities'; + +const STORE_URL = { + ios: 'https://apps.apple.com/app/id6831237775', + android: 'https://play.google.com/store/apps/details?id=com.liketrek.trek', +} as const; + +export default function NativeUpdateNotice(): React.ReactElement | null { + const { t } = useTranslation(); + const { caps, updateRequired } = useNativeCapabilities(); + const [dismissed, setDismissed] = useState(false); + if (!updateRequired || dismissed || caps.platform === 'web') return null; + const store = caps.platform === 'android' ? STORE_URL.android : STORE_URL.ios; + + return ( +
+
+ ); +} diff --git a/client/src/native/capabilities.test.ts b/client/src/native/capabilities.test.ts new file mode 100644 index 000000000..510673e08 --- /dev/null +++ b/client/src/native/capabilities.test.ts @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + _resetNativeCapabilities, + appUpdateRequired, + checkAppUpdate, + hasNativeFeature, + loadNativeCapabilities, + nativeCapabilities, +} from './capabilities'; +import { isNativeApp } from './platform'; +import { TrekShell } from './trekShell'; + +vi.mock('./platform', () => ({ isNativeApp: vi.fn() })); +vi.mock('./trekShell', () => ({ + TrekShell: { getCapabilities: vi.fn() }, + isUnimplemented: (err: unknown) => (err as { code?: string })?.code === 'UNIMPLEMENTED', +})); + +const CAPS = { shellApi: 2, platform: 'ios', appVersion: '5.2.0', build: '12', features: ['capabilities', 'edgeBack'] }; + +beforeEach(() => { + _resetNativeCapabilities(); + vi.mocked(isNativeApp).mockReturnValue(true); + vi.mocked(TrekShell.getCapabilities) + .mockReset() + .mockResolvedValue(CAPS as never); +}); + +afterEach(() => vi.unstubAllGlobals()); + +describe('native capabilities', () => { + it('asks the binary once and answers features by name', async () => { + await loadNativeCapabilities(); + await loadNativeCapabilities(); + expect(TrekShell.getCapabilities).toHaveBeenCalledTimes(1); + expect(hasNativeFeature('edgeBack')).toBe(true); + expect(hasNativeFeature('tileCache')).toBe(false); + expect(nativeCapabilities().appVersion).toBe('5.2.0'); + }); + + it('counts a binary from before the handshake as shell API 1 with no features', async () => { + vi.mocked(TrekShell.getCapabilities).mockRejectedValue({ code: 'UNIMPLEMENTED' }); + const caps = await loadNativeCapabilities(); + expect(caps.shellApi).toBe(1); + expect(hasNativeFeature('capabilities')).toBe(false); + }); + + it('reports a real failure but still settles on the legacy answer', async () => { + const report = vi.fn(); + vi.stubGlobal('reportError', report); + vi.mocked(TrekShell.getCapabilities).mockRejectedValue(new Error('bridge down')); + expect((await loadNativeCapabilities()).shellApi).toBe(1); + expect(report).toHaveBeenCalled(); + }); + + it('asks nothing in a browser', async () => { + vi.mocked(isNativeApp).mockReturnValue(false); + expect((await loadNativeCapabilities()).platform).toBe('web'); + expect(TrekShell.getCapabilities).not.toHaveBeenCalled(); + expect(await checkAppUpdate(async () => ({ minNativeShellApi: 9 }))).toBe(false); + }); + + it('asks for an update only when the server needs a newer binary', async () => { + expect(await checkAppUpdate(async () => ({ minNativeShellApi: 2 }))).toBe(false); + expect(await checkAppUpdate(async () => ({ minNativeShellApi: 3 }))).toBe(true); + expect(appUpdateRequired()).toBe(true); + }); + + it('requires nothing from a server that names no minimum, or cannot be asked', async () => { + expect(await checkAppUpdate(async () => ({}))).toBe(false); + expect( + await checkAppUpdate(async () => { + throw new Error('offline'); + }) + ).toBe(false); + }); +}); diff --git a/client/src/native/capabilities.ts b/client/src/native/capabilities.ts new file mode 100644 index 000000000..ec7b01085 --- /dev/null +++ b/client/src/native/capabilities.ts @@ -0,0 +1,102 @@ +import { useSyncExternalStore } from 'react'; +import { isNativeApp } from './platform'; +import { reportNativeFailure } from './reportFailure'; +import { TrekShell, isUnimplemented } from './trekShell'; + +/** + * What the installed app binary offers the UI this server delivers. + * + * The two update on their own clocks: the UI ships with the server, the binary + * through the stores. So the UI asks before it calls into anything native the + * binary may predate, and turns each feature on by name (hasNativeFeature). A + * binary from before this handshake answers UNIMPLEMENTED and counts as shell + * API 1 with no named features, which is exactly what it can do. + * + * The other direction runs through the server's features: minNativeShellApi + * names the oldest binary its UI works with, and an app below it is asked to + * update (appUpdateRequired) rather than left calling methods it lacks. + */ +export interface NativeCapabilities { + shellApi: number; + platform: 'ios' | 'android' | 'web'; + appVersion: string; + build: string; + features: string[]; +} + +const WEB: NativeCapabilities = { shellApi: 0, platform: 'web', appVersion: '', build: '', features: [] }; +const LEGACY_APP: NativeCapabilities = { shellApi: 1, platform: 'web', appVersion: '', build: '', features: [] }; + +let current: NativeCapabilities = WEB; +let updateRequired = false; +let loading: Promise | null = null; +const listeners = new Set<() => void>(); + +function emit(): void { + for (const listener of listeners) listener(); +} + +/** Asks the binary once and keeps the answer for the session. */ +export function loadNativeCapabilities(): Promise { + if (!isNativeApp()) return Promise.resolve(WEB); + loading ??= TrekShell.getCapabilities() + .then( + (caps) => caps, + (err: unknown) => { + if (!isUnimplemented(err)) reportNativeFailure(err); + return LEGACY_APP; + } + ) + .then((caps) => { + current = caps; + emit(); + return caps; + }); + return loading; +} + +/** Whether the installed binary has a native feature, as far as it is known yet. */ +export function hasNativeFeature(feature: string): boolean { + return current.features.includes(feature); +} + +export function nativeCapabilities(): NativeCapabilities { + return current; +} + +/** + * Compares the binary with the oldest one this server's UI works with. A + * server that says nothing (older than the handshake) requires nothing. + */ +export async function checkAppUpdate(fetchFeatures: () => Promise<{ minNativeShellApi?: unknown }>): Promise { + if (!isNativeApp()) return false; + const [caps, features] = await Promise.all([loadNativeCapabilities(), fetchFeatures().catch(() => ({}))]); + const min = typeof features.minNativeShellApi === 'number' ? features.minNativeShellApi : 0; + updateRequired = caps.shellApi < min; + emit(); + return updateRequired; +} + +export function appUpdateRequired(): boolean { + return updateRequired; +} + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +/** Re-renders when the capabilities arrive or the update check answers. */ +export function useNativeCapabilities(): { caps: NativeCapabilities; updateRequired: boolean } { + const caps = useSyncExternalStore(subscribe, nativeCapabilities, nativeCapabilities); + const required = useSyncExternalStore(subscribe, appUpdateRequired, appUpdateRequired); + return { caps, updateRequired: required }; +} + +/** Test helper: forget what was asked. */ +export function _resetNativeCapabilities(): void { + current = WEB; + updateRequired = false; + loading = null; + listeners.clear(); +} diff --git a/client/src/native/nativeApp.test.ts b/client/src/native/nativeApp.test.ts index 7710c502e..1dbb85a58 100644 --- a/client/src/native/nativeApp.test.ts +++ b/client/src/native/nativeApp.test.ts @@ -4,6 +4,7 @@ import { SplashScreen } from '@capacitor/splash-screen'; import { StatusBar, Style } from '@capacitor/status-bar'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { isEffectivelyOffline } from '../sync/networkMode'; +import { checkAppUpdate } from './capabilities'; import { startNativeApp } from './nativeApp'; import { keepOfflineShellFresh } from './offlineShell'; import { isNativeApp, isNativeIos } from './platform'; @@ -15,6 +16,7 @@ vi.mock('@capacitor/status-bar', () => ({ StatusBar: { setStyle: vi.fn() }, Styl vi.mock('./platform', () => ({ isNativeApp: vi.fn(), isNativeIos: vi.fn() })); vi.mock('./offlineShell', () => ({ keepOfflineShellFresh: vi.fn() })); vi.mock('../sync/networkMode', () => ({ isEffectivelyOffline: vi.fn() })); +vi.mock('./capabilities', () => ({ checkAppUpdate: vi.fn(async () => false) })); function backButtonHandler(): (event: { canGoBack: boolean }) => void { const call = vi.mocked(App.addListener).mock.calls.find(([name]) => name === 'backButton'); @@ -44,6 +46,11 @@ describe('startNativeApp', () => { expect(StatusBar.setStyle).not.toHaveBeenCalled(); }); + it('asks the binary what it can do and the server which binary it needs', () => { + startNativeApp(); + expect(checkAppUpdate).toHaveBeenCalledWith(expect.any(Function)); + }); + it('hides the splash after the first frame', () => { startNativeApp(); vi.runOnlyPendingTimers(); diff --git a/client/src/native/nativeApp.ts b/client/src/native/nativeApp.ts index ba57310d6..c4ead14a4 100644 --- a/client/src/native/nativeApp.ts +++ b/client/src/native/nativeApp.ts @@ -3,6 +3,7 @@ import { Keyboard } from '@capacitor/keyboard'; import { SplashScreen } from '@capacitor/splash-screen'; import { StatusBar, Style } from '@capacitor/status-bar'; import { isEffectivelyOffline } from '../sync/networkMode'; +import { checkAppUpdate } from './capabilities'; import { keepOfflineShellFresh } from './offlineShell'; import { isNativeApp, isNativeIos } from './platform'; import { reportNativeFailure } from './reportFailure'; @@ -20,6 +21,9 @@ export function startNativeApp(): void { followThemeInStatusBar(); + // What this binary can do, and whether this server's UI needs a newer one. + void checkAppUpdate(fetchServerFeatures); + // Android's back gesture: through the app's own history first, and only // then out of the app, the way a browser tab would behave. void App.addListener('backButton', ({ canGoBack }) => { @@ -58,3 +62,10 @@ function followThemeInStatusBar(): void { apply(); new MutationObserver(apply).observe(root, { attributes: true, attributeFilter: ['class'] }); } + +/** The server's feature flags, bounded so a slow server cannot hold anything up. */ +async function fetchServerFeatures(): Promise<{ minNativeShellApi?: unknown }> { + const res = await fetch('/api/health/features', { credentials: 'same-origin', signal: AbortSignal.timeout(8000) }); + if (!res.ok) return {}; + return (await res.json()) as { minNativeShellApi?: unknown }; +} diff --git a/client/src/native/trekShell.ts b/client/src/native/trekShell.ts index 548f12183..90d645f0e 100644 --- a/client/src/native/trekShell.ts +++ b/client/src/native/trekShell.ts @@ -1,4 +1,5 @@ import { registerPlugin } from '@capacitor/core'; +import type { NativeCapabilities } from './capabilities'; /** * The app's own native plugin, implemented in @@ -29,6 +30,8 @@ export interface TrekShellPlugin { refreshOfflineCopy(): Promise<{ saved: boolean }>; /** iOS: loads that copy under the server's origin. False when there is none. */ openOfflineCopy(): Promise<{ opened: boolean }>; + /** What this binary offers the UI (native/capabilities.ts). Missing before shell API 2. */ + getCapabilities(): Promise; } export const TrekShell = registerPlugin('TrekShell'); diff --git a/server/src/nest/health/features.controller.ts b/server/src/nest/health/features.controller.ts index 93dfb022e..a1d67d934 100644 --- a/server/src/nest/health/features.controller.ts +++ b/server/src/nest/health/features.controller.ts @@ -70,6 +70,10 @@ export class FeaturesController { // routes, the shell bridge in the served UI). Raised only for a change an // older app cannot follow; the app refuses a server below its minimum. nativeApi: 1, + // The other way round: the oldest app binary this server's UI works with, + // as the shell API the app reports (TrekShell.getCapabilities). An app + // below it is asked to update instead of running native calls it lacks. + minNativeShellApi: 1, }; } } diff --git a/server/tests/unit/nest/features.controller.test.ts b/server/tests/unit/nest/features.controller.test.ts index 26038ea8f..0babcd507 100644 --- a/server/tests/unit/nest/features.controller.test.ts +++ b/server/tests/unit/nest/features.controller.test.ts @@ -38,6 +38,7 @@ describe('FeaturesController (GET /api/health/features)', () => { aiAssistant: true, nativeApp: true, nativeApi: 1, + minNativeShellApi: 1, }); }); @@ -49,6 +50,7 @@ describe('FeaturesController (GET /api/health/features)', () => { aiAssistant: false, nativeApp: true, nativeApi: 1, + minNativeShellApi: 1, }); }); @@ -59,6 +61,7 @@ describe('FeaturesController (GET /api/health/features)', () => { aiAssistant: false, nativeApp: true, nativeApi: 1, + minNativeShellApi: 1, }); expect(await make(false, true).controller.features()).toEqual({ bookingImport: false, @@ -66,6 +69,7 @@ describe('FeaturesController (GET /api/health/features)', () => { aiAssistant: true, nativeApp: true, nativeApi: 1, + minNativeShellApi: 1, }); }); @@ -91,6 +95,8 @@ describe('FeaturesController (GET /api/health/features)', () => { const features = await make(false, false).controller.features(); expect(features.nativeApp).toBe(true); expect(features.nativeApi).toBe(1); + // And which app binaries its UI still works with. + expect(features.minNativeShellApi).toBe(1); }); it('FEAT-005: bookingImport is read live, not cached at construction', async () => { diff --git a/shared/src/i18n/ar/native.ts b/shared/src/i18n/ar/native.ts index 8ab139c79..b0acf4595 100644 --- a/shared/src/i18n/ar/native.ts +++ b/shared/src/i18n/ar/native.ts @@ -71,6 +71,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'شارف على الجهوزية', 'native.provisioning.failedSub': 'TREK الخاص بك في الطريق. حاول مرة أخرى بعد قليل، أو افتح التطبيق لاحقًا.', 'native.provisioning.retry': 'حاول الآن', + 'native.update.title': 'حدّث تطبيق TREK', + 'native.update.body': 'يحتاج هذا الخادم إلى إصدار أحدث من التطبيق ليعمل كل شيء.', + 'native.update.open': 'تحديث', }; export default native; diff --git a/shared/src/i18n/az/native.ts b/shared/src/i18n/az/native.ts index 4740b6759..3e4c663e5 100644 --- a/shared/src/i18n/az/native.ts +++ b/shared/src/i18n/az/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Demək olar ki hazırdır', 'native.provisioning.failedSub': 'TREK-iniz yoldadır. Bir azdan yenidən cəhd edin və ya tətbiqi sonra açın.', 'native.provisioning.retry': 'İndi cəhd edin', + 'native.update.title': 'TREK tətbiqini yeniləyin', + 'native.update.body': 'Hər şeyin işləməsi üçün bu server tətbiqin daha yeni versiyasını tələb edir.', + 'native.update.open': 'Yenilə', }; export default native; diff --git a/shared/src/i18n/br/native.ts b/shared/src/i18n/br/native.ts index c429a9ae9..8b266344d 100644 --- a/shared/src/i18n/br/native.ts +++ b/shared/src/i18n/br/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Quase pronto', 'native.provisioning.failedSub': 'Seu TREK está a caminho. Tente novamente em instantes ou abra o app mais tarde.', 'native.provisioning.retry': 'Tentar agora', + 'native.update.title': 'Atualize o app TREK', + 'native.update.body': 'Este servidor precisa de uma versão mais nova do app para tudo funcionar.', + 'native.update.open': 'Atualizar', }; export default native; diff --git a/shared/src/i18n/ca/native.ts b/shared/src/i18n/ca/native.ts index c44e5885e..5a3d79f8d 100644 --- a/shared/src/i18n/ca/native.ts +++ b/shared/src/i18n/ca/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Gairebé a punt', 'native.provisioning.failedSub': 'El teu TREK està en camí. Torna-ho a provar en un moment o obre l’app més tard.', 'native.provisioning.retry': 'Prova-ho ara', + 'native.update.title': 'Actualitza l’app TREK', + 'native.update.body': 'Aquest servidor necessita una versió més nova de l’app perquè tot funcioni.', + 'native.update.open': 'Actualitza', }; export default native; diff --git a/shared/src/i18n/cs/native.ts b/shared/src/i18n/cs/native.ts index 029fec1d8..4a5df0cc8 100644 --- a/shared/src/i18n/cs/native.ts +++ b/shared/src/i18n/cs/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Téměř hotovo', 'native.provisioning.failedSub': 'Váš TREK je na cestě. Zkuste to za chvíli znovu nebo otevřete aplikaci později.', 'native.provisioning.retry': 'Zkusit teď', + 'native.update.title': 'Aktualizujte aplikaci TREK', + 'native.update.body': 'Tento server potřebuje novější verzi aplikace, aby vše fungovalo.', + 'native.update.open': 'Aktualizovat', }; export default native; diff --git a/shared/src/i18n/de/native.ts b/shared/src/i18n/de/native.ts index 812db3277..f4f3c38d0 100644 --- a/shared/src/i18n/de/native.ts +++ b/shared/src/i18n/de/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Gleich fertig', 'native.provisioning.failedSub': 'Deine TREK ist unterwegs. Versuch es gleich noch einmal oder öffne die App später.', 'native.provisioning.retry': 'Jetzt versuchen', + 'native.update.title': 'TREK-App aktualisieren', + 'native.update.body': 'Dieser Server braucht eine neuere App-Version, damit alles funktioniert.', + 'native.update.open': 'Aktualisieren', }; export default native; diff --git a/shared/src/i18n/en/native.ts b/shared/src/i18n/en/native.ts index eaa9a517f..944bf5367 100644 --- a/shared/src/i18n/en/native.ts +++ b/shared/src/i18n/en/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Almost ready', 'native.provisioning.failedSub': 'Your TREK is on its way. Try again in a moment, or open the app later.', 'native.provisioning.retry': 'Try now', + 'native.update.title': 'Update the TREK app', + 'native.update.body': 'This server needs a newer version of the app for everything to work.', + 'native.update.open': 'Update', }; export default native; diff --git a/shared/src/i18n/es/native.ts b/shared/src/i18n/es/native.ts index c01135a1b..acca41415 100644 --- a/shared/src/i18n/es/native.ts +++ b/shared/src/i18n/es/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Casi listo', 'native.provisioning.failedSub': 'Tu TREK está en camino. Inténtalo en un momento o abre la app más tarde.', 'native.provisioning.retry': 'Probar ahora', + 'native.update.title': 'Actualiza la app TREK', + 'native.update.body': 'Este servidor necesita una versión más reciente de la app para que todo funcione.', + 'native.update.open': 'Actualizar', }; export default native; diff --git a/shared/src/i18n/et/native.ts b/shared/src/i18n/et/native.ts index 032006784..caf028739 100644 --- a/shared/src/i18n/et/native.ts +++ b/shared/src/i18n/et/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Peaaegu valmis', 'native.provisioning.failedSub': 'Sinu TREK on teel. Proovi hetke pärast uuesti või ava rakendus hiljem.', 'native.provisioning.retry': 'Proovi nüüd', + 'native.update.title': 'Uuenda TREK-i rakendust', + 'native.update.body': 'See server vajab kõige toimimiseks rakenduse uuemat versiooni.', + 'native.update.open': 'Uuenda', }; export default native; diff --git a/shared/src/i18n/fr/native.ts b/shared/src/i18n/fr/native.ts index a7f705205..df103e932 100644 --- a/shared/src/i18n/fr/native.ts +++ b/shared/src/i18n/fr/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Presque prêt', 'native.provisioning.failedSub': 'Votre TREK arrive. Réessayez dans un instant ou rouvrez l’app plus tard.', 'native.provisioning.retry': 'Réessayer', + 'native.update.title': 'Mettez à jour l’app TREK', + 'native.update.body': 'Ce serveur a besoin d’une version plus récente de l’app pour que tout fonctionne.', + 'native.update.open': 'Mettre à jour', }; export default native; diff --git a/shared/src/i18n/gr/native.ts b/shared/src/i18n/gr/native.ts index ad49162a6..d284e878a 100644 --- a/shared/src/i18n/gr/native.ts +++ b/shared/src/i18n/gr/native.ts @@ -75,6 +75,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Σχεδόν έτοιμο', 'native.provisioning.failedSub': 'Το TREK σας έρχεται. Δοκιμάστε ξανά σε λίγο ή ανοίξτε την εφαρμογή αργότερα.', 'native.provisioning.retry': 'Δοκιμάστε τώρα', + 'native.update.title': 'Ενημερώστε την εφαρμογή TREK', + 'native.update.body': 'Αυτός ο διακομιστής χρειάζεται νεότερη έκδοση της εφαρμογής για να λειτουργούν όλα.', + 'native.update.open': 'Ενημέρωση', }; export default native; diff --git a/shared/src/i18n/he/native.ts b/shared/src/i18n/he/native.ts index bc376a049..c815a9a88 100644 --- a/shared/src/i18n/he/native.ts +++ b/shared/src/i18n/he/native.ts @@ -71,6 +71,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'כמעט מוכן', 'native.provisioning.failedSub': 'ה-TREK שלכם בדרך. נסו שוב עוד רגע, או פתחו את האפליקציה מאוחר יותר.', 'native.provisioning.retry': 'נסו עכשיו', + 'native.update.title': 'עדכנו את אפליקציית TREK', + 'native.update.body': 'השרת הזה צריך גרסה חדשה יותר של האפליקציה כדי שהכול יעבוד.', + 'native.update.open': 'עדכון', }; export default native; diff --git a/shared/src/i18n/hu/native.ts b/shared/src/i18n/hu/native.ts index ffa880b6c..b11ba037c 100644 --- a/shared/src/i18n/hu/native.ts +++ b/shared/src/i18n/hu/native.ts @@ -74,6 +74,9 @@ const native: TranslationStrings = { 'native.provisioning.failedSub': 'A TREK-ed úton van. Próbáld újra egy pillanat múlva, vagy nyisd meg az appot később.', 'native.provisioning.retry': 'Próbáld most', + 'native.update.title': 'Frissítsd a TREK alkalmazást', + 'native.update.body': 'Ehhez a szerverhez az alkalmazás újabb verziója kell, hogy minden működjön.', + 'native.update.open': 'Frissítés', }; export default native; diff --git a/shared/src/i18n/id/native.ts b/shared/src/i18n/id/native.ts index c97ccef07..0a6a48f81 100644 --- a/shared/src/i18n/id/native.ts +++ b/shared/src/i18n/id/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedSub': 'TREK Anda sedang dalam perjalanan. Coba lagi sebentar lagi, atau buka aplikasi nanti.', 'native.provisioning.retry': 'Coba sekarang', + 'native.update.title': 'Perbarui aplikasi TREK', + 'native.update.body': 'Server ini memerlukan versi aplikasi yang lebih baru agar semuanya berfungsi.', + 'native.update.open': 'Perbarui', }; export default native; diff --git a/shared/src/i18n/it/native.ts b/shared/src/i18n/it/native.ts index ac316f7eb..e84b4433f 100644 --- a/shared/src/i18n/it/native.ts +++ b/shared/src/i18n/it/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Quasi pronto', 'native.provisioning.failedSub': 'Il tuo TREK sta arrivando. Riprova tra poco o riapri l’app più tardi.', 'native.provisioning.retry': 'Riprova', + 'native.update.title': 'Aggiorna l’app TREK', + 'native.update.body': 'Questo server richiede una versione più recente dell’app perché tutto funzioni.', + 'native.update.open': 'Aggiorna', }; export default native; diff --git a/shared/src/i18n/ja/native.ts b/shared/src/i18n/ja/native.ts index 7ef40096b..98b465367 100644 --- a/shared/src/i18n/ja/native.ts +++ b/shared/src/i18n/ja/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedSub': 'TREKはまもなく準備できます。少ししてからもう一度試すか、後でアプリを開いてください。', 'native.provisioning.retry': '今すぐ試す', + 'native.update.title': 'TREKアプリを更新してください', + 'native.update.body': 'すべての機能を使うには、このサーバーに対応した新しいバージョンのアプリが必要です。', + 'native.update.open': '更新', }; export default native; diff --git a/shared/src/i18n/ko/native.ts b/shared/src/i18n/ko/native.ts index 76d5bedc0..7af517085 100644 --- a/shared/src/i18n/ko/native.ts +++ b/shared/src/i18n/ko/native.ts @@ -71,6 +71,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': '거의 준비됐어요', 'native.provisioning.failedSub': 'TREK이 곧 준비됩니다. 잠시 후 다시 시도하거나 나중에 앱을 열어 보세요.', 'native.provisioning.retry': '지금 시도', + 'native.update.title': 'TREK 앱을 업데이트하세요', + 'native.update.body': '모든 기능을 사용하려면 이 서버에 맞는 최신 앱 버전이 필요합니다.', + 'native.update.open': '업데이트', }; export default native; diff --git a/shared/src/i18n/nl/native.ts b/shared/src/i18n/nl/native.ts index 525bf24c8..567f58b32 100644 --- a/shared/src/i18n/nl/native.ts +++ b/shared/src/i18n/nl/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Bijna klaar', 'native.provisioning.failedSub': 'Je TREK is onderweg. Probeer het zo opnieuw of open de app later.', 'native.provisioning.retry': 'Nu proberen', + 'native.update.title': 'Werk de TREK-app bij', + 'native.update.body': 'Deze server heeft een nieuwere versie van de app nodig om alles te laten werken.', + 'native.update.open': 'Bijwerken', }; export default native; diff --git a/shared/src/i18n/pl/native.ts b/shared/src/i18n/pl/native.ts index 2556a5d13..de910bc79 100644 --- a/shared/src/i18n/pl/native.ts +++ b/shared/src/i18n/pl/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Już prawie', 'native.provisioning.failedSub': 'Twój TREK jest w drodze. Spróbuj za chwilę lub otwórz aplikację później.', 'native.provisioning.retry': 'Spróbuj teraz', + 'native.update.title': 'Zaktualizuj aplikację TREK', + 'native.update.body': 'Ten serwer wymaga nowszej wersji aplikacji, aby wszystko działało.', + 'native.update.open': 'Aktualizuj', }; export default native; diff --git a/shared/src/i18n/ru/native.ts b/shared/src/i18n/ru/native.ts index 34c723abd..acabe6cc2 100644 --- a/shared/src/i18n/ru/native.ts +++ b/shared/src/i18n/ru/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedSub': 'Ваш TREK уже в пути. Повторите попытку через мгновение или откройте приложение позже.', 'native.provisioning.retry': 'Повторить сейчас', + 'native.update.title': 'Обновите приложение TREK', + 'native.update.body': 'Для полной работы этому серверу нужна более новая версия приложения.', + 'native.update.open': 'Обновить', }; export default native; diff --git a/shared/src/i18n/sk/native.ts b/shared/src/i18n/sk/native.ts index 3df71215f..581df2052 100644 --- a/shared/src/i18n/sk/native.ts +++ b/shared/src/i18n/sk/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Takmer hotovo', 'native.provisioning.failedSub': 'Váš TREK je na ceste. Skúste to o chvíľu znova alebo otvorte aplikáciu neskôr.', 'native.provisioning.retry': 'Skúsiť teraz', + 'native.update.title': 'Aktualizujte aplikáciu TREK', + 'native.update.body': 'Tento server potrebuje novšiu verziu aplikácie, aby všetko fungovalo.', + 'native.update.open': 'Aktualizovať', }; export default native; diff --git a/shared/src/i18n/sv/native.ts b/shared/src/i18n/sv/native.ts index 2531be4aa..7e4858a53 100644 --- a/shared/src/i18n/sv/native.ts +++ b/shared/src/i18n/sv/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Snart klart', 'native.provisioning.failedSub': 'Din TREK är på väg. Försök igen om en stund eller öppna appen senare.', 'native.provisioning.retry': 'Försök nu', + 'native.update.title': 'Uppdatera TREK-appen', + 'native.update.body': 'Den här servern behöver en nyare version av appen för att allt ska fungera.', + 'native.update.open': 'Uppdatera', }; export default native; diff --git a/shared/src/i18n/th/native.ts b/shared/src/i18n/th/native.ts index f0a2222c3..ce9054a23 100644 --- a/shared/src/i18n/th/native.ts +++ b/shared/src/i18n/th/native.ts @@ -71,6 +71,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'เกือบเสร็จแล้ว', 'native.provisioning.failedSub': 'TREK ของคุณกำลังมา ลองอีกครั้งในอีกสักครู่ หรือเปิดแอปในภายหลัง', 'native.provisioning.retry': 'ลองตอนนี้', + 'native.update.title': 'อัปเดตแอป TREK', + 'native.update.body': 'เซิร์ฟเวอร์นี้ต้องใช้แอปเวอร์ชันใหม่กว่าเพื่อให้ทุกอย่างทำงานได้', + 'native.update.open': 'อัปเดต', }; export default native; diff --git a/shared/src/i18n/tr/native.ts b/shared/src/i18n/tr/native.ts index dd52b1ddc..4f4099805 100644 --- a/shared/src/i18n/tr/native.ts +++ b/shared/src/i18n/tr/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Neredeyse hazır', 'native.provisioning.failedSub': 'TREK’iniz yolda. Birazdan tekrar deneyin ya da uygulamayı sonra açın.', 'native.provisioning.retry': 'Şimdi dene', + 'native.update.title': 'TREK uygulamasını güncelleyin', + 'native.update.body': 'Her şeyin çalışması için bu sunucu uygulamanın daha yeni bir sürümüne ihtiyaç duyuyor.', + 'native.update.open': 'Güncelle', }; export default native; diff --git a/shared/src/i18n/uk/native.ts b/shared/src/i18n/uk/native.ts index 892e98e8f..c03987d00 100644 --- a/shared/src/i18n/uk/native.ts +++ b/shared/src/i18n/uk/native.ts @@ -72,6 +72,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Майже готово', 'native.provisioning.failedSub': 'Ваш TREK уже в дорозі. Повторіть спробу за мить або відкрийте застосунок пізніше.', 'native.provisioning.retry': 'Повторити зараз', + 'native.update.title': 'Оновіть застосунок TREK', + 'native.update.body': 'Для повної роботи цьому серверу потрібна новіша версія застосунку.', + 'native.update.open': 'Оновити', }; export default native; diff --git a/shared/src/i18n/vi/native.ts b/shared/src/i18n/vi/native.ts index 0e019bac6..612f88226 100644 --- a/shared/src/i18n/vi/native.ts +++ b/shared/src/i18n/vi/native.ts @@ -73,6 +73,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': 'Gần xong', 'native.provisioning.failedSub': 'TREK của bạn đang trên đường. Hãy thử lại sau giây lát, hoặc mở ứng dụng sau.', 'native.provisioning.retry': 'Thử ngay', + 'native.update.title': 'Cập nhật ứng dụng TREK', + 'native.update.body': 'Máy chủ này cần phiên bản ứng dụng mới hơn để mọi thứ hoạt động.', + 'native.update.open': 'Cập nhật', }; export default native; diff --git a/shared/src/i18n/zh-TW/native.ts b/shared/src/i18n/zh-TW/native.ts index d5e630fc6..1779c58d5 100644 --- a/shared/src/i18n/zh-TW/native.ts +++ b/shared/src/i18n/zh-TW/native.ts @@ -71,6 +71,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': '即將就緒', 'native.provisioning.failedSub': '你的 TREK 即將就緒。請稍後重試,或稍後開啟應用程式。', 'native.provisioning.retry': '立即重試', + 'native.update.title': '請更新 TREK 應用程式', + 'native.update.body': '此伺服器需要較新版本的應用程式才能正常使用所有功能。', + 'native.update.open': '更新', }; export default native; diff --git a/shared/src/i18n/zh/native.ts b/shared/src/i18n/zh/native.ts index 9d2d0c009..0bbce90b9 100644 --- a/shared/src/i18n/zh/native.ts +++ b/shared/src/i18n/zh/native.ts @@ -71,6 +71,9 @@ const native: TranslationStrings = { 'native.provisioning.failedTitle': '即将就绪', 'native.provisioning.failedSub': '你的 TREK 正在路上。请稍后重试,或稍后打开应用。', 'native.provisioning.retry': '立即重试', + 'native.update.title': '请更新 TREK 应用', + 'native.update.body': '此服务器需要更新版本的应用才能正常使用全部功能。', + 'native.update.open': '更新', }; export default native;