← back to Homesonspec

apps/mobile/app/(tabs)/alerts.tsx

300 lines

/**
 * Tab 4 — Alerts (NATIVE — App Store 4.2 differentiator)
 *
 * Push notification opt-in screen. Users set preferences for new-listing
 * alerts. Registers for push permissions via expo-notifications.
 *
 * IMPORTANT: This tab ONLY registers and persists preferences locally.
 * It does NOT send any notifications. Connecting to a backend push
 * endpoint is a future wiring step (see the TODO in lib/notifications.ts).
 *
 * This is native-only value: the website has no equivalent notification
 * capability for anonymous users.
 */

import { useCallback, useState } from 'react';
import {
  View,
  Text,
  Switch,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  Alert,
  SafeAreaView,
  Linking,
  Platform,
  ActivityIndicator,
} from 'react-native';
import { useFocusEffect } from 'expo-router';
import {
  requestPushPermission,
  registerForPushNotifications,
  getAlertsEnabled,
  setAlertsEnabled,
  getStoredPushToken,
  type PermissionStatus,
} from '../../lib/notifications';

const BRAND = '#1a3a6e';
const ACCENT = '#e07b39';

type SetupState = 'idle' | 'requesting' | 'granted' | 'denied';

export default function AlertsTab() {
  const [alertsEnabled, setAlertsEnabledState] = useState(false);
  const [permStatus, setPermStatus] = useState<PermissionStatus>('undetermined');
  const [setupState, setSetupState] = useState<SetupState>('idle');
  const [pushToken, setPushToken] = useState<string | null>(null);

  const loadState = useCallback(async () => {
    const [enabled, token] = await Promise.all([
      getAlertsEnabled(),
      getStoredPushToken(),
    ]);
    setAlertsEnabledState(enabled);
    setPushToken(token);
    if (token) setPermStatus('granted');
  }, []);

  useFocusEffect(
    useCallback(() => {
      loadState();
    }, [loadState]),
  );

  async function handleToggle(value: boolean) {
    if (value && permStatus !== 'granted') {
      // First-time enable — request permission
      setSetupState('requesting');
      const token = await registerForPushNotifications();
      if (token) {
        setPushToken(token);
        setPermStatus('granted');
        setSetupState('granted');
        await setAlertsEnabled(true);
        setAlertsEnabledState(true);
      } else {
        setSetupState('denied');
        // Check whether truly denied or just simulator
        const status = await requestPushPermission();
        setPermStatus(status);
        if (status === 'denied') {
          Alert.alert(
            'Notifications Blocked',
            'To receive alerts, allow notifications for Homes on Spec in your iPhone Settings.',
            [
              { text: 'Open Settings', onPress: () => Linking.openSettings() },
              { text: 'Cancel', style: 'cancel' },
            ],
          );
        }
      }
    } else {
      await setAlertsEnabled(value);
      setAlertsEnabledState(value);
    }
  }

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>

        {/* Hero */}
        <View style={styles.hero}>
          <Text style={styles.heroIcon}>🔔</Text>
          <Text style={styles.heroTitle}>New Listing Alerts</Text>
          <Text style={styles.heroSubtitle}>
            New-listing alerts are coming soon — we are rolling delivery out
            builder by builder.
          </Text>
        </View>

        {/* Main toggle card */}
        <View style={styles.card}>
          <View style={styles.toggleRow}>
            <View style={styles.toggleLabel}>
              <Text style={styles.toggleTitle}>Enable Alerts</Text>
              <Text style={styles.toggleDesc}>
                Coming soon — new-listing alerts are not available yet
              </Text>
            </View>
            {setupState === 'requesting' ? (
              <ActivityIndicator color={BRAND} />
            ) : (
              <Switch
                value={false}
                disabled={true}
                onValueChange={handleToggle}
                trackColor={{ false: '#d1d5db', true: BRAND }}
                thumbColor={'#f3f4f6'}
              />
            )}
          </View>
        </View>

        {/* Status banner */}
        {permStatus === 'granted' && alertsEnabled && (
          <View style={[styles.statusBanner, styles.statusGranted]}>
            <Text style={styles.statusText}>
              Notifications are enabled on this device. You are set up to receive
              new-listing alerts as we roll delivery out.
            </Text>
          </View>
        )}

        {permStatus === 'denied' && (
          <View style={[styles.statusBanner, styles.statusDenied]}>
            <Text style={styles.statusText}>
              Notifications are blocked. Open Settings to allow them.
            </Text>
            <TouchableOpacity
              style={styles.settingsBtn}
              onPress={() => Linking.openSettings()}
            >
              <Text style={styles.settingsBtnText}>Open Settings</Text>
            </TouchableOpacity>
          </View>
        )}

        {/* What you will get */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Coming soon</Text>
          {[
            ['New listings', 'Alert when a builder publishes a matching home'],
            ['Price drops', 'When a saved home drops in price'],
            ['Move-in ready', 'When a home you are watching becomes move-in ready'],
          ].map(([title, desc]) => (
            <View key={title} style={styles.featureRow}>
              <Text style={styles.featureDot}>•</Text>
              <View style={{ flex: 1 }}>
                <Text style={styles.featureTitle}>{title}</Text>
                <Text style={styles.featureDesc}>{desc}</Text>
              </View>
            </View>
          ))}
        </View>

        {/* Privacy note */}
        <View style={styles.privacyNote}>
          <Text style={styles.privacyText}>
            Your push token is stored only on this device. No account required.
            We never sell your data. Unsubscribe any time by disabling the toggle above.
          </Text>
        </View>

        {/* Dev info — visible only in __DEV__ */}
        {__DEV__ && pushToken && (
          <View style={styles.devCard}>
            <Text style={styles.devTitle}>Dev: Push Token</Text>
            <Text style={styles.devToken} selectable>
              {pushToken.slice(0, 40)}…
            </Text>
            <Text style={styles.devNote}>
              Wire this token to homesonspec.com/api/push/register when ready.
            </Text>
          </View>
        )}

        {/* Future: granular filters — market, price range, beds */}
        <View style={styles.comingSoon}>
          <Text style={styles.comingSoonTitle}>Coming soon</Text>
          <Text style={styles.comingSoonBody}>
            Filter alerts by market (state / city), price range, and bed count.
            Set up your ideal search once and we will alert you automatically.
          </Text>
        </View>

      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#f9fafb' },
  scroll: { padding: 20, gap: 16 },
  hero: {
    alignItems: 'center',
    paddingVertical: 24,
    paddingHorizontal: 16,
  },
  heroIcon: { fontSize: 52, marginBottom: 12 },
  heroTitle: { fontSize: 24, fontWeight: '800', color: BRAND, marginBottom: 8, textAlign: 'center' },
  heroSubtitle: {
    fontSize: 15,
    color: '#6b7280',
    textAlign: 'center',
    lineHeight: 22,
  },
  card: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.07,
    shadowRadius: 3,
    elevation: 2,
  },
  toggleRow: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  toggleLabel: { flex: 1, marginRight: 12 },
  toggleTitle: { fontSize: 16, fontWeight: '700', color: '#111827' },
  toggleDesc: { fontSize: 13, color: '#6b7280', marginTop: 2 },
  statusBanner: {
    borderRadius: 10,
    padding: 14,
  },
  statusGranted: { backgroundColor: '#d1fae5' },
  statusDenied: { backgroundColor: '#fee2e2' },
  statusText: { fontSize: 13, color: '#374151', lineHeight: 18 },
  settingsBtn: {
    marginTop: 8,
    backgroundColor: BRAND,
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderRadius: 6,
    alignSelf: 'flex-start',
  },
  settingsBtnText: { color: '#fff', fontWeight: '600', fontSize: 13 },
  section: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    gap: 12,
  },
  sectionTitle: { fontSize: 14, fontWeight: '700', color: BRAND, marginBottom: 4 },
  featureRow: { flexDirection: 'row', gap: 10, alignItems: 'flex-start' },
  featureDot: { color: ACCENT, fontSize: 18, lineHeight: 20, marginTop: -1 },
  featureTitle: { fontSize: 14, fontWeight: '600', color: '#111827' },
  featureDesc: { fontSize: 13, color: '#6b7280', marginTop: 1 },
  privacyNote: {
    padding: 14,
    backgroundColor: '#f3f4f6',
    borderRadius: 10,
  },
  privacyText: { fontSize: 12, color: '#9ca3af', lineHeight: 18 },
  devCard: {
    backgroundColor: '#fef9c3',
    borderRadius: 8,
    padding: 12,
    borderLeftWidth: 3,
    borderLeftColor: '#eab308',
  },
  devTitle: { fontSize: 11, fontWeight: '700', color: '#713f12', marginBottom: 4 },
  devToken: { fontSize: 10, color: '#451a03', fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace' }) },
  devNote: { fontSize: 10, color: '#92400e', marginTop: 4 },
  comingSoon: {
    backgroundColor: '#eff6ff',
    borderRadius: 10,
    padding: 16,
    borderWidth: 1,
    borderColor: '#bfdbfe',
  },
  comingSoonTitle: { fontSize: 13, fontWeight: '700', color: BRAND, marginBottom: 4 },
  comingSoonBody: { fontSize: 13, color: '#3b82f6', lineHeight: 18 },
});