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`,