mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
8b6a65023f
Brings the native apps branch up to dev (NestJS-only server, MikroORM
repositories, tracked sessions, per-domain locales, the lint ratchets) and
ports its new code onto that architecture:
- server: /api/auth/native moves onto injected services. The session is
minted by the auth domain's new LoginSessionService (login bookkeeping and
SessionsService.issue in one transaction), the one-time codes live behind a
NativeHandoffStore port with a capped in-memory implementation that drops
expired codes on write instead of running a timer, both routes declare
response contracts, and the exchange answers { success: true }. Unit and
e2e suites run on createTestModule and the ORM factories.
- shared: native strings for az, et, sk and th, native wired into every
locale, the address placeholder as a full URL.
- client: the account and export changes go into dev's shared hooks
(useAccountSettings, useTripExport), the native server card is a
desktop/phone pair on useNativeServer, the hand-off call moves out of the
page hook into native/, best-effort native failures go through
reportError instead of the console, and the address screen is known to
knip and lint:i18n-keys.
- package-lock.json regenerated on Linux.
126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
import { CapacitorHttp } from '@capacitor/core'
|
|
import { SplashScreen } from '@capacitor/splash-screen'
|
|
import { TrekShell } from '../src/native/trekShell'
|
|
import { probeServer, type ProbeFailure, type ProbeGet } from '../src/native/serverAddress'
|
|
import { pickLocale, translator } from './i18n'
|
|
import logoUrl from '../public/icons/icon-white.svg'
|
|
import './shell.css'
|
|
|
|
/**
|
|
* The only page the app ships itself. It asks for a server on first start and
|
|
* stands in when the remembered one cannot be reached; everything else is the
|
|
* UI that server delivers.
|
|
*/
|
|
|
|
const locale = pickLocale(navigator.languages?.length ? navigator.languages : [navigator.language])
|
|
const t = translator(locale)
|
|
document.documentElement.lang = locale
|
|
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr'
|
|
|
|
const root = document.getElementById('shell')!
|
|
|
|
const PROBE_TIMEOUT_MS = 8000
|
|
|
|
// Native HTTP rather than fetch: the probe goes to a server of the user's
|
|
// choosing, which has no reason to answer this page's origin with CORS.
|
|
const get: ProbeGet = async (url) => {
|
|
const response = await CapacitorHttp.get({ url, connectTimeout: PROBE_TIMEOUT_MS, readTimeout: PROBE_TIMEOUT_MS })
|
|
return { status: response.status, data: response.data, url: response.url }
|
|
}
|
|
|
|
function failureMessage(reason: ProbeFailure): string {
|
|
switch (reason) {
|
|
case 'invalid':
|
|
return t('native.connect.errorInvalid')
|
|
case 'unreachable':
|
|
return t('native.connect.errorUnreachable')
|
|
case 'notTrek':
|
|
return t('native.connect.errorNotTrek')
|
|
case 'tooOld':
|
|
return t('native.connect.errorTooOld')
|
|
}
|
|
}
|
|
|
|
function el<K extends keyof HTMLElementTagNameMap>(tag: K, props: Partial<HTMLElementTagNameMap[K]> = {}, ...children: (Node | string)[]) {
|
|
const node = Object.assign(document.createElement(tag), props)
|
|
node.append(...children)
|
|
return node
|
|
}
|
|
|
|
function header(title: string, hint: string) {
|
|
return [
|
|
el('img', { src: logoUrl, alt: '', className: 'shell-logo' }),
|
|
el('h1', { className: 'shell-title' }, title),
|
|
el('p', { className: 'shell-hint' }, hint),
|
|
]
|
|
}
|
|
|
|
function showConnect(prefill = '') {
|
|
const input = el('input', {
|
|
type: 'url',
|
|
className: 'shell-input',
|
|
placeholder: t('native.connect.placeholder'),
|
|
value: prefill,
|
|
autocapitalize: 'off',
|
|
spellcheck: false,
|
|
required: true,
|
|
})
|
|
input.setAttribute('inputmode', 'url')
|
|
input.setAttribute('autocomplete', 'url')
|
|
input.setAttribute('aria-label', t('native.connect.title'))
|
|
const error = el('p', { className: 'shell-error', role: 'alert' })
|
|
const submit = el('button', { type: 'submit', className: 'shell-button' }, t('native.connect.submit'))
|
|
const form = el('form', { className: 'shell-form', noValidate: true }, input, submit, error)
|
|
|
|
form.addEventListener('submit', async (event) => {
|
|
event.preventDefault()
|
|
error.textContent = ''
|
|
input.disabled = true
|
|
submit.disabled = true
|
|
submit.textContent = t('native.connect.checking')
|
|
const result = await probeServer(input.value, get)
|
|
if (result.ok) {
|
|
await TrekShell.setServer({ url: result.url })
|
|
return
|
|
}
|
|
error.textContent = failureMessage(result.reason)
|
|
input.disabled = false
|
|
submit.disabled = false
|
|
submit.textContent = t('native.connect.submit')
|
|
input.focus()
|
|
})
|
|
|
|
root.replaceChildren(...header(t('native.connect.title'), t('native.connect.hint')), form)
|
|
}
|
|
|
|
function showUnreachable(server: string) {
|
|
const retry = el('button', { type: 'button', className: 'shell-button' }, t('native.offline.retry'))
|
|
retry.addEventListener('click', () => {
|
|
retry.disabled = true
|
|
void TrekShell.reload()
|
|
})
|
|
const change = el('button', { type: 'button', className: 'shell-link' }, t('native.changeServer'))
|
|
change.addEventListener('click', () => { void TrekShell.resetServer() })
|
|
|
|
root.replaceChildren(
|
|
...header(t('native.offline.title'), t('native.offline.hint', { server: new URL(server).host })),
|
|
el('div', { className: 'shell-form' }, retry, change),
|
|
)
|
|
}
|
|
|
|
async function start() {
|
|
const { url } = await TrekShell.getServer()
|
|
if (!url) {
|
|
showConnect()
|
|
} else {
|
|
// Only reached when loading the server failed. On iOS the stored copy of
|
|
// its start page brings the offline-capable UI up anyway.
|
|
const { opened } = await TrekShell.openOfflineCopy()
|
|
if (opened) return
|
|
showUnreachable(url)
|
|
}
|
|
await SplashScreen.hide()
|
|
}
|
|
|
|
void start()
|