diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 5ceaa8e..c9f4a66 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + @@ -30,7 +32,7 @@ - + diff --git a/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png index 7aac5c2..8758ad2 100644 Binary files a/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png and b/apps/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png index fc26717..6998211 100644 Binary files a/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png and b/apps/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png index 22d475f..c914387 100644 Binary files a/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png and b/apps/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png index 2e530a0..2e2869a 100644 Binary files a/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png and b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png index ecd89a2..a743f36 100644 Binary files a/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png and b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 73b8462..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp index ecc5942..c744021 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 73b8462..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index 207bd47..2fad8c8 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 73b8462..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 1e2a669..3465f09 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 0234774..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp index b751c90..f0411fe 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 0234774..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index b17484d..af86f94 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 0234774..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 2b44bdd..2444ea8 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index d8041f5..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index b24aa36..43bef14 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index d8041f5..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index b0d7f09..39ef023 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index d8041f5..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 85001b4..cc63235 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 07031ba..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 4c99c44..6322ba8 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 07031ba..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index c88dc30..a1b4a87 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 07031ba..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 0be0493..f5fd34c 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 497ec46..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 48d9945..c1b4385 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 497ec46..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index 415b052..d7956d8 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 497ec46..0000000 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 0801290..f647025 100644 Binary files a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/apps/mobile/android/gradle.properties b/apps/mobile/android/gradle.properties index 7531e9e..de47429 100644 --- a/apps/mobile/android/gradle.properties +++ b/apps/mobile/android/gradle.properties @@ -35,7 +35,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # your application. You should enable this flag either if you want # to write custom TurboModules/Fabric components OR use libraries that # are providing them. -newArchEnabled=true +newArchEnabled=false # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 5f5d7f2..16a14a0 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -5,7 +5,7 @@ "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", - "scheme": "sauci", + "scheme": "app.sauci", "userInterfaceStyle": "automatic", "newArchEnabled": false, "splash": { @@ -20,7 +20,8 @@ "infoPlist": { "UIBackgroundModes": [ "remote-notification" - ] + ], + "NSFaceIDUsageDescription": "Sauci uses Face ID to unlock the app and keep your conversations private." } }, "android": { @@ -50,6 +51,12 @@ } ], "expo-secure-store", + [ + "expo-local-authentication", + { + "faceIDPermission": "Sauci uses Face ID to unlock the app and keep your conversations private." + } + ], [ "@sentry/react-native/expo", { diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx index a9eabd5..6c6aa74 100755 --- a/apps/mobile/app/(app)/_layout.tsx +++ b/apps/mobile/app/(app)/_layout.tsx @@ -1,12 +1,14 @@ import { Tabs, useRouter, useSegments } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; -import { View, ActivityIndicator, StyleSheet, Platform, Modal, Text, Pressable, Animated, AppState } from "react-native"; +import { View, ActivityIndicator, StyleSheet, Platform, Modal, Text, Pressable, Animated, AppState, AppStateStatus } from "react-native"; import { BlurView } from "expo-blur"; import { LinearGradient } from "expo-linear-gradient"; import { useEffect, useState, useCallback, useRef } from "react"; import { useAuthStore, useMatchStore, useMessageStore, useSubscriptionStore, usePacksStore } from "../../src/store"; import { colors, gradients, blur, radius, spacing, typography, shadows } from "../../src/theme"; import { supabase } from "../../src/lib/supabase"; +import { isBiometricEnabled } from "../../src/lib/biometricAuth"; +import { BiometricLockScreen } from "../../src/components/BiometricLockScreen"; import type { MatchWithQuestion } from "../../src/types"; import type { Database } from "../../src/types/supabase"; @@ -15,11 +17,14 @@ type Message = Database["public"]["Tables"]["messages"]["Row"]; function TabBarBackground() { if (Platform.OS === 'ios') { return ( - + + + + ); } return ; @@ -72,6 +77,9 @@ export default function AppLayout() { const [matchNotification, setMatchNotification] = useState(null); const messageToastAnim = useRef(new Animated.Value(-100)).current; const messageTimeoutRef = useRef(null); + const [isLocked, setIsLocked] = useState(false); + const appStateRef = useRef(AppState.currentState); + const hasCheckedInitialBiometric = useRef(false); // Check if we're currently on the onboarding screen const isOnOnboarding = segments.includes("onboarding"); @@ -110,6 +118,49 @@ export default function AppLayout() { }; }, [signOut]); + // Handle biometric lock when app goes to background/foreground + useEffect(() => { + // Check if biometric should be shown on initial mount + const checkInitialBiometric = async () => { + if (hasCheckedInitialBiometric.current) return; + hasCheckedInitialBiometric.current = true; + + const enabled = await isBiometricEnabled(); + if (enabled && isAuthenticated) { + setIsLocked(true); + } + }; + + if (isAuthenticated && !isLoading) { + checkInitialBiometric(); + } + + const subscription = AppState.addEventListener('change', async (nextAppState) => { + const previousState = appStateRef.current; + appStateRef.current = nextAppState; + + // When app comes to foreground from background + if ( + previousState.match(/inactive|background/) && + nextAppState === 'active' && + isAuthenticated + ) { + const enabled = await isBiometricEnabled(); + if (enabled) { + setIsLocked(true); + } + } + }); + + return () => { + subscription.remove(); + }; + }, [isAuthenticated, isLoading]); + + const handleBiometricUnlock = useCallback(() => { + setIsLocked(false); + }, []); + // Fetch unread message count on mount useEffect(() => { if (user?.id) { @@ -451,6 +502,12 @@ export default function AppLayout() { )} + + {/* Biometric Lock Screen */} + ); } diff --git a/apps/mobile/app/(app)/onboarding.tsx b/apps/mobile/app/(app)/onboarding.tsx index 6bcc125..968aa61 100644 --- a/apps/mobile/app/(app)/onboarding.tsx +++ b/apps/mobile/app/(app)/onboarding.tsx @@ -8,6 +8,7 @@ import { KeyboardAvoidingView, Platform, Keyboard, + ScrollView, } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; @@ -27,6 +28,7 @@ import { GlassButton } from '../../src/components/ui/GlassButton'; import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme'; import { useAuthStore } from '../../src/store'; import { supabase } from '../../src/lib/supabase'; +import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications'; import type { Gender } from '../../src/types'; const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [ @@ -36,7 +38,7 @@ const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [ { value: 'prefer-not-to-say', label: 'Skip', icon: 'remove-circle-outline' }, ]; -type Stage = 'name' | 'gender' | 'explicit'; +type Stage = 'name' | 'gender' | 'explicit' | 'notifications'; export default function OnboardingScreen() { const router = useRouter(); @@ -44,7 +46,7 @@ export default function OnboardingScreen() { const [stage, setStage] = useState('name'); const [name, setName] = useState(user?.name || ''); const [gender, setGender] = useState(user?.gender || null); - const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content || false); + const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content ?? true); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const inputRef = useRef(null); @@ -62,7 +64,33 @@ export default function OnboardingScreen() { const handleGenderSelect = (selectedGender: Gender) => { setGender(selectedGender); setError(null); - setTimeout(() => setStage('explicit'), 300); + }; + + const handleGenderContinue = () => { + if (!gender) { + setError('Please select a gender or skip'); + return; + } + setStage('explicit'); + }; + + const handleExplicitContinue = () => { + setStage('notifications'); + }; + + const handleEnableNotifications = async () => { + setIsLoading(true); + try { + const token = await registerForPushNotificationsAsync(); + if (token && user?.id) { + await savePushToken(user.id, token); + } + } catch (err) { + console.error('Failed to enable notifications:', err); + } finally { + setIsLoading(false); + await handleComplete(); + } }; const handleComplete = async () => { @@ -205,7 +233,19 @@ export default function OnboardingScreen() { ))} + {error && ( + + + {error} + + )} + + + + Continue + + ); @@ -307,13 +347,78 @@ export default function OnboardingScreen() { + Continue + + + + ); + + case 'notifications': + return ( + + + + + + Stay in the Loop + + Get notified when you and your partner match on answers + + + + + + + + + + Match Alerts + + Know instantly when you both agree + + + + + + + + + Messages + + Never miss a message from your partner + + + + + + + - Let's Go! + Enable Notifications + + Maybe Later + ); @@ -325,13 +430,18 @@ export default function OnboardingScreen() { {/* Header with back button and progress dots */} {stage !== 'name' ? ( setStage(stage === 'explicit' ? 'gender' : 'name')} + onPress={() => { + if (stage === 'notifications') setStage('explicit'); + else if (stage === 'explicit') setStage('gender'); + else setStage('name'); + }} > @@ -342,11 +452,18 @@ export default function OnboardingScreen() { + - {renderStage()} + + {renderStage()} + ); @@ -356,6 +473,9 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + scrollContent: { + flexGrow: 1, + }, headerBar: { flexDirection: 'row', alignItems: 'center', @@ -552,4 +672,41 @@ const styles = StyleSheet.create({ footer: { marginTop: 'auto', }, + notificationFeature: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.md, + borderBottomWidth: 1, + borderBottomColor: colors.glass.border, + }, + notificationIcon: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: colors.primaryLight, + alignItems: 'center', + justifyContent: 'center', + marginRight: spacing.md, + }, + notificationText: { + flex: 1, + }, + notificationTitle: { + ...typography.headline, + color: colors.text, + marginBottom: spacing.xs, + }, + notificationDescription: { + ...typography.caption1, + color: colors.textSecondary, + }, + skipButton: { + alignItems: 'center', + paddingVertical: spacing.md, + marginTop: spacing.sm, + }, + skipText: { + ...typography.subhead, + color: colors.textSecondary, + }, }); diff --git a/apps/mobile/app/(app)/packs.tsx b/apps/mobile/app/(app)/packs.tsx index 4538b55..1187a68 100755 --- a/apps/mobile/app/(app)/packs.tsx +++ b/apps/mobile/app/(app)/packs.tsx @@ -1,4 +1,4 @@ -import { View, Text, StyleSheet, FlatList, TouchableOpacity, Switch, RefreshControl, Platform } from "react-native"; +import { View, Text, StyleSheet, FlatList, TouchableOpacity, Switch, RefreshControl, Platform, Alert } from "react-native"; import { usePacksStore, useAuthStore, useSubscriptionStore } from "../../src/store"; import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; @@ -33,13 +33,23 @@ export default function PacksScreen() { }, []); // Handle toggling a pack - show teaser if premium pack without access - const handleTogglePack = useCallback((pack: QuestionPack) => { + const handleTogglePack = useCallback(async (pack: QuestionPack) => { if (pack.is_premium && !hasPremiumAccess) { setTeaserPack(pack); setShowTeaser(true); return; } - togglePack(pack.id); + const result = await togglePack(pack.id); + if (!result.success && result.reason === "no_couple") { + Alert.alert( + "Pair with Partner", + "You need to pair with your partner before you can customise question packs.", + [ + { text: "Later", style: "cancel" }, + { text: "Pair Now", onPress: () => router.push("/pairing") } + ] + ); + } }, [hasPremiumAccess, togglePack]); // Handle teaser unlock button - show paywall diff --git a/apps/mobile/app/(app)/pairing.tsx b/apps/mobile/app/(app)/pairing.tsx index 916a616..135d1b1 100755 --- a/apps/mobile/app/(app)/pairing.tsx +++ b/apps/mobile/app/(app)/pairing.tsx @@ -204,6 +204,12 @@ export default function PairingScreen() { style={styles.container} behavior={Platform.OS === "ios" ? "padding" : "height"} > + router.back()} + > + + 🔗 Pair Up diff --git a/apps/mobile/app/(app)/profile.tsx b/apps/mobile/app/(app)/profile.tsx index 49ed088..aa8876b 100755 --- a/apps/mobile/app/(app)/profile.tsx +++ b/apps/mobile/app/(app)/profile.tsx @@ -1,10 +1,17 @@ import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Platform, useWindowDimensions, TextInput, Modal, ActivityIndicator, Linking, Switch } from "react-native"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import { useAuthStore, useMatchStore, useMessageStore, usePacksStore, useSubscriptionStore } from "../../src/store"; import { Ionicons } from "@expo/vector-icons"; import { LinearGradient } from "expo-linear-gradient"; import Animated, { FadeInDown } from "react-native-reanimated"; import { supabase } from "../../src/lib/supabase"; +import { resetSwipeTutorial } from "../../src/lib/swipeTutorialSeen"; +import { + isBiometricAvailable, + isBiometricEnabled, + setBiometricEnabled, + getBiometricType, +} from "../../src/lib/biometricAuth"; import { router } from "expo-router"; import { GradientBackground, GlassCard } from "../../src/components/ui"; import { FeedbackModal } from "../../src/components/FeedbackModal"; @@ -29,12 +36,34 @@ export default function ProfileScreen() { const [showPaywall, setShowPaywall] = useState(false); const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content ?? false); const [isUpdatingExplicit, setIsUpdatingExplicit] = useState(false); + const [biometricAvailable, setBiometricAvailable] = useState(false); + const [biometricEnabled, setBiometricEnabledState] = useState(false); + const [biometricType, setBiometricType] = useState("Face ID"); + const [isUpdatingBiometric, setIsUpdatingBiometric] = useState(false); // Check if user or partner has premium access const hasPremiumAccess = useMemo(() => { return user?.is_premium || partner?.is_premium || subscription.isProUser; }, [user?.is_premium, partner?.is_premium, subscription.isProUser]); + // Check biometric availability on mount + useEffect(() => { + const checkBiometric = async () => { + const available = await isBiometricAvailable(); + setBiometricAvailable(available); + + if (available) { + const enabled = await isBiometricEnabled(); + setBiometricEnabledState(enabled); + + const type = await getBiometricType(); + setBiometricType(type); + } + }; + + checkBiometric(); + }, []); + // Check if it's the user's own subscription (vs partner's) const isOwnSubscription = user?.is_premium && subscription.isProUser; @@ -86,6 +115,21 @@ export default function ProfileScreen() { } }; + const handleBiometricToggle = async (value: boolean) => { + setBiometricEnabledState(value); + setIsUpdatingBiometric(true); + + try { + await setBiometricEnabled(value); + } catch (error) { + // Revert on error + setBiometricEnabledState(!value); + Alert.alert("Error", `Failed to ${value ? "enable" : "disable"} ${biometricType}. Please try again.`); + } finally { + setIsUpdatingBiometric(false); + } + }; + const handleManageSubscription = () => { Linking.openURL("https://apps.apple.com/account/subscriptions"); }; @@ -393,6 +437,39 @@ export default function ProfileScreen() { ios_backgroundColor={colors.glass.border} /> + {biometricAvailable && ( + <> + + + + + + + + {biometricType} + Require unlock when opening app + + + + + + )} @@ -452,6 +529,39 @@ export default function ProfileScreen() { + {/* Debug Section - Only show in development builds */} + {__DEV__ && ( + + Debug + + { + await resetSwipeTutorial(); + Alert.alert("Success", "Swipe tutorial has been reset. It will show again next time you visit the swipe screen."); + }} + activeOpacity={0.7} + > + + + + + + Reset Swipe Tutorial + Show the tutorial again + + + + + + + + + )} + {/* Danger Zone - Only show if in a couple */} {couple && ( ([]); const [isLoading, setIsLoading] = useState(true); const [currentIndex, setCurrentIndex] = useState(0); + const [showTutorial, setShowTutorial] = useState(false); const { enabledPackIds, fetchPacks } = usePacksStore(); + const { partner, couple } = useAuthStore(); useEffect(() => { fetchPacks().then(() => fetchQuestions()); + checkTutorial(); }, [packId]); + const checkTutorial = async () => { + const seen = await hasSeenSwipeTutorial(); + if (!seen) { + setShowTutorial(true); + } + }; + + const handleTutorialComplete = async () => { + await markSwipeTutorialSeen(); + setShowTutorial(false); + }; + // Filter out questions from disabled packs immediately when enabledPackIds changes useEffect(() => { if (!packId && enabledPackIds.length > 0 && questions.length > 0) { @@ -126,6 +143,45 @@ export default function SwipeScreen() { ); } + // Show "pair with partner" prompt when user doesn't have a partner yet + if (!partner) { + return ( + + + + + + + + {couple ? "Waiting for your partner" : "Pair with your partner"} + + + {couple + ? "Share your invite code so they can join you. Once paired, you'll both answer questions and discover what you agree on!" + : "Sauci is made for two! Connect with your partner to start answering questions together and discover what you have in common." + } + + router.push("/pairing")} + style={{ marginTop: spacing.xl }} + size="lg" + > + {couple ? "View Invite Code" : "Pair Now"} + + + + + ); + } + // Show "no packs enabled" message when not viewing a specific pack and no packs are enabled if (!packId && enabledPackIds.length === 0) { return ( @@ -238,6 +294,11 @@ export default function SwipeScreen() { {/* Bottom spacing for tab bar */} + + {/* Tutorial Overlay */} + {showTutorial && ( + + )} ); } @@ -318,6 +379,21 @@ const styles = StyleSheet.create({ alignItems: "center", marginBottom: spacing.lg, }, + pairIconContainer: { + width: 120, + height: 120, + borderRadius: 60, + justifyContent: "center", + alignItems: "center", + marginBottom: spacing.xl, + ...shadows.lg, + }, + pairTitle: { + ...typography.title1, + color: colors.text, + marginBottom: spacing.md, + textAlign: "center", + }, noPacksIconContainer: { width: 120, height: 120, diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 30bbc1f..edb401b 100755 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -7,8 +7,6 @@ initSentry(); // Import push notification utilities import { - registerForPushNotificationsAsync, - savePushToken, clearPushToken, setupNotificationResponseListener, getInitialNotification, @@ -54,19 +52,78 @@ export default function RootLayout() { // Handle initial URL if app was opened via deep link const handleDeepLink = async (url: string | null) => { - if (url) { - // Supabase needs to handle the URL to extract session - // getSession() will automatically check window.location on web if detectSessionInUrl is true - // For native, we rely on onAuthStateChange or manual processing if needed + console.log('[DeepLink] handleDeepLink called with URL:', url); + if (!url) return; + + try { + // Parse the URL to extract auth parameters + const parsedUrl = new URL(url); + console.log('[DeepLink] Parsed URL:', { + protocol: parsedUrl.protocol, + host: parsedUrl.host, + pathname: parsedUrl.pathname, + search: parsedUrl.search, + hash: parsedUrl.hash, + }); + const params = new URLSearchParams(parsedUrl.search); + + // Also check hash fragment (some auth flows use fragments) + const hashParams = new URLSearchParams(parsedUrl.hash.replace('#', '')); + + // Get token parameters - check both query and hash + const accessToken = params.get('access_token') || hashParams.get('access_token'); + const refreshToken = params.get('refresh_token') || hashParams.get('refresh_token'); + const tokenHash = params.get('token_hash') || hashParams.get('token_hash'); + const type = params.get('type') || hashParams.get('type'); + + console.log('[DeepLink] Extracted params:', { + accessToken: accessToken ? 'present' : 'missing', + refreshToken: refreshToken ? 'present' : 'missing', + tokenHash: tokenHash ? 'present' : 'missing', + type, + }); + + if (accessToken && refreshToken) { + // OAuth flow - set session directly + console.log('[DeepLink] Setting session with tokens'); + const { data, error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }); + console.log('[DeepLink] setSession result:', { + hasSession: !!data?.session, + userId: data?.session?.user?.id, + error: error?.message + }); + } else if (tokenHash && type) { + // Magic link / email verification flow - verify the OTP + console.log('[DeepLink] Verifying OTP with token_hash and type:', type); + const { data, error } = await supabase.auth.verifyOtp({ + token_hash: tokenHash, + type: type as 'signup' | 'magiclink' | 'recovery' | 'invite' | 'email', + }); + console.log('[DeepLink] verifyOtp result:', { data: !!data?.session, error: error?.message }); + } else { + // Fallback for web or other scenarios + console.log('[DeepLink] No auth params found, falling back to getSession'); + await supabase.auth.getSession(); + } + } catch (error) { + console.error('[DeepLink] Error handling deep link:', error); + // Still try to get session as fallback await supabase.auth.getSession(); } }; // Get initial URL - Linking.getInitialURL().then(handleDeepLink); + Linking.getInitialURL().then((url) => { + console.log('[DeepLink] getInitialURL returned:', url); + handleDeepLink(url); + }); // Listen for new deep links while app is open const linkingSubscription = Linking.addEventListener("url", (event) => { + console.log('[DeepLink] URL event listener triggered with:', event.url); handleDeepLink(event.url); }); @@ -83,17 +140,13 @@ export default function RootLayout() { // Listen for auth changes const { data: { subscription: authSubscription } } = supabase.auth.onAuthStateChange( async (event, session) => { + console.log('[Auth] onAuthStateChange:', event, 'hasSession:', !!session, 'userId:', session?.user?.id); if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") { + console.log('[Auth] Calling fetchUser...'); fetchUser(); // Set Sentry user context for error tracking if (session?.user) { setUserContext(session.user.id, session.user.email); - - // Register for push notifications and save token - const token = await registerForPushNotificationsAsync(); - if (token) { - await savePushToken(session.user.id, token); - } } } else if (event === "SIGNED_OUT") { // Clear push token before clearing user state diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 9629ea7..0aab696 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -246,6 +246,8 @@ PODS: - ExpoModulesCore - ExpoLinking (7.0.5): - ExpoModulesCore + - ExpoLocalAuthentication (17.0.8): + - ExpoModulesCore - ExpoModulesCore (2.2.3): - DoubleConversion - glog @@ -1786,6 +1788,8 @@ PODS: - React-perflogger (= 0.76.5) - React-utils (= 0.76.5) - RevenueCat (5.51.1) + - RNCAsyncStorage (2.2.0): + - React-Core - RNGestureHandler (2.20.2): - DoubleConversion - glog @@ -1972,6 +1976,7 @@ DEPENDENCIES: - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`) - ExpoLinking (from `../node_modules/expo-linking/ios`) + - ExpoLocalAuthentication (from `../node_modules/expo-local-authentication/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoSecureStore (from `../node_modules/expo-secure-store/ios`) - EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`) @@ -2038,6 +2043,7 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNPurchases (from `../node_modules/react-native-purchases`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -2103,6 +2109,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-linear-gradient/ios" ExpoLinking: :path: "../node_modules/expo-linking/ios" + ExpoLocalAuthentication: + :path: "../node_modules/expo-local-authentication/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" ExpoSecureStore: @@ -2232,6 +2240,8 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNPurchases: @@ -2271,6 +2281,7 @@ SPEC CHECKSUMS: ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 ExpoLinearGradient: 35ebd83b16f80b3add053a2fd68cc328ed927f60 ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402 + ExpoLocalAuthentication: 8a31808565da7af926dd9b595e98594d8b1553b6 ExpoModulesCore: 15f60d00e33ca0cea27147543877ca6e70c42ef5 ExpoSecureStore: 9a3665d7161dd1031be386e278bffd623658f316 EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db @@ -2337,6 +2348,7 @@ SPEC CHECKSUMS: ReactCodegen: 3a68408bf68d0957abcd13d610f76420005c1d91 ReactCommon: 5809a8ee421b7219221a475b78180f8f34b5c5ec RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0 + RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f RNGestureHandler: 27a63f2218affdf1a426d56682f9b174904838b3 RNPurchases: cc3333cede6c0f040545a95c38ae627d0174c058 RNReanimated: 66cf0f600a26d2b5e74c6e0b1c77c1ab1f62fc05 diff --git a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj index 646515d..d410361 100644 --- a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj @@ -11,26 +11,26 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; - 46CB71552F030871008AC4FC /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46CB71542F030871008AC4FC /* StoreKit.framework */; }; + 46CB71572F0334FF008AC4FC /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46CB71562F0334FF008AC4FC /* StoreKit.framework */; }; + 753494A6E21B10482BBED79D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 426EFCCADE400D3F5060F5FE /* PrivacyInfo.xcprivacy */; }; + 7BA3DCB069BE4EDFB54CC861 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA143F69E734D11B2959507 /* noop-file.swift */; }; 96905EF65AED1B983A6B3ABC /* libPods-Sauci.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */; }; B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; - B6ADEA6ED390C90F199467A5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1C9767CAD348ACB05A4870BE /* PrivacyInfo.xcprivacy */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; - E6717E3EEE9541E7BCC0E9B8 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63D24AC64FB84C3AB9F4302E /* noop-file.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 08DDB0F6A9A146CE84A39C1C /* Sauci-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Sauci-Bridging-Header.h"; path = "Sauci/Sauci-Bridging-Header.h"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* Sauci.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sauci.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Sauci/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Sauci/AppDelegate.mm; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sauci/main.m; sourceTree = ""; }; - 1C9767CAD348ACB05A4870BE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 1E5EDAB25D1C4414926F678F /* Sauci-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Sauci-Bridging-Header.h"; path = "Sauci/Sauci-Bridging-Header.h"; sourceTree = ""; }; - 46CB71542F030871008AC4FC /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 426EFCCADE400D3F5060F5FE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 46CB71562F0334FF008AC4FC /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sauci.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 63D24AC64FB84C3AB9F4302E /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Sauci/noop-file.swift"; sourceTree = ""; }; + 6AA143F69E734D11B2959507 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Sauci/noop-file.swift"; sourceTree = ""; }; 6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.debug.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.debug.xcconfig"; sourceTree = ""; }; 7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.release.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.release.xcconfig"; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = ""; }; @@ -45,7 +45,7 @@ buildActionMask = 2147483647; files = ( 96905EF65AED1B983A6B3ABC /* libPods-Sauci.a in Frameworks */, - 46CB71552F030871008AC4FC /* StoreKit.framework in Frameworks */, + 46CB71572F0334FF008AC4FC /* StoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -62,9 +62,9 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, - 63D24AC64FB84C3AB9F4302E /* noop-file.swift */, - 1E5EDAB25D1C4414926F678F /* Sauci-Bridging-Header.h */, - 1C9767CAD348ACB05A4870BE /* PrivacyInfo.xcprivacy */, + 6AA143F69E734D11B2959507 /* noop-file.swift */, + 08DDB0F6A9A146CE84A39C1C /* Sauci-Bridging-Header.h */, + 426EFCCADE400D3F5060F5FE /* PrivacyInfo.xcprivacy */, ); name = Sauci; sourceTree = ""; @@ -72,7 +72,7 @@ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( - 46CB71542F030871008AC4FC /* StoreKit.framework */, + 46CB71562F0334FF008AC4FC /* StoreKit.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */, ); @@ -151,14 +151,14 @@ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */; buildPhases = ( 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, - 6F4D4033C27C2AC6612A535A /* [Expo] Configure project */, + 3F25679DC28AE5BB78AFEBF5 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, - 6B8CABAC69F8483D9920303C /* Upload Debug Symbols to Sentry */, - A435DFC7C52C70693FBE0CD3 /* [CP] Embed Pods Frameworks */, + 2269DDED21E54A6284DF755C /* Upload Debug Symbols to Sentry */, + F51873069048543029DDD07B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -210,7 +210,7 @@ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, - B6ADEA6ED390C90F199467A5 /* PrivacyInfo.xcprivacy in Resources */, + 753494A6E21B10482BBED79D /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -254,7 +254,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6B8CABAC69F8483D9920303C /* Upload Debug Symbols to Sentry */ = { + 2269DDED21E54A6284DF755C /* Upload Debug Symbols to Sentry */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -268,7 +268,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; }; - 6F4D4033C27C2AC6612A535A /* [Expo] Configure project */ = { + 3F25679DC28AE5BB78AFEBF5 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -301,6 +301,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.bundle", @@ -319,6 +320,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.bundle", @@ -333,7 +335,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n"; showEnvVarsInLog = 0; }; - A435DFC7C52C70693FBE0CD3 /* [CP] Embed Pods Frameworks */ = { + F51873069048543029DDD07B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -361,7 +363,7 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */, - E6717E3EEE9541E7BCC0E9B8 /* noop-file.swift in Sources */, + 7BA3DCB069BE4EDFB54CC861 /* noop-file.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/100.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/100.png deleted file mode 100644 index 3cefbb1..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/100.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/102.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/102.png deleted file mode 100644 index 8df32da..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/102.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/1024.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/1024.png deleted file mode 100644 index 1703eb9..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/1024.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/108.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/108.png deleted file mode 100644 index a5ca5f1..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/108.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/114.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/114.png deleted file mode 100644 index 6735413..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/114.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/120.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/120.png deleted file mode 100644 index 710c1c3..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/120.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/128.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/128.png deleted file mode 100644 index eb43e98..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/128.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/144.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/144.png deleted file mode 100644 index 43b924a..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/144.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/152.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/152.png deleted file mode 100644 index 7308d19..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/152.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/16.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/16.png deleted file mode 100644 index 4288199..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/16.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/167.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/167.png deleted file mode 100644 index a1a57bd..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/167.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/172.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/172.png deleted file mode 100644 index bd7a185..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/172.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/180.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/180.png deleted file mode 100644 index e4e1ce5..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/180.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/196.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/196.png deleted file mode 100644 index f05bd15..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/196.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/20.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/20.png deleted file mode 100644 index 1188b35..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/20.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/216.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/216.png deleted file mode 100644 index bd8df9b..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/216.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/234.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/234.png deleted file mode 100644 index 50dd5fb..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/234.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/256.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/256.png deleted file mode 100644 index e842700..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/256.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/258.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/258.png deleted file mode 100644 index c8dff87..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/258.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/29.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/29.png deleted file mode 100644 index 4552f46..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/29.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/32.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/32.png deleted file mode 100644 index bd75b21..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/32.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/40.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/40.png deleted file mode 100644 index 8c5ee74..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/40.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/48.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/48.png deleted file mode 100644 index 102df6e..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/48.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/50.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/50.png deleted file mode 100644 index 2e2d432..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/50.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/512.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/512.png deleted file mode 100644 index 089ef57..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/512.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/55.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/55.png deleted file mode 100644 index cbc32ae..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/55.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/57.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/57.png deleted file mode 100644 index c5766e2..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/57.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/58.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index c93688a..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/58.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/60.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/60.png deleted file mode 100644 index 8bf4567..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/60.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/64.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/64.png deleted file mode 100644 index 016cd33..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/64.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/66.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/66.png deleted file mode 100644 index 5f2ccab..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/66.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/72.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/72.png deleted file mode 100644 index 9a13723..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/72.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/76.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/76.png deleted file mode 100644 index fd7e723..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/76.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/80.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index a88179c..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/80.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/87.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/87.png deleted file mode 100644 index 7b1ec13..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/87.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/88.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/88.png deleted file mode 100644 index 01cdd14..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/88.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/92.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/92.png deleted file mode 100644 index b661428..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/92.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png index 1703eb9..5ba74b7 100644 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png and b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png deleted file mode 100644 index 3ff3bc4..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png deleted file mode 100644 index 3ff3bc4..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png deleted file mode 100644 index a3fb927..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png deleted file mode 100644 index 728386e..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29.png deleted file mode 100644 index d161e5b..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png deleted file mode 100644 index eed2cc8..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png deleted file mode 100644 index eed2cc8..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png deleted file mode 100644 index 8f888e3..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png deleted file mode 100644 index d161e5b..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png deleted file mode 100644 index 0fb8cfd..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png deleted file mode 100644 index 0fb8cfd..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png deleted file mode 100644 index 9b21e5f..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png deleted file mode 100644 index 3ff3bc4..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png deleted file mode 100644 index 9b21e5f..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png deleted file mode 100644 index 8aa72a3..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png deleted file mode 100644 index 584c93e..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@2x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@2x.png deleted file mode 100644 index 9b21e5f..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@2x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png deleted file mode 100644 index aec7eb7..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@3x.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@3x.png deleted file mode 100644 index 8aa72a3..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon@3x.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png deleted file mode 100644 index 81d5f3c..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon~ipad.png b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon~ipad.png deleted file mode 100644 index f9c2f67..0000000 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/AppIcon~ipad.png and /dev/null differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/Contents.json index bd04914..90d8d4c 100644 --- a/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/apps/mobile/ios/Sauci/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,134 +1,14 @@ { "images": [ { - "filename": "AppIcon@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "60x60" - }, - { - "filename": "AppIcon@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "60x60" - }, - { - "filename": "AppIcon~ipad.png", - "idiom": "ipad", - "scale": "1x", - "size": "76x76" - }, - { - "filename": "AppIcon@2x~ipad.png", - "idiom": "ipad", - "scale": "2x", - "size": "76x76" - }, - { - "filename": "AppIcon-83.5@2x~ipad.png", - "idiom": "ipad", - "scale": "2x", - "size": "83.5x83.5" - }, - { - "filename": "AppIcon-40@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "40x40" - }, - { - "filename": "AppIcon-40@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "40x40" - }, - { - "filename": "AppIcon-40~ipad.png", - "idiom": "ipad", - "scale": "1x", - "size": "40x40" - }, - { - "filename": "AppIcon-40@2x~ipad.png", - "idiom": "ipad", - "scale": "2x", - "size": "40x40" - }, - { - "filename": "AppIcon-20@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "20x20" - }, - { - "filename": "AppIcon-20@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "20x20" - }, - { - "filename": "AppIcon-20~ipad.png", - "idiom": "ipad", - "scale": "1x", - "size": "20x20" - }, - { - "filename": "AppIcon-20@2x~ipad.png", - "idiom": "ipad", - "scale": "2x", - "size": "20x20" - }, - { - "filename": "AppIcon-29.png", - "idiom": "iphone", - "scale": "1x", - "size": "29x29" - }, - { - "filename": "AppIcon-29@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "29x29" - }, - { - "filename": "AppIcon-29@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "29x29" - }, - { - "filename": "AppIcon-29~ipad.png", - "idiom": "ipad", - "scale": "1x", - "size": "29x29" - }, - { - "filename": "AppIcon-29@2x~ipad.png", - "idiom": "ipad", - "scale": "2x", - "size": "29x29" - }, - { - "filename": "AppIcon-60@2x~car.png", - "idiom": "car", - "scale": "2x", - "size": "60x60" - }, - { - "filename": "AppIcon-60@3x~car.png", - "idiom": "car", - "scale": "3x", - "size": "60x60" - }, - { - "filename": "AppIcon~ios-marketing.png", - "idiom": "ios-marketing", - "scale": "1x", + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "platform": "ios", "size": "1024x1024" } ], "info": { - "author": "iconkitchen", - "version": 1 + "version": 1, + "author": "expo" } } \ No newline at end of file diff --git a/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image.png b/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image.png index fb6b945..0c8b836 100644 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image.png and b/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image.png differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@2x.png b/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@2x.png index fb6b945..0c8b836 100644 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@2x.png and b/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@2x.png differ diff --git a/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@3x.png b/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@3x.png index fb6b945..0c8b836 100644 Binary files a/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@3x.png and b/apps/mobile/ios/Sauci/Images.xcassets/SplashScreenLogo.imageset/image@3x.png differ diff --git a/apps/mobile/ios/Sauci/Info.plist b/apps/mobile/ios/Sauci/Info.plist index d30b439..fe25a57 100644 --- a/apps/mobile/ios/Sauci/Info.plist +++ b/apps/mobile/ios/Sauci/Info.plist @@ -4,6 +4,8 @@ CADisableMinimumFrameDurationOnPhone + CFBundleAllowMixedLocalizations + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -27,7 +29,7 @@ CFBundleURLSchemes - sauci + app.sauci com.sauci.app @@ -54,7 +56,7 @@ NSCameraUsageDescription Allow $(PRODUCT_NAME) to access your camera NSFaceIDUsageDescription - Allow $(PRODUCT_NAME) to access your Face ID biometric data. + Sauci uses Face ID to unlock the app and keep your conversations private. NSMicrophoneUsageDescription Allow $(PRODUCT_NAME) to access your microphone NSPhotoLibraryUsageDescription diff --git a/apps/mobile/ios/Sauci/Sauci.entitlements b/apps/mobile/ios/Sauci/Sauci.entitlements index 018a6e2..3f2ea61 100644 --- a/apps/mobile/ios/Sauci/Sauci.entitlements +++ b/apps/mobile/ios/Sauci/Sauci.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.applesignin + + Default + \ No newline at end of file diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index 76d3954..9a2eb25 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@expo/ngrok": "^4.1.0", "@expo/vector-icons": "^14.0.0", + "@react-native-async-storage/async-storage": "^2.2.0", "@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/native": "^7.0.0", "@react-navigation/native-stack": "^7.0.0", @@ -28,6 +29,7 @@ "expo-image-picker": "~16.0.6", "expo-linear-gradient": "~14.0.2", "expo-linking": "~7.0.0", + "expo-local-authentication": "^17.0.8", "expo-notifications": "~0.29.0", "expo-router": "~4.0.0", "expo-secure-store": "~14.0.0", @@ -3513,6 +3515,18 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.76.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", @@ -7108,6 +7122,18 @@ "react-native": "*" } }, + "node_modules/expo-local-authentication": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-17.0.8.tgz", + "integrity": "sha512-Q5fXHhu6w3pVPlFCibU72SYIAN+9wX7QpFn9h49IUqs0Equ44QgswtGrxeh7fdnDqJrrYGPet5iBzjnE70uolA==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-manifests": { "version": "0.15.8", "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.15.8.tgz", @@ -8370,6 +8396,15 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -9312,6 +9347,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 684bd43..a0d6324 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -14,6 +14,7 @@ "dependencies": { "@expo/ngrok": "^4.1.0", "@expo/vector-icons": "^14.0.0", + "@react-native-async-storage/async-storage": "^2.2.0", "@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/native": "^7.0.0", "@react-navigation/native-stack": "^7.0.0", @@ -32,6 +33,7 @@ "expo-image-picker": "~16.0.6", "expo-linear-gradient": "~14.0.2", "expo-linking": "~7.0.0", + "expo-local-authentication": "^17.0.8", "expo-notifications": "~0.29.0", "expo-router": "~4.0.0", "expo-secure-store": "~14.0.0", diff --git a/apps/mobile/src/components/BiometricLockScreen.tsx b/apps/mobile/src/components/BiometricLockScreen.tsx new file mode 100644 index 0000000..4aa5943 --- /dev/null +++ b/apps/mobile/src/components/BiometricLockScreen.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useState } from "react"; +import { + View, + Text, + StyleSheet, + Modal, + TouchableOpacity, + Platform, +} from "react-native"; +import { BlurView } from "expo-blur"; +import { Ionicons } from "@expo/vector-icons"; +import { LinearGradient } from "expo-linear-gradient"; +import { colors, gradients, spacing, radius, typography, shadows, blur } from "../theme"; +import { authenticateWithBiometric, getBiometricType } from "../lib/biometricAuth"; + +interface BiometricLockScreenProps { + visible: boolean; + onUnlock: () => void; +} + +export function BiometricLockScreen({ visible, onUnlock }: BiometricLockScreenProps) { + const [biometricType, setBiometricType] = useState("Face ID"); + const [isAuthenticating, setIsAuthenticating] = useState(false); + + useEffect(() => { + const loadBiometricType = async () => { + const type = await getBiometricType(); + setBiometricType(type); + }; + loadBiometricType(); + }, []); + + useEffect(() => { + if (visible) { + // Auto-trigger authentication when the lock screen appears + handleAuthenticate(); + } + }, [visible]); + + const handleAuthenticate = async () => { + if (isAuthenticating) return; + + setIsAuthenticating(true); + try { + const success = await authenticateWithBiometric(); + if (success) { + onUnlock(); + } + } finally { + setIsAuthenticating(false); + } + }; + + const getBiometricIcon = () => { + if (biometricType === "Face ID" || biometricType === "Face Recognition") { + return "scan-outline"; + } + return "finger-print-outline"; + }; + + return ( + + + {Platform.OS === "ios" ? ( + + ) : ( + + )} + + + {/* Logo/Icon */} + + + + + + + Sauci is Locked + + Use {biometricType} to unlock + + + {/* Unlock Button */} + + + + + {isAuthenticating ? "Authenticating..." : `Unlock with ${biometricType}`} + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + androidBackground: { + backgroundColor: "rgba(13, 13, 26, 0.95)", + }, + content: { + alignItems: "center", + paddingHorizontal: spacing.xl, + }, + logoContainer: { + marginBottom: spacing.xl, + }, + logoGradient: { + width: 100, + height: 100, + borderRadius: 50, + justifyContent: "center", + alignItems: "center", + ...shadows.lg, + }, + title: { + ...typography.title1, + color: colors.text, + textAlign: "center", + marginBottom: spacing.sm, + }, + subtitle: { + ...typography.body, + color: colors.textSecondary, + textAlign: "center", + marginBottom: spacing.xl, + }, + unlockButton: { + borderRadius: radius.lg, + overflow: "hidden", + ...shadows.lg, + }, + unlockButtonGradient: { + flexDirection: "row", + alignItems: "center", + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + gap: spacing.md, + }, + unlockButtonText: { + ...typography.headline, + color: colors.text, + }, +}); diff --git a/apps/mobile/src/components/SwipeTutorial.tsx b/apps/mobile/src/components/SwipeTutorial.tsx new file mode 100644 index 0000000..a6108c1 --- /dev/null +++ b/apps/mobile/src/components/SwipeTutorial.tsx @@ -0,0 +1,689 @@ +import { useEffect, useState } from "react"; +import { View, Text, StyleSheet, Pressable, useWindowDimensions, Platform } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { BlurView } from "expo-blur"; +import { LinearGradient } from "expo-linear-gradient"; +import { Ionicons } from "@expo/vector-icons"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + withRepeat, + withSequence, + withDelay, + withSpring, + FadeIn, + FadeOut, + interpolate, + Easing, + runOnJS, +} from "react-native-reanimated"; +import { colors, gradients, radius, shadows, blur, typography, spacing, animations } from "../theme"; + +interface Props { + onComplete: () => void; +} + +interface TutorialStep { + key: string; + label: string; + description: string; + icon: string; + color: string; + gradient: [string, string]; + translateX: number; + translateY: number; + isSwipe: boolean; +} + +const STEPS: TutorialStep[] = [ + { + key: "right", + label: "YES", + description: "Swipe right if you're interested", + icon: "heart", + color: colors.success, + gradient: gradients.success as [string, string], + translateX: 80, + translateY: 0, + isSwipe: true, + }, + { + key: "left", + label: "NO", + description: "Swipe left if you're not interested", + icon: "close", + color: colors.error, + gradient: gradients.error as [string, string], + translateX: -80, + translateY: 0, + isSwipe: true, + }, + { + key: "up", + label: "MAYBE", + description: "Swipe up if you're unsure", + icon: "help", + color: colors.warning, + gradient: gradients.warning as [string, string], + translateX: 0, + translateY: -80, + isSwipe: true, + }, + { + key: "down", + label: "SKIP", + description: "Swipe down to skip for now", + icon: "arrow-down", + color: colors.textTertiary, + gradient: ["#6c757d", "#495057"], + translateX: 0, + translateY: 80, + isSwipe: true, + }, + { + key: "feedback", + label: "FEEDBACK", + description: "Tap the flag to report issues with a question", + icon: "flag-outline", + color: colors.primary, + gradient: gradients.primary as [string, string], + translateX: 0, + translateY: 0, + isSwipe: false, + }, +]; + +export default function SwipeTutorial({ onComplete }: Props) { + const { width: screenWidth, height: screenHeight } = useWindowDimensions(); + const insets = useSafeAreaInsets(); + const [currentStep, setCurrentStep] = useState(0); + + const cardTranslateX = useSharedValue(0); + const cardTranslateY = useSharedValue(0); + const cardRotation = useSharedValue(0); + const cardScale = useSharedValue(1); + const overlayOpacity = useSharedValue(0); + const pulseScale = useSharedValue(1); + const handOffset = useSharedValue(0); + + const currentStep_ = STEPS[currentStep]; + const isSwipeStep = currentStep_.isSwipe; + + useEffect(() => { + // Reset animations for current step + cardTranslateX.value = 0; + cardTranslateY.value = 0; + cardRotation.value = 0; + overlayOpacity.value = 0; + handOffset.value = 0; + + // Start the swipe demonstration animation + const animateSwipe = () => { + if (!isSwipeStep) { + // For non-swipe steps (feedback), just pulse the card + cardScale.value = withRepeat( + withSequence( + withTiming(1.02, { duration: 600 }), + withTiming(1, { duration: 600 }) + ), + -1, + true + ); + return; + } + + // Pulse the card first + cardScale.value = withSequence( + withTiming(1.02, { duration: 200 }), + withTiming(1, { duration: 200 }) + ); + + // Animate hand movement + handOffset.value = withRepeat( + withSequence( + withTiming(1, { duration: 800, easing: Easing.out(Easing.cubic) }), + withDelay(300, withTiming(0, { duration: 400 })) + ), + -1, + false + ); + + // Animate card swipe + cardTranslateX.value = withRepeat( + withSequence( + withDelay(200, withTiming(currentStep_.translateX * 0.7, { + duration: 600, + easing: Easing.out(Easing.cubic) + })), + withDelay(400, withSpring(0, animations.spring)) + ), + -1, + false + ); + + cardTranslateY.value = withRepeat( + withSequence( + withDelay(200, withTiming(currentStep_.translateY * 0.7, { + duration: 600, + easing: Easing.out(Easing.cubic) + })), + withDelay(400, withSpring(0, animations.spring)) + ), + -1, + false + ); + + // Rotate card for horizontal swipes + if (currentStep_.translateX !== 0) { + cardRotation.value = withRepeat( + withSequence( + withDelay(200, withTiming(currentStep_.translateX > 0 ? 8 : -8, { + duration: 600 + })), + withDelay(400, withSpring(0, animations.spring)) + ), + -1, + false + ); + } + + // Show overlay during swipe + overlayOpacity.value = withRepeat( + withSequence( + withDelay(400, withTiming(0.8, { duration: 400 })), + withDelay(200, withTiming(0, { duration: 400 })) + ), + -1, + false + ); + }; + + // Pulse animation for the indicator + pulseScale.value = withRepeat( + withSequence( + withTiming(1.2, { duration: 800 }), + withTiming(1, { duration: 800 }) + ), + -1, + true + ); + + const timeout = setTimeout(animateSwipe, 300); + return () => clearTimeout(timeout); + }, [currentStep]); + + const handleNext = () => { + if (currentStep < STEPS.length - 1) { + setCurrentStep(currentStep + 1); + } else { + onComplete(); + } + }; + + const handleSkip = () => { + onComplete(); + }; + + const cardAnimatedStyle = useAnimatedStyle(() => ({ + transform: [ + { translateX: cardTranslateX.value }, + { translateY: cardTranslateY.value }, + { rotate: `${cardRotation.value}deg` }, + { scale: cardScale.value }, + ], + })); + + const overlayAnimatedStyle = useAnimatedStyle(() => ({ + opacity: overlayOpacity.value, + })); + + const pulseAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: pulseScale.value }], + opacity: interpolate(pulseScale.value, [1, 1.2], [0.6, 0.2]), + })); + + const handAnimatedStyle = useAnimatedStyle(() => { + const progress = handOffset.value; + return { + transform: [ + { translateX: currentStep_.translateX * progress * 0.5 }, + { translateY: currentStep_.translateY * progress * 0.5 }, + ], + opacity: interpolate(progress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]), + }; + }); + + return ( + + + + {/* Skip button */} + + Skip + + + {/* Title */} + + How to Play + + Swipe to answer questions and find your matches + + + + {/* Demo Card Area */} + + {/* Direction indicators around the card - only for swipe steps */} + {isSwipeStep && ( + + {STEPS.filter(s => s.isSwipe).map((dir, index) => ( + + {index === currentStep && ( + + + + )} + + + + + ))} + + )} + + {/* Demo Card */} + + + {/* Feedback button indicator */} + + {!isSwipeStep && ( + + + + )} + + + + {/* Overlay - only for swipe steps */} + {isSwipeStep && ( + + + {currentStep_.label} + + + )} + + {/* Card content */} + + + + + + + Would you try a new restaurant together? + + + + + {/* Hand gesture indicator - only for swipe steps */} + {isSwipeStep && ( + + + + + + )} + + + + {/* Current instruction */} + + + + {currentStep_.label} + + {currentStep_.description} + + + {/* Progress dots */} + + {STEPS.map((_, index) => ( + + ))} + + + {/* Next button */} + [ + styles.nextButton, + pressed && styles.nextButtonPressed + ]}> + + + {currentStep < STEPS.length - 1 ? "Next" : "Got it!"} + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject, + zIndex: 1000, + }, + blur: { + flex: 1, + backgroundColor: 'rgba(13, 13, 26, 0.85)', + }, + content: { + flex: 1, + paddingHorizontal: spacing.lg, + paddingTop: 80, + alignItems: "center", + }, + skipButton: { + position: "absolute", + top: 60, + right: spacing.lg, + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + }, + skipText: { + ...typography.callout, + color: colors.textSecondary, + }, + title: { + ...typography.title1, + color: colors.text, + textAlign: "center", + marginBottom: spacing.sm, + }, + subtitle: { + ...typography.body, + color: colors.textSecondary, + textAlign: "center", + marginBottom: spacing.xl, + }, + demoArea: { + flex: 1, + justifyContent: "center", + alignItems: "center", + width: "100%", + maxHeight: 350, + }, + indicatorsContainer: { + position: "absolute", + width: 280, + height: 280, + }, + directionIndicator: { + position: "absolute", + alignItems: "center", + justifyContent: "center", + }, + indicatorRight: { + right: -20, + top: "50%", + marginTop: -24, + }, + indicatorLeft: { + left: -20, + top: "50%", + marginTop: -24, + }, + indicatorUp: { + top: -20, + left: "50%", + marginLeft: -24, + }, + indicatorDown: { + bottom: -20, + left: "50%", + marginLeft: -24, + }, + pulseRing: { + position: "absolute", + width: 64, + height: 64, + borderRadius: 32, + justifyContent: "center", + alignItems: "center", + }, + pulseInner: { + width: "100%", + height: "100%", + borderRadius: 32, + }, + indicatorBadge: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: "center", + alignItems: "center", + borderWidth: 1, + borderColor: colors.glass.border, + }, + indicatorActive: { + borderColor: "transparent", + }, + demoCard: { + width: 220, + height: 280, + borderRadius: radius.xl, + overflow: "visible", + }, + demoCardInner: { + flex: 1, + borderRadius: radius.xl, + borderWidth: 1, + borderColor: colors.glass.border, + overflow: "hidden", + }, + feedbackButtonDemo: { + position: 'absolute', + top: spacing.md, + right: spacing.md, + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: colors.glass.background, + justifyContent: 'center', + alignItems: 'center', + zIndex: 100, + borderWidth: 1, + borderColor: colors.glass.border, + }, + feedbackButtonHighlight: { + backgroundColor: colors.primary, + borderColor: colors.primary, + }, + feedbackPulse: { + position: "absolute", + width: 56, + height: 56, + borderRadius: 28, + justifyContent: "center", + alignItems: "center", + }, + demoOverlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: "center", + alignItems: "center", + zIndex: 10, + borderRadius: radius.xl, + }, + overlayBadge: { + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: radius.sm, + transform: [{ rotate: "-15deg" }], + }, + overlayText: { + fontSize: 24, + fontWeight: "900", + color: colors.text, + letterSpacing: 2, + }, + demoCardContent: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: spacing.lg, + }, + intensityDemo: { + flexDirection: "row", + backgroundColor: colors.primaryLight, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: radius.full, + gap: 2, + marginBottom: spacing.md, + }, + demoQuestion: { + ...typography.headline, + color: colors.text, + textAlign: "center", + }, + handIndicator: { + position: "absolute", + bottom: 40, + left: "50%", + marginLeft: -20, + }, + handCircle: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.primary, + justifyContent: "center", + alignItems: "center", + ...shadows.md, + }, + instructionContainer: { + alignItems: "center", + marginTop: spacing.xl, + marginBottom: spacing.lg, + }, + instructionBadge: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: spacing.lg, + paddingVertical: spacing.sm, + borderRadius: radius.full, + gap: spacing.sm, + marginBottom: spacing.md, + }, + instructionLabel: { + ...typography.headline, + color: colors.text, + fontWeight: "700", + }, + instructionText: { + ...typography.body, + color: colors.textSecondary, + textAlign: "center", + }, + dotsContainer: { + flexDirection: "row", + gap: spacing.sm, + marginBottom: spacing.xl, + }, + dot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.glass.border, + }, + dotActive: { + backgroundColor: colors.primary, + width: 24, + }, + dotCompleted: { + backgroundColor: colors.primary, + }, + nextButton: { + width: "100%", + maxWidth: 280, + borderRadius: radius.lg, + overflow: "hidden", + }, + nextButtonPressed: { + opacity: 0.9, + transform: [{ scale: 0.98 }], + }, + nextButtonGradient: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingVertical: spacing.md, + gap: spacing.sm, + }, + nextButtonText: { + ...typography.headline, + color: colors.text, + }, +}); diff --git a/apps/mobile/src/lib/biometricAuth.ts b/apps/mobile/src/lib/biometricAuth.ts new file mode 100644 index 0000000..66aed75 --- /dev/null +++ b/apps/mobile/src/lib/biometricAuth.ts @@ -0,0 +1,112 @@ +import * as LocalAuthentication from "expo-local-authentication"; +import * as SecureStore from "expo-secure-store"; +import { Platform } from "react-native"; + +const BIOMETRIC_ENABLED_KEY = "biometric_auth_enabled"; + +/** + * Check if biometric authentication (Face ID/Touch ID) is available on the device + */ +export async function isBiometricAvailable(): Promise { + if (Platform.OS === "web") { + return false; + } + + try { + const compatible = await LocalAuthentication.hasHardwareAsync(); + if (!compatible) return false; + + const enrolled = await LocalAuthentication.isEnrolledAsync(); + return enrolled; + } catch (error) { + console.error("Error checking biometric availability:", error); + return false; + } +} + +/** + * Get the type of biometric authentication available (Face ID, Touch ID, etc.) + */ +export async function getBiometricType(): Promise { + if (Platform.OS === "web") { + return "None"; + } + + try { + const types = await LocalAuthentication.supportedAuthenticationTypesAsync(); + + if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) { + return Platform.OS === "ios" ? "Face ID" : "Face Recognition"; + } + if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) { + return Platform.OS === "ios" ? "Touch ID" : "Fingerprint"; + } + if (types.includes(LocalAuthentication.AuthenticationType.IRIS)) { + return "Iris"; + } + + return "Biometric"; + } catch (error) { + console.error("Error getting biometric type:", error); + return "Biometric"; + } +} + +/** + * Check if biometric authentication is enabled by the user + */ +export async function isBiometricEnabled(): Promise { + if (Platform.OS === "web") { + return false; + } + + try { + const value = await SecureStore.getItemAsync(BIOMETRIC_ENABLED_KEY); + return value === "true"; + } catch (error) { + console.error("Error reading biometric setting:", error); + return false; + } +} + +/** + * Enable or disable biometric authentication + */ +export async function setBiometricEnabled(enabled: boolean): Promise { + if (Platform.OS === "web") { + return; + } + + try { + await SecureStore.setItemAsync(BIOMETRIC_ENABLED_KEY, enabled ? "true" : "false"); + } catch (error) { + console.error("Error saving biometric setting:", error); + throw error; + } +} + +/** + * Authenticate the user using biometrics + * @returns true if authentication was successful, false otherwise + */ +export async function authenticateWithBiometric(): Promise { + if (Platform.OS === "web") { + return true; + } + + try { + const biometricType = await getBiometricType(); + + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: `Unlock Sauci with ${biometricType}`, + cancelLabel: "Cancel", + disableDeviceFallback: false, // Allow passcode fallback + fallbackLabel: "Use Passcode", + }); + + return result.success; + } catch (error) { + console.error("Error during biometric authentication:", error); + return false; + } +} diff --git a/apps/mobile/src/lib/supabase.ts b/apps/mobile/src/lib/supabase.ts index e46519a..50729a1 100755 --- a/apps/mobile/src/lib/supabase.ts +++ b/apps/mobile/src/lib/supabase.ts @@ -1,14 +1,16 @@ import "react-native-url-polyfill/auto"; import { createClient } from "@supabase/supabase-js"; -import * as SecureStore from "expo-secure-store"; +import AsyncStorage from "@react-native-async-storage/async-storage"; import { Platform } from "react-native"; // In-memory storage fallback for SSR/Node.js environments const memoryStorage: Record = {}; -// SecureStore adapter for Supabase auth -const ExpoSecureStoreAdapter = { - getItem: (key: string) => { +// AsyncStorage adapter for Supabase auth +// Note: We use AsyncStorage instead of SecureStore because Supabase JWTs +// can exceed SecureStore's 2048 byte limit +const ExpoStorageAdapter = { + getItem: async (key: string): Promise => { if (Platform.OS === "web") { // Check if we're in a browser environment with localStorage if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") { @@ -17,9 +19,9 @@ const ExpoSecureStoreAdapter = { // Fallback for SSR/Node.js return memoryStorage[key] ?? null; } - return SecureStore.getItemAsync(key); + return AsyncStorage.getItem(key); }, - setItem: (key: string, value: string) => { + setItem: async (key: string, value: string): Promise => { if (Platform.OS === "web") { if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") { window.localStorage.setItem(key, value); @@ -29,9 +31,9 @@ const ExpoSecureStoreAdapter = { memoryStorage[key] = value; return; } - return SecureStore.setItemAsync(key, value); + await AsyncStorage.setItem(key, value); }, - removeItem: (key: string) => { + removeItem: async (key: string): Promise => { if (Platform.OS === "web") { if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") { window.localStorage.removeItem(key); @@ -41,7 +43,7 @@ const ExpoSecureStoreAdapter = { delete memoryStorage[key]; return; } - return SecureStore.deleteItemAsync(key); + await AsyncStorage.removeItem(key); }, }; @@ -53,7 +55,7 @@ console.log("DEBUG - Anon key preview:", supabaseAnonKey?.substring(0, 30) + ".. export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { - storage: ExpoSecureStoreAdapter, + storage: ExpoStorageAdapter, autoRefreshToken: true, persistSession: true, detectSessionInUrl: Platform.OS === "web", diff --git a/apps/mobile/src/lib/swipeTutorialSeen.ts b/apps/mobile/src/lib/swipeTutorialSeen.ts new file mode 100644 index 0000000..71300b0 --- /dev/null +++ b/apps/mobile/src/lib/swipeTutorialSeen.ts @@ -0,0 +1,51 @@ +import * as SecureStore from "expo-secure-store"; +import { Platform } from "react-native"; + +const STORAGE_KEY = "swipe_tutorial_seen"; + +async function getStorage(): Promise { + try { + let data: string | null = null; + + if (Platform.OS === "web") { + if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") { + data = window.localStorage.getItem(STORAGE_KEY); + } + } else { + data = await SecureStore.getItemAsync(STORAGE_KEY); + } + + return data === "true"; + } catch (error) { + console.error("Error reading swipe tutorial state:", error); + } + return false; +} + +async function setStorage(seen: boolean): Promise { + try { + const data = seen ? "true" : "false"; + + if (Platform.OS === "web") { + if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") { + window.localStorage.setItem(STORAGE_KEY, data); + } + } else { + await SecureStore.setItemAsync(STORAGE_KEY, data); + } + } catch (error) { + console.error("Error saving swipe tutorial state:", error); + } +} + +export async function hasSeenSwipeTutorial(): Promise { + return getStorage(); +} + +export async function markSwipeTutorialSeen(): Promise { + await setStorage(true); +} + +export async function resetSwipeTutorial(): Promise { + await setStorage(false); +} diff --git a/apps/mobile/src/store/index.ts b/apps/mobile/src/store/index.ts index 75719c5..e268611 100755 --- a/apps/mobile/src/store/index.ts +++ b/apps/mobile/src/store/index.ts @@ -243,7 +243,7 @@ interface PacksState { isLoading: boolean; fetchPacks: () => Promise; fetchEnabledPacks: () => Promise; - togglePack: (packId: string) => Promise; + togglePack: (packId: string) => Promise<{ success: boolean; reason?: string }>; clearPacks: () => void; } @@ -306,9 +306,11 @@ export const usePacksStore = create((set, get) => ({ set({ enabledPackIds: ids }); }, - togglePack: async (packId: string) => { + togglePack: async (packId: string): Promise<{ success: boolean; reason?: string }> => { const coupleId = useAuthStore.getState().user?.couple_id; - if (!coupleId) return; + if (!coupleId) { + return { success: false, reason: "no_couple" }; + } const isEnabled = get().enabledPackIds.includes(packId); const newValue = !isEnabled; @@ -332,7 +334,10 @@ export const usePacksStore = create((set, get) => ({ console.error("Error toggling pack:", error); // Revert by refetching get().fetchEnabledPacks(); + return { success: false, reason: "error" }; } + + return { success: true }; }, clearPacks: () => { diff --git a/apps/supabase/functions/send-message-notification/index.ts b/apps/supabase/functions/send-message-notification/index.ts index e68b461..38cc9cc 100644 --- a/apps/supabase/functions/send-message-notification/index.ts +++ b/apps/supabase/functions/send-message-notification/index.ts @@ -90,35 +90,21 @@ Deno.serve(async (req) => { ); } - // Get sender name - const { data: sender } = await supabase - .from("profiles") - .select("name") - .eq("id", sender_id) - .single(); - - // Get the most recent message from this sender in this match + // Get the most recent message ID for navigation const { data: message } = await supabase .from("messages") - .select("id, content, media_path") + .select("id") .eq("match_id", match_id) .eq("user_id", sender_id) .order("created_at", { ascending: false }) .limit(1) .single(); - const senderName = sender?.name || "Your partner"; - const messagePreview = message?.content - ? message.content.length > 100 - ? message.content.substring(0, 100) + "..." - : message.content - : "Sent an image"; - - // Build push message + // Build push message (privacy-friendly - no content in notification) const pushMessage: ExpoPushMessage = { to: recipient.push_token, - title: senderName, - body: messagePreview, + title: "New message", + body: "Your partner sent you a message", sound: "default", data: { type: "message", diff --git a/apps/supabase/functions/send-notification/index.ts b/apps/supabase/functions/send-notification/index.ts index 63a1936..2ef82b2 100755 --- a/apps/supabase/functions/send-notification/index.ts +++ b/apps/supabase/functions/send-notification/index.ts @@ -93,15 +93,13 @@ Deno.serve(async (req) => { .eq("id", match_id) .single(); - // Build push messages + // Build push messages (privacy-friendly - no content in notification) const messages: ExpoPushMessage[] = profiles .filter((p) => p.push_token) .map((profile) => ({ to: profile.push_token!, title: "It's a match! 💕", - body: match?.question?.text - ? `You both agreed: "${match.question.text.substring(0, 50)}..."` - : "You and your partner agreed on something new!", + body: "You and your partner matched on something new!", sound: "default", data: { match_id, type: "match" }, }));