mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
Add streaks, scheduled releases, and notification settings
- Couple streaks with milestone notifications - Scheduled pack releases with admin UI - Notification preferences screen - Refactored profile settings screens - Partner activity notifications
This commit is contained in:
1 parent
12f312421b
commit
494ed17fe2
82 files changed
+4189
-725
No files matched your search
@@ -15,6 +15,10 @@ export interface MenuItemProps {
|
||||
onPress: () => void;
|
||||
/** Optional element to show on the right */
|
||||
rightElement?: React.ReactNode;
|
||||
/** Text to display on the right side (e.g., partner name) */
|
||||
rightText?: string;
|
||||
/** Badge label to display (e.g., "Premium") */
|
||||
badge?: string;
|
||||
/** Whether to show chevron arrow */
|
||||
showChevron?: boolean;
|
||||
/** Visual variant */
|
||||
@@ -34,6 +38,8 @@ export function MenuItem({
|
||||
description,
|
||||
onPress,
|
||||
rightElement,
|
||||
rightText,
|
||||
badge,
|
||||
showChevron = true,
|
||||
variant = 'default',
|
||||
disabled = false,
|
||||
@@ -65,17 +71,29 @@ export function MenuItem({
|
||||
</LinearGradient>
|
||||
)}
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={[styles.label, { color: labelColor }]}>{label}</Text>
|
||||
<View style={styles.labelRow}>
|
||||
<Text style={[styles.label, { color: labelColor }]}>{label}</Text>
|
||||
{badge && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{badge}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{description && (
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{rightElement || (showChevron && (
|
||||
<View style={styles.chevron}>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.rightSection}>
|
||||
{rightText && (
|
||||
<Text style={styles.rightText} numberOfLines={1}>{rightText}</Text>
|
||||
)}
|
||||
{rightElement || (showChevron && (
|
||||
<View style={styles.chevron}>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -113,15 +131,43 @@ const styles = StyleSheet.create({
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
labelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
label: {
|
||||
...typography.body,
|
||||
fontWeight: '500',
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: colors.premium.goldLight,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 2,
|
||||
borderRadius: radius.full,
|
||||
},
|
||||
badgeText: {
|
||||
...typography.caption2,
|
||||
color: colors.premium.gold,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
description: {
|
||||
...typography.caption1,
|
||||
color: colors.textSecondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
rightSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
rightText: {
|
||||
...typography.subhead,
|
||||
color: colors.textSecondary,
|
||||
maxWidth: 120,
|
||||
},
|
||||
chevron: {
|
||||
padding: spacing.xs,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import { colors, spacing, typography } from '../../../theme';
|
||||
import { useNotificationPreferencesStore } from '../../../store';
|
||||
import { useAuthStore } from '../../../store';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { SwitchItem } from './SwitchItem';
|
||||
|
||||
@@ -16,16 +19,132 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({
|
||||
onPushToggle,
|
||||
delay = 360,
|
||||
}) => {
|
||||
const { user } = useAuthStore();
|
||||
const { preferences, isUpdating, fetchPreferences, updatePreference } = useNotificationPreferencesStore();
|
||||
|
||||
// Fetch notification preferences on mount when user is available
|
||||
useEffect(() => {
|
||||
if (user?.id && !preferences) {
|
||||
fetchPreferences();
|
||||
}
|
||||
}, [user?.id, preferences, fetchPreferences]);
|
||||
|
||||
// Memoize handlers to prevent unnecessary re-renders
|
||||
const handlers = useMemo(() => ({
|
||||
onMatchesToggle: (value: boolean) => updatePreference('matches_enabled', value),
|
||||
onMessagesToggle: (value: boolean) => updatePreference('messages_enabled', value),
|
||||
onPartnerActivityToggle: (value: boolean) => updatePreference('partner_activity_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),
|
||||
}), [updatePreference]);
|
||||
|
||||
return (
|
||||
<SettingsSection title="Notifications" delay={delay}>
|
||||
{/* Master push toggle */}
|
||||
<SwitchItem
|
||||
icon="notifications-outline"
|
||||
label="Push Notifications"
|
||||
description="Get alerts for matches and messages"
|
||||
description="Enable notifications on this device"
|
||||
value={pushEnabled}
|
||||
onValueChange={onPushToggle}
|
||||
disabled={isUpdatingPush}
|
||||
/>
|
||||
|
||||
{/* Granular settings - only show when push is enabled and preferences are loaded */}
|
||||
{pushEnabled && preferences && (
|
||||
<>
|
||||
<View style={styles.sectionDivider}>
|
||||
<Text style={styles.subsectionLabel}>Notification Types</Text>
|
||||
</View>
|
||||
|
||||
<SwitchItem
|
||||
icon="heart-outline"
|
||||
label="Matches"
|
||||
description="When you and your partner match"
|
||||
value={preferences.matches_enabled}
|
||||
onValueChange={handlers.onMatchesToggle}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
|
||||
<View style={styles.itemDivider} />
|
||||
|
||||
<SwitchItem
|
||||
icon="chatbubble-outline"
|
||||
label="Messages"
|
||||
description="When your partner sends you a message"
|
||||
value={preferences.messages_enabled}
|
||||
onValueChange={handlers.onMessagesToggle}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
|
||||
<View style={styles.itemDivider} />
|
||||
|
||||
<SwitchItem
|
||||
icon="person-outline"
|
||||
label="Partner Activity"
|
||||
description="When your partner answers questions"
|
||||
value={preferences.partner_activity_enabled}
|
||||
onValueChange={handlers.onPartnerActivityToggle}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
|
||||
<View style={styles.itemDivider} />
|
||||
|
||||
<SwitchItem
|
||||
icon="layers-outline"
|
||||
label="Pack Updates"
|
||||
description="When your partner enables new packs"
|
||||
value={preferences.pack_changes_enabled}
|
||||
onValueChange={handlers.onPackChangesToggle}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
|
||||
<View style={styles.itemDivider} />
|
||||
|
||||
<SwitchItem
|
||||
icon="sparkles-outline"
|
||||
label="New Packs"
|
||||
description="When new question packs are available"
|
||||
value={preferences.new_packs_enabled}
|
||||
onValueChange={handlers.onNewPacksToggle}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
|
||||
<View style={styles.itemDivider} />
|
||||
|
||||
<SwitchItem
|
||||
icon="flame-outline"
|
||||
label="Streak Milestones"
|
||||
description="Celebrate your streak achievements"
|
||||
value={preferences.streak_milestones_enabled}
|
||||
onValueChange={handlers.onStreakMilestonesToggle}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
sectionDivider: {
|
||||
marginTop: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
paddingTop: spacing.md,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.glass.border,
|
||||
},
|
||||
subsectionLabel: {
|
||||
...typography.caption1,
|
||||
color: colors.textTertiary,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
itemDivider: {
|
||||
height: 1,
|
||||
backgroundColor: colors.glass.border,
|
||||
marginVertical: spacing.sm,
|
||||
},
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Modal, Platform, ScrollView } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { SwitchItem } from './SwitchItem';
|
||||
import { IntensitySlider } from '../../../components/ui/IntensitySlider';
|
||||
import { GlassButton } from '../../../components/ui';
|
||||
import { colors, spacing, radius, typography, featureColors } from '../../../theme';
|
||||
import type { IntensityLevel } from '../../../types';
|
||||
|
||||
@@ -38,7 +40,10 @@ export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
|
||||
partnerName,
|
||||
partnerAvatar,
|
||||
}) => {
|
||||
const [showHelpModal, setShowHelpModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection title="Preferences" delay={375}>
|
||||
{/* Comfort Zone Header */}
|
||||
<View style={styles.comfortHeader}>
|
||||
@@ -51,7 +56,16 @@ export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
|
||||
<Ionicons name="heart-circle" size={22} color={colors.text} />
|
||||
</LinearGradient>
|
||||
<View style={styles.comfortText}>
|
||||
<Text style={styles.comfortLabel}>Comfort Zone</Text>
|
||||
<View style={styles.comfortLabelRow}>
|
||||
<Text style={styles.comfortLabel}>Comfort Zone</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowHelpModal(true)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="help-circle-outline" size={20} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.comfortDescription}>
|
||||
Set the intimacy level you're comfortable with
|
||||
</Text>
|
||||
@@ -85,6 +99,100 @@ export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
|
||||
</>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
{/* Help Modal */}
|
||||
<Modal
|
||||
visible={showHelpModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowHelpModal(false)}
|
||||
>
|
||||
<BlurView
|
||||
intensity={Platform.OS === 'ios' ? 20 : 0}
|
||||
tint="dark"
|
||||
style={StyleSheet.absoluteFill}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>How Comfort Zone Works</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowHelpModal(false)}
|
||||
style={styles.closeButton}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="close" size={24} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.helpScrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.helpSection}>
|
||||
<View style={styles.helpItem}>
|
||||
<Ionicons name="layers-outline" size={20} color={colors.primary} style={styles.helpIcon} />
|
||||
<View style={styles.helpTextContainer}>
|
||||
<Text style={styles.helpItemTitle}>Intensity Levels</Text>
|
||||
<Text style={styles.helpItemDescription}>
|
||||
Questions range from 1 (mild) to 5 (spicy). You'll only see questions at or below your comfort level.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.helpItem}>
|
||||
<Ionicons name="albums-outline" size={20} color={colors.primary} style={styles.helpIcon} />
|
||||
<View style={styles.helpTextContainer}>
|
||||
<Text style={styles.helpItemTitle}>Hidden Packs</Text>
|
||||
<Text style={styles.helpItemDescription}>
|
||||
Some question packs are designed for higher intensity levels. These packs won't appear until you raise your comfort zone.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.helpItem}>
|
||||
<Ionicons name="people-outline" size={20} color={colors.primary} style={styles.helpIcon} />
|
||||
<View style={styles.helpTextContainer}>
|
||||
<Text style={styles.helpItemTitle}>Partner Sync</Text>
|
||||
<Text style={styles.helpItemDescription}>
|
||||
Questions only appear when both of you are comfortable. The lower setting between you and your partner determines what you both see.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.helpItem}>
|
||||
<Ionicons name="eye-outline" size={20} color={colors.primary} style={styles.helpIcon} />
|
||||
<View style={styles.helpTextContainer}>
|
||||
<Text style={styles.helpItemTitle}>Visible to Partner</Text>
|
||||
<Text style={styles.helpItemDescription}>
|
||||
Your partner can see the comfort zone level you choose so you can stay aligned. This doesn’t reveal your individual swipes.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.helpItem, styles.helpItemLast]}>
|
||||
<Ionicons name="refresh-outline" size={20} color={colors.primary} style={styles.helpIcon} />
|
||||
<View style={styles.helpTextContainer}>
|
||||
<Text style={styles.helpItemTitle}>Change Anytime</Text>
|
||||
<Text style={styles.helpItemDescription}>
|
||||
Adjust whenever you want. New questions and packs will appear or hide based on your new setting.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<GlassButton
|
||||
onPress={() => setShowHelpModal(false)}
|
||||
fullWidth
|
||||
>
|
||||
Got it
|
||||
</GlassButton>
|
||||
</View>
|
||||
</View>
|
||||
</BlurView>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -105,6 +213,11 @@ const styles = StyleSheet.create({
|
||||
comfortText: {
|
||||
flex: 1,
|
||||
},
|
||||
comfortLabelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
comfortLabel: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
@@ -122,4 +235,71 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.glass.border,
|
||||
marginVertical: spacing.lg,
|
||||
},
|
||||
// Modal styles
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
modalContent: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
maxHeight: '80%',
|
||||
backgroundColor: colors.backgroundLight,
|
||||
borderRadius: radius.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.glass.border,
|
||||
padding: spacing.lg,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.md,
|
||||
paddingBottom: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.glass.border,
|
||||
},
|
||||
modalTitle: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
},
|
||||
closeButton: {
|
||||
padding: spacing.xs,
|
||||
},
|
||||
helpScrollView: {
|
||||
flexGrow: 0,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
helpSection: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
helpItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
helpItemLast: {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
helpIcon: {
|
||||
marginRight: spacing.sm,
|
||||
marginTop: 2,
|
||||
},
|
||||
helpTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
helpItemTitle: {
|
||||
...typography.subhead,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
marginBottom: 2,
|
||||
},
|
||||
helpItemDescription: {
|
||||
...typography.caption1,
|
||||
color: colors.textSecondary,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,584 @@
|
||||
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';
|
||||
|
||||
interface RelationshipDangerZoneProps {
|
||||
onResetProgress?: () => Promise<void>;
|
||||
onDeleteRelationship?: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Danger zone component specifically for relationship-related actions.
|
||||
* Does NOT include delete account - that's in AccountScreen.
|
||||
*/
|
||||
export const RelationshipDangerZone: React.FC<RelationshipDangerZoneProps> = ({
|
||||
onResetProgress,
|
||||
onDeleteRelationship,
|
||||
}) => {
|
||||
const [showResetModal, setShowResetModal] = useState(false);
|
||||
const [showDeleteRelationshipModal, setShowDeleteRelationshipModal] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState("");
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const [isDeletingRelationship, setIsDeletingRelationship] = useState(false);
|
||||
|
||||
const handleResetProgress = async () => {
|
||||
if (confirmText !== "RESET" || !onResetProgress) return;
|
||||
setIsResetting(true);
|
||||
try {
|
||||
await onResetProgress();
|
||||
setShowResetModal(false);
|
||||
setConfirmText("");
|
||||
} catch (error) {
|
||||
// Error handling relies on parent
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeResetModal = () => {
|
||||
if (!isResetting) {
|
||||
setShowResetModal(false);
|
||||
setConfirmText("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRelationship = async () => {
|
||||
if (confirmText !== "DELETE" || !onDeleteRelationship) return;
|
||||
setIsDeletingRelationship(true);
|
||||
try {
|
||||
await onDeleteRelationship();
|
||||
setShowDeleteRelationshipModal(false);
|
||||
setConfirmText("");
|
||||
} catch (error) {
|
||||
// Error handling relies on parent
|
||||
} finally {
|
||||
setIsDeletingRelationship(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeRelationshipModal = () => {
|
||||
if (!isDeletingRelationship) {
|
||||
setShowDeleteRelationshipModal(false);
|
||||
setConfirmText("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(100).duration(500)}
|
||||
style={styles.section}
|
||||
>
|
||||
<Text style={styles.dangerSectionTitle}>Danger Zone</Text>
|
||||
|
||||
{/* Reset Progress */}
|
||||
{onResetProgress && (
|
||||
<GlassCard style={styles.warningCard}>
|
||||
<View style={styles.dangerContent}>
|
||||
<View style={styles.dangerInfo}>
|
||||
<View style={styles.warningIconContainer}>
|
||||
<Ionicons name="refresh" size={20} color={colors.warning} />
|
||||
</View>
|
||||
<View style={styles.dangerTextContainer}>
|
||||
<Text style={styles.dangerTitle}>Reset Progress</Text>
|
||||
<Text style={styles.dangerDescription}>
|
||||
Start fresh by clearing all swipes, matches, and chats. Your partner connection stays.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.warningButton}
|
||||
onPress={() => setShowResetModal(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="refresh-outline" size={16} color={colors.warning} />
|
||||
<Text style={styles.warningButtonText}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
{/* Delete Relationship */}
|
||||
{onDeleteRelationship && (
|
||||
<GlassCard style={StyleSheet.flatten([styles.dangerCard, onResetProgress && styles.dangerCardSpacing]) as ViewStyle}>
|
||||
<View style={styles.dangerContent}>
|
||||
<View style={styles.dangerInfo}>
|
||||
<View style={styles.dangerIconContainer}>
|
||||
<Ionicons name="heart-dislike" size={20} color={colors.error} />
|
||||
</View>
|
||||
<View style={styles.dangerTextContainer}>
|
||||
<Text style={styles.dangerTitle}>Delete Relationship</Text>
|
||||
<Text style={styles.dangerDescription}>
|
||||
End your connection and delete all shared data. Your account will remain.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<GlassButton
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onPress={() => setShowDeleteRelationshipModal(true)}
|
||||
icon={<Ionicons name="trash-outline" size={16} color={colors.text} />}
|
||||
style={styles.dangerButton}
|
||||
>
|
||||
Delete
|
||||
</GlassButton>
|
||||
</View>
|
||||
</GlassCard>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
dangerSectionTitle: {
|
||||
...typography.caption1,
|
||||
color: colors.error,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1.5,
|
||||
marginBottom: spacing.sm,
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
warningCard: {
|
||||
borderColor: 'rgba(243, 156, 18, 0.3)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
warningIconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: 'rgba(243, 156, 18, 0.15)',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
warningButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: 'rgba(243, 156, 18, 0.15)',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(243, 156, 18, 0.3)',
|
||||
gap: spacing.xs,
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
warningButtonText: {
|
||||
...typography.subhead,
|
||||
color: colors.warning,
|
||||
fontWeight: "600",
|
||||
},
|
||||
dangerCard: {
|
||||
borderColor: 'rgba(231, 76, 60, 0.3)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
dangerCardSpacing: {
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
dangerContent: {
|
||||
gap: spacing.md,
|
||||
},
|
||||
dangerInfo: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
dangerIconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: colors.errorLight,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
dangerTextContainer: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
dangerTitle: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
dangerDescription: {
|
||||
...typography.subhead,
|
||||
color: colors.textSecondary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
dangerButton: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
// Modal styles
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
justifyContent: "center",
|
||||
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,
|
||||
},
|
||||
modalHeader: {
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
modalIconContainer: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: colors.errorLight,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalTitle: {
|
||||
...typography.title2,
|
||||
color: colors.text,
|
||||
textAlign: "center",
|
||||
},
|
||||
modalDescription: {
|
||||
...typography.body,
|
||||
color: colors.textSecondary,
|
||||
marginBottom: spacing.lg,
|
||||
textAlign: "center",
|
||||
},
|
||||
deleteList: {
|
||||
backgroundColor: "rgba(231, 76, 60, 0.1)",
|
||||
borderRadius: radius.lg,
|
||||
padding: spacing.md,
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(231, 76, 60, 0.2)',
|
||||
},
|
||||
warningList: {
|
||||
backgroundColor: "rgba(243, 156, 18, 0.1)",
|
||||
borderRadius: radius.lg,
|
||||
padding: spacing.md,
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(243, 156, 18, 0.2)',
|
||||
},
|
||||
keepList: {
|
||||
backgroundColor: "rgba(46, 204, 113, 0.1)",
|
||||
borderRadius: radius.lg,
|
||||
padding: spacing.md,
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(46, 204, 113, 0.2)',
|
||||
},
|
||||
warningModalIconContainer: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: 'rgba(243, 156, 18, 0.15)',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
warningKeyword: {
|
||||
fontWeight: "bold",
|
||||
color: colors.warning,
|
||||
},
|
||||
warningInput: {
|
||||
backgroundColor: colors.glass.background,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.warning,
|
||||
padding: spacing.md,
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
warningActionButton: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.warning,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
gap: spacing.xs,
|
||||
},
|
||||
deleteListItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
deleteListText: {
|
||||
...typography.subhead,
|
||||
color: colors.text,
|
||||
},
|
||||
confirmLabel: {
|
||||
...typography.subhead,
|
||||
color: colors.textSecondary,
|
||||
marginBottom: spacing.xs,
|
||||
textAlign: "center",
|
||||
},
|
||||
confirmKeyword: {
|
||||
fontWeight: "bold",
|
||||
color: colors.error,
|
||||
},
|
||||
confirmInput: {
|
||||
backgroundColor: colors.glass.background,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.error,
|
||||
padding: spacing.md,
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
modalButtons: {
|
||||
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,
|
||||
},
|
||||
cancelButtonText: {
|
||||
...typography.body,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
deleteButton: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.error,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: radius.md,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
gap: spacing.xs,
|
||||
},
|
||||
deleteButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
deleteButtonText: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
export default RelationshipDangerZone;
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { colors, spacing, typography, featureColors } from '../../../theme';
|
||||
|
||||
export interface ScreenHeaderProps {
|
||||
/** Screen title */
|
||||
title: string;
|
||||
/** Optional element to render on the right side */
|
||||
rightElement?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable header with back navigation for settings sub-screens.
|
||||
*/
|
||||
export function ScreenHeader({ title, rightElement }: ScreenHeaderProps) {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const handleBack = () => {
|
||||
// Navigate back to the settings hub (profile tab)
|
||||
router.navigate('/(app)/profile');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top + spacing.sm }]}>
|
||||
<TouchableOpacity
|
||||
onPress={handleBack}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
<View style={styles.rightContainer}>
|
||||
{rightElement || <View style={styles.placeholder} />}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: `${featureColors.profile.accent}15`,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.glass.background,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
marginHorizontal: spacing.md,
|
||||
},
|
||||
rightContainer: {
|
||||
minWidth: 40,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
placeholder: {
|
||||
width: 40,
|
||||
},
|
||||
});
|
||||
|
||||
export default ScreenHeader;
|
||||
@@ -8,6 +8,7 @@ export type { NotificationSettingsProps } from './NotificationSettings';
|
||||
export { CoupleStatus } from './CoupleStatus';
|
||||
export { PrivacySettings } from './PrivacySettings';
|
||||
export { DangerZone } from './DangerZone';
|
||||
export { RelationshipDangerZone } from './RelationshipDangerZone';
|
||||
export { ResetProgress } from './ResetProgress';
|
||||
|
||||
export { SettingsSection } from './SettingsSection';
|
||||
@@ -18,3 +19,5 @@ export { MenuItem } from './MenuItem';
|
||||
export type { MenuItemProps } from './MenuItem';
|
||||
export { SubscriptionCard } from './SubscriptionCard';
|
||||
export type { SubscriptionCardProps } from './SubscriptionCard';
|
||||
export { ScreenHeader } from './ScreenHeader';
|
||||
export type { ScreenHeaderProps } from './ScreenHeader';
|
||||
Reference in new issue
Block a user