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 ( + + + + {t('native.update.title')} + {t('native.update.body')} + + + {t('native.update.open')} + + setDismissed(true)} + aria-label={t('common.close')} + className="shrink-0 rounded-full p-1 text-content-muted" + > + + + + ); +} 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;