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.
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { hasNativeFeature } from './capabilities';
|
|
import { isNativeApp } from './platform';
|
|
import {
|
|
NativePushDenied,
|
|
forgetNativePush,
|
|
nativePushActive,
|
|
nativePushEndpoint,
|
|
openPushTarget,
|
|
registerNativePush,
|
|
startNativePush,
|
|
} from './push';
|
|
import { TrekShell } from './trekShell';
|
|
|
|
vi.mock('./platform', () => ({ isNativeApp: vi.fn() }));
|
|
vi.mock('./capabilities', () => ({ hasNativeFeature: vi.fn() }));
|
|
vi.mock('./trekShell', () => ({
|
|
TrekShell: {
|
|
registerPush: vi.fn(),
|
|
takeOpenedPush: vi.fn(),
|
|
addListener: vi.fn(async () => ({ remove: async () => {} })),
|
|
},
|
|
}));
|
|
|
|
const SUB = {
|
|
endpoint: 'https://push.liketrek.com/v1/apns/production/abcd',
|
|
keys: { p256dh: 'BPk', auth: 'aa' },
|
|
};
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
vi.mocked(isNativeApp).mockReturnValue(true);
|
|
vi.mocked(hasNativeFeature).mockReturnValue(true);
|
|
vi.mocked(TrekShell.registerPush).mockReset().mockResolvedValue(SUB);
|
|
vi.mocked(TrekShell.takeOpenedPush).mockReset().mockResolvedValue({ url: null });
|
|
window.history.replaceState({}, '', '/dashboard');
|
|
});
|
|
|
|
describe('native push', () => {
|
|
it('is on only in an iOS binary that has it', () => {
|
|
expect(nativePushActive()).toBe(true);
|
|
vi.mocked(hasNativeFeature).mockReturnValue(false);
|
|
expect(nativePushActive()).toBe(false);
|
|
});
|
|
|
|
it('remembers the relay endpoint it subscribed with, and forgets it again', async () => {
|
|
expect(await registerNativePush()).toEqual(SUB);
|
|
expect(nativePushEndpoint()).toBe(SUB.endpoint);
|
|
forgetNativePush();
|
|
expect(nativePushEndpoint()).toBeNull();
|
|
});
|
|
|
|
it('tells a refused permission apart from a failure', async () => {
|
|
vi.mocked(TrekShell.registerPush).mockRejectedValue({ code: 'DENIED' });
|
|
await expect(registerNativePush()).rejects.toBeInstanceOf(NativePushDenied);
|
|
vi.mocked(TrekShell.registerPush).mockRejectedValue(new Error('apns down'));
|
|
await expect(registerNativePush()).rejects.toThrow('apns down');
|
|
expect(nativePushEndpoint()).toBeNull();
|
|
});
|
|
|
|
it('opens a tapped notification inside the app, and never another origin', () => {
|
|
const pops = vi.fn();
|
|
window.addEventListener('popstate', pops);
|
|
openPushTarget('/trips/12?tab=chat');
|
|
expect(window.location.pathname).toBe('/trips/12');
|
|
expect(window.location.search).toBe('?tab=chat');
|
|
openPushTarget('https://evil.example/phish');
|
|
expect(window.location.pathname).toBe('/trips/12');
|
|
expect(pops).toHaveBeenCalledTimes(1);
|
|
window.removeEventListener('popstate', pops);
|
|
});
|
|
|
|
it('follows the notification the app was opened from', async () => {
|
|
vi.mocked(TrekShell.takeOpenedPush).mockResolvedValue({ url: '/trips/3' });
|
|
await startNativePush();
|
|
expect(window.location.pathname).toBe('/trips/3');
|
|
expect(TrekShell.addListener).toHaveBeenCalledWith('pushOpened', expect.any(Function));
|
|
});
|
|
});
|