Merge remote-tracking branch 'origin/master' into task/self-hosted-backend

# Conflicts:
#	apps/mobile/app/(app)/onboarding.tsx
#	apps/mobile/src/__tests__/stores/packsStore.test.ts
#	apps/mobile/src/features/profile/hooks/useProfileSettings.ts
#	apps/mobile/src/store/packsStore.ts
This commit is contained in:
jjbrunton committed 2026-08-28 14:44:51 +01:00
commit be20391788
77 files changed
+48548 -671

No files matched your search

@@ -160,9 +160,9 @@ export function ProfileScreen() {
/>
<View style={styles.divider} />
<MenuItem
icon="options-outline"
label="Preferences"
description="Comfort zone and security"
icon="lock-closed-outline"
label="App Lock"
description="Biometric access controls"
onPress={() => router.push('/(app)/settings/preferences')}
/>
</SettingsSection>
@@ -1,5 +1,5 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Text, View, StyleSheet } from 'react-native';
import { SettingsSection } from './SettingsSection';
import { SwitchItem } from './SwitchItem';
import { colors, spacing } from '../../../theme';
@@ -10,9 +10,6 @@ interface PrivacySettingsProps {
biometricType: string;
isUpdatingBiometric: boolean;
onBiometricToggle: (value: boolean) => void;
hideNsfw: boolean;
isUpdatingHideNsfw: boolean;
onHideNsfwToggle: (value: boolean) => void;
}
export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
@@ -21,44 +18,37 @@ export const PrivacySettings: React.FC<PrivacySettingsProps> = ({
biometricType,
isUpdatingBiometric,
onBiometricToggle,
hideNsfw,
isUpdatingHideNsfw,
onHideNsfwToggle,
}) => {
return (
<SettingsSection title="Preferences" 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 */}
<SettingsSection title="Security" delay={375}>
{biometricAvailable && (
<>
<View style={styles.divider} />
<SwitchItem
icon={biometricType === "Face ID" || biometricType === "Face Recognition" ? "scan-outline" : "finger-print-outline"}
label={biometricType}
description="Require unlock when opening app"
value={biometricEnabled}
onValueChange={onBiometricToggle}
disabled={isUpdatingBiometric}
/>
</>
<SwitchItem
icon={biometricType === "Face ID" || biometricType === "Face Recognition" ? "scan-outline" : "finger-print-outline"}
label={biometricType}
description="Require unlock when opening app"
value={biometricEnabled}
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>
);
};
const styles = StyleSheet.create({
divider: {
height: 1,
backgroundColor: colors.border,
marginVertical: spacing.lg,
unavailableRow: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
},
unavailableText: {
color: colors.textSecondary,
lineHeight: 20,
},
});
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { Alert, Linking, Platform } from 'react-native';
import { useAuthStore, useSubscriptionStore, usePacksStore } from '../../../store';
import { useAuthStore, useSubscriptionStore } from '../../../store';
import { profileSettingsApi } from '../../../lib/profileSettingsApi';
import { Events } from '../../../lib/analytics';
import { useAvatarPicker } from '../../../hooks/useAvatarPicker';
@@ -16,15 +16,8 @@ import {
getBiometricType,
} 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() {
const { user, fetchUser } = useAuthStore();
const { fetchPacks } = usePacksStore();
const { restorePurchases, isPurchasing } = useSubscriptionStore();
// Name Editing
@@ -59,10 +52,6 @@ export function useProfileSettings() {
onUploadError: handleAvatarUploadError,
});
// Hide NSFW
const [hideNsfw, setHideNsfw] = useState(!!user?.hide_nsfw);
const [isUpdatingHideNsfw, setIsUpdatingHideNsfw] = useState(false);
// Notifications
const [pushEnabled, setPushEnabled] = useState(!!user?.push_token);
const [isUpdatingPush, setIsUpdatingPush] = useState(false);
@@ -82,7 +71,6 @@ export function useProfileSettings() {
setNewName(user.name);
}
setPushEnabled(!!user?.push_token);
setHideNsfw(!!user?.hide_nsfw);
}, [user]);
// Check biometric availability
@@ -211,37 +199,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 {
await profileSettingsApi.updateProfile({
hide_nsfw: value,
max_intensity: maxIntensity,
show_explicit_content: showExplicitContent,
});
// 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
const handleRestorePurchases = async () => {
const restored = await restorePurchases();
@@ -270,8 +227,6 @@ export function useProfileSettings() {
biometricEnabled: biometricEnabledState,
biometricType,
isUpdatingBiometric,
hideNsfw,
isUpdatingHideNsfw,
showPaywall,
setShowPaywall,
isPurchasing,
@@ -282,7 +237,6 @@ export function useProfileSettings() {
handleAvatarPress,
handlePushToggle,
handleBiometricToggle,
handleHideNsfwToggle,
handleRestorePurchases,
handleManageSubscription,
};
@@ -7,14 +7,14 @@ import { useProfileSettings } from '../hooks';
import { ScreenHeader, PrivacySettings } from '../components';
/**
* Preferences sub-screen for content and biometric settings.
* App-lock settings shared by iOS and Android.
*/
export function PreferencesScreen() {
const settings = useProfileSettings();
return (
<GradientBackground>
<ScreenHeader title="Preferences" />
<ScreenHeader title="App Lock" />
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
@@ -26,10 +26,7 @@ export function PreferencesScreen() {
biometricType={settings.biometricType}
isUpdatingBiometric={settings.isUpdatingBiometric}
onBiometricToggle={settings.handleBiometricToggle}
hideNsfw={settings.hideNsfw}
isUpdatingHideNsfw={settings.isUpdatingHideNsfw}
onHideNsfwToggle={settings.handleHideNsfwToggle}
/>
/>
</ScrollView>
</GradientBackground>
);