diff --git a/app/_layout.tsx b/app/_layout.tsx index 976bc474b..cf0d7bbb8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -301,13 +301,6 @@ export default Sentry.wrap(function RootLayout() { animation: 'none', }} /> - { - // Wait for store hydration before any actions - if (!_hasHydrated) return; - try { - const detectedReferralCode = detectAndSaveReferralCode(); - if (detectedReferralCode) { - console.warn('Referral code detected from URL:', detectedReferralCode); - } - } catch (err) { - console.warn('Error detecting referral code:', err); - } - }, [_hasHydrated]); - // Clear rate limit error when email changes useEffect(() => { // Wait for store hydration before any actions @@ -90,14 +77,24 @@ export default function SignupEmail() { } }, [_hasHydrated, watchedEmail, email, rateLimitError, setRateLimitError]); - // Reset flow state on mount (only after hydration completes) - // Note: Using getState() to avoid dependency on reset function reference - // which can change during Zustand hydration and cause infinite loops + // Reset flow state on mount while preserving referral code + // Attribution hook captures referral code at root - we preserve it here useEffect(() => { - // Wait for store hydration before resetting to prevent race conditions + // Wait for store hydration before any actions if (!_hasHydrated) return; + + // Capture referral code BEFORE reset (from attribution or referral store) + const existingReferral = getReferralCodeForSignup(); + + // Reset the signup flow store useSignupFlowStore.getState().reset(); - }, [_hasHydrated]); + + // Restore referral code AFTER reset to prevent data loss + if (existingReferral) { + setReferralCode(existingReferral); + console.warn('✅ Referral code preserved after reset:', existingReferral); + } + }, [_hasHydrated, setReferralCode]); // Wait for store hydration before rendering if (!_hasHydrated) { @@ -130,10 +127,15 @@ export default function SignupEmail() { setMarketingConsent(data.marketingConsent); setLastOtpSentAt(Date.now()); - // Store referral code if present - const storedReferralCode = getReferralCodeForSignup() || ''; + // Store referral code with multi-source fallback for reliability + // Priority: URL params > referral store > attribution store + const storedReferralCode = + getReferralCodeForSignup() || + useAttributionStore.getState().attributionData.referral_code || + ''; if (storedReferralCode) { setReferralCode(storedReferralCode); + console.warn('✅ Referral code saved to signup flow:', storedReferralCode); } track(TRACKING_EVENTS.EMAIL_SUBMITTED, { diff --git a/constants/path.ts b/constants/path.ts index 3e9e6e9f5..f80dca774 100644 --- a/constants/path.ts +++ b/constants/path.ts @@ -2,7 +2,6 @@ import { Href, Route } from 'expo-router'; type Path = { ONBOARDING: Href; - REGISTER: Route; WELCOME: Href; HOME: Href; // Email-first signup flow @@ -42,7 +41,6 @@ type Path = { export const path: Path = { ONBOARDING: '/onboarding', - REGISTER: '/register', WELCOME: '/welcome', HOME: '/', // Email-first signup flow diff --git a/hooks/useAttributionInitialization.ts b/hooks/useAttributionInitialization.ts index 019916526..2864b559f 100644 --- a/hooks/useAttributionInitialization.ts +++ b/hooks/useAttributionInitialization.ts @@ -5,6 +5,7 @@ import { Platform } from 'react-native'; import { formatAttributionForLogging, getCurrentURL, getReferrer } from '@/lib/attribution'; import { useAttributionStore } from '@/store/useAttributionStore'; +import { useReferralStore } from '@/store/useReferralStore'; /** * Hook to initialize attribution tracking on app launch @@ -27,14 +28,16 @@ import { useAttributionStore } from '@/store/useAttributionStore'; export const useAttributionInitialization = () => { const attributionStore = useAttributionStore(); const _hasHydrated = useAttributionStore(state => state._hasHydrated); + const _referralHasHydrated = useReferralStore(state => state._hasHydrated); const hasInitialized = useRef(false); const subscriptionRef = useRef | null>(null); useEffect(() => { - // Wait for store hydration before initialization + // Wait for BOTH stores' hydration before initialization // This ensures we don't overwrite existing first-touch attribution - if (!_hasHydrated) { - console.warn('Waiting for attribution store hydration...'); + // and prevents referral code sync failures + if (!_hasHydrated || !_referralHasHydrated) { + console.warn('Waiting for attribution and referral store hydration...'); return; } @@ -60,6 +63,26 @@ export const useAttributionInitialization = () => { if (currentUrl) { const captured = attributionStore.captureFromURL(currentUrl); + // Verify referral code sync if present + if (captured && captured.referral_code) { + const verifyReferral = useReferralStore.getState().referralCode; + console.warn('✅ Referral code captured:', { + code: captured.referral_code, + synced: captured.referral_code === verifyReferral, + }); + + if (captured.referral_code !== verifyReferral) { + console.error('❌ REFERRAL SYNC FAILED - stores out of sync!'); + Sentry.captureMessage('Referral code sync mismatch', { + level: 'error', + extra: { + captured: captured.referral_code, + inReferralStore: verifyReferral, + }, + }); + } + } + // Enrich with HTTP referrer if available if (captured && referrer) { attributionStore.updateAttribution({ @@ -134,5 +157,5 @@ export const useAttributionInitialization = () => { subscriptionRef.current = null; } }; - }, [_hasHydrated, attributionStore]); + }, [_hasHydrated, _referralHasHydrated, attributionStore]); }; diff --git a/store/useReferralStore.ts b/store/useReferralStore.ts index b63b58888..b305cd31a 100644 --- a/store/useReferralStore.ts +++ b/store/useReferralStore.ts @@ -8,7 +8,9 @@ import mmkvStorage from '@/lib/mmvkStorage'; interface ReferralState { referralCode: string | null; + _hasHydrated: boolean; setReferralCode: (code: string | null) => void; + setHasHydrated: (state: boolean) => void; clearReferralCode: () => void; detectAndSaveReferralCode: () => string | null; getReferralCodeForSignup: () => string | null; @@ -21,6 +23,7 @@ export const useReferralStore = create()( persist( (set, get) => ({ referralCode: null, + _hasHydrated: false, setReferralCode: (code: string | null) => { const trimmedCode = code && code.trim() ? code.trim() : null; @@ -30,6 +33,10 @@ export const useReferralStore = create()( } }, + setHasHydrated: (state: boolean) => { + set({ _hasHydrated: state }); + }, + clearReferralCode: () => { set({ referralCode: null }); console.warn('Referral code cleared'); @@ -95,6 +102,9 @@ export const useReferralStore = create()( { name: USER.referralStorageKey, storage: createJSONStorage(() => mmkvStorage(USER.referralStorageKey)), + onRehydrateStorage: () => state => { + state?.setHasHydrated(true); + }, }, ), ); diff --git a/store/useSignupFlowStore.ts b/store/useSignupFlowStore.ts index 29618b920..a029a491d 100644 --- a/store/useSignupFlowStore.ts +++ b/store/useSignupFlowStore.ts @@ -111,7 +111,14 @@ export const useSignupFlowStore = create()( setRateLimitError: rateLimitError => set({ rateLimitError }), setLastOtpSentAt: lastOtpSentAt => set({ lastOtpSentAt }), - reset: () => set({ ...initialState, _hasHydrated: true }), + reset: () => + set(state => ({ + ...initialState, + _hasHydrated: true, + // Preserve referralCode captured at root level (attribution hook) + // This prevents accidental loss during flow resets + referralCode: state.referralCode || initialState.referralCode, + })), }), { name: `${USER.storageKey}_signup_flow`,