mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
3beb5c4223
The UI now turns these on by feature on Android as well as iOS: - widgets, widgetTrips and the travel day (liveActivity) write the same snapshot to the Android app's Glance widgets; the travel day also sends the next stop's minutes and the stops' colours for the Live Update. - geolocation answers navigator.geolocation from the app's location stream. - tileCache: Android keeps Mapbox's resources in its WebView client, so the map is offline-capable there too, without the iOS trek-tile:// rewrite. - playBilling: the purchase client buys through Google Play on Android, with the purchase token as the receipt the control plane verifies. - googleWallet: a booking's button adds it to Google Wallet on Android, through the save link the server asks the TREK services for.
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
import { hasNativeFeature } from './capabilities';
|
|
import { isNativeApp } from './platform';
|
|
import { reportNativeFailure } from './reportFailure';
|
|
import { TrekShell, type NativeLocationPayload } from './trekShell';
|
|
|
|
/**
|
|
* navigator.geolocation, answered by the app's own location stream where the
|
|
* binary has the geolocation feature: CoreLocation on iOS, the system's fused
|
|
* provider on Android.
|
|
*
|
|
* WKWebView asks for the location per server origin, under the server's name,
|
|
* and asks again across launches. The app asks once, as the app. Installing the
|
|
* shim over navigator.geolocation keeps every caller as it is (the locate
|
|
* button, the tour tracking, the nearby search): one code path, the browser's
|
|
* API, with the app underneath.
|
|
*/
|
|
type Success = PositionCallback;
|
|
type Failure = PositionErrorCallback | null | undefined;
|
|
|
|
interface Watcher {
|
|
success: Success;
|
|
failure: Failure;
|
|
once: boolean;
|
|
}
|
|
|
|
const watchers = new Map<number, Watcher>();
|
|
let nextId = 1;
|
|
let running = false;
|
|
let highAccuracy = false;
|
|
let subscribed: Promise<unknown> | null = null;
|
|
|
|
function toPosition(p: Extract<NativeLocationPayload, { latitude: number }>): GeolocationPosition {
|
|
const coords = {
|
|
latitude: p.latitude,
|
|
longitude: p.longitude,
|
|
accuracy: p.accuracy,
|
|
altitude: p.altitude,
|
|
altitudeAccuracy: null,
|
|
heading: p.heading,
|
|
speed: p.speed,
|
|
toJSON() {
|
|
return { ...this };
|
|
},
|
|
};
|
|
return { coords, timestamp: p.timestamp, toJSON: () => ({ coords, timestamp: p.timestamp }) } as GeolocationPosition;
|
|
}
|
|
|
|
function toError(e: { code: number; message: string }): GeolocationPositionError {
|
|
return {
|
|
code: e.code,
|
|
message: e.message,
|
|
PERMISSION_DENIED: 1,
|
|
POSITION_UNAVAILABLE: 2,
|
|
TIMEOUT: 3,
|
|
} as GeolocationPositionError;
|
|
}
|
|
|
|
function dispatch(payload: NativeLocationPayload): void {
|
|
for (const [id, w] of [...watchers]) {
|
|
if (payload.error) w.failure?.(toError(payload.error));
|
|
else w.success(toPosition(payload as Extract<NativeLocationPayload, { latitude: number }>));
|
|
if (w.once) watchers.delete(id);
|
|
}
|
|
syncStream();
|
|
}
|
|
|
|
/** Runs the native stream while anyone listens, at the accuracy the most demanding asked for. */
|
|
function syncStream(): void {
|
|
if (watchers.size > 0 && !running) {
|
|
running = true;
|
|
subscribed ??= TrekShell.addListener('location', dispatch);
|
|
TrekShell.startLocation({ highAccuracy }).catch(reportNativeFailure);
|
|
} else if (watchers.size === 0 && running) {
|
|
running = false;
|
|
TrekShell.stopLocation().catch(reportNativeFailure);
|
|
}
|
|
}
|
|
|
|
function add(success: Success, failure: Failure, options: PositionOptions | undefined, once: boolean): number {
|
|
if (options?.enableHighAccuracy) highAccuracy = true;
|
|
const id = nextId++;
|
|
watchers.set(id, { success, failure, once });
|
|
syncStream();
|
|
return id;
|
|
}
|
|
|
|
/** Puts the app's location under navigator.geolocation. Once, inside a binary that has it. */
|
|
export function installNativeGeolocation(): boolean {
|
|
if (!isNativeApp() || !hasNativeFeature('geolocation') || typeof navigator === 'undefined') return false;
|
|
const shim: Geolocation = {
|
|
getCurrentPosition: (success, failure, options) => {
|
|
add(success, failure, options, true);
|
|
},
|
|
watchPosition: (success, failure, options) => add(success, failure, options, false),
|
|
clearWatch: (id) => {
|
|
watchers.delete(id);
|
|
syncStream();
|
|
},
|
|
};
|
|
Object.defineProperty(navigator, 'geolocation', { configurable: true, value: shim });
|
|
return true;
|
|
}
|
|
|
|
/** Test helper. */
|
|
export function _resetNativeGeolocation(): void {
|
|
watchers.clear();
|
|
running = false;
|
|
highAccuracy = false;
|
|
subscribed = null;
|
|
nextId = 1;
|
|
}
|