Skip to content
Draft
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
3 changes: 2 additions & 1 deletion app/(tabs)/reference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, TextInput, Modal } from 'react-native';
import { Star, MessageSquare, X } from 'lucide-react-native';
import { collection, addDoc } from 'firebase/firestore';
import { db } from '../../lib/firebase';
import { getDbInstance } from '../../lib/firebase';
import { isMobileWeb } from '../../lib/utils';

interface CommonDose {
Expand Down Expand Up @@ -131,6 +131,7 @@ export default function ReferenceScreen() {
setIsSubmitting(true);
try {
// Store suggestion in Firebase
const db = await getDbInstance();
await addDoc(collection(db, 'compound-suggestions'), {
compoundName: compoundName.trim(),
dosageRange: dosageRange.trim(),
Expand Down
9 changes: 0 additions & 9 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Slot, SplashScreen } from 'expo-router';
import { useEffect } from 'react';
import { AuthProvider } from '../contexts/AuthContext';
import { UserProfileProvider } from '../contexts/UserProfileContext';
import { getAnalyticsInstance } from '../lib/firebase';
import "../global.css";

export default function RootLayout() {
Expand All @@ -12,14 +11,6 @@ export default function RootLayout() {
useEffect(() => {
console.log('[RootLayout] Root layout effect running - hiding splash screen');
SplashScreen.hideAsync();

// Initialize Firebase Analytics lazily
const analytics = getAnalyticsInstance();
if (analytics) {
console.log('[Analytics] Firebase Analytics initialized');
} else {
console.log('[Analytics] Firebase Analytics not available (likely not web platform)');
}
}, []);

console.log('[RootLayout] Rendering providers and slot');
Expand Down
3 changes: 2 additions & 1 deletion app/success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { router, useLocalSearchParams } from 'expo-router';
import { doc, setDoc } from 'firebase/firestore';
import { getAuth } from 'firebase/auth';
import { db } from '../lib/firebase';
import { getDbInstance } from '../lib/firebase';
import Constants from 'expo-constants';
import { logAnalyticsEvent, setAnalyticsUserProperties, ANALYTICS_EVENTS, USER_PROPERTIES } from '../lib/analytics';

Expand Down Expand Up @@ -69,6 +69,7 @@ export default function SuccessScreen() {
const auth = getAuth();
const user = auth.currentUser;
if (user) {
const db = await getDbInstance();
const userRef = doc(db, 'users', user.uid);
await setDoc(userRef, { plan: 'plus', limit: 150, scansUsed: 0 }, { merge: true });

Expand Down
42 changes: 33 additions & 9 deletions contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState, useRef } from 'r
import { onAuthStateChanged, signInAnonymously, signOut, User, Auth } from 'firebase/auth';
import { ActivityIndicator } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { auth } from '@/lib/firebase'; // Initialized auth instance
import { getAuthInstance } from '@/lib/firebase'; // Updated to use async function
import { logAnalyticsEvent, setAnalyticsUserProperties, ANALYTICS_EVENTS, USER_PROPERTIES } from '@/lib/analytics';

interface AuthContextType {
Expand All @@ -18,10 +18,29 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [isSigningOut, setIsSigningOut] = useState(false);
const [authInstance, setAuthInstance] = useState<Auth | null>(null);
const isSigningOutRef = useRef(false);
const isSigningInAnonymouslyRef = useRef(false);

// Initialize auth instance asynchronously
useEffect(() => {
const initAuth = async () => {
try {
const auth = await getAuthInstance();
setAuthInstance(auth);
} catch (error) {
console.error('[AuthContext] Failed to initialize auth:', error);
}
};
initAuth();
}, []);

const logout = async () => {
if (!authInstance) {
console.warn('[AuthContext] Auth instance not ready for logout');
return;
}

console.log('[AuthContext] ========== LOGOUT INITIATED ==========');
console.log('[AuthContext] Current user before logout:', user ? {
uid: user.uid,
Expand All @@ -38,7 +57,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
console.log('[AuthContext] isSigningOut state and ref updated to true');

console.log('[AuthContext] Calling Firebase signOut...');
await signOut(auth);
await signOut(authInstance);
console.log('[AuthContext] Firebase signOut completed successfully');

console.log('[AuthContext] Clearing user state...');
Expand Down Expand Up @@ -98,10 +117,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}, [isSigningOut]);

useEffect(() => {
if (!authInstance) {
// Wait for auth instance to be initialized
return;
}

let timeoutId: NodeJS.Timeout | null = null;

// Subscribe to auth state changes on the single `auth` instance
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
// Subscribe to auth state changes on the auth instance
const unsubscribe = onAuthStateChanged(authInstance, (firebaseUser) => {
console.log('[AuthContext] ========== AUTH STATE CHANGED ==========');
console.log('[AuthContext] New user state:', firebaseUser ? {
uid: firebaseUser.uid,
Expand Down Expand Up @@ -149,7 +173,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (!isSigningOutRef.current && !isSigningInAnonymouslyRef.current) {
console.log('[AuthContext] Not signing out and not already signing in anonymously - signing in anonymously immediately');
isSigningInAnonymouslyRef.current = true;
signInAnonymously(auth)
signInAnonymously(authInstance)
.then(() => {
console.log('[AuthContext] ✅ Signed in anonymously successfully');
isSigningInAnonymouslyRef.current = false;
Expand All @@ -174,7 +198,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
isSigningOutRef.current = false;
if (!isSigningInAnonymouslyRef.current) {
isSigningInAnonymouslyRef.current = true;
signInAnonymously(auth)
signInAnonymously(authInstance)
.then(() => {
console.log('[AuthContext] ✅ Signed in anonymously after logout');
isSigningInAnonymouslyRef.current = false;
Expand Down Expand Up @@ -204,14 +228,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
clearTimeout(timeoutId);
}
};
}, []); // Remove isSigningOut dependency to prevent auth listener recreation
}, [authInstance]); // Add authInstance dependency

if (loading) {
if (loading || !authInstance) {
return <ActivityIndicator size="large" />;
}

return (
<AuthContext.Provider value={{ user, auth, logout, isSigningOut }}>
<AuthContext.Provider value={{ user, auth: authInstance, logout, isSigningOut }}>
{children}
</AuthContext.Provider>
);
Expand Down
7 changes: 6 additions & 1 deletion contexts/UserProfileContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { doc, setDoc, getDoc } from 'firebase/firestore';
import { db } from '@/lib/firebase';
import { getDbInstance } from '@/lib/firebase';
import { useAuth } from '@/contexts/AuthContext';
import { UserProfile, WarningLevel, getUserWarningLevel, getDisclaimerText } from '@/types/userProfile';
import { logAnalyticsEvent, ANALYTICS_EVENTS, setPersonalizationUserProperties } from '@/lib/analytics';
Expand Down Expand Up @@ -38,6 +38,7 @@ export function UserProfileProvider({ children }: { children: React.ReactNode })
// User has transitioned from anonymous to authenticated
// Check if profile needs to be backed up to Firebase
try {
const db = await getDbInstance();
const docRef = doc(db, 'userProfiles', user.uid);
const docSnap = await getDoc(docRef);

Expand Down Expand Up @@ -119,6 +120,7 @@ export function UserProfileProvider({ children }: { children: React.ReactNode })
// For authenticated users, try Firebase first, then local storage fallback
console.log('[UserProfile] Authenticated user detected - trying Firebase first');
try {
const db = await getDbInstance();
const docRef = doc(db, 'userProfiles', user.uid);
const docSnap = await getDoc(docRef);

Expand Down Expand Up @@ -150,6 +152,7 @@ export function UserProfileProvider({ children }: { children: React.ReactNode })
// try to backup this profile to Firebase (this handles cases where
// user has local profile but it's not synced to Firebase)
try {
const db = await getDbInstance();
const docRef = doc(db, 'userProfiles', user.uid);
const profileToBackup = {
...profileData,
Expand Down Expand Up @@ -241,6 +244,7 @@ export function UserProfileProvider({ children }: { children: React.ReactNode })
// Save to Firebase if user is available
if (user?.uid) {
try {
const db = await getDbInstance();
const docRef = doc(db, 'userProfiles', user.uid);
await setDoc(docRef, newProfile);
console.log('User profile saved to Firebase');
Expand Down Expand Up @@ -294,6 +298,7 @@ export function UserProfileProvider({ children }: { children: React.ReactNode })
// Clear from Firebase if user is available
if (user?.uid) {
try {
const db = await getDbInstance();
const docRef = doc(db, 'userProfiles', user.uid);
await setDoc(docRef, {}, { merge: false }); // Effectively deletes the document
console.log('User profile cleared from Firebase');
Expand Down
112 changes: 62 additions & 50 deletions lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
import { logEvent, setUserProperties } from 'firebase/analytics';
import { getAnalyticsInstance } from './firebase';
/**
* A completely safe, in-memory queue for analytics events.
* THIS FILE MUST NOT IMPORT ANYTHING FROM FIREBASE.
*/

type QueuedOperation = {
type: 'logEvent';
payload: { eventName: string; eventParams?: { [key: string]: any } };
} | {
type: 'setUserProperties';
payload: { properties: { [key: string]: any } };
};

// This queue is globally accessible and safe.
export const analyticsQueue: QueuedOperation[] = [];
export let isAnalyticsInitialized = false;

// The functions the app will call. They just add to the queue.
export const logAnalyticsEvent = (eventName: string, eventParams?: { [key: string]: any }) => {
if (isAnalyticsInitialized) {
console.warn(`[Analytics Queue] Analytics already initialized, but old log function called for ${eventName}. This is a bug.`);
return;
}
console.log(`[Analytics Queue] Queuing event: ${eventName}`);
analyticsQueue.push({ type: 'logEvent', payload: { eventName, eventParams } });
};

export const setAnalyticsUserProperties = (properties: { [key: string]: any }) => {
if (isAnalyticsInitialized) {
console.warn(`[Analytics Queue] Analytics already initialized, but old setUserProperties called. This is a bug.`);
return;
}
console.log(`[Analytics Queue] Queuing user properties:`, properties);
analyticsQueue.push({ type: 'setUserProperties', payload: { properties } });
};

// Helper function to set personalization user properties from profile
export const setPersonalizationUserProperties = (profile: any) => {
// Determine user segment based on profile
let userSegment = 'general_user';
if (profile.isLicensedProfessional) {
userSegment = 'healthcare_professional';
} else if (profile.isCosmeticUse) {
userSegment = 'cosmetic_user';
} else if (profile.isPersonalUse) {
userSegment = 'personal_medical_user';
}

setAnalyticsUserProperties({
[USER_PROPERTIES.IS_LICENSED_PROFESSIONAL]: profile.isLicensedProfessional,
[USER_PROPERTIES.IS_PERSONAL_USE]: profile.isPersonalUse,
[USER_PROPERTIES.IS_COSMETIC_USE]: profile.isCosmeticUse,
[USER_PROPERTIES.USER_SEGMENT]: userSegment,
});
};

// Custom event names as defined in the issue
export const ANALYTICS_EVENTS = {
Expand Down Expand Up @@ -58,52 +111,11 @@ export const USER_PROPERTIES = {
USER_SEGMENT: 'user_segment', // Derived from profile settings
} as const;

// Helper function to safely log analytics events
export const logAnalyticsEvent = (eventName: string, parameters?: Record<string, any>) => {
const analytics = getAnalyticsInstance();
if (analytics) {
try {
logEvent(analytics, eventName, parameters);
console.log(`[Analytics] Event logged: ${eventName}`, parameters);
} catch (error) {
console.error(`[Analytics] Failed to log event ${eventName}:`, error);
}
} else {
console.log(`[Analytics] Analytics not available, would log: ${eventName}`, parameters);
}
};

// Helper function to safely set user properties
export const setAnalyticsUserProperties = (properties: Record<string, any>) => {
const analytics = getAnalyticsInstance();
if (analytics) {
try {
setUserProperties(analytics, properties);
console.log(`[Analytics] User properties set:`, properties);
} catch (error) {
console.error(`[Analytics] Failed to set user properties:`, error);
}
} else {
console.log(`[Analytics] Analytics not available, would set properties:`, properties);
}
};

// Helper function to set personalization user properties from profile
export const setPersonalizationUserProperties = (profile: any) => {
// Determine user segment based on profile
let userSegment = 'general_user';
if (profile.isLicensedProfessional) {
userSegment = 'healthcare_professional';
} else if (profile.isCosmeticUse) {
userSegment = 'cosmetic_user';
} else if (profile.isPersonalUse) {
userSegment = 'personal_medical_user';
}

setAnalyticsUserProperties({
[USER_PROPERTIES.IS_LICENSED_PROFESSIONAL]: profile.isLicensedProfessional,
[USER_PROPERTIES.IS_PERSONAL_USE]: profile.isPersonalUse,
[USER_PROPERTIES.IS_COSMETIC_USE]: profile.isCosmeticUse,
[USER_PROPERTIES.USER_SEGMENT]: userSegment,
});
/**
* Signal that the Analytics provider has taken over.
* Called by the AnalyticsProvider component.
*/
export const markAnalyticsInitialized = () => {
isAnalyticsInitialized = true;
console.log(`[Analytics Queue] Analytics provider has taken over. Queue contained ${analyticsQueue.length} operations.`);
};
1 change: 0 additions & 1 deletion lib/dose-logging-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ describe('Automatic Dose Logging Integration', () => {

expect(formatDrawToText(legacyLog)).toBeNull();
});
});

it('should properly format timestamps', () => {
const now = new Date();
Expand Down
Loading