feat: add presence-gated Expo push notifications

This commit is contained in:
Mohamed Boudra
2026-02-03 12:59:00 +07:00
parent ad632cb3bd
commit af89de1237
7 changed files with 332 additions and 122 deletions
+20 -85
View File
@@ -1,5 +1,4 @@
import { Platform } from "react-native";
import * as Notifications from "expo-notifications";
type OsNotificationPayload = {
title: string;
@@ -7,8 +6,6 @@ type OsNotificationPayload = {
data?: Record<string, unknown>;
};
let isNativeConfigured = false;
let permissionState: "unknown" | "granted" | "denied" = "unknown";
let permissionRequest: Promise<boolean> | null = null;
function getWebNotificationConstructor(): {
@@ -20,73 +17,25 @@ function getWebNotificationConstructor(): {
return NotificationConstructor ?? null;
}
async function configureNativeNotifications(): Promise<void> {
if (isNativeConfigured || Platform.OS === "web") {
return;
}
isNativeConfigured = true;
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
}
async function ensureNotificationPermission(): Promise<boolean> {
if (Platform.OS === "web") {
const NotificationConstructor = getWebNotificationConstructor();
if (!NotificationConstructor) {
return false;
}
if (NotificationConstructor.permission === "granted") {
return true;
}
if (NotificationConstructor.permission === "denied") {
return false;
}
if (permissionRequest) {
return permissionRequest;
}
permissionRequest = Promise.resolve(
NotificationConstructor.requestPermission
? NotificationConstructor.requestPermission()
: "denied"
).then((permission) => permission === "granted");
const result = await permissionRequest;
permissionRequest = null;
return result;
const NotificationConstructor = getWebNotificationConstructor();
if (!NotificationConstructor) {
return false;
}
if (permissionState === "granted") {
if (NotificationConstructor.permission === "granted") {
return true;
}
if (permissionState === "denied") {
if (NotificationConstructor.permission === "denied") {
return false;
}
if (permissionRequest) {
return permissionRequest;
}
permissionRequest = (async () => {
const existing = await Notifications.getPermissionsAsync();
if (existing.status === "granted") {
permissionState = "granted";
return true;
}
if (!existing.canAskAgain) {
permissionState = "denied";
return false;
}
const requested = await Notifications.requestPermissionsAsync();
permissionState = requested.status === "granted" ? "granted" : "denied";
return permissionState === "granted";
})();
permissionRequest = Promise.resolve(
NotificationConstructor.requestPermission
? NotificationConstructor.requestPermission()
: "denied"
).then((permission) => permission === "granted");
const result = await permissionRequest;
permissionRequest = null;
return result;
@@ -95,36 +44,22 @@ async function ensureNotificationPermission(): Promise<boolean> {
export async function sendOsNotification(
payload: OsNotificationPayload
): Promise<boolean> {
if (Platform.OS === "web") {
const NotificationConstructor = getWebNotificationConstructor();
if (!NotificationConstructor) {
return false;
}
const granted = await ensureNotificationPermission();
if (!granted) {
return false;
}
new NotificationConstructor(payload.title, {
body: payload.body,
data: payload.data,
});
return true;
// Mobile/native notifications should be remote push only.
if (Platform.OS !== "web") {
return false;
}
await configureNativeNotifications();
const NotificationConstructor = getWebNotificationConstructor();
if (!NotificationConstructor) {
return false;
}
const granted = await ensureNotificationPermission();
if (!granted) {
return false;
}
await Notifications.scheduleNotificationAsync({
content: {
title: payload.title,
body: payload.body,
data: payload.data,
},
trigger: null,
new NotificationConstructor(payload.title, {
body: payload.body,
data: payload.data,
});
return true;
}