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:
jjbruntonandClaude Fable 5 committed 2026-08-30 19:12:56 +01:00
1 parent f22d4a3649
commit fd8981fa28
14 files changed
+567 -77

No files matched your search

@@ -5,6 +5,7 @@ import { useAuthStore, usePacksStore, useResponsesStore, useStreakStore } from "
import { skipQuestion, getSkippedQuestionIds } from "../../../lib/skippedQuestions";
import { ApiError, apiClient } from "../../../lib/apiClient";
import { Events } from "../../../lib/analytics";
import { hasSeenAvatarPrompt, markAvatarPromptSeen } from "../../../lib/avatarPromptSeen";
import type { AnswerType } from "../../../types";
import type { DailyLimitInfo, PackInfo, ResponseData } from "../types";
import {
@@ -45,7 +46,9 @@ export const useSwipeScreen = () => {
const [showPaywall, setShowPaywall] = useState(false);
const [packContext, setPackContext] = useState<{ name: string; icon: string } | null>(null);
const [showConfetti, setShowConfetti] = useState(false);
const [showAvatarPrompt, setShowAvatarPrompt] = useState(false);
const [countdown, setCountdown] = useState<string>("");
const hasEvaluatedAvatarPrompt = useRef(false);
const { enabledPackIds, ensureEnabledPacksLoaded, packs } = usePacksStore();
const { user, partner, couple } = useAuthStore();
@@ -387,9 +390,40 @@ export const useSwipeScreen = () => {
}
}, [checkAnswerGap, checkDailyLimit, currentIndex, dailyLimitInfo, filteredQuestions, mode, user?.id]);
const maybeShowAvatarPrompt = useCallback(async () => {
const currentUser = useAuthStore.getState().user;
if (!currentUser?.id || currentUser.avatar_url) return;
const alreadySeen = await hasSeenAvatarPrompt(currentUser.id);
if (alreadySeen) return;
// Marked seen as soon as it is shown, so the prompt appears at most once
// even if the user leaves the app before responding to it.
await markAvatarPromptSeen(currentUser.id);
setShowAvatarPrompt(true);
Events.avatarPromptShown('first_match');
}, []);
const handleConfettiComplete = useCallback(() => {
setShowConfetti(false);
setCurrentIndex(prev => prev + 1);
// Deferred avatar ask: offered once, right after the couple's first match
// celebration finishes, never on top of it.
if (!hasEvaluatedAvatarPrompt.current) {
hasEvaluatedAvatarPrompt.current = true;
void maybeShowAvatarPrompt();
}
}, [maybeShowAvatarPrompt]);
const handleAvatarPromptAddPhoto = useCallback(() => {
setShowAvatarPrompt(false);
Events.avatarPromptAccepted('first_match');
}, []);
const handleAvatarPromptDismiss = useCallback(() => {
setShowAvatarPrompt(false);
Events.avatarPromptDismissed('first_match');
}, []);
const getQuestionPackInfo = useCallback(
@@ -419,6 +453,7 @@ export const useSwipeScreen = () => {
showPaywall,
packContext,
showConfetti,
showAvatarPrompt,
countdown,
user,
partner,
@@ -428,6 +463,8 @@ export const useSwipeScreen = () => {
fetchQuestions,
handleAnswer,
handleConfettiComplete,
handleAvatarPromptAddPhoto,
handleAvatarPromptDismiss,
getQuestionPackInfo,
checkAnswerGap,
setFeedbackQuestion,