Update
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"extra": {
|
||||||
|
"eas": {
|
||||||
|
"projectId": "807ca0af-e9bd-40e8-a658-f3382072c724"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"owner": "jjbrunton-apps"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ android {
|
|||||||
applicationId 'com.sauci.app'
|
applicationId 'com.sauci.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 3
|
versionCode 1
|
||||||
versionName "1.0.0"
|
versionName "1.0.0"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 10 KiB |
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/splash-icon.png",
|
"foregroundImage": "./assets/adaptive-icon-foreground.png",
|
||||||
"backgroundColor": "#1a1a2e"
|
"backgroundColor": "#1a1a2e"
|
||||||
},
|
},
|
||||||
"package": "com.sauci.app",
|
"package": "com.sauci.app",
|
||||||
|
|||||||
@@ -149,12 +149,10 @@ export default function AppLayout() {
|
|||||||
router.push('/(app)/swipe');
|
router.push('/(app)/swipe');
|
||||||
break;
|
break;
|
||||||
case 'dares':
|
case 'dares':
|
||||||
// TODO: Navigate to dares screen
|
router.push('/(app)/dares');
|
||||||
console.log('Dares pressed - not yet implemented');
|
|
||||||
break;
|
break;
|
||||||
case 'quiz':
|
case 'quiz':
|
||||||
// TODO: Navigate to quiz screen
|
router.push('/(app)/quiz');
|
||||||
console.log('Quiz pressed - not yet implemented');
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [router, closeRadialMenu]);
|
}, [router, closeRadialMenu]);
|
||||||
@@ -609,6 +607,18 @@ export default function AppLayout() {
|
|||||||
href: null, // Hide from tab bar
|
href: null, // Hide from tab bar
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="dares"
|
||||||
|
options={{
|
||||||
|
href: null, // Hide from tab bar
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="quiz"
|
||||||
|
options={{
|
||||||
|
href: null, // Hide from tab bar
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Radial Menu Overlay */}
|
{/* Radial Menu Overlay */}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { View, Text, StyleSheet, Platform } from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import Animated, { FadeInDown, FadeIn } from "react-native-reanimated";
|
||||||
|
import { GradientBackground } from "../../src/components/ui";
|
||||||
|
import { colors, spacing, radius, typography } from "../../src/theme";
|
||||||
|
|
||||||
|
const ACCENT_COLOR = colors.premium.gold;
|
||||||
|
|
||||||
|
export default function DaresScreen() {
|
||||||
|
return (
|
||||||
|
<GradientBackground>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(600).springify()}
|
||||||
|
style={styles.content}
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<Ionicons name="flame" size={36} color={ACCENT_COLOR} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title section */}
|
||||||
|
<Text style={styles.label}>ADVENTURE</Text>
|
||||||
|
<Text style={styles.title}>Dares</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Coming soon badge */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.badge}
|
||||||
|
>
|
||||||
|
<Text style={styles.badgeText}>ARRIVING SOON</Text>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Text style={styles.description}>
|
||||||
|
Push your boundaries together with tantalising challenges designed to ignite passion and create unforgettable moments.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Feature hints */}
|
||||||
|
<View style={styles.features}>
|
||||||
|
<View style={styles.featureItem}>
|
||||||
|
<Ionicons name="sparkles" size={16} color={ACCENT_COLOR} />
|
||||||
|
<Text style={styles.featureText}>Provocative challenges</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.featureItem}>
|
||||||
|
<Ionicons name="thermometer-outline" size={16} color={ACCENT_COLOR} />
|
||||||
|
<Text style={styles.featureText}>Multiple intensity levels</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.featureItem}>
|
||||||
|
<Ionicons name="people-outline" size={16} color={ACCENT_COLOR} />
|
||||||
|
<Text style={styles.featureText}>Designed for two</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom teaser */}
|
||||||
|
<Text style={styles.teaser}>Are you ready to play?</Text>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</GradientBackground>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingTop: Platform.OS === 'ios' ? 60 : 40,
|
||||||
|
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
borderRadius: 36,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: ACCENT_COLOR,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...typography.largeTitle,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.lg,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: ACCENT_COLOR,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: ACCENT_COLOR,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
featureItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
featureText: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.text,
|
||||||
|
marginLeft: spacing.sm,
|
||||||
|
},
|
||||||
|
teaser: {
|
||||||
|
...typography.footnote,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
color: colors.textTertiary,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { View, Text, StyleSheet, Platform } from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import Animated, { FadeInDown, FadeIn } from "react-native-reanimated";
|
||||||
|
import { GradientBackground } from "../../src/components/ui";
|
||||||
|
import { colors, spacing, radius, typography } from "../../src/theme";
|
||||||
|
|
||||||
|
const ACCENT_COLOR = colors.premium.rose;
|
||||||
|
|
||||||
|
export default function QuizScreen() {
|
||||||
|
return (
|
||||||
|
<GradientBackground>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(600).springify()}
|
||||||
|
style={styles.content}
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<Ionicons name="heart" size={36} color={ACCENT_COLOR} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title section */}
|
||||||
|
<Text style={styles.label}>INTIMACY</Text>
|
||||||
|
<Text style={styles.title}>Quiz</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.separator}>
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
<View style={styles.separatorDiamond} />
|
||||||
|
<View style={styles.separatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Coming soon badge */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.badge}
|
||||||
|
>
|
||||||
|
<Text style={styles.badgeText}>ARRIVING SOON</Text>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Text style={styles.description}>
|
||||||
|
Discover the depths of your connection through carefully crafted questions that reveal how well you truly know each other.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Feature hints */}
|
||||||
|
<View style={styles.features}>
|
||||||
|
<View style={styles.featureItem}>
|
||||||
|
<Ionicons name="sparkles" size={16} color={ACCENT_COLOR} />
|
||||||
|
<Text style={styles.featureText}>Uncover hidden desires</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.featureItem}>
|
||||||
|
<Ionicons name="flame-outline" size={16} color={ACCENT_COLOR} />
|
||||||
|
<Text style={styles.featureText}>Deepen your bond</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.featureItem}>
|
||||||
|
<Ionicons name="ribbon-outline" size={16} color={ACCENT_COLOR} />
|
||||||
|
<Text style={styles.featureText}>Track your journey</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom teaser */}
|
||||||
|
<Text style={styles.teaser}>Something special is being crafted for you</Text>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</GradientBackground>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingTop: Platform.OS === 'ios' ? 60 : 40,
|
||||||
|
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
borderRadius: 36,
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(232, 164, 174, 0.2)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: ACCENT_COLOR,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...typography.largeTitle,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.lg,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
separatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.3)',
|
||||||
|
},
|
||||||
|
separatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: ACCENT_COLOR,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.1)',
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(232, 164, 174, 0.2)',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: ACCENT_COLOR,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
featureItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
featureText: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.text,
|
||||||
|
marginLeft: spacing.sm,
|
||||||
|
},
|
||||||
|
teaser: {
|
||||||
|
...typography.footnote,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
color: colors.textTertiary,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -270,34 +270,68 @@ export default function SwipeScreen() {
|
|||||||
|
|
||||||
// Show "pair with partner" prompt when user doesn't have a partner yet
|
// Show "pair with partner" prompt when user doesn't have a partner yet
|
||||||
if (!partner) {
|
if (!partner) {
|
||||||
|
const PAIR_ACCENT = colors.premium.rose;
|
||||||
return (
|
return (
|
||||||
<GradientBackground showAccent>
|
<GradientBackground>
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInUp.duration(600).springify()}
|
entering={FadeInUp.duration(600).springify()}
|
||||||
style={styles.emptyContent}
|
style={styles.waitingContent}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
{/* Icon */}
|
||||||
colors={gradients.primary as [string, string]}
|
<View style={styles.waitingIconContainer}>
|
||||||
style={styles.pairIconContainer}
|
<Ionicons name="heart" size={36} color={PAIR_ACCENT} />
|
||||||
start={{ x: 0, y: 0 }}
|
</View>
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
|
{/* Title section */}
|
||||||
|
<Text style={styles.waitingLabel}>{couple ? "ALMOST THERE" : "CONNECT"}</Text>
|
||||||
|
<Text style={styles.waitingTitle}>{couple ? "Waiting" : "Pair Up"}</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.waitingSeparator}>
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
<View style={styles.waitingSeparatorDiamond} />
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status badge */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.waitingBadge}
|
||||||
>
|
>
|
||||||
<Ionicons name="heart" size={48} color={colors.text} />
|
<Text style={styles.waitingBadgeText}>{couple ? "INVITE SENT" : "MADE FOR TWO"}</Text>
|
||||||
</LinearGradient>
|
</Animated.View>
|
||||||
<Text style={styles.pairTitle}>
|
|
||||||
{couple ? "Waiting for your partner" : "Pair with your partner"}
|
{/* Description */}
|
||||||
</Text>
|
<Text style={styles.waitingDescription}>
|
||||||
<Text style={styles.emptySubtitle}>
|
|
||||||
{couple
|
{couple
|
||||||
? "Share your invite code so they can join you. Once paired, you'll both answer questions and discover what you agree on!"
|
? "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."
|
: "Sauci is made for two! Connect with your partner to start answering questions together and discover what you have in common."
|
||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Feature hints */}
|
||||||
|
<View style={styles.waitingFeatures}>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="lock-closed-outline" size={16} color={PAIR_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Private and secure</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="sparkles" size={16} color={PAIR_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Discover hidden desires</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="chatbubble-ellipses-outline" size={16} color={PAIR_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Chat about your matches</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom teaser */}
|
||||||
|
<Text style={styles.waitingTeaser}>{couple ? "Your partner is just a code away" : "Begin your intimate journey together"}</Text>
|
||||||
|
|
||||||
<GlassButton
|
<GlassButton
|
||||||
onPress={() => router.push("/pairing")}
|
onPress={() => router.push("/pairing")}
|
||||||
style={{ marginTop: spacing.xl }}
|
style={{ marginTop: spacing.lg }}
|
||||||
size="lg"
|
|
||||||
>
|
>
|
||||||
{couple ? "View Invite Code" : "Pair Now"}
|
{couple ? "View Invite Code" : "Pair Now"}
|
||||||
</GlassButton>
|
</GlassButton>
|
||||||
@@ -309,27 +343,62 @@ export default function SwipeScreen() {
|
|||||||
|
|
||||||
// Show "waiting for partner" message when user is too far ahead
|
// Show "waiting for partner" message when user is too far ahead
|
||||||
if (isBlocked && gapInfo) {
|
if (isBlocked && gapInfo) {
|
||||||
|
const WAITING_ACCENT = colors.premium.rose;
|
||||||
return (
|
return (
|
||||||
<GradientBackground showAccent>
|
<GradientBackground>
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInUp.duration(600).springify()}
|
entering={FadeInUp.duration(600).springify()}
|
||||||
style={styles.emptyContent}
|
style={styles.waitingContent}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
{/* Icon */}
|
||||||
colors={gradients.warning as [string, string]}
|
<View style={styles.waitingIconContainer}>
|
||||||
style={styles.pairIconContainer}
|
<Ionicons name="hourglass-outline" size={36} color={WAITING_ACCENT} />
|
||||||
start={{ x: 0, y: 0 }}
|
</View>
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
|
{/* Title section */}
|
||||||
|
<Text style={styles.waitingLabel}>PATIENCE</Text>
|
||||||
|
<Text style={styles.waitingTitle}>Waiting</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.waitingSeparator}>
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
<View style={styles.waitingSeparatorDiamond} />
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status badge */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.waitingBadge}
|
||||||
>
|
>
|
||||||
<Ionicons name="hourglass-outline" size={48} color={colors.text} />
|
<Text style={styles.waitingBadgeText}>{gapInfo.unanswered} UNANSWERED</Text>
|
||||||
</LinearGradient>
|
</Animated.View>
|
||||||
<Text style={styles.pairTitle}>
|
|
||||||
Waiting for your partner
|
{/* Description */}
|
||||||
</Text>
|
<Text style={styles.waitingDescription}>
|
||||||
<Text style={styles.emptySubtitle}>
|
You're ahead by {gapInfo.unanswered} questions. Give your partner some time to catch up so you can discover matches together.
|
||||||
You've answered {gapInfo.unanswered} questions that your partner hasn't seen yet. Give them some time to catch up!
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Feature hints */}
|
||||||
|
<View style={styles.waitingFeatures}>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="heart-outline" size={16} color={WAITING_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Matches happen together</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="notifications-outline" size={16} color={WAITING_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>We'll notify your partner</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="refresh-outline" size={16} color={WAITING_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Check back anytime</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom teaser */}
|
||||||
|
<Text style={styles.waitingTeaser}>Good things come to those who wait</Text>
|
||||||
|
|
||||||
<GlassButton
|
<GlassButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onPress={checkAnswerGap}
|
onPress={checkAnswerGap}
|
||||||
@@ -345,20 +414,62 @@ export default function SwipeScreen() {
|
|||||||
|
|
||||||
// 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) {
|
||||||
|
const NO_PACKS_ACCENT = colors.premium.rose;
|
||||||
return (
|
return (
|
||||||
<GradientBackground showAccent>
|
<GradientBackground>
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInUp.duration(600).springify()}
|
entering={FadeInUp.duration(600).springify()}
|
||||||
style={styles.emptyContent}
|
style={styles.waitingContent}
|
||||||
>
|
>
|
||||||
<View style={styles.noPacksIconContainer}>
|
{/* Icon */}
|
||||||
<Ionicons name="layers-outline" size={64} color={colors.textTertiary} />
|
<View style={styles.waitingIconContainer}>
|
||||||
|
<Ionicons name="layers-outline" size={36} color={NO_PACKS_ACCENT} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.emptyTitle}>No packs enabled</Text>
|
|
||||||
<Text style={styles.emptySubtitle}>
|
{/* Title section */}
|
||||||
Enable at least one question pack to start playing.
|
<Text style={styles.waitingLabel}>GET STARTED</Text>
|
||||||
|
<Text style={styles.waitingTitle}>Choose Packs</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.waitingSeparator}>
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
<View style={styles.waitingSeparatorDiamond} />
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status badge */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.waitingBadge}
|
||||||
|
>
|
||||||
|
<Text style={styles.waitingBadgeText}>NO PACKS ENABLED</Text>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Text style={styles.waitingDescription}>
|
||||||
|
Select the question packs that interest you and your partner. Each pack explores different aspects of your relationship.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Feature hints */}
|
||||||
|
<View style={styles.waitingFeatures}>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="flame-outline" size={16} color={NO_PACKS_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>From playful to passionate</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="shield-checkmark-outline" size={16} color={NO_PACKS_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Safe space to explore</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="infinite-outline" size={16} color={NO_PACKS_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>New packs added regularly</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom teaser */}
|
||||||
|
<Text style={styles.waitingTeaser}>Your journey of discovery awaits</Text>
|
||||||
|
|
||||||
<GlassButton
|
<GlassButton
|
||||||
onPress={() => router.push("/packs")}
|
onPress={() => router.push("/packs")}
|
||||||
style={{ marginTop: spacing.lg }}
|
style={{ marginTop: spacing.lg }}
|
||||||
@@ -372,20 +483,62 @@ export default function SwipeScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentIndex >= questions.length) {
|
if (currentIndex >= questions.length) {
|
||||||
|
const CAUGHT_UP_ACCENT = colors.premium.rose;
|
||||||
return (
|
return (
|
||||||
<GradientBackground showAccent>
|
<GradientBackground>
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInUp.duration(600).springify()}
|
entering={FadeInUp.duration(600).springify()}
|
||||||
style={styles.emptyContent}
|
style={styles.waitingContent}
|
||||||
>
|
>
|
||||||
<View style={styles.emptyIconContainer}>
|
{/* Icon */}
|
||||||
<Ionicons name="checkmark-circle" size={64} color={colors.primary} />
|
<View style={styles.caughtUpIconContainer}>
|
||||||
|
<Ionicons name="checkmark" size={36} color={CAUGHT_UP_ACCENT} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.emptyTitle}>All caught up!</Text>
|
|
||||||
<Text style={styles.emptySubtitle}>
|
{/* Title section */}
|
||||||
Check back later for more questions or try a different pack.
|
<Text style={styles.caughtUpLabel}>COMPLETE</Text>
|
||||||
|
<Text style={styles.waitingTitle}>All Caught Up</Text>
|
||||||
|
|
||||||
|
{/* Decorative separator */}
|
||||||
|
<View style={styles.waitingSeparator}>
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
<View style={styles.waitingSeparatorDiamond} />
|
||||||
|
<View style={styles.waitingSeparatorLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status badge */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(300).duration(400)}
|
||||||
|
style={styles.waitingBadge}
|
||||||
|
>
|
||||||
|
<Text style={styles.waitingBadgeText}>YOU'RE AHEAD</Text>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Text style={styles.waitingDescription}>
|
||||||
|
You've answered all available questions. New questions are added regularly, or explore different packs.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Feature hints */}
|
||||||
|
<View style={styles.waitingFeatures}>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="sparkles" size={16} color={CAUGHT_UP_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>New questions weekly</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="layers-outline" size={16} color={CAUGHT_UP_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Try different packs</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.waitingFeatureItem}>
|
||||||
|
<Ionicons name="chatbubbles-outline" size={16} color={CAUGHT_UP_ACCENT} />
|
||||||
|
<Text style={styles.waitingFeatureText}>Chat about your matches</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom teaser */}
|
||||||
|
<Text style={styles.waitingTeaser}>More ways to connect are on the way</Text>
|
||||||
|
|
||||||
<GlassButton
|
<GlassButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onPress={fetchQuestions}
|
onPress={fetchQuestions}
|
||||||
@@ -600,54 +753,119 @@ const styles = StyleSheet.create({
|
|||||||
...typography.caption1,
|
...typography.caption1,
|
||||||
color: colors.textTertiary,
|
color: colors.textTertiary,
|
||||||
},
|
},
|
||||||
emptyContent: {
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: spacing.xl,
|
|
||||||
},
|
|
||||||
emptyIconContainer: {
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
backgroundColor: colors.primaryLight,
|
|
||||||
justifyContent: "center",
|
|
||||||
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,
|
|
||||||
borderRadius: 60,
|
|
||||||
backgroundColor: colors.glass.background,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: spacing.lg,
|
|
||||||
},
|
|
||||||
emptyTitle: {
|
|
||||||
...typography.title2,
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: spacing.sm,
|
|
||||||
},
|
|
||||||
emptySubtitle: {
|
|
||||||
...typography.body,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
bottomSpacer: {
|
bottomSpacer: {
|
||||||
height: Platform.OS === 'ios' ? 100 : 80,
|
height: Platform.OS === 'ios' ? 100 : 80,
|
||||||
},
|
},
|
||||||
|
// Premium "waiting for partner" styles
|
||||||
|
waitingContent: {
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
waitingIconContainer: {
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
borderRadius: 36,
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(232, 164, 174, 0.2)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
waitingLabel: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: colors.premium.rose,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
waitingTitle: {
|
||||||
|
...typography.largeTitle,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
waitingSeparator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.lg,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
waitingSeparatorLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.3)',
|
||||||
|
},
|
||||||
|
waitingSeparatorDiamond: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
backgroundColor: colors.premium.rose,
|
||||||
|
transform: [{ rotate: '45deg' }],
|
||||||
|
marginHorizontal: spacing.md,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
waitingBadge: {
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.1)',
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(232, 164, 174, 0.2)',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
waitingBadgeText: {
|
||||||
|
...typography.caption2,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: colors.premium.rose,
|
||||||
|
},
|
||||||
|
waitingDescription: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
waitingFeatures: {
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
waitingFeatureItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
waitingFeatureText: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.text,
|
||||||
|
marginLeft: spacing.sm,
|
||||||
|
},
|
||||||
|
waitingTeaser: {
|
||||||
|
...typography.footnote,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
color: colors.textTertiary,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
// Caught up specific styles
|
||||||
|
caughtUpIconContainer: {
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
borderRadius: 36,
|
||||||
|
backgroundColor: 'rgba(232, 164, 174, 0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(232, 164, 174, 0.2)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
caughtUpLabel: {
|
||||||
|
...typography.caption1,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: 3,
|
||||||
|
color: colors.premium.rose,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
After Width: | Height: | Size: 147 KiB |
@@ -101,6 +101,47 @@ export async function clearPushToken(userId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Expo push token with retry logic for Android.
|
||||||
|
* On Android, Firebase may not be initialized immediately on app start,
|
||||||
|
* so we retry a few times with increasing delays.
|
||||||
|
*/
|
||||||
|
async function getExpoPushTokenWithRetry(
|
||||||
|
projectId: string,
|
||||||
|
maxRetries = 5
|
||||||
|
): Promise<string | null> {
|
||||||
|
console.log(`[Push] Getting token for project: ${projectId}, platform: ${Platform.OS}`);
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
console.log(`[Push] Attempt ${attempt + 1}/${maxRetries} to get push token...`);
|
||||||
|
const tokenData = await Notifications.getExpoPushTokenAsync({ projectId });
|
||||||
|
console.log(`[Push] Successfully got token: ${tokenData.data}`);
|
||||||
|
return tokenData.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.log(`[Push] Attempt ${attempt + 1} failed: ${errorMessage}`);
|
||||||
|
|
||||||
|
// Retry for any Firebase-related errors on Android
|
||||||
|
const isFirebaseError =
|
||||||
|
error instanceof Error &&
|
||||||
|
(error.message.includes("Firebase") ||
|
||||||
|
error.message.includes("FCM") ||
|
||||||
|
error.message.includes("Google Play"));
|
||||||
|
|
||||||
|
if (Platform.OS === "android" && attempt < maxRetries - 1) {
|
||||||
|
const delay = (attempt + 1) * 1000;
|
||||||
|
console.log(`[Push] Retrying in ${delay}ms...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if current device needs to register for push notifications.
|
* Check if current device needs to register for push notifications.
|
||||||
* This handles the case where a user onboarded on one device (e.g., iOS)
|
* This handles the case where a user onboarded on one device (e.g., iOS)
|
||||||
@@ -110,32 +151,44 @@ export async function clearPushToken(userId: string): Promise<void> {
|
|||||||
* Returns true if a new token was registered.
|
* Returns true if a new token was registered.
|
||||||
*/
|
*/
|
||||||
export async function checkAndRegisterPushToken(userId: string): Promise<boolean> {
|
export async function checkAndRegisterPushToken(userId: string): Promise<boolean> {
|
||||||
|
console.log(`[Push] checkAndRegisterPushToken called for user: ${userId}`);
|
||||||
|
|
||||||
if (Platform.OS === "web") {
|
if (Platform.OS === "web") {
|
||||||
|
console.log("[Push] Skipping - web platform");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First check if permissions are already granted
|
// First check if permissions are already granted
|
||||||
const { status } = await Notifications.getPermissionsAsync();
|
const { status } = await Notifications.getPermissionsAsync();
|
||||||
|
console.log(`[Push] Current permission status: ${status}`);
|
||||||
|
|
||||||
// If permissions not granted, we need to prompt the user
|
// If permissions not granted, we need to prompt the user
|
||||||
// But only do this if we don't already have a token for this device
|
// But only do this if we don't already have a token for this device
|
||||||
if (status !== "granted") {
|
if (status !== "granted") {
|
||||||
// Request permissions
|
console.log("[Push] Requesting permissions...");
|
||||||
const { status: newStatus } = await Notifications.requestPermissionsAsync();
|
const { status: newStatus } = await Notifications.requestPermissionsAsync();
|
||||||
|
console.log(`[Push] New permission status: ${newStatus}`);
|
||||||
if (newStatus !== "granted") {
|
if (newStatus !== "granted") {
|
||||||
|
console.log("[Push] Permission denied");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current device's push token
|
// Get the current device's push token
|
||||||
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
|
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
|
||||||
|
console.log(`[Push] Project ID: ${projectId}`);
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
|
console.log("[Push] No project ID configured");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenData = await Notifications.getExpoPushTokenAsync({ projectId });
|
const currentToken = await getExpoPushTokenWithRetry(projectId);
|
||||||
const currentToken = tokenData.data;
|
console.log(`[Push] Current token: ${currentToken}`);
|
||||||
|
if (!currentToken) {
|
||||||
|
console.log("[Push] Failed to get token");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the stored token from the database
|
// Fetch the stored token from the database
|
||||||
const { data: profile } = await supabase
|
const { data: profile } = await supabase
|
||||||
@@ -144,16 +197,20 @@ export async function checkAndRegisterPushToken(userId: string): Promise<boolean
|
|||||||
.eq("id", userId)
|
.eq("id", userId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
console.log(`[Push] Stored token: ${profile?.push_token}`);
|
||||||
|
|
||||||
// If the stored token is different (or null), save the new one
|
// If the stored token is different (or null), save the new one
|
||||||
if (profile?.push_token !== currentToken) {
|
if (profile?.push_token !== currentToken) {
|
||||||
|
console.log("[Push] Token changed, saving new token...");
|
||||||
await savePushToken(userId, currentToken);
|
await savePushToken(userId, currentToken);
|
||||||
console.log("Push token updated for new device");
|
console.log("[Push] Push token updated for new device");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[Push] Token unchanged");
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check/register push token:", error);
|
console.error("[Push] Failed to check/register push token:", error);
|
||||||
captureError(error as Error, { context: "push_token_check" });
|
captureError(error as Error, { context: "push_token_check" });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 16.28.0",
|
||||||
|
"appVersionSource": "remote"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"autoIncrement": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||