← 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);
}