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" },
}));