mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
5ca44453ae
- wallet.ts: the Google Wallet state is read through a function after it was set, so TypeScript no longer narrows it to its old value (TS2367) - iap.ts: a store listener that cannot be added is reported through reportNativeFailure instead of an empty catch - iap.test.ts: the restored purchase is checked instead of asserted non-null - trekShell.ts: the fold's rectangle is typed as a DOMRect pick, geometry the RTL lint does not read as text sides
166 lines
6.2 KiB
TypeScript
166 lines
6.2 KiB
TypeScript
import { useEffect, useSyncExternalStore } from 'react';
|
|
import { toBase64 } from './blobBase64';
|
|
import { hasNativeFeature } from './capabilities';
|
|
import { isNativeAndroid, isNativeIos } from './platform';
|
|
import { reportNativeFailure } from './reportFailure';
|
|
import { TrekShell } from './trekShell';
|
|
|
|
/** A file that is a single Apple Wallet pass, by its name (the MIME type is often missing). */
|
|
export function isWalletPassName(filename: string): boolean {
|
|
return /\.pkpass$/i.test(filename.trim());
|
|
}
|
|
|
|
/** Whether this app can hand a pass to Apple Wallet itself (iOS, feature wallet). */
|
|
export function canAddToWallet(): boolean {
|
|
return isNativeIos() && hasNativeFeature('wallet');
|
|
}
|
|
|
|
/**
|
|
* Hands a booking's Wallet pass to Apple Wallet: the system sheet with the pass
|
|
* and "Add", or the pass opened in Wallet when it is already there. True when
|
|
* Wallet took it over (including the user closing the sheet); false when the app
|
|
* cannot, so the caller falls back to the share sheet.
|
|
*/
|
|
export async function addToWallet(blob: Blob): Promise<boolean> {
|
|
if (!canAddToWallet()) return false;
|
|
try {
|
|
await TrekShell.addToWallet({ data: await toBase64(blob) });
|
|
return true;
|
|
} catch (err) {
|
|
// INVALID (not really a pass) and UNAVAILABLE fall back to the share sheet quietly.
|
|
const code = (err as { code?: unknown })?.code;
|
|
if (code !== 'INVALID' && code !== 'UNAVAILABLE') reportNativeFailure(err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** What a tap on "Add to Apple Wallet" came to. */
|
|
export type ReservationWalletOutcome = 'added' | 'undated' | 'failed';
|
|
|
|
/**
|
|
* A booking's pass, made by this server (GET .../reservations/:id/wallet-pass,
|
|
* signed by the TREK services) and handed to Apple Wallet's sheet. `undated`:
|
|
* the booking has no date yet, which Wallet needs to bring the pass up.
|
|
*/
|
|
export async function addReservationToWallet(
|
|
tripId: number,
|
|
reservationId: number,
|
|
language: string
|
|
): Promise<ReservationWalletOutcome> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(
|
|
`/api/trips/${tripId}/reservations/${reservationId}/wallet-pass?lang=${encodeURIComponent(language)}`,
|
|
{ credentials: 'include' }
|
|
);
|
|
} catch {
|
|
return 'failed';
|
|
}
|
|
if (response.status === 422) return 'undated';
|
|
if (!response.ok) return 'failed';
|
|
return (await addToWallet(await response.blob())) ? 'added' : 'failed';
|
|
}
|
|
|
|
/** Whether this app can hand a booking to Google Wallet (Android, feature googleWallet). */
|
|
export function canAddToGoogleWallet(): boolean {
|
|
return isNativeAndroid() && hasNativeFeature('googleWallet');
|
|
}
|
|
|
|
/**
|
|
* Whether this server can make Google Wallet passes, asked once per session
|
|
* (GET /api/trips/:tripId/reservations/google-wallet): the TREK services can
|
|
* only once liketrek has a Wallet issuer, and an older server has no such route.
|
|
* Until it says yes the Android app offers no button, so nobody taps one that
|
|
* can only fail.
|
|
*/
|
|
let googleState: 'unknown' | 'asking' | 'yes' | 'no' = 'unknown';
|
|
const googleListeners = new Set<() => void>();
|
|
|
|
function setGoogleState(next: typeof googleState): void {
|
|
googleState = next;
|
|
for (const listener of googleListeners) listener();
|
|
}
|
|
|
|
/** Read through a call, so a check after setGoogleState() sees the new state rather than a narrowed one. */
|
|
function googleSaidYes(): boolean {
|
|
return googleState === 'yes';
|
|
}
|
|
|
|
export async function loadGoogleWalletAvailability(tripId: number): Promise<boolean> {
|
|
if (!canAddToGoogleWallet()) return false;
|
|
if (googleState === 'yes' || googleState === 'no') return googleState === 'yes';
|
|
if (googleState === 'asking') return false;
|
|
setGoogleState('asking');
|
|
try {
|
|
const response = await fetch(`/api/trips/${tripId}/reservations/google-wallet`, { credentials: 'include' });
|
|
const body = response.ok ? ((await response.json()) as { available?: unknown }) : {};
|
|
setGoogleState(body.available === true ? 'yes' : 'no');
|
|
} catch {
|
|
// Offline: ask again next time.
|
|
setGoogleState('unknown');
|
|
}
|
|
return googleSaidYes();
|
|
}
|
|
|
|
/** True once this server has said Google Wallet works; asks it the first time a booking shows. */
|
|
export function useGoogleWalletAvailable(tripId: number): boolean {
|
|
const available = useSyncExternalStore((listener) => {
|
|
googleListeners.add(listener);
|
|
return () => googleListeners.delete(listener);
|
|
}, googleSaidYes);
|
|
useEffect(() => {
|
|
void loadGoogleWalletAvailability(tripId);
|
|
}, [tripId]);
|
|
return available;
|
|
}
|
|
|
|
/** The wallet a booking's button adds to here, if any: Apple Wallet on iOS, Google Wallet on Android where the server can. */
|
|
export function bookingWallet(): 'apple' | 'google' | null {
|
|
if (canAddToWallet()) return 'apple';
|
|
if (canAddToGoogleWallet() && googleState === 'yes') return 'google';
|
|
return null;
|
|
}
|
|
|
|
/** Test helper: forget what the server said. */
|
|
export function _resetGoogleWalletAvailability(): void {
|
|
googleState = 'unknown';
|
|
googleListeners.clear();
|
|
}
|
|
|
|
/**
|
|
* A booking as a Google Wallet pass: this server asks the TREK services for a
|
|
* save link (GET .../reservations/:id/wallet-pass/google), and the app opens it,
|
|
* where Google Wallet shows the pass with its own Save button. `undated`: the
|
|
* booking has no date yet.
|
|
*/
|
|
export async function addReservationToGoogleWallet(
|
|
tripId: number,
|
|
reservationId: number,
|
|
language: string
|
|
): Promise<ReservationWalletOutcome> {
|
|
if (!canAddToGoogleWallet()) return 'failed';
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(
|
|
`/api/trips/${tripId}/reservations/${reservationId}/wallet-pass/google?lang=${encodeURIComponent(language)}`,
|
|
{ credentials: 'include' }
|
|
);
|
|
} catch {
|
|
return 'failed';
|
|
}
|
|
if (response.status === 422) return 'undated';
|
|
// Google Wallet went away here (switched off, issuer gone): the button goes with it.
|
|
if (response.status === 404) setGoogleState('no');
|
|
if (!response.ok) return 'failed';
|
|
try {
|
|
const { saveUrl } = (await response.json()) as { saveUrl?: unknown };
|
|
if (typeof saveUrl !== 'string') return 'failed';
|
|
await TrekShell.addToGoogleWallet({ url: saveUrl });
|
|
return 'added';
|
|
} catch (err) {
|
|
const code = (err as { code?: unknown })?.code;
|
|
if (code !== 'INVALID' && code !== 'UNAVAILABLE') reportNativeFailure(err);
|
|
return 'failed';
|
|
}
|
|
}
|