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:
parent
170f2fa63c
commit
6b19fc79dd
10 changed files with 9699 additions and 17 deletions
|
|
@ -26,12 +26,7 @@
|
|||
"locationWhenInUsePermission": "Show nearby parking meters on the map."
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
"icon": "./assets/notification-icon.png"
|
||||
}
|
||||
],
|
||||
"expo-notifications",
|
||||
[
|
||||
"react-native-vision-camera",
|
||||
{
|
||||
|
|
|
|||
BIN
app/assets/joelangel.png
Normal file
BIN
app/assets/joelangel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -7,7 +7,8 @@
|
|||
"start": "expo start --dev-client",
|
||||
"android": "expo run:android",
|
||||
"prebuild": "expo prebuild --platform android --clean",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"ios": "expo run:ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"parksmarter-client": "*",
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@maplibre/maplibre-react-native": "^10.0.0",
|
||||
"react-native-vision-camera": "^4.6.0",
|
||||
"react-native-unifiedpush": "^2.0.0",
|
||||
"react-native-unifiedpush-connector": "^0.4.0",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
72
app/src/screens/AboutScreen.tsx
Normal file
72
app/src/screens/AboutScreen.tsx
Normal 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%' },
|
||||
});
|
||||
BIN
joel.jpeg
Normal file
BIN
joel.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
joelangel.png
Normal file
BIN
joelangel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 MiB |
9586
package-lock.json
generated
Normal file
9586
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue