mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
3c4c880c31
The Android app now subscribes like the iOS one: TrekShell.registerPush asks for the notification permission (Android 13+), creates the device's P-256 Web Push keys and returns the relay endpoint for its FCM token (push.liketrek.com/v1/fcm/<token>). TrekPushService receives the relay's data message, decrypts the record (RFC 8291, TrekWebPush) and shows the notification. A tap opens the linked page, also when it starts the app. The keys are sealed with an AES key kept in the Android Keystore. Firebase is configured from res/values/firebase.xml, so no google-services plugin is needed. The decryption is tested against the RFC 8291 example, and the native workflow now runs the Android unit tests. On the web side push is no longer limited to iOS: it follows the nativePush capability on both platforms.
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import type { PushSubscriptionInput } from '@trek/shared';
|
|
import { hasNativeFeature } from './capabilities';
|
|
import { isNativeApp } from './platform';
|
|
import { reportNativeFailure } from './reportFailure';
|
|
import { TrekShell } from './trekShell';
|
|
|
|
/**
|
|
* Push inside the app: the same Web Push subscription a browser makes, only
|
|
* the app makes it (TrekShell.registerPush). Its endpoint is liketrek's relay
|
|
* for this device's APNs or FCM token and its keys live on the device, so the
|
|
* server encrypts exactly as for a browser and the relay hands on ciphertext
|
|
* that the app decrypts (iOS: the notification extension, Android:
|
|
* TrekPushService). push/webPush.ts takes this path
|
|
* wherever the binary reports the nativePush feature; the settings card, the
|
|
* test push and the logout teardown stay what they are.
|
|
*
|
|
* The endpoint is remembered on the device, the way a browser remembers its
|
|
* subscription, so the card can say on or off and the logout can name it.
|
|
*/
|
|
const KEY = 'trek.nativePushEndpoint';
|
|
|
|
export function nativePushActive(): boolean {
|
|
return isNativeApp() && hasNativeFeature('nativePush');
|
|
}
|
|
|
|
export function nativePushEndpoint(): string | null {
|
|
try {
|
|
return window.localStorage.getItem(KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function rememberEndpoint(endpoint: string | null): void {
|
|
try {
|
|
if (endpoint) window.localStorage.setItem(KEY, endpoint);
|
|
else window.localStorage.removeItem(KEY);
|
|
} catch {
|
|
// Storage refused: the card then reads off and offers to switch it on again.
|
|
}
|
|
}
|
|
|
|
export class NativePushDenied extends Error {}
|
|
|
|
/** Permission, APNs and the device keys, as a subscription the server can store. */
|
|
export async function registerNativePush(): Promise<PushSubscriptionInput> {
|
|
try {
|
|
const subscription = await TrekShell.registerPush();
|
|
rememberEndpoint(subscription.endpoint);
|
|
return subscription;
|
|
} catch (err) {
|
|
if ((err as { code?: string } | null)?.code === 'DENIED') throw new NativePushDenied();
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export function forgetNativePush(): void {
|
|
rememberEndpoint(null);
|
|
}
|
|
|
|
/** Opens the place a tapped notification points at, inside the app's own routing. */
|
|
export function openPushTarget(url: string): void {
|
|
let target: URL;
|
|
try {
|
|
target = new URL(url, window.location.origin);
|
|
} catch {
|
|
return;
|
|
}
|
|
// Only this server's pages: a notification never navigates the app elsewhere.
|
|
if (target.origin !== window.location.origin) return;
|
|
window.history.pushState({}, '', `${target.pathname}${target.search}${target.hash}`);
|
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
}
|
|
|
|
/** Follows taps on notifications, including the one the app was opened from. */
|
|
export async function startNativePush(): Promise<void> {
|
|
if (!nativePushActive()) return;
|
|
void TrekShell.addListener('pushOpened', ({ url }) => openPushTarget(url));
|
|
try {
|
|
const { url } = await TrekShell.takeOpenedPush();
|
|
if (url) openPushTarget(url);
|
|
} catch (err) {
|
|
reportNativeFailure(err);
|
|
}
|
|
}
|