mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat(mobile): align store-safe experience
Remove client-side adult-content preferences and badges so the centrally reviewed catalogue is the single visibility boundary.\n\nUse neutral relationship-focused copy across onboarding, discovery, paywall, and login surfaces, and retain biometric controls under App Lock for both native platforms.
This commit is contained in:
1 parent
72e031eb84
commit
7a0de45482
18 files changed
+68
-368
No files matched your search
@@ -54,7 +54,7 @@ export default function DaresScreen() {
|
|||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<Text style={styles.description}>
|
<Text style={styles.description}>
|
||||||
Push your boundaries together with tantalising challenges designed to ignite passion and create unforgettable moments.
|
Try playful challenges together and create unforgettable moments.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Feature hints */}
|
{/* Feature hints */}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import Animated, {
|
|||||||
import { GradientBackground } from '../../src/components/ui/GradientBackground';
|
import { GradientBackground } from '../../src/components/ui/GradientBackground';
|
||||||
import { GlassCard } from '../../src/components/ui/GlassCard';
|
import { GlassCard } from '../../src/components/ui/GlassCard';
|
||||||
import { GlassButton } from '../../src/components/ui/GlassButton';
|
import { GlassButton } from '../../src/components/ui/GlassButton';
|
||||||
import { GlassToggle } from '../../src/components/ui/GlassToggle';
|
|
||||||
import { Paywall } from '../../src/components/paywall';
|
import { Paywall } from '../../src/components/paywall';
|
||||||
import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme';
|
import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme';
|
||||||
import { useAuthStore } from '../../src/store';
|
import { useAuthStore } from '../../src/store';
|
||||||
@@ -44,23 +43,17 @@ const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
|
|||||||
{ value: 'prefer-not-to-say', label: 'Skip', icon: 'remove-circle-outline' },
|
{ value: 'prefer-not-to-say', label: 'Skip', icon: 'remove-circle-outline' },
|
||||||
];
|
];
|
||||||
|
|
||||||
type UsageReason = 'improve_communication' | 'spice_up_intimacy' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship';
|
type UsageReason = 'improve_communication' | 'reconnect' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship';
|
||||||
|
|
||||||
const PURPOSE_OPTIONS: { value: UsageReason; label: string; icon: string }[] = [
|
const PURPOSE_OPTIONS: { value: UsageReason; label: string; icon: string }[] = [
|
||||||
{ value: 'improve_communication', label: 'Improve communication', icon: 'chatbubbles' },
|
{ value: 'improve_communication', label: 'Improve communication', icon: 'chatbubbles' },
|
||||||
{ value: 'spice_up_intimacy', label: 'Spice up intimacy', icon: 'flame' },
|
{ value: 'reconnect', label: 'Feel more connected', icon: 'flame' },
|
||||||
{ value: 'deeper_connection', label: 'Build deeper connection', icon: 'heart' },
|
{ value: 'deeper_connection', label: 'Build deeper connection', icon: 'heart' },
|
||||||
{ value: 'have_fun', label: 'Have fun together', icon: 'happy' },
|
{ value: 'have_fun', label: 'Have fun together', icon: 'happy' },
|
||||||
{ value: 'strengthen_relationship', label: 'Strengthen our relationship', icon: 'shield-checkmark' },
|
{ value: 'strengthen_relationship', label: 'Strengthen our relationship', icon: 'shield-checkmark' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Intensity thresholds for backwards compatibility
|
type Stage = 'avatar' | 'name' | 'gender' | 'purpose' | 'notifications';
|
||||||
// When hide_nsfw=true, max_intensity=2 (mild content only)
|
|
||||||
// When hide_nsfw=false, max_intensity=5 (all content)
|
|
||||||
const NSFW_OFF_INTENSITY = 2;
|
|
||||||
const NSFW_ON_INTENSITY = 5;
|
|
||||||
|
|
||||||
type Stage = 'avatar' | 'name' | 'gender' | 'purpose' | 'content' | 'notifications';
|
|
||||||
|
|
||||||
export default function OnboardingScreen() {
|
export default function OnboardingScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -72,7 +65,6 @@ export default function OnboardingScreen() {
|
|||||||
const [name, setName] = useState(initialUser.current?.name || '');
|
const [name, setName] = useState(initialUser.current?.name || '');
|
||||||
const [gender, setGender] = useState<Gender | null>(initialUser.current?.gender || null);
|
const [gender, setGender] = useState<Gender | null>(initialUser.current?.gender || null);
|
||||||
const [usageReason, setUsageReason] = useState<UsageReason | null>(null);
|
const [usageReason, setUsageReason] = useState<UsageReason | null>(null);
|
||||||
const [hideNsfw, setHideNsfw] = useState(initialUser.current?.hide_nsfw ?? false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showPaywall, setShowPaywall] = useState(false);
|
const [showPaywall, setShowPaywall] = useState(false);
|
||||||
@@ -137,14 +129,9 @@ export default function OnboardingScreen() {
|
|||||||
setError('Please select what brings you to Sauci');
|
setError('Please select what brings you to Sauci');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStage('content');
|
setStage('notifications');
|
||||||
Events.onboardingStageComplete('purpose');
|
Events.onboardingStageComplete('purpose');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContentContinue = () => {
|
|
||||||
setStage('notifications');
|
|
||||||
Events.onboardingStageComplete('content');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnableNotifications = async () => {
|
const handleEnableNotifications = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -182,18 +169,17 @@ export default function OnboardingScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive intensity from hide_nsfw for backwards compatibility
|
const updatedFields: string[] = ["name", "gender", "usage_reason"];
|
||||||
const maxIntensity = hideNsfw ? NSFW_OFF_INTENSITY : NSFW_ON_INTENSITY;
|
|
||||||
const showExplicitContent = !hideNsfw;
|
|
||||||
const updatedFields: string[] = ["name", "gender", "usage_reason", "max_intensity", "show_explicit_content", "hide_nsfw"];
|
|
||||||
|
|
||||||
const updateData: Record<string, any> = {
|
const updateData: Record<string, any> = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
gender,
|
gender,
|
||||||
usage_reason: usageReason,
|
usage_reason: usageReason,
|
||||||
max_intensity: maxIntensity,
|
// Preserve the safest legacy profile values. Catalogue
|
||||||
show_explicit_content: showExplicitContent,
|
// eligibility is enforced centrally by reviewed status.
|
||||||
hide_nsfw: hideNsfw,
|
max_intensity: 2,
|
||||||
|
show_explicit_content: false,
|
||||||
|
hide_nsfw: true,
|
||||||
onboarding_completed: true,
|
onboarding_completed: true,
|
||||||
onboarding_version: REQUIRED_ONBOARDING_VERSION,
|
onboarding_version: REQUIRED_ONBOARDING_VERSION,
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
@@ -505,73 +491,6 @@ export default function OnboardingScreen() {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'content':
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
key="content"
|
|
||||||
entering={SlideInRight.duration(400)}
|
|
||||||
exiting={FadeOut.duration(200)}
|
|
||||||
style={styles.stageContainer}
|
|
||||||
>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={gradients.primary as [string, string]}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
>
|
|
||||||
<Ionicons name="flame" size={44} color={colors.text} />
|
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.title}>Content Preferences</Text>
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
Choose what type of content you'd like to see
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<GlassCard style={styles.card}>
|
|
||||||
<View style={styles.contentToggleRow}>
|
|
||||||
<View style={styles.contentToggleLeft}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={gradients.primary as [string, string]}
|
|
||||||
style={styles.contentToggleIcon}
|
|
||||||
>
|
|
||||||
<Ionicons name="eye-off" size={24} color={colors.text} />
|
|
||||||
</LinearGradient>
|
|
||||||
<View style={styles.contentToggleText}>
|
|
||||||
<Text style={styles.contentToggleLabel}>Hide Adult Content</Text>
|
|
||||||
<Text style={styles.contentToggleDescription}>
|
|
||||||
Only show mild, family-friendly question packs
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<GlassToggle
|
|
||||||
value={hideNsfw}
|
|
||||||
onValueChange={setHideNsfw}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.contentNote}>
|
|
||||||
You can change this anytime in settings
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<Ionicons name="alert-circle" size={16} color={colors.error} />
|
|
||||||
<Text style={styles.errorText}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</GlassCard>
|
|
||||||
|
|
||||||
<View style={styles.footer}>
|
|
||||||
<GlassButton
|
|
||||||
onPress={handleContentContinue}
|
|
||||||
fullWidth
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</GlassButton>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'notifications':
|
case 'notifications':
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
@@ -660,8 +579,7 @@ export default function OnboardingScreen() {
|
|||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
if (stage === 'notifications') setStage('content');
|
if (stage === 'notifications') setStage('purpose');
|
||||||
else if (stage === 'content') setStage('purpose');
|
|
||||||
else if (stage === 'purpose') setStage('gender');
|
else if (stage === 'purpose') setStage('gender');
|
||||||
else if (stage === 'gender') setStage('name');
|
else if (stage === 'gender') setStage('name');
|
||||||
else if (stage === 'name') setStage('avatar');
|
else if (stage === 'name') setStage('avatar');
|
||||||
@@ -677,8 +595,7 @@ export default function OnboardingScreen() {
|
|||||||
<View style={[styles.progressDot, stage === 'name' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'name' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'gender' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'gender' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'purpose' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'purpose' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'content' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'notifications' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'notifications' && styles.progressDotActive]} />
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.backButtonPlaceholder} />
|
<View style={styles.backButtonPlaceholder} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -954,44 +871,6 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
contentToggleRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
contentToggleLeft: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: spacing.md,
|
|
||||||
},
|
|
||||||
contentToggleIcon: {
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
borderRadius: 24,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: spacing.md,
|
|
||||||
},
|
|
||||||
contentToggleText: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
contentToggleLabel: {
|
|
||||||
...typography.headline,
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
contentToggleDescription: {
|
|
||||||
...typography.caption1,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
contentNote: {
|
|
||||||
...typography.caption1,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: spacing.lg,
|
|
||||||
},
|
|
||||||
|
|
||||||
radioOuterSelected: {
|
radioOuterSelected: {
|
||||||
borderColor: colors.primary,
|
borderColor: colors.primary,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function QuizScreen() {
|
|||||||
style={styles.content}
|
style={styles.content}
|
||||||
>
|
>
|
||||||
{/* Title section */}
|
{/* Title section */}
|
||||||
<Text style={styles.label}>INTIMACY</Text>
|
<Text style={styles.label}>CONNECTION</Text>
|
||||||
<Text style={styles.title}>Quiz</Text>
|
<Text style={styles.title}>Quiz</Text>
|
||||||
|
|
||||||
{/* Decorative separator */}
|
{/* Decorative separator */}
|
||||||
@@ -61,7 +61,7 @@ export default function QuizScreen() {
|
|||||||
<View style={styles.features}>
|
<View style={styles.features}>
|
||||||
<View style={styles.featureItem}>
|
<View style={styles.featureItem}>
|
||||||
<Ionicons name="sparkles" size={16} color={ACCENT_COLOR} />
|
<Ionicons name="sparkles" size={16} color={ACCENT_COLOR} />
|
||||||
<Text style={styles.featureText}>Uncover hidden desires</Text>
|
<Text style={styles.featureText}>Learn what makes each other tick</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.featureItem}>
|
<View style={styles.featureItem}>
|
||||||
<Ionicons name="flame-outline" size={16} color={ACCENT_COLOR} />
|
<Ionicons name="flame-outline" size={16} color={ACCENT_COLOR} />
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ export default function LoginScreen() {
|
|||||||
/>
|
/>
|
||||||
<Text style={styles.title}>Sauci</Text>
|
<Text style={styles.title}>Sauci</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Explore intimacy together
|
Grow closer together
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
|||||||
@@ -242,14 +242,6 @@ export default function PackDetailsScreen() {
|
|||||||
{category ? category.name.toUpperCase() : 'PREVIEW'}
|
{category ? category.name.toUpperCase() : 'PREVIEW'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Explicit badge */}
|
|
||||||
{pack?.is_explicit && (
|
|
||||||
<View style={styles.explicitBadge}>
|
|
||||||
<Ionicons name="flame" size={12} color={colors.text} />
|
|
||||||
<Text style={styles.explicitBadgeText}>18+ EXPLICIT</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Icon with enhanced glow for premium */}
|
{/* Icon with enhanced glow for premium */}
|
||||||
<View style={styles.iconWrapper}>
|
<View style={styles.iconWrapper}>
|
||||||
{pack?.is_premium && (
|
{pack?.is_premium && (
|
||||||
@@ -591,23 +583,6 @@ const styles = StyleSheet.create({
|
|||||||
labelPremium: {
|
labelPremium: {
|
||||||
color: colors.premium.gold,
|
color: colors.premium.gold,
|
||||||
},
|
},
|
||||||
explicitBadge: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.error,
|
|
||||||
paddingHorizontal: spacing.md,
|
|
||||||
paddingVertical: spacing.xs,
|
|
||||||
borderRadius: radius.full,
|
|
||||||
gap: spacing.xs,
|
|
||||||
marginBottom: spacing.md,
|
|
||||||
},
|
|
||||||
explicitBadgeText: {
|
|
||||||
...typography.caption2,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: colors.text,
|
|
||||||
letterSpacing: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Removed glow styles
|
// Removed glow styles
|
||||||
iconGlowOuter: {
|
iconGlowOuter: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
|
|||||||
@@ -18,15 +18,12 @@ describe('packsStore', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
usePacksStore.setState({ packs: [], categories: [], enabledPackIds: [], packProgress: new Map(), isLoading: false } as any);
|
usePacksStore.setState({ packs: [], categories: [], enabledPackIds: [], packProgress: new Map(), isLoading: false } as any);
|
||||||
useAuthStore.setState({ user: { id: 'me', couple_id: 'c1', hide_nsfw: false } } as any);
|
useAuthStore.setState({ user: { id: 'me', couple_id: 'c1' } } as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches categories/packs and filters by hide_nsfw', async () => {
|
it('fetches the centrally reviewed public catalogue', async () => {
|
||||||
// Set hide_nsfw to true - should filter intensity to max 2
|
|
||||||
useAuthStore.setState({ user: { id: 'me', couple_id: 'c1', hide_nsfw: true } } as any);
|
|
||||||
|
|
||||||
const categoriesQuery = createThenableQuery({ data: [{ id: 'cat1' }] });
|
const categoriesQuery = createThenableQuery({ data: [{ id: 'cat1' }] });
|
||||||
const packsQuery = createThenableQuery({ data: [{ id: 'pack1', is_explicit: false }] });
|
const packsQuery = createThenableQuery({ data: [{ id: 'pack1' }] });
|
||||||
const enabledQuery = createThenableQuery({ data: [{ pack_id: 'pack1' }] });
|
const enabledQuery = createThenableQuery({ data: [{ pack_id: 'pack1' }] });
|
||||||
const responsesQuery = createThenableQuery({ data: [] });
|
const responsesQuery = createThenableQuery({ data: [] });
|
||||||
|
|
||||||
@@ -38,14 +35,12 @@ describe('packsStore', () => {
|
|||||||
|
|
||||||
await usePacksStore.getState().fetchPacks();
|
await usePacksStore.getState().fetchPacks();
|
||||||
|
|
||||||
// When hide_nsfw=true, max_intensity=2, so or filter is called
|
expect(packsQuery.eq).toHaveBeenCalledWith('is_public', true);
|
||||||
expect(packsQuery.or).toHaveBeenCalledWith('max_intensity.is.null,max_intensity.lte.2');
|
expect(packsQuery.or).not.toHaveBeenCalled();
|
||||||
// When hide_nsfw=true, explicit packs are filtered out
|
|
||||||
expect(packsQuery.eq).toHaveBeenCalledWith('is_explicit', false);
|
|
||||||
|
|
||||||
const state = usePacksStore.getState();
|
const state = usePacksStore.getState();
|
||||||
expect(state.categories).toEqual([{ id: 'cat1' }]);
|
expect(state.categories).toEqual([{ id: 'cat1' }]);
|
||||||
expect(state.packs).toEqual([{ id: 'pack1', is_explicit: false }]);
|
expect(state.packs).toEqual([{ id: 'pack1' }]);
|
||||||
expect(state.enabledPackIds).toEqual(['pack1']);
|
expect(state.enabledPackIds).toEqual(['pack1']);
|
||||||
expect(state.isLoading).toBe(false);
|
expect(state.isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,18 +92,6 @@ export function BoutiquePackCard({
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Explicit Badge (top-left) */}
|
|
||||||
{pack.is_explicit && (
|
|
||||||
<View style={styles.explicitBadge}>
|
|
||||||
<Ionicons
|
|
||||||
name="flame"
|
|
||||||
size={10}
|
|
||||||
color={colors.text}
|
|
||||||
/>
|
|
||||||
<Text style={styles.explicitBadgeText}>18+</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* PRO Badge (top-right) */}
|
{/* PRO Badge (top-right) */}
|
||||||
{pack.is_premium && (
|
{pack.is_premium && (
|
||||||
<View style={[
|
<View style={[
|
||||||
@@ -242,24 +230,6 @@ const styles = StyleSheet.create({
|
|||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
explicitBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: spacing.sm,
|
|
||||||
left: spacing.sm,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.error,
|
|
||||||
paddingHorizontal: spacing.sm,
|
|
||||||
paddingVertical: 3,
|
|
||||||
borderRadius: radius.xs,
|
|
||||||
gap: 3,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
explicitBadgeText: {
|
|
||||||
...typography.caption2,
|
|
||||||
color: colors.text,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
proBadge: {
|
proBadge: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: spacing.sm,
|
top: spacing.sm,
|
||||||
|
|||||||
@@ -54,12 +54,6 @@ export function ContentTile({ pack, isLocked, isNew = false, progress, onPress }
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.badges}>
|
<View style={styles.badges}>
|
||||||
{pack.is_explicit && (
|
|
||||||
<View style={styles.explicitBadge}>
|
|
||||||
<Ionicons name="flame" size={10} color={colors.text} />
|
|
||||||
<Text style={styles.explicitBadgeText}>18+</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{isNew && (
|
{isNew && (
|
||||||
<View style={styles.newBadge}>
|
<View style={styles.newBadge}>
|
||||||
<Text style={styles.newBadgeText}>NEW</Text>
|
<Text style={styles.newBadgeText}>NEW</Text>
|
||||||
@@ -129,20 +123,6 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
},
|
},
|
||||||
explicitBadge: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.error,
|
|
||||||
paddingHorizontal: spacing.sm,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: radius.xs,
|
|
||||||
gap: 2,
|
|
||||||
},
|
|
||||||
explicitBadgeText: {
|
|
||||||
...typography.caption2,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
newBadge: {
|
newBadge: {
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const FEATURES = [
|
|||||||
{
|
{
|
||||||
icon: "layers" as const,
|
icon: "layers" as const,
|
||||||
title: "All Premium Packs",
|
title: "All Premium Packs",
|
||||||
description: "Unlock every intimate collection",
|
description: "Unlock every question collection",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "sparkles" as const,
|
icon: "sparkles" as const,
|
||||||
@@ -206,7 +206,7 @@ export function Paywall({ visible, onClose, onSuccess, offeringId }: PaywallProp
|
|||||||
|
|
||||||
<Text style={styles.title}>Sauci Pro</Text>
|
<Text style={styles.title}>Sauci Pro</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Unlock the full intimate experience
|
Unlock the full couples experience
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const STEPS: TutorialStep[] = [
|
|||||||
{
|
{
|
||||||
key: "photos",
|
key: "photos",
|
||||||
title: "Share Pictures Privately",
|
title: "Share Pictures Privately",
|
||||||
description: "Send photos that only your partner can see. Your intimate moments stay between the two of you, safe and private.",
|
description: "Send photos that only your partner can see. Your shared moments stay between the two of you, safe and private.",
|
||||||
icon: "images",
|
icon: "images",
|
||||||
gradient: gradients.premiumRose as [string, string],
|
gradient: gradients.premiumRose as [string, string],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ export interface IntensityLevelConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const INTENSITY_LEVELS: IntensityLevelConfig[] = [
|
export const INTENSITY_LEVELS: IntensityLevelConfig[] = [
|
||||||
{ level: 1, label: 'Gentle', emoji: '💭', description: 'Pure emotional connection & non-sexual bonding' },
|
{ level: 1, label: 'Gentle', emoji: '💭', description: 'Easy conversation and everyday connection' },
|
||||||
{ level: 2, label: 'Warm', emoji: '💕', description: 'Romantic atmosphere & affectionate touch' },
|
{ level: 2, label: 'Warm', emoji: '💕', description: 'Romance, appreciation, and thoughtful moments' },
|
||||||
{ level: 3, label: 'Playful', emoji: '😏', description: 'Light sexual exploration & sensual discovery' },
|
{ level: 3, label: 'Playful', emoji: '✨', description: 'Light-hearted questions and shared laughter' },
|
||||||
{ level: 4, label: 'Steamy', emoji: '🔥', description: 'Explicit sexual activities & moderate adventure' },
|
{ level: 4, label: 'Adventurous', emoji: '🌍', description: 'New experiences and ideas to try together' },
|
||||||
{ level: 5, label: 'Intense', emoji: '🌶️', description: 'Advanced/BDSM/Extreme exploration' },
|
{ level: 5, label: 'Deep', emoji: '🌱', description: 'Meaningful reflection and honest conversation' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const HEAT_COLORS = [
|
export const HEAT_COLORS = [
|
||||||
'#9b59b6', // Purple - gentle
|
'#9b59b6', // Purple - gentle
|
||||||
'#e94560', // Rose - warm
|
'#e94560', // Rose - warm
|
||||||
'#ff6b6b', // Coral - playful
|
'#ff6b6b', // Coral - playful
|
||||||
'#ff4757', // Red - steamy
|
'#ff4757', // Red - adventurous
|
||||||
'#ff3333', // Bright red - intense
|
'#ff3333', // Bright red - intense
|
||||||
];
|
];
|
||||||
@@ -160,9 +160,9 @@ export function ProfileScreen() {
|
|||||||
/>
|
/>
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="options-outline"
|
icon="lock-closed-outline"
|
||||||
label="Preferences"
|
label="App Lock"
|
||||||
description="Comfort zone and security"
|
description="Biometric access controls"
|
||||||
onPress={() => router.push('/(app)/settings/preferences')}
|
onPress={() => router.push('/(app)/settings/preferences')}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { Text, View, StyleSheet } from 'react-native';
|
||||||
import { SettingsSection } from './SettingsSection';
|
import { SettingsSection } from './SettingsSection';
|
||||||
import { SwitchItem } from './SwitchItem';
|
import { SwitchItem } from './SwitchItem';
|
||||||
import { colors, spacing } from '../../../theme';
|
import { colors, spacing } from '../../../theme';
|
||||||
@@ -10,9 +10,6 @@ interface PrivacySettingsProps {
|
|||||||
biometricType: string;
|
biometricType: string;
|
||||||
isUpdatingBiometric: boolean;
|
isUpdatingBiometric: boolean;
|
||||||
onBiometricToggle: (value: boolean) => void;
|
onBiometricToggle: (value: boolean) => void;
|
||||||
hideNsfw: boolean;
|
|
||||||
isUpdatingHideNsfw: boolean;
|
|
||||||
onHideNsfwToggle: (value: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
|
export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
|
||||||
@@ -21,44 +18,37 @@ export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
|
|||||||
biometricType,
|
biometricType,
|
||||||
isUpdatingBiometric,
|
isUpdatingBiometric,
|
||||||
onBiometricToggle,
|
onBiometricToggle,
|
||||||
hideNsfw,
|
|
||||||
isUpdatingHideNsfw,
|
|
||||||
onHideNsfwToggle,
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<SettingsSection title="Preferences" delay={375}>
|
<SettingsSection title="Security" delay={375}>
|
||||||
{/* Hide Adult Content Toggle */}
|
|
||||||
<SwitchItem
|
|
||||||
icon="eye-off-outline"
|
|
||||||
label="Hide Adult Content"
|
|
||||||
description="Only show mild, family-friendly question packs"
|
|
||||||
value={hideNsfw}
|
|
||||||
onValueChange={onHideNsfwToggle}
|
|
||||||
disabled={isUpdatingHideNsfw}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Biometric Toggle */}
|
|
||||||
{biometricAvailable && (
|
{biometricAvailable && (
|
||||||
<>
|
<SwitchItem
|
||||||
<View style={styles.divider} />
|
icon={biometricType === "Face ID" || biometricType === "Face Recognition" ? "scan-outline" : "finger-print-outline"}
|
||||||
<SwitchItem
|
label={biometricType}
|
||||||
icon={biometricType === "Face ID" || biometricType === "Face Recognition" ? "scan-outline" : "finger-print-outline"}
|
description="Require unlock when opening app"
|
||||||
label={biometricType}
|
value={biometricEnabled}
|
||||||
description="Require unlock when opening app"
|
onValueChange={onBiometricToggle}
|
||||||
value={biometricEnabled}
|
disabled={isUpdatingBiometric}
|
||||||
onValueChange={onBiometricToggle}
|
/>
|
||||||
disabled={isUpdatingBiometric}
|
)}
|
||||||
/>
|
{!biometricAvailable && (
|
||||||
</>
|
<View style={styles.unavailableRow}>
|
||||||
|
<Text style={styles.unavailableText}>
|
||||||
|
Biometric app lock is not available on this device.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
divider: {
|
unavailableRow: {
|
||||||
height: 1,
|
paddingHorizontal: spacing.lg,
|
||||||
backgroundColor: colors.border,
|
paddingVertical: spacing.md,
|
||||||
marginVertical: spacing.lg,
|
},
|
||||||
|
unavailableText: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Alert, Linking, Platform } from 'react-native';
|
import { Alert, Linking, Platform } from 'react-native';
|
||||||
import { useAuthStore, useSubscriptionStore, usePacksStore } from '../../../store';
|
import { useAuthStore, useSubscriptionStore } from '../../../store';
|
||||||
import { supabase } from '../../../lib/supabase';
|
import { supabase } from '../../../lib/supabase';
|
||||||
import { Events } from '../../../lib/analytics';
|
import { Events } from '../../../lib/analytics';
|
||||||
import { useAvatarPicker } from '../../../hooks/useAvatarPicker';
|
import { useAvatarPicker } from '../../../hooks/useAvatarPicker';
|
||||||
@@ -16,15 +16,8 @@ import {
|
|||||||
getBiometricType,
|
getBiometricType,
|
||||||
} from '../../../lib/biometricAuth';
|
} from '../../../lib/biometricAuth';
|
||||||
|
|
||||||
// Intensity thresholds for backwards compatibility
|
|
||||||
// When hide_nsfw=true, max_intensity=2 (mild content only)
|
|
||||||
// When hide_nsfw=false, max_intensity=5 (all content)
|
|
||||||
const NSFW_OFF_INTENSITY = 2;
|
|
||||||
const NSFW_ON_INTENSITY = 5;
|
|
||||||
|
|
||||||
export function useProfileSettings() {
|
export function useProfileSettings() {
|
||||||
const { user, fetchUser } = useAuthStore();
|
const { user, fetchUser } = useAuthStore();
|
||||||
const { fetchPacks } = usePacksStore();
|
|
||||||
const { restorePurchases, isPurchasing } = useSubscriptionStore();
|
const { restorePurchases, isPurchasing } = useSubscriptionStore();
|
||||||
|
|
||||||
// Name Editing
|
// Name Editing
|
||||||
@@ -69,10 +62,6 @@ export function useProfileSettings() {
|
|||||||
onUploadError: handleAvatarUploadError,
|
onUploadError: handleAvatarUploadError,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hide NSFW
|
|
||||||
const [hideNsfw, setHideNsfw] = useState(!!user?.hide_nsfw);
|
|
||||||
const [isUpdatingHideNsfw, setIsUpdatingHideNsfw] = useState(false);
|
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
const [pushEnabled, setPushEnabled] = useState(!!user?.push_token);
|
const [pushEnabled, setPushEnabled] = useState(!!user?.push_token);
|
||||||
const [isUpdatingPush, setIsUpdatingPush] = useState(false);
|
const [isUpdatingPush, setIsUpdatingPush] = useState(false);
|
||||||
@@ -92,7 +81,6 @@ export function useProfileSettings() {
|
|||||||
setNewName(user.name);
|
setNewName(user.name);
|
||||||
}
|
}
|
||||||
setPushEnabled(!!user?.push_token);
|
setPushEnabled(!!user?.push_token);
|
||||||
setHideNsfw(!!user?.hide_nsfw);
|
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
// Check biometric availability
|
// Check biometric availability
|
||||||
@@ -229,43 +217,6 @@ export function useProfileSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHideNsfwToggle = async (value: boolean) => {
|
|
||||||
if (!user?.id) return;
|
|
||||||
|
|
||||||
const previous = hideNsfw;
|
|
||||||
setHideNsfw(value);
|
|
||||||
setIsUpdatingHideNsfw(true);
|
|
||||||
|
|
||||||
// Derive intensity from hide_nsfw for backwards compatibility
|
|
||||||
const maxIntensity = value ? NSFW_OFF_INTENSITY : NSFW_ON_INTENSITY;
|
|
||||||
const showExplicitContent = !value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('profiles')
|
|
||||||
.update({
|
|
||||||
hide_nsfw: value,
|
|
||||||
max_intensity: maxIntensity,
|
|
||||||
show_explicit_content: showExplicitContent,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', user.id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
// Refresh user data and packs to reflect the change
|
|
||||||
await fetchUser();
|
|
||||||
await fetchPacks();
|
|
||||||
Events.profileUpdated(["hide_nsfw", "max_intensity", "show_explicit_content"]);
|
|
||||||
} catch (error) {
|
|
||||||
// Revert on error
|
|
||||||
setHideNsfw(previous);
|
|
||||||
Alert.alert("Error", "Failed to update preference. Please try again.");
|
|
||||||
} finally {
|
|
||||||
setIsUpdatingHideNsfw(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Subscription Actions
|
// Subscription Actions
|
||||||
const handleRestorePurchases = async () => {
|
const handleRestorePurchases = async () => {
|
||||||
const restored = await restorePurchases();
|
const restored = await restorePurchases();
|
||||||
@@ -294,8 +245,6 @@ export function useProfileSettings() {
|
|||||||
biometricEnabled: biometricEnabledState,
|
biometricEnabled: biometricEnabledState,
|
||||||
biometricType,
|
biometricType,
|
||||||
isUpdatingBiometric,
|
isUpdatingBiometric,
|
||||||
hideNsfw,
|
|
||||||
isUpdatingHideNsfw,
|
|
||||||
showPaywall,
|
showPaywall,
|
||||||
setShowPaywall,
|
setShowPaywall,
|
||||||
isPurchasing,
|
isPurchasing,
|
||||||
@@ -306,7 +255,6 @@ export function useProfileSettings() {
|
|||||||
handleAvatarPress,
|
handleAvatarPress,
|
||||||
handlePushToggle,
|
handlePushToggle,
|
||||||
handleBiometricToggle,
|
handleBiometricToggle,
|
||||||
handleHideNsfwToggle,
|
|
||||||
handleRestorePurchases,
|
handleRestorePurchases,
|
||||||
handleManageSubscription,
|
handleManageSubscription,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import { useProfileSettings } from '../hooks';
|
|||||||
import { ScreenHeader, PrivacySettings } from '../components';
|
import { ScreenHeader, PrivacySettings } from '../components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences sub-screen for content and biometric settings.
|
* App-lock settings shared by iOS and Android.
|
||||||
*/
|
*/
|
||||||
export function PreferencesScreen() {
|
export function PreferencesScreen() {
|
||||||
const settings = useProfileSettings();
|
const settings = useProfileSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientBackground>
|
<GradientBackground>
|
||||||
<ScreenHeader title="Preferences" />
|
<ScreenHeader title="App Lock" />
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
contentContainerStyle={styles.content}
|
contentContainerStyle={styles.content}
|
||||||
@@ -26,10 +26,7 @@ export function PreferencesScreen() {
|
|||||||
biometricType={settings.biometricType}
|
biometricType={settings.biometricType}
|
||||||
isUpdatingBiometric={settings.isUpdatingBiometric}
|
isUpdatingBiometric={settings.isUpdatingBiometric}
|
||||||
onBiometricToggle={settings.handleBiometricToggle}
|
onBiometricToggle={settings.handleBiometricToggle}
|
||||||
hideNsfw={settings.hideNsfw}
|
/>
|
||||||
isUpdatingHideNsfw={settings.isUpdatingHideNsfw}
|
|
||||||
onHideNsfwToggle={settings.handleHideNsfwToggle}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const SwipeNoPacksState = ({ onBrowsePacks }: SwipeNoPacksStateProps) =>
|
|||||||
badgeText="NO PACKS ENABLED"
|
badgeText="NO PACKS ENABLED"
|
||||||
description="Select the question packs that interest you and your partner. Each pack explores different aspects of your relationship."
|
description="Select the question packs that interest you and your partner. Each pack explores different aspects of your relationship."
|
||||||
features={[
|
features={[
|
||||||
{ icon: "flame-outline", text: "From playful to passionate", color: accent },
|
{ icon: "flame-outline", text: "From playful to adventurous", color: accent },
|
||||||
{ icon: "shield-checkmark-outline", text: "Safe space to explore", color: accent },
|
{ icon: "shield-checkmark-outline", text: "Safe space to explore", color: accent },
|
||||||
{ icon: "infinite-outline", text: "New packs added regularly", color: accent },
|
{ icon: "infinite-outline", text: "New packs added regularly", color: accent },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ export const SwipeNoPartnerState = ({ hasCouple, onPairPress }: SwipeNoPartnerSt
|
|||||||
}
|
}
|
||||||
features={[
|
features={[
|
||||||
{ icon: "lock-closed-outline", text: "Private and secure", color: accent },
|
{ icon: "lock-closed-outline", text: "Private and secure", color: accent },
|
||||||
{ icon: "sparkles", text: "Discover hidden desires", color: accent },
|
{ icon: "sparkles", text: "Discover new things together", color: accent },
|
||||||
{ icon: "chatbubble-ellipses-outline", text: "Chat about your matches", color: accent },
|
{ icon: "chatbubble-ellipses-outline", text: "Chat about your matches", color: accent },
|
||||||
]}
|
]}
|
||||||
teaser={hasCouple ? "Your partner is just a code away" : "Begin your intimate journey together"}
|
teaser={hasCouple ? "Your partner is just a code away" : "Begin your journey together"}
|
||||||
action={{
|
action={{
|
||||||
label: hasCouple ? "View Invite Code" : "Pair Now",
|
label: hasCouple ? "View Invite Code" : "Pair Now",
|
||||||
onPress: onPairPress,
|
onPress: onPairPress,
|
||||||
|
|||||||
@@ -4,16 +4,6 @@ import { Events } from "../lib/analytics";
|
|||||||
import type { QuestionPack, Category } from "@/types";
|
import type { QuestionPack, Category } from "@/types";
|
||||||
import { useAuthStore } from "./authStore";
|
import { useAuthStore } from "./authStore";
|
||||||
|
|
||||||
// Intensity thresholds derived from hide_nsfw for backwards compatibility
|
|
||||||
// When hide_nsfw=true, max_intensity=2 (mild content only)
|
|
||||||
// When hide_nsfw=false, max_intensity=5 (all content)
|
|
||||||
const NSFW_OFF_INTENSITY = 2;
|
|
||||||
const NSFW_ON_INTENSITY = 5;
|
|
||||||
|
|
||||||
const getMaxIntensityFromHideNsfw = (hideNsfw: boolean) => {
|
|
||||||
return hideNsfw ? NSFW_OFF_INTENSITY : NSFW_ON_INTENSITY;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Progress data for a pack
|
// Progress data for a pack
|
||||||
export interface PackProgressData {
|
export interface PackProgressData {
|
||||||
totalQuestions: number;
|
totalQuestions: number;
|
||||||
@@ -26,13 +16,11 @@ interface PacksState {
|
|||||||
enabledPackIds: string[];
|
enabledPackIds: string[];
|
||||||
packProgress: Map<string, PackProgressData>;
|
packProgress: Map<string, PackProgressData>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
showAllIntensities: boolean;
|
|
||||||
fetchPacks: () => Promise<void>;
|
fetchPacks: () => Promise<void>;
|
||||||
fetchEnabledPacks: () => Promise<void>;
|
fetchEnabledPacks: () => Promise<void>;
|
||||||
fetchPackProgress: () => Promise<void>;
|
fetchPackProgress: () => Promise<void>;
|
||||||
ensureEnabledPacksLoaded: () => Promise<void>;
|
ensureEnabledPacksLoaded: () => Promise<void>;
|
||||||
togglePack: (packId: string) => Promise<{ success: boolean; reason?: string }>;
|
togglePack: (packId: string) => Promise<{ success: boolean; reason?: string }>;
|
||||||
setShowAllIntensities: (value: boolean) => void;
|
|
||||||
clearPacks: () => void;
|
clearPacks: () => void;
|
||||||
getPackProgress: (packId: string) => PackProgressData | undefined;
|
getPackProgress: (packId: string) => PackProgressData | undefined;
|
||||||
}
|
}
|
||||||
@@ -43,16 +31,10 @@ export const usePacksStore = create<PacksState>((set, get) => ({
|
|||||||
enabledPackIds: [],
|
enabledPackIds: [],
|
||||||
packProgress: new Map(),
|
packProgress: new Map(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
showAllIntensities: false,
|
|
||||||
|
|
||||||
fetchPacks: async () => {
|
fetchPacks: async () => {
|
||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
|
|
||||||
// Get hide_nsfw preference and derive max_intensity from it
|
|
||||||
const hideNsfw = useAuthStore.getState().user?.hide_nsfw ?? false;
|
|
||||||
const maxIntensity = getMaxIntensityFromHideNsfw(hideNsfw);
|
|
||||||
const showAllIntensities = get().showAllIntensities;
|
|
||||||
|
|
||||||
// Fetch categories
|
// Fetch categories
|
||||||
const { data: categories } = await supabase
|
const { data: categories } = await supabase
|
||||||
.from("categories")
|
.from("categories")
|
||||||
@@ -64,22 +46,12 @@ export const usePacksStore = create<PacksState>((set, get) => ({
|
|||||||
const visibleCategoryIds = new Set(visibleCategories.map(category => category.id));
|
const visibleCategoryIds = new Set(visibleCategories.map(category => category.id));
|
||||||
|
|
||||||
// Fetch packs with category info and question count
|
// Fetch packs with category info and question count
|
||||||
let query = supabase
|
const query = supabase
|
||||||
.from("question_packs")
|
.from("question_packs")
|
||||||
.select("*, category:categories(*), questions(count)")
|
.select("*, category:categories(*), questions(count)")
|
||||||
.eq("is_public", true)
|
.eq("is_public", true)
|
||||||
.order("sort_order");
|
.order("sort_order");
|
||||||
|
|
||||||
// Filter out packs above user's intensity preference (unless showing all)
|
|
||||||
if (maxIntensity < 5 && !showAllIntensities) {
|
|
||||||
query = query.or(`max_intensity.is.null,max_intensity.lte.${maxIntensity}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out explicit packs when user has hide_nsfw enabled
|
|
||||||
if (hideNsfw) {
|
|
||||||
query = query.eq("is_explicit", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: packs } = await query;
|
const { data: packs } = await query;
|
||||||
|
|
||||||
const visiblePacks = (packs ?? []).filter(pack => {
|
const visiblePacks = (packs ?? []).filter(pack => {
|
||||||
@@ -171,12 +143,6 @@ export const usePacksStore = create<PacksState>((set, get) => ({
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
setShowAllIntensities: (value: boolean) => {
|
|
||||||
set({ showAllIntensities: value });
|
|
||||||
// Refetch packs with new filter setting
|
|
||||||
get().fetchPacks();
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchPackProgress: async () => {
|
fetchPackProgress: async () => {
|
||||||
const userId = useAuthStore.getState().user?.id;
|
const userId = useAuthStore.getState().user?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|||||||
Reference in new issue
Block a user