Add About screen with 7-tap joelangel easter egg; fix deps

- About screen surfaces getAbout() content; tapping the title 7x reveals
  assets/joelangel.png (placeholder committed; replace with the real image)
- Correct UnifiedPush package to react-native-unifiedpush-connector (lazy-required)
- Fix expo-notifications NotificationBehavior (shouldShowAlert)
- Reachable via an ⓘ header button on the main tabs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-06 08:55:37 -07:00
parent 170f2fa63c
commit 6b19fc79dd
10 changed files with 9699 additions and 17 deletions

View file

@ -1,7 +1,10 @@
import React from 'react';
import { ActivityIndicator, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
import { NavigationContainer, useNavigation } from '@react-navigation/native';
import {
createNativeStackNavigator,
type NativeStackNavigationProp,
} from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useAuth } from '@/auth/AuthContext';
import { LoginScreen } from '@/screens/LoginScreen';
@ -10,11 +13,13 @@ import { ScanScreen } from '@/screens/ScanScreen';
import { FavoritesScreen } from '@/screens/FavoritesScreen';
import { SessionsScreen } from '@/screens/SessionsScreen';
import { MeterDetailScreen } from '@/screens/MeterDetailScreen';
import { AboutScreen } from '@/screens/AboutScreen';
import type { Zone } from 'parksmarter-client';
export type RootStackParamList = {
Tabs: undefined;
MeterDetail: { zone: Zone };
About: undefined;
};
export type TabParamList = {
@ -27,9 +32,20 @@ export type TabParamList = {
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<TabParamList>();
function AboutButton() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
return (
<TouchableOpacity onPress={() => navigation.navigate('About')} style={{ paddingHorizontal: 12 }}>
<Text style={{ fontSize: 18 }}></Text>
</TouchableOpacity>
);
}
function Tabs() {
return (
<Tab.Navigator screenOptions={{ headerShown: true }}>
<Tab.Navigator
screenOptions={{ headerShown: true, headerRight: () => <AboutButton /> }}
>
<Tab.Screen name="Map" component={MapScreen} />
<Tab.Screen name="Scan" component={ScanScreen} />
<Tab.Screen name="Favorites" component={FavoritesScreen} />
@ -59,6 +75,7 @@ export function RootNavigator() {
component={MeterDetailScreen}
options={{ title: 'Meter' }}
/>
<Stack.Screen name="About" component={AboutScreen} options={{ title: 'About' }} />
</Stack.Navigator>
) : (
<LoginScreen />

View file

@ -18,6 +18,8 @@ export async function ensureNotificationPermission(): Promise<boolean> {
Notifications.setNotificationHandler({
handleNotification: async () => ({
// shouldShowAlert is the legacy field; Banner/List are the newer split.
shouldShowAlert: true,
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,

View file

@ -22,18 +22,27 @@ import { ps } from '@/api/client';
* the app is ready to be a push target once the bridge (or a self-hosted relay
* that we register with `PUT /api/Device`) is in place.
*
* The `react-native-unifiedpush` API surface varies by version; treat the calls
* below as the integration point to confirm against the installed version.
* The `react-native-unifiedpush-connector` API surface varies by version; treat the
* calls below as the integration point to confirm against the installed version.
* It's lazy-required so the app runs fine even before push is fully wired.
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires
const UnifiedPush = require('react-native-unifiedpush');
const ENDPOINT_KEY = 'ps_unifiedpush_endpoint';
const INSTANCE = 'default';
function loadUnifiedPush(): any | null {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('react-native-unifiedpush-connector');
} catch {
return null;
}
}
/** Kick off distributor discovery + registration (call after login). */
export async function registerUnifiedPush(): Promise<void> {
const UnifiedPush = loadUnifiedPush();
if (!UnifiedPush) return;
const distributors: string[] = await UnifiedPush.getDistributors();
if (!distributors.length) {
// No UnifiedPush distributor installed (e.g. ntfy). Local reminders still work.

View file

@ -0,0 +1,72 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Image,
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { ps } from '@/api/client';
const TAPS_TO_UNLOCK = 7;
export function AboutScreen() {
const [about, setAbout] = useState<string | null>(null);
const [showJoel, setShowJoel] = useState(false);
const taps = useRef(0);
const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
ps.getAbout()
.then((r) => setAbout(r.Value ?? ''))
.catch(() => setAbout(''));
}, []);
// Tap the title 7 times (within a rolling window) to summon Joel.
const onTitleTap = () => {
taps.current += 1;
if (resetTimer.current) clearTimeout(resetTimer.current);
resetTimer.current = setTimeout(() => (taps.current = 0), 1500);
if (taps.current >= TAPS_TO_UNLOCK) {
taps.current = 0;
setShowJoel(true);
}
};
return (
<ScrollView contentContainerStyle={styles.container}>
<Pressable onPress={onTitleTap}>
<Text style={styles.title}>About BigBrainParking</Text>
</Pressable>
<Text style={styles.body}>{about ?? 'Loading…'}</Text>
<Text style={styles.meta}>
Unofficial, de-Googled client for ParkSmarter. Not affiliated with IPS Group.
</Text>
<Modal visible={showJoel} transparent animationType="fade" onRequestClose={() => setShowJoel(false)}>
<TouchableOpacity style={styles.joelBackdrop} activeOpacity={1} onPress={() => setShowJoel(false)}>
<Image source={require('../../assets/joelangel.png')} style={styles.joel} resizeMode="contain" />
</TouchableOpacity>
</Modal>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { padding: 20 },
title: { fontSize: 24, fontWeight: '700', marginBottom: 16 },
body: { fontSize: 15, lineHeight: 22, color: '#333' },
meta: { fontSize: 12, color: '#999', marginTop: 24 },
joelBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.92)',
alignItems: 'center',
justifyContent: 'center',
},
joel: { width: '92%', height: '80%' },
});