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'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 3
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
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": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/splash-icon.png",
|
||||
"foregroundImage": "./assets/adaptive-icon-foreground.png",
|
||||
"backgroundColor": "#1a1a2e"
|
||||
},
|
||||
"package": "com.sauci.app",
|
||||
|
||||
@@ -149,12 +149,10 @@ export default function AppLayout() {
|
||||
router.push('/(app)/swipe');
|
||||
break;
|
||||
case 'dares':
|
||||
// TODO: Navigate to dares screen
|
||||
console.log('Dares pressed - not yet implemented');
|
||||
router.push('/(app)/dares');
|
||||
break;
|
||||
case 'quiz':
|
||||
// TODO: Navigate to quiz screen
|
||||
console.log('Quiz pressed - not yet implemented');
|
||||
router.push('/(app)/quiz');
|
||||
break;
|
||||
}
|
||||
}, [router, closeRadialMenu]);
|
||||
@@ -609,6 +607,18 @@ export default function AppLayout() {
|
||||
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>
|
||||
|
||||
{/* 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
|
||||
if (!partner) {
|
||||
const PAIR_ACCENT = colors.premium.rose;
|
||||
return (
|
||||
<GradientBackground showAccent>
|
||||
<GradientBackground>
|
||||
<View style={styles.centerContainer}>
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(600).springify()}
|
||||
style={styles.emptyContent}
|
||||
style={styles.waitingContent}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={gradients.primary as [string, string]}
|
||||
style={styles.pairIconContainer}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
{/* Icon */}
|
||||
<View style={styles.waitingIconContainer}>
|
||||
<Ionicons name="heart" size={36} color={PAIR_ACCENT} />
|
||||
</View>
|
||||
|
||||
{/* 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} />
|
||||
</LinearGradient>
|
||||
<Text style={styles.pairTitle}>
|
||||
{couple ? "Waiting for your partner" : "Pair with your partner"}
|
||||
</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
<Text style={styles.waitingBadgeText}>{couple ? "INVITE SENT" : "MADE FOR TWO"}</Text>
|
||||
</Animated.View>
|
||||
|
||||
{/* Description */}
|
||||
<Text style={styles.waitingDescription}>
|
||||
{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>
|
||||
|
||||
{/* 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
|
||||
onPress={() => router.push("/pairing")}
|
||||
style={{ marginTop: spacing.xl }}
|
||||
size="lg"
|
||||
style={{ marginTop: spacing.lg }}
|
||||
>
|
||||
{couple ? "View Invite Code" : "Pair Now"}
|
||||
</GlassButton>
|
||||
@@ -309,27 +343,62 @@ export default function SwipeScreen() {
|
||||
|
||||
// Show "waiting for partner" message when user is too far ahead
|
||||
if (isBlocked && gapInfo) {
|
||||
const WAITING_ACCENT = colors.premium.rose;
|
||||
return (
|
||||
<GradientBackground showAccent>
|
||||
<GradientBackground>
|
||||
<View style={styles.centerContainer}>
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(600).springify()}
|
||||
style={styles.emptyContent}
|
||||
style={styles.waitingContent}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={gradients.warning as [string, string]}
|
||||
style={styles.pairIconContainer}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
{/* Icon */}
|
||||
<View style={styles.waitingIconContainer}>
|
||||
<Ionicons name="hourglass-outline" size={36} color={WAITING_ACCENT} />
|
||||
</View>
|
||||
|
||||
{/* 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} />
|
||||
</LinearGradient>
|
||||
<Text style={styles.pairTitle}>
|
||||
Waiting for your partner
|
||||
</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
You've answered {gapInfo.unanswered} questions that your partner hasn't seen yet. Give them some time to catch up!
|
||||
<Text style={styles.waitingBadgeText}>{gapInfo.unanswered} UNANSWERED</Text>
|
||||
</Animated.View>
|
||||
|
||||
{/* Description */}
|
||||
<Text style={styles.waitingDescription}>
|
||||
You're ahead by {gapInfo.unanswered} questions. Give your partner some time to catch up so you can discover matches together.
|
||||
</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
|
||||
variant="secondary"
|
||||
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
|
||||
if (!packId && enabledPackIds.length === 0) {
|
||||
const NO_PACKS_ACCENT = colors.premium.rose;
|
||||
return (
|
||||
<GradientBackground showAccent>
|
||||
<GradientBackground>
|
||||
<View style={styles.centerContainer}>
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(600).springify()}
|
||||
style={styles.emptyContent}
|
||||
style={styles.waitingContent}
|
||||
>
|
||||
<View style={styles.noPacksIconContainer}>
|
||||
<Ionicons name="layers-outline" size={64} color={colors.textTertiary} />
|
||||
{/* Icon */}
|
||||
<View style={styles.waitingIconContainer}>
|
||||
<Ionicons name="layers-outline" size={36} color={NO_PACKS_ACCENT} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>No packs enabled</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
Enable at least one question pack to start playing.
|
||||
|
||||
{/* Title section */}
|
||||
<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>
|
||||
|
||||
{/* 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
|
||||
onPress={() => router.push("/packs")}
|
||||
style={{ marginTop: spacing.lg }}
|
||||
@@ -372,20 +483,62 @@ export default function SwipeScreen() {
|
||||
}
|
||||
|
||||
if (currentIndex >= questions.length) {
|
||||
const CAUGHT_UP_ACCENT = colors.premium.rose;
|
||||
return (
|
||||
<GradientBackground showAccent>
|
||||
<GradientBackground>
|
||||
<View style={styles.centerContainer}>
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(600).springify()}
|
||||
style={styles.emptyContent}
|
||||
style={styles.waitingContent}
|
||||
>
|
||||
<View style={styles.emptyIconContainer}>
|
||||
<Ionicons name="checkmark-circle" size={64} color={colors.primary} />
|
||||
{/* Icon */}
|
||||
<View style={styles.caughtUpIconContainer}>
|
||||
<Ionicons name="checkmark" size={36} color={CAUGHT_UP_ACCENT} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>All caught up!</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
Check back later for more questions or try a different pack.
|
||||
|
||||
{/* Title section */}
|
||||
<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>
|
||||
|
||||
{/* 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
|
||||
variant="secondary"
|
||||
onPress={fetchQuestions}
|
||||
@@ -600,54 +753,119 @@ const styles = StyleSheet.create({
|
||||
...typography.caption1,
|
||||
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: {
|
||||
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.
|
||||
* 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.
|
||||
*/
|
||||
export async function checkAndRegisterPushToken(userId: string): Promise<boolean> {
|
||||
console.log(`[Push] checkAndRegisterPushToken called for user: ${userId}`);
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
console.log("[Push] Skipping - web platform");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// First check if permissions are already granted
|
||||
const { status } = await Notifications.getPermissionsAsync();
|
||||
console.log(`[Push] Current permission status: ${status}`);
|
||||
|
||||
// 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
|
||||
if (status !== "granted") {
|
||||
// Request permissions
|
||||
console.log("[Push] Requesting permissions...");
|
||||
const { status: newStatus } = await Notifications.requestPermissionsAsync();
|
||||
console.log(`[Push] New permission status: ${newStatus}`);
|
||||
if (newStatus !== "granted") {
|
||||
console.log("[Push] Permission denied");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current device's push token
|
||||
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
|
||||
console.log(`[Push] Project ID: ${projectId}`);
|
||||
if (!projectId) {
|
||||
console.log("[Push] No project ID configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokenData = await Notifications.getExpoPushTokenAsync({ projectId });
|
||||
const currentToken = tokenData.data;
|
||||
const currentToken = await getExpoPushTokenWithRetry(projectId);
|
||||
console.log(`[Push] Current token: ${currentToken}`);
|
||||
if (!currentToken) {
|
||||
console.log("[Push] Failed to get token");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch the stored token from the database
|
||||
const { data: profile } = await supabase
|
||||
@@ -144,16 +197,20 @@ export async function checkAndRegisterPushToken(userId: string): Promise<boolean
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
console.log(`[Push] Stored token: ${profile?.push_token}`);
|
||||
|
||||
// If the stored token is different (or null), save the new one
|
||||
if (profile?.push_token !== currentToken) {
|
||||
console.log("[Push] Token changed, saving new token...");
|
||||
await savePushToken(userId, currentToken);
|
||||
console.log("Push token updated for new device");
|
||||
console.log("[Push] Push token updated for new device");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log("[Push] Token unchanged");
|
||||
return false;
|
||||
} 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" });
|
||||
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": {}
|
||||
}
|
||||
}
|
||||