mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
feat(onboarding): skippable avatar with first-match prompt and usage-reason pack picks
Lets new users skip the onboarding avatar step and instead defers the ask to a lightweight, dismissible prompt shown once after the couple's first match. Also personalizes the home screen's first pack recommendation using the usage_reason already collected during onboarding, which now round-trips through GET /v1/me. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f22d4a3649
commit
fd8981fa28
14 files changed
+567
-77
No files matched your search
@@ -27,14 +27,14 @@ import { GlassButton } from '../../src/components/ui/GlassButton';
|
||||
import { Paywall } from '../../src/components/paywall';
|
||||
import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme';
|
||||
import { useAuthStore } from '../../src/store';
|
||||
import { profileSettingsApi } from '../../src/lib/profileSettingsApi';
|
||||
import { profileSettingsApi } from '../../src/lib/profileSettingsApi';
|
||||
import { getProfileError } from '../../src/lib/errors';
|
||||
import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications';
|
||||
import { Events } from '../../src/lib/analytics';
|
||||
import { hasSeenOnboardingPaywall, markOnboardingPaywallSeen } from '../../src/lib/onboardingPaywallSeen';
|
||||
import { useAvatarPicker } from '../../src/hooks/useAvatarPicker';
|
||||
import { REQUIRED_ONBOARDING_VERSION, ONBOARDING_OFFERING_ID } from '../../src/constants/onboarding';
|
||||
import type { Gender } from '../../src/types';
|
||||
import type { Gender, UsageReason } from '../../src/types';
|
||||
|
||||
const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
|
||||
{ value: 'male', label: 'Male', icon: 'male' },
|
||||
@@ -43,17 +43,15 @@ const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
|
||||
{ value: 'prefer-not-to-say', label: 'Skip', icon: 'remove-circle-outline' },
|
||||
];
|
||||
|
||||
type UsageReason = 'improve_communication' | 'reconnect' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship';
|
||||
|
||||
const PURPOSE_OPTIONS: { value: UsageReason; label: string; icon: string }[] = [
|
||||
{ value: 'improve_communication', label: 'Improve communication', icon: 'chatbubbles' },
|
||||
{ value: 'reconnect', label: 'Feel more connected', icon: 'flame' },
|
||||
{ value: 'reconnect', label: 'Feel more connected', icon: 'flame' },
|
||||
{ value: 'deeper_connection', label: 'Build deeper connection', icon: 'heart' },
|
||||
{ value: 'have_fun', label: 'Have fun together', icon: 'happy' },
|
||||
{ value: 'strengthen_relationship', label: 'Strengthen our relationship', icon: 'shield-checkmark' },
|
||||
];
|
||||
|
||||
type Stage = 'avatar' | 'name' | 'gender' | 'purpose' | 'notifications';
|
||||
type Stage = 'avatar' | 'name' | 'gender' | 'purpose' | 'notifications';
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const router = useRouter();
|
||||
@@ -94,6 +92,13 @@ export default function OnboardingScreen() {
|
||||
Events.onboardingStageComplete('avatar');
|
||||
};
|
||||
|
||||
const handleAvatarSkip = () => {
|
||||
setError(null);
|
||||
setStage('name');
|
||||
Events.avatarSkipped();
|
||||
Events.onboardingStageComplete('avatar');
|
||||
};
|
||||
|
||||
const handleNameSubmit = () => {
|
||||
if (!name.trim()) {
|
||||
setError('Please enter your name');
|
||||
@@ -129,9 +134,9 @@ export default function OnboardingScreen() {
|
||||
setError('Please select what brings you to Sauci');
|
||||
return;
|
||||
}
|
||||
setStage('notifications');
|
||||
Events.onboardingStageComplete('purpose');
|
||||
};
|
||||
setStage('notifications');
|
||||
Events.onboardingStageComplete('purpose');
|
||||
};
|
||||
|
||||
const handleEnableNotifications = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -169,26 +174,26 @@ export default function OnboardingScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
const updatedFields: string[] = ["name", "gender", "usage_reason", "max_intensity", "show_explicit_content", "hide_nsfw"];
|
||||
|
||||
const updateData = {
|
||||
name: name.trim(),
|
||||
gender,
|
||||
usage_reason: usageReason,
|
||||
// Preserve the safest legacy profile values. Catalogue
|
||||
// eligibility is enforced centrally by reviewed status.
|
||||
max_intensity: 2 as const,
|
||||
show_explicit_content: false,
|
||||
hide_nsfw: true,
|
||||
onboarding_completed: true,
|
||||
const updatedFields: string[] = ["name", "gender", "usage_reason", "max_intensity", "show_explicit_content", "hide_nsfw"];
|
||||
|
||||
const updateData = {
|
||||
name: name.trim(),
|
||||
gender,
|
||||
usage_reason: usageReason,
|
||||
// Preserve the safest legacy profile values. Catalogue
|
||||
// eligibility is enforced centrally by reviewed status.
|
||||
max_intensity: 2 as const,
|
||||
show_explicit_content: false,
|
||||
hide_nsfw: true,
|
||||
onboarding_completed: true,
|
||||
onboarding_version: REQUIRED_ONBOARDING_VERSION,
|
||||
};
|
||||
|
||||
await profileSettingsApi.updateProfile(updateData);
|
||||
|
||||
if (avatarUrl) {
|
||||
updatedFields.push("avatar_url");
|
||||
}
|
||||
};
|
||||
|
||||
await profileSettingsApi.updateProfile(updateData);
|
||||
|
||||
if (avatarUrl) {
|
||||
updatedFields.push("avatar_url");
|
||||
}
|
||||
|
||||
console.log('[Onboarding] Profile updated successfully, fetching user...');
|
||||
await fetchUser();
|
||||
@@ -280,6 +285,13 @@ export default function OnboardingScreen() {
|
||||
<GlassButton onPress={handleAvatarContinue} fullWidth size="lg">
|
||||
Continue
|
||||
</GlassButton>
|
||||
<Pressable
|
||||
style={styles.skipButton}
|
||||
onPress={handleAvatarSkip}
|
||||
testID="avatar-skip-button"
|
||||
>
|
||||
<Text style={styles.skipText}>Skip for now</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
@@ -569,7 +581,7 @@ export default function OnboardingScreen() {
|
||||
style={styles.backButton}
|
||||
onPress={() => {
|
||||
setError(null);
|
||||
if (stage === 'notifications') setStage('purpose');
|
||||
if (stage === 'notifications') setStage('purpose');
|
||||
else if (stage === 'purpose') setStage('gender');
|
||||
else if (stage === 'gender') setStage('name');
|
||||
else if (stage === 'name') setStage('avatar');
|
||||
@@ -585,7 +597,7 @@ export default function OnboardingScreen() {
|
||||
<View style={[styles.progressDot, stage === 'name' && styles.progressDotActive]} />
|
||||
<View style={[styles.progressDot, stage === 'gender' && styles.progressDotActive]} />
|
||||
<View style={[styles.progressDot, stage === 'purpose' && styles.progressDotActive]} />
|
||||
<View style={[styles.progressDot, stage === 'notifications' && styles.progressDotActive]} />
|
||||
<View style={[styles.progressDot, stage === 'notifications' && styles.progressDotActive]} />
|
||||
</View>
|
||||
<View style={styles.backButtonPlaceholder} />
|
||||
</Animated.View>
|
||||
|
||||
Reference in new issue
Block a user