← back to Homesonspec
apps/mobile/lib/notifications.ts
115 lines
/**
* Homes on Spec — push notifications scaffolding
*
* Registers for push permissions via expo-notifications.
* Does NOT send any notifications — this is opt-in scaffolding only.
*
* The push TOKEN is deliberately not minted while PUSH_BACKEND_READY is false:
* getExpoPushTokenAsync() transmits a device identifier to Expo, and until the
* backend registration exists that is a third-party data flow bought for a
* feature that cannot deliver anything. See PUSH_BACKEND_READY below.
*/
import * as Notifications from 'expo-notifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
const EAS_PROJECT_ID = Constants.expoConfig?.extra?.eas?.projectId ?? '';
/**
* Is there a server that can actually SEND a push yet?
*
* No. The backend registration call below is still commented out, so no token
* ever reaches homesonspec.com and no alert can ever be delivered. Minting a
* token anyway is not harmless: `getExpoPushTokenAsync()` round-trips to Expo's
* servers, so a THIRD PARTY receives a device push token — a device identifier —
* for a feature that cannot function. That is both a privacy-label problem
* ("Data Not Collected" would be false) and a Guideline 2.1 completeness problem
* (the Alerts tab promises alerts a reviewer will never receive).
*
* So: do not mint the token until the backend exists. Flip this to true in the
* SAME change that uncomments the POST below — never before. (TK-10387)
*/
const PUSH_BACKEND_READY = false;
const PUSH_TOKEN_KEY = '@homesonspec/push_token_v1';
const ALERTS_ENABLED_KEY = '@homesonspec/alerts_enabled_v1';
// ---------------------------------------------------------------------------
// Permission + registration
// ---------------------------------------------------------------------------
export type PermissionStatus = 'granted' | 'denied' | 'undetermined';
export async function requestPushPermission(): Promise<PermissionStatus> {
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'New Listings',
importance: Notifications.AndroidImportance.DEFAULT,
});
}
const { status: existing } = await Notifications.getPermissionsAsync();
if (existing === 'granted') return 'granted';
const { status } = await Notifications.requestPermissionsAsync();
return status as PermissionStatus;
}
/**
* Registers for push and stores the Expo push token locally.
* Returns the token string, or null if permission denied / simulator.
*
* NOTE: Token is stored locally only — no server endpoint is called.
* Wire to a backend endpoint when ready.
*/
export async function registerForPushNotifications(): Promise<string | null> {
const status = await requestPushPermission();
if (status !== 'granted') return null;
if (!EAS_PROJECT_ID) return null;
// Nothing can send a push yet, so do not hand a device identifier to Expo for
// a feature that cannot work. Permission is still requested above, which is
// what the Alerts toggle reflects; only the token round-trip is withheld.
if (!PUSH_BACKEND_READY) return null;
try {
const tokenData = await Notifications.getExpoPushTokenAsync({
projectId: EAS_PROJECT_ID,
});
const token = tokenData.data;
await AsyncStorage.setItem(PUSH_TOKEN_KEY, token);
// TODO: POST token to your backend:
// await fetch('https://homesonspec.com/api/push/register', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ token, platform: Platform.OS }),
// });
return token;
} catch {
// Silently fail in Simulator / dev build without push entitlements
return null;
}
}
// ---------------------------------------------------------------------------
// Alert preferences (local toggle — persisted)
// ---------------------------------------------------------------------------
export async function getAlertsEnabled(): Promise<boolean> {
const val = await AsyncStorage.getItem(ALERTS_ENABLED_KEY);
return val === 'true';
}
export async function setAlertsEnabled(enabled: boolean): Promise<void> {
await AsyncStorage.setItem(ALERTS_ENABLED_KEY, enabled ? 'true' : 'false');
}
export async function getStoredPushToken(): Promise<string | null> {
return AsyncStorage.getItem(PUSH_TOKEN_KEY);
}