Update
No files matched your search
@@ -4,6 +4,8 @@
|
|||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<queries>
|
<queries>
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data android:scheme="sauci"/>
|
<data android:scheme="app.sauci"/>
|
||||||
<data android:scheme="com.sauci.app"/>
|
<data android:scheme="com.sauci.app"/>
|
||||||
<data android:scheme="exp+sauci"/>
|
<data android:scheme="exp+sauci"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 928 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 10 KiB |
@@ -35,7 +35,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
|||||||
# your application. You should enable this flag either if you want
|
# your application. You should enable this flag either if you want
|
||||||
# to write custom TurboModules/Fabric components OR use libraries that
|
# to write custom TurboModules/Fabric components OR use libraries that
|
||||||
# are providing them.
|
# are providing them.
|
||||||
newArchEnabled=true
|
newArchEnabled=false
|
||||||
|
|
||||||
# Use this property to enable or disable the Hermes JS engine.
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
# If set to false, you will be using JSC instead.
|
# If set to false, you will be using JSC instead.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"scheme": "sauci",
|
"scheme": "app.sauci",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": false,
|
"newArchEnabled": false,
|
||||||
"splash": {
|
"splash": {
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": [
|
||||||
"remote-notification"
|
"remote-notification"
|
||||||
]
|
],
|
||||||
|
"NSFaceIDUsageDescription": "Sauci uses Face ID to unlock the app and keep your conversations private."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
@@ -50,6 +51,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-secure-store",
|
"expo-secure-store",
|
||||||
|
[
|
||||||
|
"expo-local-authentication",
|
||||||
|
{
|
||||||
|
"faceIDPermission": "Sauci uses Face ID to unlock the app and keep your conversations private."
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"@sentry/react-native/expo",
|
"@sentry/react-native/expo",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Tabs, useRouter, useSegments } from "expo-router";
|
import { Tabs, useRouter, useSegments } from "expo-router";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
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 { BlurView } from "expo-blur";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { useAuthStore, useMatchStore, useMessageStore, useSubscriptionStore, usePacksStore } from "../../src/store";
|
import { useAuthStore, useMatchStore, useMessageStore, useSubscriptionStore, usePacksStore } from "../../src/store";
|
||||||
import { colors, gradients, blur, radius, spacing, typography, shadows } from "../../src/theme";
|
import { colors, gradients, blur, radius, spacing, typography, shadows } from "../../src/theme";
|
||||||
import { supabase } from "../../src/lib/supabase";
|
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 { MatchWithQuestion } from "../../src/types";
|
||||||
import type { Database } from "../../src/types/supabase";
|
import type { Database } from "../../src/types/supabase";
|
||||||
|
|
||||||
@@ -15,11 +17,14 @@ type Message = Database["public"]["Tables"]["messages"]["Row"];
|
|||||||
function TabBarBackground() {
|
function TabBarBackground() {
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
return (
|
return (
|
||||||
<BlurView
|
<View style={StyleSheet.absoluteFill}>
|
||||||
intensity={blur.heavy}
|
<BlurView
|
||||||
tint="systemChromeMaterialDark"
|
intensity={blur.heavy}
|
||||||
style={StyleSheet.absoluteFill}
|
tint="dark"
|
||||||
/>
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(22, 33, 62, 0.7)' }]} />
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(22, 33, 62, 0.9)' }]} />;
|
return <View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(22, 33, 62, 0.9)' }]} />;
|
||||||
@@ -72,6 +77,9 @@ export default function AppLayout() {
|
|||||||
const [matchNotification, setMatchNotification] = useState<MatchWithQuestion | null>(null);
|
const [matchNotification, setMatchNotification] = useState<MatchWithQuestion | null>(null);
|
||||||
const messageToastAnim = useRef(new Animated.Value(-100)).current;
|
const messageToastAnim = useRef(new Animated.Value(-100)).current;
|
||||||
const messageTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const messageTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
|
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
||||||
|
const hasCheckedInitialBiometric = useRef(false);
|
||||||
|
|
||||||
// Check if we're currently on the onboarding screen
|
// Check if we're currently on the onboarding screen
|
||||||
const isOnOnboarding = segments.includes("onboarding");
|
const isOnOnboarding = segments.includes("onboarding");
|
||||||
@@ -110,6 +118,49 @@ export default function AppLayout() {
|
|||||||
};
|
};
|
||||||
}, [signOut]);
|
}, [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
|
// Fetch unread message count on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.id) {
|
if (user?.id) {
|
||||||
@@ -451,6 +502,12 @@ export default function AppLayout() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Biometric Lock Screen */}
|
||||||
|
<BiometricLockScreen
|
||||||
|
visible={isLocked}
|
||||||
|
onUnlock={handleBiometricUnlock}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
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 { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme';
|
||||||
import { useAuthStore } from '../../src/store';
|
import { useAuthStore } from '../../src/store';
|
||||||
import { supabase } from '../../src/lib/supabase';
|
import { supabase } from '../../src/lib/supabase';
|
||||||
|
import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications';
|
||||||
import type { Gender } from '../../src/types';
|
import type { Gender } from '../../src/types';
|
||||||
|
|
||||||
const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
|
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' },
|
{ 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() {
|
export default function OnboardingScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -44,7 +46,7 @@ export default function OnboardingScreen() {
|
|||||||
const [stage, setStage] = useState<Stage>('name');
|
const [stage, setStage] = useState<Stage>('name');
|
||||||
const [name, setName] = useState(user?.name || '');
|
const [name, setName] = useState(user?.name || '');
|
||||||
const [gender, setGender] = useState<Gender | null>(user?.gender || null);
|
const [gender, setGender] = useState<Gender | null>(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 [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
@@ -62,7 +64,33 @@ export default function OnboardingScreen() {
|
|||||||
const handleGenderSelect = (selectedGender: Gender) => {
|
const handleGenderSelect = (selectedGender: Gender) => {
|
||||||
setGender(selectedGender);
|
setGender(selectedGender);
|
||||||
setError(null);
|
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 () => {
|
const handleComplete = async () => {
|
||||||
@@ -205,7 +233,19 @@ export default function OnboardingScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
{error && (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Ionicons name="alert-circle" size={16} color={colors.error} />
|
||||||
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<GlassButton onPress={handleGenderContinue} fullWidth size="lg">
|
||||||
|
Continue
|
||||||
|
</GlassButton>
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -307,13 +347,78 @@ export default function OnboardingScreen() {
|
|||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<GlassButton
|
<GlassButton
|
||||||
onPress={handleComplete}
|
onPress={handleExplicitContinue}
|
||||||
|
fullWidth
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</GlassButton>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'notifications':
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
key="notifications"
|
||||||
|
entering={SlideInRight.duration(400)}
|
||||||
|
exiting={FadeOut.duration(200)}
|
||||||
|
style={styles.stageContainer}
|
||||||
|
>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={gradients.primary as [string, string]}
|
||||||
|
style={styles.iconContainer}
|
||||||
|
>
|
||||||
|
<Ionicons name="notifications" size={40} color={colors.text} />
|
||||||
|
</LinearGradient>
|
||||||
|
<Text style={styles.title}>Stay in the Loop</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Get notified when you and your partner match on answers
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<GlassCard style={styles.card}>
|
||||||
|
<View style={styles.notificationFeature}>
|
||||||
|
<View style={styles.notificationIcon}>
|
||||||
|
<Ionicons name="heart" size={24} color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.notificationText}>
|
||||||
|
<Text style={styles.notificationTitle}>Match Alerts</Text>
|
||||||
|
<Text style={styles.notificationDescription}>
|
||||||
|
Know instantly when you both agree
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.notificationFeature}>
|
||||||
|
<View style={styles.notificationIcon}>
|
||||||
|
<Ionicons name="chatbubble" size={24} color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.notificationText}>
|
||||||
|
<Text style={styles.notificationTitle}>Messages</Text>
|
||||||
|
<Text style={styles.notificationDescription}>
|
||||||
|
Never miss a message from your partner
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</GlassCard>
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<GlassButton
|
||||||
|
onPress={handleEnableNotifications}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
Let's Go!
|
Enable Notifications
|
||||||
</GlassButton>
|
</GlassButton>
|
||||||
|
<Pressable
|
||||||
|
style={styles.skipButton}
|
||||||
|
onPress={handleComplete}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Text style={styles.skipText}>Maybe Later</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
@@ -325,13 +430,18 @@ export default function OnboardingScreen() {
|
|||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||||||
>
|
>
|
||||||
{/* Header with back button and progress dots */}
|
{/* Header with back button and progress dots */}
|
||||||
<Animated.View entering={FadeIn.duration(600)} style={styles.headerBar}>
|
<Animated.View entering={FadeIn.duration(600)} style={styles.headerBar}>
|
||||||
{stage !== 'name' ? (
|
{stage !== 'name' ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => setStage(stage === 'explicit' ? 'gender' : 'name')}
|
onPress={() => {
|
||||||
|
if (stage === 'notifications') setStage('explicit');
|
||||||
|
else if (stage === 'explicit') setStage('gender');
|
||||||
|
else setStage('name');
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -342,11 +452,18 @@ export default function OnboardingScreen() {
|
|||||||
<View style={[styles.progressDot, stage === 'name' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'name' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'gender' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'gender' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'explicit' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'explicit' && styles.progressDotActive]} />
|
||||||
|
<View style={[styles.progressDot, stage === 'notifications' && styles.progressDotActive]} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.backButtonPlaceholder} />
|
<View style={styles.backButtonPlaceholder} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{renderStage()}
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{renderStage()}
|
||||||
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
);
|
);
|
||||||
@@ -356,6 +473,9 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
scrollContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
headerBar: {
|
headerBar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -552,4 +672,41 @@ const styles = StyleSheet.create({
|
|||||||
footer: {
|
footer: {
|
||||||
marginTop: 'auto',
|
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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -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 { usePacksStore, useAuthStore, useSubscriptionStore } from "../../src/store";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
@@ -33,13 +33,23 @@ export default function PacksScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle toggling a pack - show teaser if premium pack without access
|
// 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) {
|
if (pack.is_premium && !hasPremiumAccess) {
|
||||||
setTeaserPack(pack);
|
setTeaserPack(pack);
|
||||||
setShowTeaser(true);
|
setShowTeaser(true);
|
||||||
return;
|
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]);
|
}, [hasPremiumAccess, togglePack]);
|
||||||
|
|
||||||
// Handle teaser unlock button - show paywall
|
// Handle teaser unlock button - show paywall
|
||||||
|
|||||||
@@ -204,6 +204,12 @@ export default function PairingScreen() {
|
|||||||
style={styles.container}
|
style={styles.container}
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
>
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={() => router.back()}
|
||||||
|
>
|
||||||
|
<Ionicons name="arrow-back" size={24} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.emoji}>🔗</Text>
|
<Text style={styles.emoji}>🔗</Text>
|
||||||
<Text style={styles.title}>Pair Up</Text>
|
<Text style={styles.title}>Pair Up</Text>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Platform, useWindowDimensions, TextInput, Modal, ActivityIndicator, Linking, Switch } from "react-native";
|
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 { useAuthStore, useMatchStore, useMessageStore, usePacksStore, useSubscriptionStore } from "../../src/store";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||||
import { supabase } from "../../src/lib/supabase";
|
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 { router } from "expo-router";
|
||||||
import { GradientBackground, GlassCard } from "../../src/components/ui";
|
import { GradientBackground, GlassCard } from "../../src/components/ui";
|
||||||
import { FeedbackModal } from "../../src/components/FeedbackModal";
|
import { FeedbackModal } from "../../src/components/FeedbackModal";
|
||||||
@@ -29,12 +36,34 @@ export default function ProfileScreen() {
|
|||||||
const [showPaywall, setShowPaywall] = useState(false);
|
const [showPaywall, setShowPaywall] = useState(false);
|
||||||
const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content ?? false);
|
const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content ?? false);
|
||||||
const [isUpdatingExplicit, setIsUpdatingExplicit] = useState(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
|
// Check if user or partner has premium access
|
||||||
const hasPremiumAccess = useMemo(() => {
|
const hasPremiumAccess = useMemo(() => {
|
||||||
return user?.is_premium || partner?.is_premium || subscription.isProUser;
|
return user?.is_premium || partner?.is_premium || subscription.isProUser;
|
||||||
}, [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)
|
// Check if it's the user's own subscription (vs partner's)
|
||||||
const isOwnSubscription = user?.is_premium && subscription.isProUser;
|
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 = () => {
|
const handleManageSubscription = () => {
|
||||||
Linking.openURL("https://apps.apple.com/account/subscriptions");
|
Linking.openURL("https://apps.apple.com/account/subscriptions");
|
||||||
};
|
};
|
||||||
@@ -393,6 +437,39 @@ export default function ProfileScreen() {
|
|||||||
ios_backgroundColor={colors.glass.border}
|
ios_backgroundColor={colors.glass.border}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
{biometricAvailable && (
|
||||||
|
<>
|
||||||
|
<View style={styles.preferencesDivider} />
|
||||||
|
<View style={styles.rowContainer}>
|
||||||
|
<View style={styles.rowLeft}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={gradients.primary as [string, string]}
|
||||||
|
style={styles.partnerIconGradient}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={biometricType === "Face ID" || biometricType === "Face Recognition" ? "scan-outline" : "finger-print-outline"}
|
||||||
|
size={20}
|
||||||
|
color={colors.text}
|
||||||
|
/>
|
||||||
|
</LinearGradient>
|
||||||
|
<View style={styles.rowTextContainer}>
|
||||||
|
<Text style={styles.rowValue}>{biometricType}</Text>
|
||||||
|
<Text style={styles.rowLabel}>Require unlock when opening app</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={biometricEnabled}
|
||||||
|
onValueChange={handleBiometricToggle}
|
||||||
|
disabled={isUpdatingBiometric}
|
||||||
|
trackColor={{ false: colors.glass.border, true: colors.primaryLight }}
|
||||||
|
thumbColor={biometricEnabled ? colors.primary : colors.textTertiary}
|
||||||
|
ios_backgroundColor={colors.glass.border}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
@@ -452,6 +529,39 @@ export default function ProfileScreen() {
|
|||||||
</GlassCard>
|
</GlassCard>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Debug Section - Only show in development builds */}
|
||||||
|
{__DEV__ && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.delay(500).duration(500)}
|
||||||
|
style={styles.section}
|
||||||
|
>
|
||||||
|
<Text style={styles.debugSectionTitle}>Debug</Text>
|
||||||
|
<GlassCard style={styles.debugCard}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.rowContainer}
|
||||||
|
onPress={async () => {
|
||||||
|
await resetSwipeTutorial();
|
||||||
|
Alert.alert("Success", "Swipe tutorial has been reset. It will show again next time you visit the swipe screen.");
|
||||||
|
}}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.rowLeft}>
|
||||||
|
<View style={styles.debugIcon}>
|
||||||
|
<Ionicons name="refresh" size={20} color={colors.warning} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.rowTextContainer}>
|
||||||
|
<Text style={styles.rowValue}>Reset Swipe Tutorial</Text>
|
||||||
|
<Text style={styles.rowLabel}>Show the tutorial again</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.chevronContainer}>
|
||||||
|
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</GlassCard>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Danger Zone - Only show if in a couple */}
|
{/* Danger Zone - Only show if in a couple */}
|
||||||
{couple && (
|
{couple && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
@@ -722,6 +832,11 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.textTertiary,
|
color: colors.textTertiary,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
|
preferencesDivider: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: colors.glass.border,
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
},
|
||||||
unlinkButton: {
|
unlinkButton: {
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
@@ -810,6 +925,27 @@ const styles = StyleSheet.create({
|
|||||||
bottomSpacer: {
|
bottomSpacer: {
|
||||||
height: spacing.lg,
|
height: spacing.lg,
|
||||||
},
|
},
|
||||||
|
// Debug styles
|
||||||
|
debugSectionTitle: {
|
||||||
|
...typography.caption1,
|
||||||
|
color: colors.warning,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
marginLeft: spacing.xs,
|
||||||
|
},
|
||||||
|
debugCard: {
|
||||||
|
borderColor: 'rgba(243, 156, 18, 0.3)',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
debugIcon: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 22,
|
||||||
|
backgroundColor: colors.warningLight,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
// Danger Zone styles
|
// Danger Zone styles
|
||||||
dangerSectionTitle: {
|
dangerSectionTitle: {
|
||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import Animated, { FadeIn, FadeInUp } from "react-native-reanimated";
|
import Animated, { FadeIn, FadeInUp } from "react-native-reanimated";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { supabase } from "../../src/lib/supabase";
|
import { supabase } from "../../src/lib/supabase";
|
||||||
import { usePacksStore } from "../../src/store";
|
import { usePacksStore, useAuthStore } from "../../src/store";
|
||||||
import { skipQuestion, getSkippedQuestionIds } from "../../src/lib/skippedQuestions";
|
import { skipQuestion, getSkippedQuestionIds } from "../../src/lib/skippedQuestions";
|
||||||
|
import { hasSeenSwipeTutorial, markSwipeTutorialSeen } from "../../src/lib/swipeTutorialSeen";
|
||||||
import SwipeCard from "../../src/components/SwipeCard";
|
import SwipeCard from "../../src/components/SwipeCard";
|
||||||
|
import SwipeTutorial from "../../src/components/SwipeTutorial";
|
||||||
import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
|
import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
|
||||||
import { colors, gradients, spacing, typography, radius, shadows } from "../../src/theme";
|
import { colors, gradients, spacing, typography, radius, shadows } from "../../src/theme";
|
||||||
|
|
||||||
@@ -16,12 +18,27 @@ export default function SwipeScreen() {
|
|||||||
const [questions, setQuestions] = useState<any[]>([]);
|
const [questions, setQuestions] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [showTutorial, setShowTutorial] = useState(false);
|
||||||
const { enabledPackIds, fetchPacks } = usePacksStore();
|
const { enabledPackIds, fetchPacks } = usePacksStore();
|
||||||
|
const { partner, couple } = useAuthStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPacks().then(() => fetchQuestions());
|
fetchPacks().then(() => fetchQuestions());
|
||||||
|
checkTutorial();
|
||||||
}, [packId]);
|
}, [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
|
// Filter out questions from disabled packs immediately when enabledPackIds changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!packId && enabledPackIds.length > 0 && questions.length > 0) {
|
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 (
|
||||||
|
<GradientBackground showAccent>
|
||||||
|
<View style={styles.centerContainer}>
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInUp.duration(600).springify()}
|
||||||
|
style={styles.emptyContent}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={gradients.primary as [string, string]}
|
||||||
|
style={styles.pairIconContainer}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
>
|
||||||
|
<Ionicons name="heart" size={48} color={colors.text} />
|
||||||
|
</LinearGradient>
|
||||||
|
<Text style={styles.pairTitle}>
|
||||||
|
{couple ? "Waiting for your partner" : "Pair with your partner"}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.emptySubtitle}>
|
||||||
|
{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."
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
<GlassButton
|
||||||
|
onPress={() => router.push("/pairing")}
|
||||||
|
style={{ marginTop: spacing.xl }}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{couple ? "View Invite Code" : "Pair Now"}
|
||||||
|
</GlassButton>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</GradientBackground>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Show "no packs enabled" message when not viewing a specific pack and no packs are enabled
|
// Show "no packs enabled" message when not viewing a specific pack and no packs are enabled
|
||||||
if (!packId && enabledPackIds.length === 0) {
|
if (!packId && enabledPackIds.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -238,6 +294,11 @@ export default function SwipeScreen() {
|
|||||||
{/* Bottom spacing for tab bar */}
|
{/* Bottom spacing for tab bar */}
|
||||||
<View style={styles.bottomSpacer} />
|
<View style={styles.bottomSpacer} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Tutorial Overlay */}
|
||||||
|
{showTutorial && (
|
||||||
|
<SwipeTutorial onComplete={handleTutorialComplete} />
|
||||||
|
)}
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -318,6 +379,21 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: spacing.lg,
|
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: {
|
noPacksIconContainer: {
|
||||||
width: 120,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ initSentry();
|
|||||||
|
|
||||||
// Import push notification utilities
|
// Import push notification utilities
|
||||||
import {
|
import {
|
||||||
registerForPushNotificationsAsync,
|
|
||||||
savePushToken,
|
|
||||||
clearPushToken,
|
clearPushToken,
|
||||||
setupNotificationResponseListener,
|
setupNotificationResponseListener,
|
||||||
getInitialNotification,
|
getInitialNotification,
|
||||||
@@ -54,19 +52,78 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
// Handle initial URL if app was opened via deep link
|
// Handle initial URL if app was opened via deep link
|
||||||
const handleDeepLink = async (url: string | null) => {
|
const handleDeepLink = async (url: string | null) => {
|
||||||
if (url) {
|
console.log('[DeepLink] handleDeepLink called with URL:', url);
|
||||||
// Supabase needs to handle the URL to extract session
|
if (!url) return;
|
||||||
// getSession() will automatically check window.location on web if detectSessionInUrl is true
|
|
||||||
// For native, we rely on onAuthStateChange or manual processing if needed
|
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();
|
await supabase.auth.getSession();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get initial URL
|
// 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
|
// Listen for new deep links while app is open
|
||||||
const linkingSubscription = Linking.addEventListener("url", (event) => {
|
const linkingSubscription = Linking.addEventListener("url", (event) => {
|
||||||
|
console.log('[DeepLink] URL event listener triggered with:', event.url);
|
||||||
handleDeepLink(event.url);
|
handleDeepLink(event.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,17 +140,13 @@ export default function RootLayout() {
|
|||||||
// Listen for auth changes
|
// Listen for auth changes
|
||||||
const { data: { subscription: authSubscription } } = supabase.auth.onAuthStateChange(
|
const { data: { subscription: authSubscription } } = supabase.auth.onAuthStateChange(
|
||||||
async (event, session) => {
|
async (event, session) => {
|
||||||
|
console.log('[Auth] onAuthStateChange:', event, 'hasSession:', !!session, 'userId:', session?.user?.id);
|
||||||
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
|
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
|
||||||
|
console.log('[Auth] Calling fetchUser...');
|
||||||
fetchUser();
|
fetchUser();
|
||||||
// Set Sentry user context for error tracking
|
// Set Sentry user context for error tracking
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
setUserContext(session.user.id, session.user.email);
|
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") {
|
} else if (event === "SIGNED_OUT") {
|
||||||
// Clear push token before clearing user state
|
// Clear push token before clearing user state
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ PODS:
|
|||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoLinking (7.0.5):
|
- ExpoLinking (7.0.5):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- ExpoLocalAuthentication (17.0.8):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoModulesCore (2.2.3):
|
- ExpoModulesCore (2.2.3):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
@@ -1786,6 +1788,8 @@ PODS:
|
|||||||
- React-perflogger (= 0.76.5)
|
- React-perflogger (= 0.76.5)
|
||||||
- React-utils (= 0.76.5)
|
- React-utils (= 0.76.5)
|
||||||
- RevenueCat (5.51.1)
|
- RevenueCat (5.51.1)
|
||||||
|
- RNCAsyncStorage (2.2.0):
|
||||||
|
- React-Core
|
||||||
- RNGestureHandler (2.20.2):
|
- RNGestureHandler (2.20.2):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
@@ -1972,6 +1976,7 @@ DEPENDENCIES:
|
|||||||
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
||||||
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
|
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
|
||||||
- ExpoLinking (from `../node_modules/expo-linking/ios`)
|
- ExpoLinking (from `../node_modules/expo-linking/ios`)
|
||||||
|
- ExpoLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
|
||||||
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
||||||
- ExpoSecureStore (from `../node_modules/expo-secure-store/ios`)
|
- ExpoSecureStore (from `../node_modules/expo-secure-store/ios`)
|
||||||
- EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`)
|
- EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`)
|
||||||
@@ -2038,6 +2043,7 @@ DEPENDENCIES:
|
|||||||
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
|
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
|
||||||
- ReactCodegen (from `build/generated/ios`)
|
- ReactCodegen (from `build/generated/ios`)
|
||||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
- 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`)
|
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||||
- RNPurchases (from `../node_modules/react-native-purchases`)
|
- RNPurchases (from `../node_modules/react-native-purchases`)
|
||||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||||
@@ -2103,6 +2109,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/expo-linear-gradient/ios"
|
:path: "../node_modules/expo-linear-gradient/ios"
|
||||||
ExpoLinking:
|
ExpoLinking:
|
||||||
:path: "../node_modules/expo-linking/ios"
|
:path: "../node_modules/expo-linking/ios"
|
||||||
|
ExpoLocalAuthentication:
|
||||||
|
:path: "../node_modules/expo-local-authentication/ios"
|
||||||
ExpoModulesCore:
|
ExpoModulesCore:
|
||||||
:path: "../node_modules/expo-modules-core"
|
:path: "../node_modules/expo-modules-core"
|
||||||
ExpoSecureStore:
|
ExpoSecureStore:
|
||||||
@@ -2232,6 +2240,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: build/generated/ios
|
:path: build/generated/ios
|
||||||
ReactCommon:
|
ReactCommon:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../node_modules/react-native/ReactCommon"
|
||||||
|
RNCAsyncStorage:
|
||||||
|
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||||
RNGestureHandler:
|
RNGestureHandler:
|
||||||
:path: "../node_modules/react-native-gesture-handler"
|
:path: "../node_modules/react-native-gesture-handler"
|
||||||
RNPurchases:
|
RNPurchases:
|
||||||
@@ -2271,6 +2281,7 @@ SPEC CHECKSUMS:
|
|||||||
ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680
|
ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680
|
||||||
ExpoLinearGradient: 35ebd83b16f80b3add053a2fd68cc328ed927f60
|
ExpoLinearGradient: 35ebd83b16f80b3add053a2fd68cc328ed927f60
|
||||||
ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402
|
ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402
|
||||||
|
ExpoLocalAuthentication: 8a31808565da7af926dd9b595e98594d8b1553b6
|
||||||
ExpoModulesCore: 15f60d00e33ca0cea27147543877ca6e70c42ef5
|
ExpoModulesCore: 15f60d00e33ca0cea27147543877ca6e70c42ef5
|
||||||
ExpoSecureStore: 9a3665d7161dd1031be386e278bffd623658f316
|
ExpoSecureStore: 9a3665d7161dd1031be386e278bffd623658f316
|
||||||
EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db
|
EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db
|
||||||
@@ -2337,6 +2348,7 @@ SPEC CHECKSUMS:
|
|||||||
ReactCodegen: 3a68408bf68d0957abcd13d610f76420005c1d91
|
ReactCodegen: 3a68408bf68d0957abcd13d610f76420005c1d91
|
||||||
ReactCommon: 5809a8ee421b7219221a475b78180f8f34b5c5ec
|
ReactCommon: 5809a8ee421b7219221a475b78180f8f34b5c5ec
|
||||||
RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0
|
RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0
|
||||||
|
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
||||||
RNGestureHandler: 27a63f2218affdf1a426d56682f9b174904838b3
|
RNGestureHandler: 27a63f2218affdf1a426d56682f9b174904838b3
|
||||||
RNPurchases: cc3333cede6c0f040545a95c38ae627d0174c058
|
RNPurchases: cc3333cede6c0f040545a95c38ae627d0174c058
|
||||||
RNReanimated: 66cf0f600a26d2b5e74c6e0b1c77c1ab1f62fc05
|
RNReanimated: 66cf0f600a26d2b5e74c6e0b1c77c1ab1f62fc05
|
||||||
|
|||||||
@@ -11,26 +11,26 @@
|
|||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
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 */; };
|
96905EF65AED1B983A6B3ABC /* libPods-Sauci.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */; };
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
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 */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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 = "<group>"; };
|
||||||
13B07F961A680F5B00A75B9A /* Sauci.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sauci.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 = "<group>"; };
|
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Sauci/AppDelegate.h; sourceTree = "<group>"; };
|
||||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Sauci/AppDelegate.mm; sourceTree = "<group>"; };
|
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Sauci/AppDelegate.mm; sourceTree = "<group>"; };
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = "<group>"; };
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = "<group>"; };
|
||||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sauci/main.m; sourceTree = "<group>"; };
|
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sauci/main.m; sourceTree = "<group>"; };
|
||||||
1C9767CAD348ACB05A4870BE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
426EFCCADE400D3F5060F5FE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
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 = "<group>"; };
|
46CB71562F0334FF008AC4FC /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||||
46CB71542F030871008AC4FC /* 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; };
|
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
96905EF65AED1B983A6B3ABC /* libPods-Sauci.a in Frameworks */,
|
96905EF65AED1B983A6B3ABC /* libPods-Sauci.a in Frameworks */,
|
||||||
46CB71552F030871008AC4FC /* StoreKit.framework in Frameworks */,
|
46CB71572F0334FF008AC4FC /* StoreKit.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||||
13B07FB71A68108700A75B9A /* main.m */,
|
13B07FB71A68108700A75B9A /* main.m */,
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||||
63D24AC64FB84C3AB9F4302E /* noop-file.swift */,
|
6AA143F69E734D11B2959507 /* noop-file.swift */,
|
||||||
1E5EDAB25D1C4414926F678F /* Sauci-Bridging-Header.h */,
|
08DDB0F6A9A146CE84A39C1C /* Sauci-Bridging-Header.h */,
|
||||||
1C9767CAD348ACB05A4870BE /* PrivacyInfo.xcprivacy */,
|
426EFCCADE400D3F5060F5FE /* PrivacyInfo.xcprivacy */,
|
||||||
);
|
);
|
||||||
name = Sauci;
|
name = Sauci;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
46CB71542F030871008AC4FC /* StoreKit.framework */,
|
46CB71562F0334FF008AC4FC /* StoreKit.framework */,
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */,
|
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */,
|
||||||
);
|
);
|
||||||
@@ -151,14 +151,14 @@
|
|||||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */;
|
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||||
6F4D4033C27C2AC6612A535A /* [Expo] Configure project */,
|
3F25679DC28AE5BB78AFEBF5 /* [Expo] Configure project */,
|
||||||
13B07F871A680F5B00A75B9A /* Sources */,
|
13B07F871A680F5B00A75B9A /* Sources */,
|
||||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||||
6B8CABAC69F8483D9920303C /* Upload Debug Symbols to Sentry */,
|
2269DDED21E54A6284DF755C /* Upload Debug Symbols to Sentry */,
|
||||||
A435DFC7C52C70693FBE0CD3 /* [CP] Embed Pods Frameworks */,
|
F51873069048543029DDD07B /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||||
B6ADEA6ED390C90F199467A5 /* PrivacyInfo.xcprivacy in Resources */,
|
753494A6E21B10482BBED79D /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
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";
|
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;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
6B8CABAC69F8483D9920303C /* Upload Debug Symbols to Sentry */ = {
|
2269DDED21E54A6284DF755C /* Upload Debug Symbols to Sentry */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
shellPath = /bin/sh;
|
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'\"`";
|
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;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -301,6 +301,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.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-Core/React-Core_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.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}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.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}/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-Core_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.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";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
A435DFC7C52C70693FBE0CD3 /* [CP] Embed Pods Frameworks */ = {
|
F51873069048543029DDD07B /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -361,7 +363,7 @@
|
|||||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||||
E6717E3EEE9541E7BCC0E9B8 /* noop-file.swift in Sources */,
|
7BA3DCB069BE4EDFB54CC861 /* noop-file.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 510 B |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 605 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 909 B |
|
Before Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 28 KiB |