BigBrainParking/app/src/screens/NotificationsScreen.tsx
Hank 02e2c750ea Notifications: local-only session reminders; drop the FCM-bridge framing
- Session-expiry reminders are purely on-device (AlarmManager); added an Android
  notification channel and a "Send a test reminder" button (Account > Notifications)
  to verify delivery without a real session
- Strip all FCM/bridge talk from unifiedPush.ts + README; UnifiedPush(ntfy) is now
  an optional no-op stub for hypothetical future server-initiated messages only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:14:34 -07:00

135 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useState } from 'react';
import { Alert, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import { useTheme } from '@/theme/ThemeContext';
import { sendTestReminder } from '@/notifications/localReminders';
import {
DEFAULT_LEAD_MINUTES,
LEAD_STEP,
MAX_LEAD_MINUTES,
MIN_LEAD_MINUTES,
getReminderLeadMinutes,
getRemindersEnabled,
setReminderLeadMinutes,
setRemindersEnabled,
} from '@/features/notifications/reminderPrefs';
export function NotificationsScreen() {
const { colors } = useTheme();
const [enabled, setEnabled] = useState(true);
const [lead, setLead] = useState(DEFAULT_LEAD_MINUTES);
useFocusEffect(
useCallback(() => {
void getRemindersEnabled().then(setEnabled);
void getReminderLeadMinutes().then(setLead);
}, []),
);
const toggle = (v: boolean) => {
setEnabled(v);
void setRemindersEnabled(v);
};
const bump = (delta: number) => {
const next = Math.min(MAX_LEAD_MINUTES, Math.max(MIN_LEAD_MINUTES, lead + delta));
setLead(next);
void setReminderLeadMinutes(next);
};
return (
<View style={[styles.container, { backgroundColor: colors.bg }]}>
<View style={[styles.card, { backgroundColor: colors.card }]}>
<View style={styles.row}>
<View style={{ flex: 1 }}>
<Text style={[styles.rowTitle, { color: colors.text }]}>
Session expiry reminders
</Text>
<Text style={[styles.rowSub, { color: colors.subtext }]}>
A local notification before your parking runs out.
</Text>
</View>
<Switch value={enabled} onValueChange={toggle} />
</View>
</View>
<Text style={[styles.section, { color: colors.subtext }]}>Remind me</Text>
<View style={[styles.card, { backgroundColor: colors.card, opacity: enabled ? 1 : 0.4 }]}>
<View style={styles.stepper}>
<TouchableOpacity
style={[styles.stepBtn, { borderColor: colors.border }]}
disabled={!enabled || lead <= MIN_LEAD_MINUTES}
onPress={() => bump(-LEAD_STEP)}
>
<Text style={[styles.stepText, { color: colors.text }]}></Text>
</TouchableOpacity>
<View style={styles.leadDisplay}>
<Text style={[styles.leadValue, { color: colors.text }]}>{lead}</Text>
<Text style={[styles.leadUnit, { color: colors.subtext }]}>
minutes before expiry
</Text>
</View>
<TouchableOpacity
style={[styles.stepBtn, { borderColor: colors.border }]}
disabled={!enabled || lead >= MAX_LEAD_MINUTES}
onPress={() => bump(LEAD_STEP)}
>
<Text style={[styles.stepText, { color: colors.text }]}>+</Text>
</TouchableOpacity>
</View>
</View>
<TouchableOpacity
style={[styles.testBtn, { borderColor: colors.primary }]}
onPress={async () => {
const ok = await sendTestReminder(10);
Alert.alert(
ok ? 'Test scheduled' : 'Notifications blocked',
ok
? 'A test reminder will appear in ~10 seconds (you can background the app).'
: 'Allow notifications for BigBrainParking in system settings.',
);
}}
>
<Text style={{ color: colors.primary, fontWeight: '700' }}>Send a test reminder</Text>
</TouchableOpacity>
<Text style={[styles.footnote, { color: colors.subtext }]}>
Reminders fire entirely on-device (AlarmManager) no server, no push, no Google
services. Adjusts in {LEAD_STEP}-minute steps, {MIN_LEAD_MINUTES}{MAX_LEAD_MINUTES} min.
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
card: { borderRadius: 12, padding: 16 },
row: { flexDirection: 'row', alignItems: 'center' },
rowTitle: { fontSize: 16, fontWeight: '600' },
rowSub: { fontSize: 13, marginTop: 2 },
section: { marginTop: 24, marginBottom: 8, marginLeft: 4, fontSize: 13, fontWeight: '600' },
stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
stepBtn: {
width: 52,
height: 52,
borderRadius: 26,
borderWidth: 1.5,
alignItems: 'center',
justifyContent: 'center',
},
stepText: { fontSize: 26, fontWeight: '700' },
leadDisplay: { alignItems: 'center' },
leadValue: { fontSize: 40, fontWeight: '800' },
leadUnit: { fontSize: 13, marginTop: 2 },
testBtn: {
marginTop: 20,
borderWidth: 1.5,
borderRadius: 12,
padding: 14,
alignItems: 'center',
},
footnote: { fontSize: 12, marginTop: 16, lineHeight: 18 },
});