mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Add intensity-based preferences and harden E2EE payload verification
This commit is contained in:
1 parent
e23f4557bb
commit
0a919f06fc
20 files changed
+2033
-251
No files matched your search
@@ -25,13 +25,14 @@ import Animated, {
|
||||
import { GradientBackground } from '../../src/components/ui/GradientBackground';
|
||||
import { GlassCard } from '../../src/components/ui/GlassCard';
|
||||
import { GlassButton } from '../../src/components/ui/GlassButton';
|
||||
import { IntensitySlider } from '../../src/components/ui/IntensitySlider';
|
||||
import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme';
|
||||
import { useAuthStore } from '../../src/store';
|
||||
import { supabase } from '../../src/lib/supabase';
|
||||
import { getProfileError } from '../../src/lib/errors';
|
||||
import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications';
|
||||
import { Events } from '../../src/lib/analytics';
|
||||
import type { Gender } from '../../src/types';
|
||||
import type { Gender, IntensityLevel } from '../../src/types';
|
||||
|
||||
const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
|
||||
{ value: 'male', label: 'Male', icon: 'male' },
|
||||
@@ -50,6 +51,19 @@ const PURPOSE_OPTIONS: { value: UsageReason; label: string; icon: string }[] = [
|
||||
{ value: 'strengthen_relationship', label: 'Strengthen our relationship', icon: 'shield-checkmark' },
|
||||
];
|
||||
|
||||
const DEFAULT_MAX_INTENSITY: IntensityLevel = 2;
|
||||
|
||||
const normalizeIntensity = (value: number | null | undefined): IntensityLevel | null => {
|
||||
if (value === 1 || value === 2 || value === 3 || value === 4 || value === 5) return value;
|
||||
return null;
|
||||
};
|
||||
|
||||
const getInitialIntensity = (profile?: { max_intensity?: number | null; show_explicit_content?: boolean | null } | null): IntensityLevel => {
|
||||
const normalized = normalizeIntensity(profile?.max_intensity);
|
||||
if (normalized) return normalized;
|
||||
return profile?.show_explicit_content ? 5 : DEFAULT_MAX_INTENSITY;
|
||||
};
|
||||
|
||||
type Stage = 'name' | 'gender' | 'purpose' | 'explicit' | 'notifications';
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
@@ -59,7 +73,7 @@ export default function OnboardingScreen() {
|
||||
const [name, setName] = useState(user?.name || '');
|
||||
const [gender, setGender] = useState<Gender | null>(user?.gender || null);
|
||||
const [usageReason, setUsageReason] = useState<UsageReason | null>(null);
|
||||
const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content ?? true);
|
||||
const [maxIntensity, setMaxIntensity] = useState<IntensityLevel>(() => getInitialIntensity(user));
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
@@ -107,7 +121,7 @@ export default function OnboardingScreen() {
|
||||
Events.onboardingStageComplete('purpose');
|
||||
};
|
||||
|
||||
const handleExplicitContinue = () => {
|
||||
const handleIntensityContinue = () => {
|
||||
setStage('notifications');
|
||||
Events.onboardingStageComplete('explicit');
|
||||
};
|
||||
@@ -139,13 +153,16 @@ export default function OnboardingScreen() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const showExplicitContent = maxIntensity >= 3;
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
name: name.trim(),
|
||||
gender,
|
||||
usage_reason: usageReason,
|
||||
show_explicit_content: showExplicit,
|
||||
max_intensity: maxIntensity,
|
||||
show_explicit_content: showExplicitContent,
|
||||
onboarding_completed: true,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
@@ -159,7 +176,7 @@ export default function OnboardingScreen() {
|
||||
console.log('[Onboarding] Profile updated successfully, fetching user...');
|
||||
await fetchUser();
|
||||
console.log('[Onboarding] User fetched, navigating to home...');
|
||||
Events.profileUpdated(["name", "gender", "usage_reason", "show_explicit_content"]);
|
||||
Events.profileUpdated(["name", "gender", "usage_reason", "max_intensity", "show_explicit_content"]);
|
||||
Events.onboardingComplete();
|
||||
router.replace('/');
|
||||
} catch (err: any) {
|
||||
@@ -379,81 +396,22 @@ export default function OnboardingScreen() {
|
||||
colors={gradients.primary as [string, string]}
|
||||
style={styles.iconContainer}
|
||||
>
|
||||
<Ionicons name="flame" size={40} color={colors.text} />
|
||||
<Ionicons name="heart-circle" size={44} color={colors.text} />
|
||||
</LinearGradient>
|
||||
<Text style={styles.title}>Almost there!</Text>
|
||||
<Text style={styles.title}>Set Your Comfort Zone</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Shall we keep it clean or show you everything?
|
||||
Choose the intimacy level you're comfortable with
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<GlassCard style={styles.card}>
|
||||
<Pressable
|
||||
style={styles.explicitOption}
|
||||
onPress={() => setShowExplicit(true)}
|
||||
>
|
||||
<View style={[
|
||||
styles.explicitIcon,
|
||||
showExplicit && styles.explicitIconSelected
|
||||
]}>
|
||||
<Ionicons
|
||||
name="flame"
|
||||
size={32}
|
||||
color={showExplicit ? colors.primary : colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.explicitText}>
|
||||
<Text style={[
|
||||
styles.explicitTitle,
|
||||
showExplicit && styles.explicitTitleSelected
|
||||
]}>
|
||||
Show me everything
|
||||
</Text>
|
||||
<Text style={styles.explicitDescription}>
|
||||
Include spicy question packs
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[
|
||||
styles.radioOuter,
|
||||
showExplicit && styles.radioOuterSelected
|
||||
]}>
|
||||
{showExplicit && <View style={styles.radioInner} />}
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={styles.explicitOption}
|
||||
onPress={() => setShowExplicit(false)}
|
||||
>
|
||||
<View style={[
|
||||
styles.explicitIcon,
|
||||
!showExplicit && styles.explicitIconSelected
|
||||
]}>
|
||||
<Ionicons
|
||||
name="heart"
|
||||
size={32}
|
||||
color={!showExplicit ? colors.primary : colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.explicitText}>
|
||||
<Text style={[
|
||||
styles.explicitTitle,
|
||||
!showExplicit && styles.explicitTitleSelected
|
||||
]}>
|
||||
Keep it clean
|
||||
</Text>
|
||||
<Text style={styles.explicitDescription}>
|
||||
Only show SFW content
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[
|
||||
styles.radioOuter,
|
||||
!showExplicit && styles.radioOuterSelected
|
||||
]}>
|
||||
{!showExplicit && <View style={styles.radioInner} />}
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
<IntensitySlider
|
||||
value={maxIntensity}
|
||||
onValueChange={setMaxIntensity}
|
||||
/>
|
||||
<Text style={styles.intensityNote}>
|
||||
Start where you're comfortable — you can always adjust later
|
||||
</Text>
|
||||
{error && (
|
||||
<View style={styles.errorContainer}>
|
||||
<Ionicons name="alert-circle" size={16} color={colors.error} />
|
||||
@@ -464,7 +422,7 @@ export default function OnboardingScreen() {
|
||||
|
||||
<View style={styles.footer}>
|
||||
<GlassButton
|
||||
onPress={handleExplicitContinue}
|
||||
onPress={handleIntensityContinue}
|
||||
fullWidth
|
||||
size="lg"
|
||||
>
|
||||
@@ -762,42 +720,11 @@ const styles = StyleSheet.create({
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
explicitOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.glass.background,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.glass.border,
|
||||
padding: spacing.md,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
explicitIcon: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: colors.glass.background,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
explicitIconSelected: {
|
||||
backgroundColor: colors.primaryLight,
|
||||
},
|
||||
explicitText: {
|
||||
flex: 1,
|
||||
},
|
||||
explicitTitle: {
|
||||
...typography.headline,
|
||||
color: colors.textSecondary,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
explicitTitleSelected: {
|
||||
color: colors.text,
|
||||
},
|
||||
explicitDescription: {
|
||||
intensityNote: {
|
||||
...typography.caption1,
|
||||
color: colors.textTertiary,
|
||||
color: colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
radioOuter: {
|
||||
width: 24,
|
||||
|
||||
@@ -20,9 +20,38 @@ import { PackTeaser } from "../../../src/components/PackTeaser";
|
||||
import { colors, spacing, radius, typography } from "../../../src/theme";
|
||||
import type { QuestionPack, Category } from "../../../src/types";
|
||||
|
||||
const HEADER_MAX_HEIGHT = 180;
|
||||
const HEADER_MIN_HEIGHT = 100;
|
||||
const HEADER_SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;
|
||||
const HEADER_MAX_HEIGHT = 180;
|
||||
const HEADER_MIN_HEIGHT = 100;
|
||||
const HEADER_SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;
|
||||
|
||||
const INTENSITY_LABELS: Record<number, string> = {
|
||||
1: "Light",
|
||||
2: "Mild",
|
||||
3: "Moderate",
|
||||
4: "Spicy",
|
||||
5: "Intense",
|
||||
};
|
||||
|
||||
const clampIntensity = (value: number) => Math.min(5, Math.max(1, Math.round(value)));
|
||||
|
||||
const isValidIntensity = (value?: number | null): value is number =>
|
||||
typeof value === "number" && value >= 1 && value <= 5;
|
||||
|
||||
const getIntensityDisplay = (pack: QuestionPack) => {
|
||||
const min = isValidIntensity(pack.min_intensity) ? pack.min_intensity : null;
|
||||
const max = isValidIntensity(pack.max_intensity) ? pack.max_intensity : null;
|
||||
const avg = typeof pack.avg_intensity === "number" ? clampIntensity(pack.avg_intensity) : null;
|
||||
|
||||
const flameCount = max ?? avg ?? (pack.is_explicit ? 5 : 2);
|
||||
const label = min && max
|
||||
? min === max
|
||||
? INTENSITY_LABELS[min]
|
||||
: `${INTENSITY_LABELS[min]} - ${INTENSITY_LABELS[max]}`
|
||||
: INTENSITY_LABELS[flameCount];
|
||||
|
||||
return { flameCount, label };
|
||||
};
|
||||
|
||||
|
||||
interface PackRowProps {
|
||||
pack: QuestionPack;
|
||||
@@ -34,8 +63,10 @@ interface PackRowProps {
|
||||
onInfoPress: () => void;
|
||||
}
|
||||
|
||||
function PackRow({ pack, isEnabled, isPremiumLocked, isExpanded, onToggle, onPress, onInfoPress }: PackRowProps) {
|
||||
const questionCount = pack.questions?.[0]?.count || 0;
|
||||
function PackRow({ pack, isEnabled, isPremiumLocked, isExpanded, onToggle, onPress, onInfoPress }: PackRowProps) {
|
||||
const questionCount = pack.questions?.[0]?.count || 0;
|
||||
const intensityDisplay = getIntensityDisplay(pack);
|
||||
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} style={styles.packRow}>
|
||||
@@ -58,11 +89,25 @@ function PackRow({ pack, isEnabled, isPremiumLocked, isExpanded, onToggle, onPre
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.packMeta}>
|
||||
{questionCount} {questionCount === 1 ? "question" : "questions"}
|
||||
{isEnabled && <Text style={styles.packMetaEnabled}> · Enabled</Text>}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.packMeta}>
|
||||
{questionCount} {questionCount === 1 ? "question" : "questions"}
|
||||
{isEnabled && <Text style={styles.packMetaEnabled}> · Enabled</Text>}
|
||||
</Text>
|
||||
<View style={styles.packIntensityRow}>
|
||||
<View style={styles.packIntensityFlames}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Ionicons
|
||||
key={`intensity-${pack.id}-${index}`}
|
||||
name={index < intensityDisplay.flameCount ? "flame" : "flame-outline"}
|
||||
size={12}
|
||||
color={index < intensityDisplay.flameCount ? colors.primary : colors.textTertiary}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<Text style={styles.packIntensityText}>{intensityDisplay.label}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<Ionicons
|
||||
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||
@@ -670,11 +715,26 @@ const styles = StyleSheet.create({
|
||||
color: colors.textTertiary,
|
||||
marginTop: 2,
|
||||
},
|
||||
packMetaEnabled: {
|
||||
color: colors.primary,
|
||||
},
|
||||
|
||||
// Expanded Pack
|
||||
packMetaEnabled: {
|
||||
color: colors.primary,
|
||||
},
|
||||
packIntensityRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
packIntensityFlames: {
|
||||
flexDirection: "row",
|
||||
gap: 2,
|
||||
},
|
||||
packIntensityText: {
|
||||
...typography.caption2,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
|
||||
// Expanded Pack
|
||||
packExpanded: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.md,
|
||||
|
||||
Reference in new issue
Block a user