Add question types, nudge feature, and share modal

- Add new question card components (photo, audio, text, who-likely)
- Add share preview modal for matches
- Add nudge notification system with rate limiting
- Add session-based notification improvements
- Add match confetti celebration animation
- Update onboarding flow with paywall tracking
- Remove deprecated swipe card and tutorial components
- Update GlassCard/GlassInput to use solid backgrounds
- Fix routing issues after packs screen removal
This commit is contained in:
jjbrunton committed 2026-01-25 19:17:39 +00:00
1 parent 8612b16b93
commit 1daf386765
114 files changed
+14877 -7911

No files matched your search

+11 -3
View File
@@ -308,7 +308,7 @@ export const ChatScreen: React.FC = () => {
return (
<GradientBackground>
{/* Ambient Orbs */}
{/* Ambient Orbs - Commented out for flat look
<Animated.View style={[styles.ambientOrb, styles.orbTopRight, orbStyle1]} pointerEvents="none">
<LinearGradient
colors={[colors.premium.goldGlow, 'transparent']}
@@ -325,6 +325,7 @@ export const ChatScreen: React.FC = () => {
end={{ x: 0, y: 0 }}
/>
</Animated.View>
*/}
<KeyboardAvoidingView
style={styles.keyboardAvoiding}
@@ -343,6 +344,8 @@ export const ChatScreen: React.FC = () => {
<ChatMessages
messages={messages}
userId={user?.id}
userName={user?.name ?? undefined}
partnerName={partner?.name ?? undefined}
match={match}
uploadStatus={uploadStatus}
partnerTyping={partnerTyping}
@@ -357,15 +360,20 @@ export const ChatScreen: React.FC = () => {
entering={FadeInDown.duration(300)}
style={[styles.inputWrapper, { paddingBottom: insets.bottom || spacing.sm }]}
>
{/* Gradient background */}
{/* Flat background */}
<View style={[StyleSheet.absoluteFill, { backgroundColor: colors.backgroundLight, borderTopWidth: 1, borderTopColor: colors.border }]} />
{/* Gradient background - removed
<LinearGradient
colors={['rgba(22, 33, 62, 0.9)', 'rgba(13, 13, 26, 0.95)']}
style={StyleSheet.absoluteFill}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* Top border glow */}
*/}
{/* Top border glow - removed
<View style={styles.inputWrapperBorder} />
*/}
<InputBar
inputText={inputText}
@@ -31,20 +31,25 @@ const ChatHeaderComponent: React.FC<ChatHeaderProps> = ({
entering={FadeIn.duration(300)}
style={[styles.header, { paddingTop: insets.top + spacing.sm }]}
>
{/* Gradient background */}
{/* Flat background */}
<View style={[StyleSheet.absoluteFill, { backgroundColor: colors.background, borderBottomWidth: 1, borderBottomColor: colors.border }]} />
{/* Gradient background - removed
<LinearGradient
colors={['rgba(22, 33, 62, 0.8)', 'rgba(13, 13, 26, 0.6)']}
style={StyleSheet.absoluteFill}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* Top silk highlight */}
*/}
{/* Top silk highlight - removed
<LinearGradient
colors={[`${ACCENT_RGBA}0.1)`, 'transparent']}
style={styles.headerSilkHighlight}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
*/}
<TouchableOpacity
onPress={onBack}
@@ -138,12 +143,25 @@ const styles = StyleSheet.create({
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: `${ACCENT_RGBA}0.1)`,
backgroundColor: colors.backgroundLight,
borderWidth: 1,
borderColor: `${ACCENT_RGBA}0.2)`,
borderColor: colors.border,
justifyContent: 'center',
alignItems: 'center',
},
// ...
headerSpacer: {
width: 40,
},
settingsButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
headerBorderBottom: {
display: 'none',
},
headerCenter: {
flex: 1,
flexDirection: 'row',
@@ -195,21 +213,5 @@ const styles = StyleSheet.create({
...typography.headline,
color: colors.text,
},
headerSpacer: {
width: 40,
},
settingsButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
headerBorderBottom: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 1,
backgroundColor: `${ACCENT_RGBA}0.15)`,
},
});
@@ -19,6 +19,8 @@ type Message = Database['public']['Tables']['messages']['Row'];
interface ChatMessagesProps {
messages: Message[];
userId: string | undefined;
userName: string | undefined;
partnerName: string | undefined;
match: Match | null;
uploadStatus: UploadStatus | null;
partnerTyping: boolean;
@@ -46,6 +48,8 @@ function isSameDay(date1: Date, date2: Date): boolean {
const ChatMessagesComponent: React.FC<ChatMessagesProps> = ({
messages,
userId,
userName,
partnerName,
match,
uploadStatus,
partnerTyping,
@@ -108,9 +112,9 @@ const ChatMessagesComponent: React.FC<ChatMessagesProps> = ({
// Memoize ListFooterComponent to prevent recreation
const listFooterComponent = useMemo(() => (
<View style={styles.footerContainer}>
<MatchCard match={match} user={userObject} />
<MatchCard match={match} user={userObject} userName={userName} partnerName={partnerName} onImagePress={onImagePress} />
</View>
), [match, userObject]);
), [match, userObject, userName, partnerName, onImagePress]);
// Memoize ListEmptyComponent to prevent recreation
const listEmptyComponent = useMemo(() => (
@@ -203,45 +203,32 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
plusButton: {
backgroundColor: 'rgba(212, 175, 55, 0.1)',
backgroundColor: colors.backgroundLight,
borderWidth: 1,
borderColor: 'rgba(212, 175, 55, 0.2)',
},
mediaMenuContainer: {
flexDirection: 'row',
overflow: 'hidden',
gap: spacing.xs,
},
mediaMenuItem: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
borderColor: colors.border,
},
// ...
mediaMenuItemInner: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
backgroundColor: colors.backgroundLight,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
borderColor: colors.border,
justifyContent: 'center',
alignItems: 'center',
},
inputFieldWrapper: {
flex: 1,
position: 'relative',
},
// ...
inputFieldBorder: {
...StyleSheet.absoluteFillObject,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: 'rgba(212, 175, 55, 0.15)',
borderColor: colors.border,
},
input: {
...typography.body,
color: colors.text,
backgroundColor: 'rgba(22, 33, 62, 0.6)',
backgroundColor: colors.background, // Nested flat background
borderRadius: radius.xl,
paddingHorizontal: spacing.md,
paddingTop: 12,
@@ -255,7 +242,27 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
// Keep gradient for primary action to make it pop
},
mediaMenuContainer: {
flexDirection: 'row',
overflow: 'hidden',
gap: spacing.xs,
},
mediaMenuItem: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
inputFieldWrapper: {
flex: 1,
position: 'relative',
},
sendButtonHighlight: {
position: 'absolute',
top: 0,
@@ -1,12 +1,16 @@
import React, { useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import React, { useRef, useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { Image } from 'expo-image';
import { Audio } from 'expo-av';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { DecorativeSeparator } from '../../../components/ui';
import { colors, gradients, spacing, radius, typography } from '../../../theme';
import { Match } from '../types';
import { ResponseData } from '@sauci/shared';
import { getCachedSignedUrl, getStoragePath } from '../../../lib/imageCache';
// Premium color palette
const ACCENT = colors.premium.gold;
@@ -15,9 +19,356 @@ const ACCENT_RGBA = 'rgba(212, 175, 55, ';
interface MatchCardProps {
match: Match | null;
user: { id: string } | null;
userName?: string;
partnerName?: string;
onImagePress?: (uri: string) => void;
}
const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user }) => {
/**
* Helper function to get display text for who_likely choices
*/
function getChoiceDisplay(chosenUserId: string, userId: string, userName: string, partnerName: string): string {
return chosenUserId === userId ? userName : partnerName;
}
/**
* Component to render text_answer response summary
*/
const TextAnswerDisplay: React.FC<{
responseSummary: Record<string, ResponseData>;
userId: string;
partnerName: string;
}> = ({ responseSummary, userId, partnerName }) => {
const entries = Object.entries(responseSummary);
return (
<View style={styles.responseSummaryContainer}>
{entries.map(([responderId, data]) => {
if (data.type !== 'text_answer') return null;
const isUser = responderId === userId;
const name = isUser ? 'You' : partnerName;
return (
<View key={responderId} style={styles.textResponseCard}>
<Text style={styles.textResponseName}>{name}</Text>
<View style={[
styles.textBubble,
isUser ? styles.textBubbleUser : styles.textBubblePartner
]}>
<Text style={styles.textBubbleText}>"{data.text}"</Text>
</View>
</View>
);
})}
</View>
);
};
/**
* Component to render photo response summary
*/
const PhotoDisplay: React.FC<{
responseSummary: Record<string, ResponseData>;
userId: string;
partnerName: string;
onImagePress?: (uri: string) => void;
}> = ({ responseSummary, userId, partnerName, onImagePress }) => {
const [photoUrls, setPhotoUrls] = useState<Record<string, string>>({});
const [loadingUrls, setLoadingUrls] = useState(true);
const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>({});
useEffect(() => {
const fetchPhotoUrls = async () => {
// Fetch all signed URLs in parallel for faster loading
const photoEntries: Array<[string, { type: 'photo'; media_path: string }]> = [];
for (const [responderId, data] of Object.entries(responseSummary)) {
if (data.type === 'photo') {
photoEntries.push([responderId, data]);
}
}
const urlPromises = photoEntries.map(async ([responderId, data]) => {
try {
const storagePath = getStoragePath(data.media_path);
const signedUrl = await getCachedSignedUrl(storagePath, 'response-media');
return { responderId, signedUrl };
} catch (err) {
console.error('Failed to get photo URL:', err);
return { responderId, signedUrl: null };
}
});
const results = await Promise.all(urlPromises);
const urls: Record<string, string> = {};
// Collect URLs and prefetch images
const prefetchPromises: Promise<boolean>[] = [];
for (const { responderId, signedUrl } of results) {
if (signedUrl) {
urls[responderId] = signedUrl;
// Prefetch image for faster display
prefetchPromises.push(Image.prefetch(signedUrl));
}
}
// Set URLs immediately, prefetch in background
setPhotoUrls(urls);
setLoadingUrls(false);
// Initialize loading state for each image
const initialLoading: Record<string, boolean> = {};
for (const responderId of Object.keys(urls)) {
initialLoading[responderId] = true;
}
setLoadingImages(initialLoading);
// Wait for prefetch to complete (non-blocking)
Promise.all(prefetchPromises).catch(() => {
// Prefetch failures are non-critical
});
};
fetchPhotoUrls();
}, [responseSummary]);
const handleImageLoad = (responderId: string) => {
setLoadingImages(prev => ({ ...prev, [responderId]: false }));
};
const entries = Object.entries(responseSummary);
return (
<View style={styles.photoContainer}>
{entries.map(([responderId, data]) => {
if (data.type !== 'photo') return null;
const isUser = responderId === userId;
const name = isUser ? 'You' : partnerName;
const photoUrl = photoUrls[responderId];
const isImageLoading = loadingImages[responderId];
return (
<View key={responderId} style={styles.photoCard}>
<Text style={styles.photoName}>{name}</Text>
<TouchableOpacity
style={styles.photoThumbnailContainer}
onPress={() => photoUrl && onImagePress?.(photoUrl)}
disabled={!photoUrl}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel={`View ${name}'s photo full screen`}
>
{loadingUrls ? (
<View style={styles.photoPlaceholder}>
<ActivityIndicator size="small" color={colors.premium.gold} />
</View>
) : photoUrl ? (
<>
<Image
source={{ uri: photoUrl }}
style={styles.photoThumbnail}
contentFit="cover"
cachePolicy="disk"
transition={200}
onLoad={() => handleImageLoad(responderId)}
/>
{isImageLoading && (
<View style={styles.photoLoadingOverlay}>
<ActivityIndicator size="small" color={colors.premium.gold} />
</View>
)}
</>
) : (
<View style={styles.photoPlaceholder}>
<Ionicons name="image-outline" size={24} color={colors.textTertiary} />
</View>
)}
</TouchableOpacity>
</View>
);
})}
</View>
);
};
/**
* Component to render who_likely response summary
*/
const WhoLikelyDisplay: React.FC<{
responseSummary: Record<string, ResponseData>;
userId: string;
userName: string;
partnerName: string;
}> = ({ responseSummary, userId, userName, partnerName }) => {
const entries = Object.entries(responseSummary);
return (
<View style={styles.whoLikelyContainer}>
{entries.map(([responderId, data]) => {
if (data.type !== 'who_likely') return null;
const isUser = responderId === userId;
const voterName = isUser ? 'You' : partnerName;
const choiceName = getChoiceDisplay(data.chosen_user_id, userId, userName, partnerName);
return (
<View key={responderId} style={styles.whoLikelyCard}>
<Text style={styles.whoLikelyVoter}>{voterName} chose:</Text>
<LinearGradient
colors={gradients.premiumRose as [string, string]}
style={styles.whoLikelyChoiceBadge}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Text style={styles.whoLikelyChoiceText}>{choiceName}</Text>
</LinearGradient>
</View>
);
})}
</View>
);
};
/**
* Component to render audio response summary
*/
const AudioDisplay: React.FC<{
responseSummary: Record<string, ResponseData>;
userId: string;
partnerName: string;
}> = ({ responseSummary, userId, partnerName }) => {
const [audioUrls, setAudioUrls] = useState<Record<string, string>>({});
const [playingId, setPlayingId] = useState<string | null>(null);
const [sounds, setSounds] = useState<Record<string, Audio.Sound>>({});
useEffect(() => {
const fetchAudioUrls = async () => {
const urls: Record<string, string> = {};
for (const [responderId, data] of Object.entries(responseSummary)) {
if (data.type === 'audio' && data.media_path) {
try {
const storagePath = getStoragePath(data.media_path);
const signedUrl = await getCachedSignedUrl(storagePath, 'response-media');
if (signedUrl) {
urls[responderId] = signedUrl;
}
} catch (err) {
console.error('Failed to get audio URL:', err);
}
}
}
setAudioUrls(urls);
};
fetchAudioUrls();
// Cleanup sounds on unmount
return () => {
Object.values(sounds).forEach(sound => {
sound.unloadAsync();
});
};
}, [responseSummary]);
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const handlePlayPause = async (responderId: string) => {
const audioUrl = audioUrls[responderId];
if (!audioUrl) return;
// If already playing this one, pause it
if (playingId === responderId && sounds[responderId]) {
await sounds[responderId].pauseAsync();
setPlayingId(null);
return;
}
// Stop any currently playing audio
if (playingId && sounds[playingId]) {
await sounds[playingId].stopAsync();
}
// Play the selected audio
try {
let sound = sounds[responderId];
if (!sound) {
const { sound: newSound } = await Audio.Sound.createAsync(
{ uri: audioUrl },
{ shouldPlay: false },
(status) => {
if (status.isLoaded && status.didJustFinish) {
setPlayingId(null);
}
}
);
sound = newSound;
setSounds(prev => ({ ...prev, [responderId]: sound }));
}
await sound.playFromPositionAsync(0);
setPlayingId(responderId);
} catch (err) {
console.error('Failed to play audio:', err);
}
};
const entries = Object.entries(responseSummary);
return (
<View style={styles.audioContainer}>
{entries.map(([responderId, data]) => {
if (data.type !== 'audio') return null;
const isUser = responderId === userId;
const name = isUser ? 'You' : partnerName;
const audioUrl = audioUrls[responderId];
const isPlaying = playingId === responderId;
return (
<View key={responderId} style={styles.audioCard}>
<Text style={styles.audioName}>{name}</Text>
<TouchableOpacity
style={styles.audioPlayer}
onPress={() => handlePlayPause(responderId)}
disabled={!audioUrl}
>
<View style={[
styles.audioPlayButton,
isPlaying && styles.audioPlayButtonActive
]}>
<Ionicons
name={isPlaying ? 'pause' : 'play'}
size={20}
color={colors.text}
/>
</View>
<View style={styles.audioWaveform}>
{[...Array(5)].map((_, i) => (
<View
key={i}
style={[
styles.audioBar,
{ height: 8 + (i % 3) * 8 },
isPlaying && styles.audioBarActive
]}
/>
))}
</View>
<Text style={styles.audioDuration}>
{formatDuration(data.duration_seconds)}
</Text>
</TouchableOpacity>
</View>
);
})}
</View>
);
};
const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user, userName = 'You', partnerName = 'Partner', onImagePress }) => {
// Track if animation has already played to prevent re-animation on re-renders
const hasAnimatedRef = useRef(false);
const shouldAnimate = !hasAnimatedRef.current;
@@ -29,6 +380,16 @@ const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user }) => {
if (!match?.question) return null;
const isYesYes = match?.match_type === 'yes_yes';
const isBothAnswered = match?.match_type === 'both_answered';
// Determine the response type from response_summary
const getResponseType = (): ResponseData['type'] | null => {
if (!match.response_summary) return null;
const firstResponse = Object.values(match.response_summary)[0];
return firstResponse?.type || null;
};
const responseType = getResponseType();
// Only animate on first render to prevent re-animation when list re-renders
const enteringAnimation = shouldAnimate
@@ -41,7 +402,7 @@ const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user }) => {
style={styles.matchCardContainer}
>
<View style={styles.matchCard}>
{/* Card gradient background */}
{/* Card gradient background - removed for flat style
<LinearGradient
colors={isYesYes
? ['rgba(212, 175, 55, 0.12)', 'rgba(184, 134, 11, 0.08)']
@@ -51,8 +412,9 @@ const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user }) => {
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
*/}
{/* Top glow for YES+YES */}
{/* Top glow for YES+YES - removed
{isYesYes && (
<LinearGradient
colors={[`${ACCENT_RGBA}0.15)`, 'transparent']}
@@ -61,10 +423,11 @@ const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user }) => {
end={{ x: 0.5, y: 1 }}
/>
)}
*/}
{/* Match Label */}
<Text style={[styles.matchLabel, isYesYes && styles.matchLabelYesYes]}>
{isYesYes ? "PERFECT MATCH" : "SOFT MATCH"}
{isYesYes ? "PERFECT MATCH" : isBothAnswered ? "BOTH ANSWERED" : "SOFT MATCH"}
</Text>
{/* Decorative Separator */}
@@ -74,8 +437,57 @@ const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user }) => {
marginVertical={spacing.md}
/>
{/* Question and Responses - Show who answered what */}
{match.responses && match.responses.length > 0 ? (
{/* Question and Responses - Show based on match type and response type */}
{isBothAnswered && match.response_summary && responseType ? (
// Render response summary for non-swipe question types
<View style={styles.responseSummaryWrapper}>
{/* Show the question text */}
<Text style={styles.matchQuestionText}>
"{match.question.text}"
</Text>
<DecorativeSeparator
variant="rose"
width="40%"
marginVertical={spacing.sm}
/>
{/* Render based on response type */}
{responseType === 'text_answer' && (
<TextAnswerDisplay
responseSummary={match.response_summary}
userId={user?.id || ''}
partnerName={partnerName}
/>
)}
{responseType === 'photo' && (
<PhotoDisplay
responseSummary={match.response_summary}
userId={user?.id || ''}
partnerName={partnerName}
onImagePress={onImagePress}
/>
)}
{responseType === 'audio' && (
<AudioDisplay
responseSummary={match.response_summary}
userId={user?.id || ''}
partnerName={partnerName}
/>
)}
{responseType === 'who_likely' && (
<WhoLikelyDisplay
responseSummary={match.response_summary}
userId={user?.id || ''}
userName={userName}
partnerName={partnerName}
/>
)}
</View>
) : match.responses && match.responses.length > 0 ? (
<View style={styles.responsesContainer}>
{(() => {
// Sort responses by created_at to determine who answered first
@@ -134,18 +546,13 @@ const MatchCardComponent: React.FC<MatchCardProps> = ({ match, user }) => {
</Text>
)}
{/* Premium border */}
<View style={[
styles.matchCardBorder,
isYesYes && styles.matchCardBorderYesYes
]} pointerEvents="none" />
{/* Premium border removed */}
</View>
</Animated.View>
);
};
// Wrap with React.memo for performance
export const MatchCard = React.memo(MatchCardComponent);
// ...
const styles = StyleSheet.create({
matchCardContainer: {
@@ -153,6 +560,9 @@ const styles = StyleSheet.create({
marginTop: spacing.md,
borderRadius: radius.lg,
overflow: 'hidden',
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.backgroundLight,
},
matchCard: {
borderRadius: radius.lg,
@@ -160,14 +570,81 @@ const styles = StyleSheet.create({
paddingHorizontal: spacing.md,
alignItems: 'center',
overflow: 'hidden',
backgroundColor: colors.backgroundLight,
},
matchCardGlow: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 60,
display: 'none',
},
// ...
responseCard: {
backgroundColor: colors.background, // Nested
borderRadius: radius.md,
padding: spacing.sm,
borderWidth: 1,
borderColor: colors.border,
},
// ...
matchCardBorder: {
display: 'none',
},
matchCardBorderYesYes: {
display: 'none',
},
// ...
textBubbleUser: {
backgroundColor: colors.background,
borderWidth: 1,
borderColor: colors.primary,
alignSelf: 'flex-end',
},
textBubblePartner: {
backgroundColor: colors.background,
borderWidth: 1,
borderColor: colors.border,
alignSelf: 'flex-start',
},
// ...
photoPlaceholder: {
width: '100%',
height: '100%',
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
},
// ...
whoLikelyCard: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.background,
borderRadius: radius.md,
padding: spacing.sm,
borderWidth: 1,
borderColor: colors.border,
},
// ...
audioPlayer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background,
borderRadius: radius.lg,
padding: spacing.sm,
gap: spacing.sm,
borderWidth: 1,
borderColor: colors.border,
},
audioPlayButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.backgroundLight,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
matchLabel: {
...typography.caption2,
fontWeight: '700',
@@ -189,13 +666,7 @@ const styles = StyleSheet.create({
width: '100%',
gap: spacing.sm,
},
responseCard: {
backgroundColor: 'rgba(255, 255, 255, 0.03)',
borderRadius: radius.md,
padding: spacing.sm,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.06)',
},
responseHeader: {
flexDirection: 'row',
alignItems: 'center',
@@ -246,17 +717,151 @@ const styles = StyleSheet.create({
fontStyle: 'italic',
lineHeight: 18,
},
matchCardBorder: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: radius.lg,
borderWidth: 1,
borderColor: `rgba(232, 164, 174, 0.2)`,
// Response summary styles for non-swipe question types
responseSummaryWrapper: {
width: '100%',
alignItems: 'center',
},
matchCardBorderYesYes: {
borderColor: `${ACCENT_RGBA}0.25)`,
responseSummaryContainer: {
width: '100%',
gap: spacing.sm,
},
// Text answer styles
textResponseCard: {
width: '100%',
gap: spacing.xs,
},
textResponseName: {
...typography.caption2,
fontWeight: '600',
color: colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: 1,
},
textBubble: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.md,
maxWidth: '100%',
},
textBubbleText: {
...typography.subhead,
color: colors.text,
fontStyle: 'italic',
lineHeight: 20,
},
// Photo styles
photoContainer: {
flexDirection: 'row',
justifyContent: 'center',
gap: spacing.md,
width: '100%',
},
photoCard: {
alignItems: 'center',
gap: spacing.xs,
flex: 1,
maxWidth: 120,
},
photoName: {
...typography.caption2,
fontWeight: '600',
color: colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: 1,
},
photoThumbnailContainer: {
width: 80,
height: 80,
borderRadius: radius.md,
overflow: 'hidden',
borderWidth: 2,
borderColor: 'rgba(232, 164, 174, 0.3)',
},
photoThumbnail: {
width: '100%',
height: '100%',
},
photoLoadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center',
alignItems: 'center',
},
// Who likely styles
whoLikelyContainer: {
width: '100%',
gap: spacing.sm,
},
whoLikelyVoter: {
...typography.caption1,
fontWeight: '500',
color: colors.textSecondary,
},
whoLikelyChoiceBadge: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: radius.full,
},
whoLikelyChoiceText: {
...typography.caption1,
fontWeight: '700',
color: colors.text,
letterSpacing: 0.5,
},
// Audio display styles
audioContainer: {
width: '100%',
gap: spacing.md,
},
audioCard: {
width: '100%',
gap: spacing.xs,
},
audioName: {
...typography.caption1,
fontWeight: '600',
color: colors.textSecondary,
textAlign: 'center',
},
audioPlayButtonActive: {
backgroundColor: colors.primary,
},
audioWaveform: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 3,
height: 32,
},
audioBar: {
width: 4,
backgroundColor: 'rgba(232, 164, 174, 0.3)',
borderRadius: radius.full,
},
audioBarActive: {
backgroundColor: colors.primary,
},
audioDuration: {
...typography.caption1,
color: colors.textTertiary,
fontVariant: ['tabular-nums'],
minWidth: 35,
textAlign: 'right',
},
});
export { MatchCardComponent as MatchCard };
@@ -51,13 +51,14 @@ const MessageBubbleComponent = ({ isMe, index, children, onLongPress }: MessageB
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
{/* Silk highlight */}
{/* Silk highlight removed for flat look
<LinearGradient
colors={['rgba(255, 255, 255, 0.15)', 'transparent']}
style={styles.bubbleSilkHighlight}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
*/}
{children}
</LinearGradient>
</TouchableOpacity>
@@ -72,20 +73,22 @@ const MessageBubbleComponent = ({ isMe, index, children, onLongPress }: MessageB
accessibilityHint="Long press to report"
>
<View style={[styles.bubble, styles.theirBubble]}>
{/* Subtle gradient background */}
{/* Subtle gradient background removed
<LinearGradient
colors={['rgba(22, 33, 62, 0.6)', 'rgba(13, 13, 26, 0.8)']}
style={StyleSheet.absoluteFill}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
{/* Silk highlight */}
*/}
{/* Silk highlight removed
<LinearGradient
colors={[`${ACCENT_RGBA}0.08)`, 'transparent']}
style={styles.bubbleSilkHighlight}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
*/}
{children}
</View>
</TouchableOpacity>
@@ -126,13 +129,10 @@ const styles = StyleSheet.create({
theirBubble: {
borderTopLeftRadius: radius.xs,
borderWidth: 1,
borderColor: `${ACCENT_RGBA}0.15)`,
borderColor: colors.border,
backgroundColor: colors.backgroundLight,
},
bubbleSilkHighlight: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 30,
display: 'none',
},
});
@@ -10,7 +10,7 @@ import { Ionicons } from '@expo/vector-icons';
import { colors, gradients, spacing, radius } from '../../../theme';
import { MessageMeta } from './MessageMeta';
import { ChatVideoPlayer } from './ChatVideoPlayer';
import { supabase } from '../../../lib/supabase';
import { getCachedSignedUrl, getStoragePath } from '../../../lib/imageCache';
import type { Message } from '../types';
const ACCENT = colors.premium.gold;
@@ -67,14 +67,17 @@ const MessageContentComponent: React.FC<MessageContentProps> = ({
setMediaLoading(true);
setMediaError(false);
try {
const { data, error } = await supabase.storage
.from('chat-media')
.createSignedUrl(item.media_path!, 3600); // 1 hour expiry
const storagePath = getStoragePath(item.media_path!);
const signedUrl = await getCachedSignedUrl(storagePath, 'chat-media');
if (error) throw error;
setMediaUri(data.signedUrl);
if (!signedUrl) {
throw new Error('Missing signed URL for chat media');
}
setMediaUri(signedUrl);
} catch (err) {
console.error('Failed to get media URL:', err);
setMediaUri(null);
setMediaError(true);
} finally {
setMediaLoading(false);
+4 -1
View File
@@ -1,4 +1,5 @@
import { Database } from '../../types/supabase';
import { ResponseData } from '@sauci/shared';
/** Message type from database */
export type Message = Database['public']['Tables']['messages']['Row'];
@@ -15,8 +16,10 @@ export interface Match {
id: string;
question_id: string;
couple_id: string;
match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe';
match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe' | 'both_answered';
created_at: string;
/** Summary of both partners' response data (keyed by user_id) for non-swipe questions */
response_summary?: Record<string, ResponseData> | null;
question?: {
id: string;
text: string;
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, useWindowDimensions, Platform, Alert, TouchableOpacity } from 'react-native';
import { BlurView } from 'expo-blur';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import Constants from 'expo-constants';
@@ -9,9 +10,9 @@ import { LinearGradient } from 'expo-linear-gradient';
import { GradientBackground } from '../../components/ui';
import { Paywall } from '../../components/paywall';
import { CompactHeader } from '../../components/discovery';
import { colors, featureColors, spacing, typography, radius } from '../../theme';
import { useAuthStore, useSubscriptionStore } from '../../store';
import { resetSwipeTutorial } from '../../lib/swipeTutorialSeen';
import { resetMatchesTutorial } from '../../lib/matchesTutorialSeen';
import { supabase } from '../../lib/supabase';
@@ -29,12 +30,13 @@ export function ProfileScreen() {
const { width } = useWindowDimensions();
const isWideScreen = width > MAX_CONTENT_WIDTH;
const { user, partner, fetchUser, isAnonymous } = useAuthStore();
const { user, partner, couple, fetchUser, isAnonymous } = useAuthStore();
const { subscription } = useSubscriptionStore();
const settings = useProfileSettings();
// Paywall state
const [showPaywall, setShowPaywall] = useState(false);
const [headerHeight, setHeaderHeight] = useState(130);
// Derived state
const isOwnSubscription = subscription.isProUser || user?.is_premium;
@@ -61,28 +63,31 @@ export function ProfileScreen() {
return (
<GradientBackground>
<View
style={[styles.stickyHeader, isWideScreen && styles.stickyHeaderWide]}
onLayout={(e) => setHeaderHeight(e.nativeEvent.layout.height)}
>
<BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(14, 14, 17, 0.85)' }]} />
<CompactHeader
user={user}
partner={partner}
couple={couple}
label="Settings"
showGreeting={false}
showPartnerBadge={false}
/>
</View>
<ScrollView
style={styles.container}
contentContainerStyle={[
styles.contentContainer,
isWideScreen && styles.contentContainerWide,
{ paddingTop: headerHeight },
]}
showsVerticalScrollIndicator={false}
>
{/* Header with user info (compact) */}
<Animated.View
entering={FadeInDown.delay(100).duration(500)}
style={styles.header}
>
<Text style={styles.headerLabel}>YOUR SAUCI</Text>
<Text style={styles.title}>Settings</Text>
<View style={styles.separator}>
<View style={styles.separatorLine} />
<View style={styles.separatorDiamond} />
<View style={styles.separatorLine} />
</View>
</Animated.View>
{/* Compact Profile Preview */}
<Animated.View
entering={FadeInDown.delay(150).duration(500)}
@@ -213,16 +218,6 @@ export function ProfileScreen() {
showChevron={false}
/>
<View style={styles.divider} />
<MenuItem
icon="refresh-outline"
label="Reset Swipe Tutorial"
description="Show the tutorial again"
onPress={async () => {
await resetSwipeTutorial();
Alert.alert("Success", "Swipe tutorial has been reset.");
}}
/>
<View style={styles.divider} />
<MenuItem
icon="heart-outline"
label="Reset Matches Tutorial"
@@ -289,59 +284,31 @@ const styles = StyleSheet.create({
width: '100%',
maxWidth: MAX_CONTENT_WIDTH,
},
header: {
paddingTop: Platform.OS === 'ios' ? 60 : 40,
paddingHorizontal: spacing.lg,
paddingBottom: spacing.md,
alignItems: 'center',
stickyHeader: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 10,
overflow: 'hidden',
},
headerLabel: {
...typography.caption2,
fontWeight: '600',
letterSpacing: 3,
color: colors.secondary,
marginBottom: spacing.xs,
},
title: {
...typography.largeTitle,
color: colors.text,
textAlign: 'center',
},
separator: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginVertical: spacing.md,
width: 100,
},
separatorLine: {
flex: 1,
height: 1,
backgroundColor: 'rgba(155, 89, 182, 0.3)',
},
separatorDiamond: {
width: 6,
height: 6,
backgroundColor: colors.secondary,
transform: [{ rotate: '45deg' }],
marginHorizontal: spacing.sm,
opacity: 0.6,
stickyHeaderWide: {
left: '50%',
right: 'auto',
width: MAX_CONTENT_WIDTH,
transform: [{ translateX: -MAX_CONTENT_WIDTH / 2 }],
},
// Profile Preview
profilePreview: {
marginHorizontal: spacing.lg,
marginBottom: spacing.lg,
backgroundColor: colors.glass.background,
backgroundColor: colors.backgroundLight, // Flat background
borderRadius: radius.lg,
borderWidth: 1,
borderColor: colors.glass.border,
borderColor: colors.border,
overflow: 'hidden',
},
profilePreviewContent: {
flexDirection: 'row',
alignItems: 'center',
padding: spacing.md,
},
// ...
avatarGradient: {
width: 56,
height: 56,
@@ -349,7 +316,26 @@ const styles = StyleSheet.create({
padding: 2,
justifyContent: 'center',
alignItems: 'center',
// Removed shadow
},
// ...
versionBadge: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.backgroundLight, // Flat background
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
gap: spacing.xs,
borderWidth: 1,
borderColor: colors.border,
},
profilePreviewContent: {
flexDirection: 'row',
alignItems: 'center',
padding: spacing.md,
},
avatar: {
width: '100%',
height: '100%',
@@ -387,7 +373,7 @@ const styles = StyleSheet.create({
},
divider: {
height: 1,
backgroundColor: colors.glass.border,
backgroundColor: colors.border,
marginVertical: spacing.md,
},
// Version Badge
@@ -395,15 +381,7 @@ const styles = StyleSheet.create({
alignItems: "center",
marginTop: spacing.lg,
},
versionBadge: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.glass.background,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
gap: spacing.xs,
},
version: {
...typography.caption1,
color: colors.textTertiary,
@@ -204,11 +204,11 @@ const styles = StyleSheet.create({
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: colors.glass.background,
backgroundColor: colors.backgroundLight,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.glass.border,
borderColor: colors.border,
borderStyle: 'dashed',
},
rowTextContainer: {
@@ -234,16 +234,20 @@ const styles = StyleSheet.create({
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.glass.background,
backgroundColor: colors.backgroundLight,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
chevronContainer: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.glass.background,
backgroundColor: colors.backgroundLight,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
});
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { View, Text, StyleSheet, Modal, Platform, KeyboardAvoidingView, TouchableOpacity, ActivityIndicator, TextInput, ViewStyle } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { BlurView } from 'expo-blur';
import { GlassCard, GlassButton } from '../../../components/ui';
import { colors, spacing, radius, typography } from '../../../theme';
@@ -185,96 +184,90 @@ export const DangerZone: React.FC<DangerZoneProps> = ({
animationType="fade"
onRequestClose={closeResetModal}
>
<BlurView
intensity={Platform.OS === 'ios' ? 20 : 0}
tint="dark"
style={StyleSheet.absoluteFill}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.warningModalIconContainer}>
<Ionicons name="refresh" size={32} color={colors.warning} />
</View>
<Text style={styles.modalTitle}>Reset Progress?</Text>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.warningModalIconContainer}>
<Ionicons name="refresh" size={32} color={colors.warning} />
</View>
<Text style={styles.modalTitle}>Reset Progress?</Text>
</View>
<Text style={styles.modalDescription}>
This will permanently delete:
</Text>
<Text style={styles.modalDescription}>
This will permanently delete:
</Text>
<View style={styles.warningList}>
<View style={styles.deleteListItem}>
<Ionicons name="swap-horizontal" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All your swipes</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="heart" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All matches</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
<View style={styles.warningList}>
<View style={styles.deleteListItem}>
<Ionicons name="swap-horizontal" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All your swipes</Text>
</View>
<View style={styles.keepList}>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={styles.deleteListText}>Your partner connection stays</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="heart" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All matches</Text>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.warningKeyword}>RESET</Text> to confirm:
</Text>
<TextInput
style={styles.warningInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type RESET"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isResetting}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeResetModal}
disabled={isResetting}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.warningActionButton,
(confirmText !== "RESET" || isResetting) && styles.deleteButtonDisabled,
]}
onPress={handleResetProgress}
disabled={confirmText !== "RESET" || isResetting}
activeOpacity={0.7}
>
{isResetting ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="refresh" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Reset</Text>
</>
)}
</TouchableOpacity>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
</View>
</KeyboardAvoidingView>
</BlurView>
<View style={styles.keepList}>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={styles.deleteListText}>Your partner connection stays</Text>
</View>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.warningKeyword}>RESET</Text> to confirm:
</Text>
<TextInput
style={styles.warningInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type RESET"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isResetting}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeResetModal}
disabled={isResetting}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.warningActionButton,
(confirmText !== "RESET" || isResetting) && styles.deleteButtonDisabled,
]}
onPress={handleResetProgress}
disabled={confirmText !== "RESET" || isResetting}
activeOpacity={0.7}
>
{isResetting ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="refresh" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Reset</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
{/* Delete Relationship Modal */}
@@ -284,93 +277,87 @@ export const DangerZone: React.FC<DangerZoneProps> = ({
animationType="fade"
onRequestClose={closeRelationshipModal}
>
<BlurView
intensity={Platform.OS === 'ios' ? 20 : 0}
tint="dark"
style={StyleSheet.absoluteFill}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="heart-dislike" size={32} color={colors.error} />
</View>
<Text style={styles.modalTitle}>Delete Relationship Data?</Text>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="heart-dislike" size={32} color={colors.error} />
</View>
<Text style={styles.modalTitle}>Delete Relationship Data?</Text>
</View>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<View style={styles.deleteList}>
<View style={styles.deleteListItem}>
<Ionicons name="heart-dislike" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your couple connection</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All matches and responses</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="images" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All shared photos</Text>
</View>
<View style={styles.deleteList}>
<View style={styles.deleteListItem}>
<Ionicons name="heart-dislike" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your couple connection</Text>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>DELETE</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type DELETE"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isDeletingRelationship}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeRelationshipModal}
disabled={isDeletingRelationship}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.deleteButton,
(confirmText !== "DELETE" || isDeletingRelationship) && styles.deleteButtonDisabled,
]}
onPress={handleDeleteRelationship}
disabled={confirmText !== "DELETE" || isDeletingRelationship}
activeOpacity={0.7}
>
{isDeletingRelationship ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="trash" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Delete</Text>
</>
)}
</TouchableOpacity>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All matches and responses</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="images" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All shared photos</Text>
</View>
</View>
</KeyboardAvoidingView>
</BlurView>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>DELETE</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type DELETE"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isDeletingRelationship}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeRelationshipModal}
disabled={isDeletingRelationship}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.deleteButton,
(confirmText !== "DELETE" || isDeletingRelationship) && styles.deleteButtonDisabled,
]}
onPress={handleDeleteRelationship}
disabled={confirmText !== "DELETE" || isDeletingRelationship}
activeOpacity={0.7}
>
{isDeletingRelationship ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="trash" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Delete</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
{/* Delete Account Modal */}
@@ -380,101 +367,95 @@ export const DangerZone: React.FC<DangerZoneProps> = ({
animationType="fade"
onRequestClose={closeAccountModal}
>
<BlurView
intensity={Platform.OS === 'ios' ? 20 : 0}
tint="dark"
style={StyleSheet.absoluteFill}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="person-remove" size={32} color={colors.error} />
</View>
<Text style={styles.modalTitle}>Delete Account?</Text>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="person-remove" size={32} color={colors.error} />
</View>
<Text style={styles.modalTitle}>Delete Account?</Text>
</View>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<View style={styles.deleteList}>
<View style={styles.deleteListItem}>
<Ionicons name="person" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your account and profile</Text>
</View>
{hasRelationship && (
<>
<View style={styles.deleteListItem}>
<Ionicons name="heart-dislike" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your relationship data</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All chat messages and photos</Text>
</View>
</>
)}
<View style={styles.deleteListItem}>
<Ionicons name="key" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your encryption keys</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="cloud-offline" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All cloud-stored data</Text>
</View>
<View style={styles.deleteList}>
<View style={styles.deleteListItem}>
<Ionicons name="person" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your account and profile</Text>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>DELETE</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type DELETE"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isDeletingAccount}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeAccountModal}
disabled={isDeletingAccount}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.deleteButton,
(confirmText !== "DELETE" || isDeletingAccount) && styles.deleteButtonDisabled,
]}
onPress={handleDeleteAccount}
disabled={confirmText !== "DELETE" || isDeletingAccount}
activeOpacity={0.7}
>
{isDeletingAccount ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="trash" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Delete Account</Text>
</>
)}
</TouchableOpacity>
{hasRelationship && (
<>
<View style={styles.deleteListItem}>
<Ionicons name="heart-dislike" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your relationship data</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All chat messages and photos</Text>
</View>
</>
)}
<View style={styles.deleteListItem}>
<Ionicons name="key" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your encryption keys</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="cloud-offline" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All cloud-stored data</Text>
</View>
</View>
</KeyboardAvoidingView>
</BlurView>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>DELETE</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type DELETE"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isDeletingAccount}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeAccountModal}
disabled={isDeletingAccount}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.deleteButton,
(confirmText !== "DELETE" || isDeletingAccount) && styles.deleteButtonDisabled,
]}
onPress={handleDeleteAccount}
disabled={confirmText !== "DELETE" || isDeletingAccount}
activeOpacity={0.7}
>
{isDeletingAccount ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="trash" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Delete Account</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
</>
);
@@ -577,7 +558,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.backgroundLight,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: colors.glass.border,
borderColor: colors.border,
},
modalHeader: {
alignItems: "center",
@@ -644,7 +625,7 @@ const styles = StyleSheet.create({
color: colors.warning,
},
warningInput: {
backgroundColor: colors.glass.background,
backgroundColor: colors.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.warning,
@@ -684,7 +665,7 @@ const styles = StyleSheet.create({
color: colors.error,
},
confirmInput: {
backgroundColor: colors.glass.background,
backgroundColor: colors.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.error,
@@ -700,12 +681,12 @@ const styles = StyleSheet.create({
},
cancelButton: {
flex: 1,
backgroundColor: colors.glass.backgroundLight,
backgroundColor: colors.background,
paddingVertical: spacing.md,
borderRadius: radius.md,
alignItems: "center",
borderWidth: 1,
borderColor: colors.glass.borderLight,
borderColor: colors.border,
},
cancelButtonText: {
...typography.body,
@@ -127,6 +127,8 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginRight: spacing.md,
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderWidth: 1,
borderColor: 'rgba(239, 68, 68, 0.2)', // Added border
},
textContainer: {
flex: 1,
@@ -34,6 +34,7 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({
onMatchesToggle: (value: boolean) => updatePreference('matches_enabled', value),
onMessagesToggle: (value: boolean) => updatePreference('messages_enabled', value),
onPartnerActivityToggle: (value: boolean) => updatePreference('partner_activity_enabled', value),
onNudgesToggle: (value: boolean) => updatePreference('nudges_enabled', value),
onPackChangesToggle: (value: boolean) => updatePreference('pack_changes_enabled', value),
onNewPacksToggle: (value: boolean) => updatePreference('new_packs_enabled', value),
onStreakMilestonesToggle: (value: boolean) => updatePreference('streak_milestones_enabled', value),
@@ -91,6 +92,17 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({
<View style={styles.itemDivider} />
<SwitchItem
icon="hand-left-outline"
label="Partner Nudges"
description="When your partner sends you a reminder"
value={preferences.nudges_enabled}
onValueChange={handlers.onNudgesToggle}
disabled={isUpdating}
/>
<View style={styles.itemDivider} />
<SwitchItem
icon="layers-outline"
label="Pack Updates"
@@ -133,7 +145,7 @@ const styles = StyleSheet.create({
marginBottom: spacing.md,
paddingTop: spacing.md,
borderTopWidth: 1,
borderTopColor: colors.glass.border,
borderTopColor: colors.border,
},
subsectionLabel: {
...typography.caption1,
@@ -144,7 +156,7 @@ const styles = StyleSheet.create({
},
itemDivider: {
height: 1,
backgroundColor: colors.glass.border,
backgroundColor: colors.border,
marginVertical: spacing.sm,
},
});
@@ -58,7 +58,7 @@ export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
const styles = StyleSheet.create({
divider: {
height: 1,
backgroundColor: colors.glass.border,
backgroundColor: colors.border,
marginVertical: spacing.lg,
},
});
@@ -155,11 +155,32 @@ const styles = StyleSheet.create({
padding: 3,
justifyContent: 'center',
alignItems: 'center',
shadowColor: featureColors.profile.accent,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6,
// Removed shadows for flat look
},
// ...
avatarEditBadge: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: colors.backgroundLight,
width: 28,
height: 28,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background,
},
// ...
cancelEditButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.backgroundLight,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
avatarImage: {
width: '100%',
@@ -191,19 +212,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
avatarEditBadge: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: colors.surfaceSolid,
width: 28,
height: 28,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background,
},
profileInfo: {
flex: 1,
justifyContent: 'center',
@@ -253,12 +262,5 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginLeft: spacing.xs,
},
cancelEditButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.glass.highlight,
justifyContent: 'center',
alignItems: 'center',
},
});
@@ -2,8 +2,7 @@ import React, { useState } from 'react';
import { View, Text, StyleSheet, Modal, Platform, KeyboardAvoidingView, TouchableOpacity, ActivityIndicator, TextInput, ViewStyle } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { BlurView } from 'expo-blur';
import { GlassCard, GlassButton } from '../../../components/ui';
import { GlassCard, GlassButton } from '../../../components/ui';
import { colors, spacing, radius, typography } from '../../../theme';
interface RelationshipDangerZoneProps {
@@ -132,199 +131,187 @@ export const RelationshipDangerZone: React.FC<RelationshipDangerZoneProps> = ({
</Animated.View>
{/* Reset Progress Modal */}
<Modal
visible={showResetModal}
transparent
animationType="fade"
onRequestClose={closeResetModal}
>
<BlurView
intensity={Platform.OS === 'ios' ? 20 : 0}
tint="dark"
style={StyleSheet.absoluteFill}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.warningModalIconContainer}>
<Ionicons name="refresh" size={32} color={colors.warning} />
</View>
<Text style={styles.modalTitle}>Reset Progress?</Text>
</View>
<Text style={styles.modalDescription}>
This will permanently delete:
</Text>
<View style={styles.warningList}>
<View style={styles.deleteListItem}>
<Ionicons name="swap-horizontal" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All your swipes</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="heart" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All matches</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
</View>
<View style={styles.keepList}>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={styles.deleteListText}>Your partner connection stays</Text>
</View>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.warningKeyword}>RESET</Text> to confirm:
</Text>
<TextInput
style={styles.warningInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type RESET"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isResetting}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeResetModal}
disabled={isResetting}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.warningActionButton,
(confirmText !== "RESET" || isResetting) && styles.deleteButtonDisabled,
]}
onPress={handleResetProgress}
disabled={confirmText !== "RESET" || isResetting}
activeOpacity={0.7}
>
{isResetting ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="refresh" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Reset</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</BlurView>
</Modal>
<Modal
visible={showResetModal}
transparent
animationType="fade"
onRequestClose={closeResetModal}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.warningModalIconContainer}>
<Ionicons name="refresh" size={32} color={colors.warning} />
</View>
<Text style={styles.modalTitle}>Reset Progress?</Text>
</View>
<Text style={styles.modalDescription}>
This will permanently delete:
</Text>
<View style={styles.warningList}>
<View style={styles.deleteListItem}>
<Ionicons name="swap-horizontal" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All your swipes</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="heart" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All matches</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.warning} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
</View>
<View style={styles.keepList}>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={styles.deleteListText}>Your partner connection stays</Text>
</View>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.warningKeyword}>RESET</Text> to confirm:
</Text>
<TextInput
style={styles.warningInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type RESET"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isResetting}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeResetModal}
disabled={isResetting}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.warningActionButton,
(confirmText !== "RESET" || isResetting) && styles.deleteButtonDisabled,
]}
onPress={handleResetProgress}
disabled={confirmText !== "RESET" || isResetting}
activeOpacity={0.7}
>
{isResetting ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="refresh" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Reset</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
{/* Delete Relationship Modal */}
<Modal
visible={showDeleteRelationshipModal}
transparent
animationType="fade"
onRequestClose={closeRelationshipModal}
>
<BlurView
intensity={Platform.OS === 'ios' ? 20 : 0}
tint="dark"
style={StyleSheet.absoluteFill}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="heart-dislike" size={32} color={colors.error} />
</View>
<Text style={styles.modalTitle}>Delete Relationship Data?</Text>
</View>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<View style={styles.deleteList}>
<View style={styles.deleteListItem}>
<Ionicons name="heart-dislike" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your couple connection</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All matches and responses</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="images" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All shared photos</Text>
</View>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>DELETE</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type DELETE"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isDeletingRelationship}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeRelationshipModal}
disabled={isDeletingRelationship}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.deleteButton,
(confirmText !== "DELETE" || isDeletingRelationship) && styles.deleteButtonDisabled,
]}
onPress={handleDeleteRelationship}
disabled={confirmText !== "DELETE" || isDeletingRelationship}
activeOpacity={0.7}
>
{isDeletingRelationship ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="trash" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Delete</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</BlurView>
</Modal>
<Modal
visible={showDeleteRelationshipModal}
transparent
animationType="fade"
onRequestClose={closeRelationshipModal}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="heart-dislike" size={32} color={colors.error} />
</View>
<Text style={styles.modalTitle}>Delete Relationship Data?</Text>
</View>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<View style={styles.deleteList}>
<View style={styles.deleteListItem}>
<Ionicons name="heart-dislike" size={16} color={colors.error} />
<Text style={styles.deleteListText}>Your couple connection</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All matches and responses</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All chat messages</Text>
</View>
<View style={styles.deleteListItem}>
<Ionicons name="images" size={16} color={colors.error} />
<Text style={styles.deleteListText}>All shared photos</Text>
</View>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>DELETE</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={confirmText}
onChangeText={setConfirmText}
placeholder="Type DELETE"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isDeletingRelationship}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={closeRelationshipModal}
disabled={isDeletingRelationship}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.deleteButton,
(confirmText !== "DELETE" || isDeletingRelationship) && styles.deleteButtonDisabled,
]}
onPress={handleDeleteRelationship}
disabled={confirmText !== "DELETE" || isDeletingRelationship}
activeOpacity={0.7}
>
{isDeletingRelationship ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="trash" size={16} color={colors.text} />
<Text style={styles.deleteButtonText}>Delete</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
</>
);
};
@@ -419,15 +406,15 @@ const styles = StyleSheet.create({
alignItems: "center",
padding: spacing.lg,
},
modalContent: {
width: "100%",
maxWidth: 400,
padding: spacing.xl,
backgroundColor: colors.backgroundLight,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: colors.glass.border,
},
modalContent: {
width: "100%",
maxWidth: 400,
padding: spacing.xl,
backgroundColor: colors.backgroundLight,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: colors.border,
},
modalHeader: {
alignItems: "center",
marginBottom: spacing.lg,
@@ -492,13 +479,13 @@ const styles = StyleSheet.create({
fontWeight: "bold",
color: colors.warning,
},
warningInput: {
backgroundColor: colors.glass.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.warning,
padding: spacing.md,
...typography.body,
warningInput: {
backgroundColor: colors.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.warning,
padding: spacing.md,
...typography.body,
color: colors.text,
textAlign: 'center',
marginBottom: spacing.xl,
@@ -532,13 +519,13 @@ const styles = StyleSheet.create({
fontWeight: "bold",
color: colors.error,
},
confirmInput: {
backgroundColor: colors.glass.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.error,
padding: spacing.md,
...typography.body,
confirmInput: {
backgroundColor: colors.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.error,
padding: spacing.md,
...typography.body,
color: colors.text,
textAlign: 'center',
marginBottom: spacing.xl,
@@ -547,15 +534,15 @@ const styles = StyleSheet.create({
flexDirection: "row",
gap: spacing.md,
},
cancelButton: {
flex: 1,
backgroundColor: colors.glass.backgroundLight,
paddingVertical: spacing.md,
borderRadius: radius.md,
alignItems: "center",
borderWidth: 1,
borderColor: colors.glass.borderLight,
},
cancelButton: {
flex: 1,
backgroundColor: colors.background,
paddingVertical: spacing.md,
borderRadius: radius.md,
alignItems: "center",
borderWidth: 1,
borderColor: colors.border,
},
cancelButtonText: {
...typography.body,
color: colors.textSecondary,
@@ -2,8 +2,7 @@ import React, { useState } from 'react';
import { View, Text, StyleSheet, Modal, Platform, KeyboardAvoidingView, TouchableOpacity, ActivityIndicator, TextInput } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { BlurView } from 'expo-blur';
import { GlassCard } from '../../../components/ui';
import { GlassCard } from '../../../components/ui';
import { colors, spacing, radius, typography } from '../../../theme';
interface ResetProgressProps {
@@ -61,106 +60,100 @@ export const ResetProgress: React.FC<ResetProgressProps> = ({ onResetProgress })
</GlassCard>
</Animated.View>
<Modal
visible={showResetModal}
transparent
animationType="fade"
onRequestClose={() => !isResetting && setShowResetModal(false)}
>
<BlurView
intensity={Platform.OS === 'ios' ? 20 : 0}
tint="dark"
style={StyleSheet.absoluteFill}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="refresh" size={32} color={colors.warning} />
</View>
<Text style={styles.modalTitle}>Reset Progress?</Text>
</View>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<View style={styles.resetList}>
<View style={styles.resetListItem}>
<Ionicons name="swap-horizontal" size={16} color={colors.warning} />
<Text style={styles.resetListText}>All your swipes</Text>
</View>
<View style={styles.resetListItem}>
<Ionicons name="heart" size={16} color={colors.warning} />
<Text style={styles.resetListText}>All matches</Text>
</View>
<View style={styles.resetListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.warning} />
<Text style={styles.resetListText}>All chat messages</Text>
</View>
</View>
<View style={styles.keepList}>
<View style={styles.keepListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={styles.keepListText}>Your partner connection stays</Text>
</View>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>RESET</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={resetConfirmText}
onChangeText={setResetConfirmText}
placeholder="Type RESET"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isResetting}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
setShowResetModal(false);
setResetConfirmText("");
}}
disabled={isResetting}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.confirmResetButton,
(resetConfirmText !== "RESET" || isResetting) && styles.confirmResetButtonDisabled,
]}
onPress={handleReset}
disabled={resetConfirmText !== "RESET" || isResetting}
activeOpacity={0.7}
>
{isResetting ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="refresh" size={16} color={colors.text} />
<Text style={styles.confirmResetButtonText}>Reset</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</BlurView>
</Modal>
<Modal
visible={showResetModal}
transparent
animationType="fade"
onRequestClose={() => !isResetting && setShowResetModal(false)}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalIconContainer}>
<Ionicons name="refresh" size={32} color={colors.warning} />
</View>
<Text style={styles.modalTitle}>Reset Progress?</Text>
</View>
<Text style={styles.modalDescription}>
This action cannot be undone. This will permanently delete:
</Text>
<View style={styles.resetList}>
<View style={styles.resetListItem}>
<Ionicons name="swap-horizontal" size={16} color={colors.warning} />
<Text style={styles.resetListText}>All your swipes</Text>
</View>
<View style={styles.resetListItem}>
<Ionicons name="heart" size={16} color={colors.warning} />
<Text style={styles.resetListText}>All matches</Text>
</View>
<View style={styles.resetListItem}>
<Ionicons name="chatbubbles" size={16} color={colors.warning} />
<Text style={styles.resetListText}>All chat messages</Text>
</View>
</View>
<View style={styles.keepList}>
<View style={styles.keepListItem}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={styles.keepListText}>Your partner connection stays</Text>
</View>
</View>
<Text style={styles.confirmLabel}>
Type <Text style={styles.confirmKeyword}>RESET</Text> to confirm:
</Text>
<TextInput
style={styles.confirmInput}
value={resetConfirmText}
onChangeText={setResetConfirmText}
placeholder="Type RESET"
placeholderTextColor={colors.textTertiary}
autoCapitalize="characters"
autoCorrect={false}
editable={!isResetting}
/>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
setShowResetModal(false);
setResetConfirmText("");
}}
disabled={isResetting}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.confirmResetButton,
(resetConfirmText !== "RESET" || isResetting) && styles.confirmResetButtonDisabled,
]}
onPress={handleReset}
disabled={resetConfirmText !== "RESET" || isResetting}
activeOpacity={0.7}
>
{isResetting ? (
<ActivityIndicator size="small" color={colors.text} />
) : (
<>
<Ionicons name="refresh" size={16} color={colors.text} />
<Text style={styles.confirmResetButtonText}>Reset</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
</>
);
};
@@ -233,15 +226,15 @@ const styles = StyleSheet.create({
alignItems: "center",
padding: spacing.lg,
},
modalContent: {
width: "100%",
maxWidth: 400,
padding: spacing.xl,
backgroundColor: colors.backgroundLight,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: colors.glass.border,
},
modalContent: {
width: "100%",
maxWidth: 400,
padding: spacing.xl,
backgroundColor: colors.backgroundLight,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: colors.border,
},
modalHeader: {
alignItems: "center",
marginBottom: spacing.lg,
@@ -312,13 +305,13 @@ const styles = StyleSheet.create({
fontWeight: "bold",
color: colors.warning,
},
confirmInput: {
backgroundColor: colors.glass.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.warning,
padding: spacing.md,
...typography.body,
confirmInput: {
backgroundColor: colors.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.warning,
padding: spacing.md,
...typography.body,
color: colors.text,
textAlign: 'center',
marginBottom: spacing.xl,
@@ -327,15 +320,15 @@ const styles = StyleSheet.create({
flexDirection: "row",
gap: spacing.md,
},
cancelButton: {
flex: 1,
backgroundColor: colors.glass.backgroundLight,
paddingVertical: spacing.md,
borderRadius: radius.md,
alignItems: "center",
borderWidth: 1,
borderColor: colors.glass.borderLight,
},
cancelButton: {
flex: 1,
backgroundColor: colors.background,
paddingVertical: spacing.md,
borderRadius: radius.md,
alignItems: "center",
borderWidth: 1,
borderColor: colors.border,
},
cancelButtonText: {
...typography.body,
color: colors.textSecondary,
@@ -53,14 +53,16 @@ const styles = StyleSheet.create({
borderBottomWidth: 1,
borderBottomColor: `${featureColors.profile.accent}15`,
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.glass.background,
justifyContent: 'center',
alignItems: 'center',
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.backgroundLight,
borderWidth: 1,
borderColor: colors.border,
justifyContent: 'center',
alignItems: 'center',
},
title: {
...typography.headline,
color: colors.text,
@@ -14,7 +14,7 @@ export interface SettingsSectionProps {
}
/**
* Reusable settings section with title and glass card container.
* Reusable settings section with title and solid card container.
*/
export function SettingsSection({ title, delay = 200, children }: SettingsSectionProps) {
return (
@@ -242,24 +242,57 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
opacity: 0.3,
},
proCardInner: {
backgroundColor: 'rgba(22, 33, 62, 0.8)',
margin: 1,
borderRadius: radius.lg - 1,
padding: spacing.md,
},
proCardInner: {
backgroundColor: colors.backgroundLight, // Solid/Flat background
margin: 1,
borderRadius: radius.lg - 1,
padding: spacing.md,
},
// ...
proBadge: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
// Removed glow
},
// ...
manageButton: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.md,
backgroundColor: colors.background,
borderWidth: 1,
borderColor: colors.border,
},
// ...
proStatusRow: {
flexDirection: 'row',
marginTop: spacing.md,
paddingTop: spacing.md,
borderTopWidth: 1,
borderTopColor: colors.border,
gap: spacing.lg,
},
// ...
freeCardInner: {
backgroundColor: colors.backgroundLight, // Solid/Flat background
margin: 1,
borderRadius: radius.lg - 1,
padding: spacing.lg,
},
// ...
ctaButtonContainer: {
borderRadius: radius.md,
overflow: 'hidden',
// Removed glow
},
proHeader: {
flexDirection: 'row',
alignItems: 'center',
},
proBadge: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
...shadows.glow(colors.premium.gold),
},
proHeaderText: {
flex: 1,
marginLeft: spacing.md,
@@ -274,27 +307,13 @@ const styles = StyleSheet.create({
color: colors.textTertiary,
marginTop: 2,
},
manageButton: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.md,
backgroundColor: colors.glass.background,
borderWidth: 1,
borderColor: `${colors.premium.gold}40`,
},
manageButtonText: {
...typography.subhead,
color: colors.premium.gold,
fontWeight: '600',
},
proStatusRow: {
flexDirection: 'row',
marginTop: spacing.md,
paddingTop: spacing.md,
borderTopWidth: 1,
borderTopColor: colors.glass.border,
gap: spacing.lg,
},
proStatusItem: {
flexDirection: 'row',
alignItems: 'center',
@@ -313,12 +332,7 @@ const styles = StyleSheet.create({
freeBorderGradient: {
...StyleSheet.absoluteFillObject,
},
freeCardInner: {
backgroundColor: 'rgba(22, 33, 62, 0.85)',
margin: 1,
borderRadius: radius.lg - 1,
padding: spacing.lg,
},
freeHeader: {
alignItems: 'center',
marginBottom: spacing.lg,
@@ -358,11 +372,7 @@ const styles = StyleSheet.create({
...typography.body,
color: colors.text,
},
ctaButtonContainer: {
borderRadius: radius.md,
overflow: 'hidden',
...shadows.glow(colors.premium.gold),
},
ctaButton: {
flexDirection: 'row',
alignItems: 'center',
@@ -16,7 +16,7 @@ import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { GradientBackground, GlassCard, GlassButton } from '../../../components/ui';
import { GradientBackground } from '../../../components/ui';
import { colors, featureColors, spacing, typography, radius } from '../../../theme';
import { useAuthStore } from '../../../store';
import { supabase } from '../../../lib/supabase';
@@ -53,7 +53,7 @@ const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
* Profile settings sub-screen for editing avatar, display name, and gender.
*/
export function ProfileSettingsScreen() {
const { user, fetchUser } = useAuthStore();
const { user, fetchUser, isAnonymous } = useAuthStore();
const settings = useProfileSettings();
const authEmail = useAuthEmail();
@@ -260,10 +260,12 @@ export function ProfileSettingsScreen() {
color={colors.textTertiary}
style={styles.inputIcon}
/>
<Text style={styles.readOnlyText}>{authEmail || 'Loading...'}</Text>
<Text style={styles.readOnlyText}>
{authEmail || (isAnonymous ? 'Guest account' : 'Loading...')}
</Text>
</View>
<Text style={styles.inputHint}>
Email cannot be changed
{isAnonymous ? 'Save your account to add an email' : 'Email cannot be changed'}
</Text>
</SettingsSection>
@@ -300,11 +302,41 @@ const styles = StyleSheet.create({
padding: 4,
justifyContent: 'center',
alignItems: 'center',
shadowColor: featureColors.profile.accent,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6,
// Removed shadows
},
// ...
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background, // Flat
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: spacing.md,
},
// ...
readOnlyField: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundLight,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
opacity: 0.7,
},
// ...
genderOption: {
width: '48%',
alignItems: 'center',
backgroundColor: colors.backgroundLight,
borderRadius: radius.lg,
borderWidth: 2,
borderColor: colors.border,
paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
gap: spacing.xs,
},
avatarImage: {
width: '100%',
@@ -354,15 +386,7 @@ const styles = StyleSheet.create({
color: colors.textTertiary,
},
// Input
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.glass.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.glass.border,
paddingHorizontal: spacing.md,
},
inputIcon: {
marginRight: spacing.sm,
},
@@ -379,17 +403,7 @@ const styles = StyleSheet.create({
marginLeft: spacing.xs,
},
// Read-only field
readOnlyField: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.glass.background,
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.glass.border,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
opacity: 0.7,
},
readOnlyText: {
...typography.body,
color: colors.textSecondary,
@@ -405,17 +419,7 @@ const styles = StyleSheet.create({
flexWrap: 'wrap',
gap: spacing.sm,
},
genderOption: {
width: '48%',
alignItems: 'center',
backgroundColor: colors.glass.background,
borderRadius: radius.lg,
borderWidth: 2,
borderColor: colors.glass.border,
paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
gap: spacing.xs,
},
genderOptionSelected: {
borderColor: colors.primary,
backgroundColor: colors.primaryLight,
@@ -510,7 +510,7 @@ const styles = StyleSheet.create({
},
modeToggleContainer: {
flexDirection: "row",
backgroundColor: colors.glass.background,
backgroundColor: colors.backgroundLight,
borderRadius: radius.md,
padding: 4,
marginBottom: spacing.md,
@@ -58,11 +58,11 @@ const styles = StyleSheet.create({
paddingTop: spacing.lg,
paddingBottom: Platform.OS === 'ios' ? 40 : 20,
},
divider: {
height: 1,
backgroundColor: colors.glass.border,
marginVertical: spacing.md,
},
});
divider: {
height: 1,
backgroundColor: colors.border,
marginVertical: spacing.md,
},
});
export default SupportScreen;