All files / lib notifications.js

81.66% Statements 49/60
75% Branches 27/36
85.71% Functions 6/7
82.75% Lines 48/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163                      11x 11x           4x 2x 2x 1x 1x     1x     2x     2x 1x 1x     1x 1x 1x                                       5x 2x 2x 1x 1x 1x     1x 1x   1x       1x 1x     1x                 1x     1x               1x 1x               1x                 1x 1x       2x                           1x 1x   2x     3x 1x 1x     2x   2x 1x             6x 2x     4x 4x    
import { Capacitor } from '@capacitor/core';
import { LocalNotifications } from '@capacitor/local-notifications';
import { millisecondsToSeconds } from 'date-fns';
 
import { getSettings } from './settings.svelte';
import { toasts } from './toasts.svelte';
 
/**
 * Maps actionTypeId to action id to action details (title and callback)
 * @type {Record<string, Record<string, {title: string, callback: () => Promise<void>}>>}
 */
let _notificationActions = {};
let _notificationActionsListenerStarted = false;
 
/**
 * Ask the user for permission to send system notifications
 */
export async function askForNotificationPermission() {
	if (Capacitor.isNativePlatform()) {
		try {
			const status = await LocalNotifications.requestPermissions();
			Eif (status.display !== 'granted') {
				toasts.error('Vous avez refusé les notifications.');
			}
		} catch (e) {
			console.error('Local notification permission request failed', e);
			// Ignore - user probably dismissed the permission prompt
		}
		return;
	}
 
	if (!('Notification' in window)) {
		toasts.error('Votre navigateur ne supporte pas les notifications système.');
		return;
	}
 
	if (Notification.permission === 'default') {
		try {
			await Notification.requestPermission();
		} catch (e) {
			console.error('Notification permission request failed', e);
			// Ignore - user probably dismissed the permission prompt
		}
	} else Eif (Notification.permission === 'denied') {
		toasts.error(
			'Vous avez refusé les notifications système. Veuillez changer cela dans les paramètres de votre navigateur.'
		);
	}
}
 
/**
 * Send a system notification if permission was granted
 * `actionTypeId` defaults to the title
 * @param {string} title
 * @param {NotificationOptions & { actionsTypeId?: string,  actions?: Array<{ id: string, callback: () => Promise<void>, title: string }> }} options actions are only supported on native
 * @returns
 */
export async function sendNotification(title, { actionsTypeId = title, actions = [], ...options }) {
	if (Capacitor.isNativePlatform()) {
		try {
			if (actions.length > 0) {
				for (const { id, ...action } of actions) {
					_notificationActions[actionsTypeId] ??= {};
					_notificationActions[actionsTypeId][id] = action;
				}
 
				await LocalNotifications.registerActionTypes({
					types: Object.entries(_notificationActions).map(([id, actions]) => ({
						id,
						actions: Object.entries(actions).map(([id, { title }]) => ({ id, title })),
					})),
				});
 
				Eif (!_notificationActionsListenerStarted) {
					await LocalNotifications.addListener(
						'localNotificationActionPerformed',
						async (event) => {
							Iif (!event.notification?.actionTypeId) {
								console.error(
									'Received notification action without actionTypeId, doing nothing',
									event
								);
								return;
							}
 
							const action =
								_notificationActions[event.notification.actionTypeId]?.[
									event.actionId
								];
							Iif (!action) {
								console.error(
									`Received notification action with unregistered id ${event.actionId}, doing nothing`,
									{ event, _notificationActions }
								);
								return;
							}
 
							try {
								await action.callback();
							} catch (e) {
								console.error(
									`Error while executing callback for notification action ${event.actionId}`,
									e
								);
							}
 
							return {
								async remove() {
									_notificationActionsListenerStarted = false;
									console.info('Local notification actions listener was removed');
								},
							};
						}
					);
 
					_notificationActionsListenerStarted = true;
					console.info('Started local notification actions listener');
				}
			}
 
			await LocalNotifications.schedule({
				notifications: [
					{
						title,
						body: options.body ?? '',
						// Needs to be a Java (signed, 32-bit) Int, so max. 2^(32-1)
						id: millisecondsToSeconds(Date.now()),
						// TODO: put an actual icon here
						smallIcon: 'ic_notification',
						actionTypeId: actionsTypeId,
					},
				],
			});
		} catch (e) {
			console.error('Failed to send local notification', e);
			toasts.error("Impossible d'envoyer la notification système.");
		}
		return;
	}
 
	if (!('Notification' in window)) {
		toasts.error('Votre navigateur ne supporte pas les notifications système.');
		return;
	}
 
	const enabled = await hasNotificationsEnabled(getSettings().notifications);
 
	if (!enabled) return;
	new Notification(title, options);
}
 
/**
 * @param {boolean|null} isSettingOn
 */
export async function hasNotificationsEnabled(isSettingOn) {
	if (Capacitor.isNativePlatform()) {
		return isSettingOn && (await LocalNotifications.checkPermissions()).display === 'granted';
	}
 
	Iif (!('Notification' in window)) return false;
	return isSettingOn && Notification.permission === 'granted';
}