Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,6 @@ export default Sentry.wrap(function RootLayout() {
animation: 'none',
}}
/>
<Stack.Screen
name="register"
options={{
headerShown: false,
animation: 'none',
}}
/>
<Stack.Screen
name="welcome"
options={{
Expand Down
46 changes: 24 additions & 22 deletions app/signup/email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { useDimension } from '@/hooks/useDimension';
import { track } from '@/lib/analytics';
import { emailExists, initSignupOtp } from '@/lib/api';
import { getAsset } from '@/lib/assets';
import { detectAndSaveReferralCode, getReferralCodeForSignup } from '@/lib/utils/referral';
import { getReferralCodeForSignup } from '@/lib/utils/referral';
import { useAttributionStore } from '@/store/useAttributionStore';
import { useSignupFlowStore } from '@/store/useSignupFlowStore';

const emailSchema = z.object({
Expand Down Expand Up @@ -67,20 +68,6 @@ export default function SignupEmail() {

const watchedEmail = watch('email');

// Detect and save referral code from URL on mount
useEffect(() => {
// 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
Expand All @@ -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) {
Expand Down Expand Up @@ -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, {
Expand Down
2 changes: 0 additions & 2 deletions constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Href, Route } from 'expo-router';

type Path = {
ONBOARDING: Href;
REGISTER: Route;
WELCOME: Href;
HOME: Href;
// Email-first signup flow
Expand Down Expand Up @@ -42,7 +41,6 @@ type Path = {

export const path: Path = {
ONBOARDING: '/onboarding',
REGISTER: '/register',
WELCOME: '/welcome',
HOME: '/',
// Email-first signup flow
Expand Down
31 changes: 27 additions & 4 deletions hooks/useAttributionInitialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ReturnType<typeof Linking.addEventListener> | 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;
}

Expand All @@ -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({
Expand Down Expand Up @@ -134,5 +157,5 @@ export const useAttributionInitialization = () => {
subscriptionRef.current = null;
}
};
}, [_hasHydrated, attributionStore]);
}, [_hasHydrated, _referralHasHydrated, attributionStore]);
};
10 changes: 10 additions & 0 deletions store/useReferralStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +23,7 @@ export const useReferralStore = create<ReferralState>()(
persist(
(set, get) => ({
referralCode: null,
_hasHydrated: false,

setReferralCode: (code: string | null) => {
const trimmedCode = code && code.trim() ? code.trim() : null;
Expand All @@ -30,6 +33,10 @@ export const useReferralStore = create<ReferralState>()(
}
},

setHasHydrated: (state: boolean) => {
set({ _hasHydrated: state });
},

clearReferralCode: () => {
set({ referralCode: null });
console.warn('Referral code cleared');
Expand Down Expand Up @@ -95,6 +102,9 @@ export const useReferralStore = create<ReferralState>()(
{
name: USER.referralStorageKey,
storage: createJSONStorage(() => mmkvStorage(USER.referralStorageKey)),
onRehydrateStorage: () => state => {
state?.setHasHydrated(true);
},
},
),
);
9 changes: 8 additions & 1 deletion store/useSignupFlowStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,14 @@ export const useSignupFlowStore = create<SignupFlowState & SignupFlowActions>()(
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`,
Expand Down