Compare commits

..
2 Commits
Author SHA1 Message Date
jjbrunton 1911f8f058 Switch analytics from PostHog to Firebase 2026-01-16 15:38:17 +00:00
jjbrunton 23e24aa4c2 Fix admin user detail page not showing email 2026-01-16 14:57:40 +00:00
11 changed files with 988 additions and 314 deletions

No files matched your search

@@ -331,7 +331,7 @@ export function UserDetailPage() {
expires_at: expiresAt, expires_at: expiresAt,
}); });
if (error) throw error; if (error) throw error;
const { data: updatedProfile } = await supabase.from('profiles').select('*').eq('id', userId).single(); const { data: updatedProfile } = await supabase.rpc('get_profile_with_auth_info', { user_id: userId }).single();
if (updatedProfile) setProfile(updatedProfile); if (updatedProfile) setProfile(updatedProfile);
setUpgradeOpen(false); setUpgradeOpen(false);
alert('User upgraded to premium successfully!'); alert('User upgraded to premium successfully!');
@@ -347,7 +347,7 @@ export function UserDetailPage() {
if (!userId) return; if (!userId) return;
setLoading(true); setLoading(true);
try { try {
const { data: profileData } = await supabase.from('profiles').select('*').eq('id', userId).single(); const { data: profileData } = await supabase.rpc('get_profile_with_auth_info', { user_id: userId }).single();
setProfile(profileData); setProfile(profileData);
// Fetch feature interests for the user // Fetch feature interests for the user
+1 -1
View File
@@ -17,7 +17,7 @@
<key>IS_ADS_ENABLED</key> <key>IS_ADS_ENABLED</key>
<false></false> <false></false>
<key>IS_ANALYTICS_ENABLED</key> <key>IS_ANALYTICS_ENABLED</key>
<false></false> <true></true>
<key>IS_APPINVITE_ENABLED</key> <key>IS_APPINVITE_ENABLED</key>
<true></true> <true></true>
<key>IS_GCM_ENABLED</key> <key>IS_GCM_ENABLED</key>
+2 -2
View File
@@ -93,8 +93,8 @@ android {
applicationId 'com.sauci.app' applicationId 'com.sauci.app'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 22 versionCode 24
versionName "1.0.1" versionName "1.0.2"
} }
signingConfigs { signingConfigs {
debug { debug {
+3
View File
@@ -57,3 +57,6 @@ expo.useLegacyPackaging=false
android.targetSdkVersion=35 android.targetSdkVersion=35
android.extraMavenRepos=[] android.extraMavenRepos=[]
# Enable 16 KB page size support (required by Google Play for API 35+)
android.bundle.pageAlignment=16kb
+6 -2
View File
@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Sauci", "name": "Sauci",
"slug": "sauci", "slug": "sauci",
"version": "1.0.1", "version": "1.0.2",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"scheme": "app.sauci", "scheme": "app.sauci",
@@ -18,6 +18,7 @@
"bundleIdentifier": "com.sauci.app", "bundleIdentifier": "com.sauci.app",
"buildNumber": "30", "buildNumber": "30",
"usesAppleSignIn": true, "usesAppleSignIn": true,
"googleServicesFile": "./GoogleService-Info.plist",
"infoPlist": { "infoPlist": {
"UIBackgroundModes": [ "UIBackgroundModes": [
"remote-notification" "remote-notification"
@@ -176,7 +177,7 @@
"backgroundColor": "#1a1a2e" "backgroundColor": "#1a1a2e"
}, },
"package": "com.sauci.app", "package": "com.sauci.app",
"versionCode": 22, "versionCode": 24,
"googleServicesFile": "./google-services.json", "googleServicesFile": "./google-services.json",
"permissions": [ "permissions": [
"RECEIVE_BOOT_COMPLETED", "RECEIVE_BOOT_COMPLETED",
@@ -197,6 +198,9 @@
{ {
"android": { "android": {
"targetSdkVersion": 35 "targetSdkVersion": 35
},
"ios": {
"useFrameworks": "static"
} }
} }
], ],
-30
View File
@@ -331,7 +331,6 @@ PODS:
- libavif/core - libavif/core
- libdav1d (>= 0.6.0) - libdav1d (>= 0.6.0)
- libdav1d (1.2.0) - libdav1d (1.2.0)
- OpenSSL-Universal (3.3.3001)
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- PurchasesHybridCommon (17.25.0): - PurchasesHybridCommon (17.25.0):
- RevenueCat (= 5.51.1) - RevenueCat (= 5.51.1)
@@ -1634,29 +1633,6 @@ PODS:
- ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- Yoga - Yoga
- react-native-quick-crypto (0.7.17):
- DoubleConversion
- glog
- hermes-engine
- OpenSSL-Universal
- RCT-Folly (= 2024.10.14.00)
- RCTRequired
- RCTTypeSafety
- React
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context (4.12.0): - react-native-safe-area-context (4.12.0):
- React-Core - React-Core
- React-nativeconfig (0.76.9) - React-nativeconfig (0.76.9)
@@ -2176,7 +2152,6 @@ DEPENDENCIES:
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`) - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-compressor (from `../../../node_modules/react-native-compressor`) - react-native-compressor (from `../../../node_modules/react-native-compressor`)
- react-native-quick-crypto (from `../../../node_modules/react-native-quick-crypto`)
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`) - React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
@@ -2224,7 +2199,6 @@ SPEC REPOS:
- GTMSessionFetcher - GTMSessionFetcher
- libavif - libavif
- libdav1d - libdav1d
- OpenSSL-Universal
- PromisesObjC - PromisesObjC
- PurchasesHybridCommon - PurchasesHybridCommon
- RevenueCat - RevenueCat
@@ -2372,8 +2346,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-compressor: react-native-compressor:
:path: "../../../node_modules/react-native-compressor" :path: "../../../node_modules/react-native-compressor"
react-native-quick-crypto:
:path: "../../../node_modules/react-native-quick-crypto"
react-native-safe-area-context: react-native-safe-area-context:
:path: "../../../node_modules/react-native-safe-area-context" :path: "../../../node_modules/react-native-safe-area-context"
React-nativeconfig: React-nativeconfig:
@@ -2494,7 +2466,6 @@ SPEC CHECKSUMS:
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PurchasesHybridCommon: 6a79a873ab52f777bfa36e9516f3fcd84d3b3428 PurchasesHybridCommon: 6a79a873ab52f777bfa36e9516f3fcd84d3b3428
RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17
@@ -2527,7 +2498,6 @@ SPEC CHECKSUMS:
React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de
React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3 React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3
react-native-compressor: 2c3a41768bdd367f6e121f555dff9d3eda1810e5 react-native-compressor: 2c3a41768bdd367f6e121f555dff9d3eda1810e5
react-native-quick-crypto: 7bb26af9b3705dbb9277c09305104d37ede74916
react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448 react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448
React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678
React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e
@@ -352,12 +352,10 @@
); );
inputPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh", "${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputPaths = ( outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -410,11 +408,11 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app; PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app;
PRODUCT_NAME = "Sauci"; PRODUCT_NAME = Sauci;
SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1"; TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Debug; name = Debug;
@@ -442,10 +440,10 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app; PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app;
PRODUCT_NAME = "Sauci"; PRODUCT_NAME = Sauci;
SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1"; TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Release; name = Release;
+2 -1
View File
@@ -22,6 +22,8 @@
"@expo/ngrok": "^4.1.0", "@expo/ngrok": "^4.1.0",
"@expo/vector-icons": "~14.0.4", "@expo/vector-icons": "~14.0.4",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-firebase/analytics": "^23.8.2",
"@react-native-firebase/app": "^23.8.2",
"@react-native-google-signin/google-signin": "^16.1.1", "@react-native-google-signin/google-signin": "^16.1.1",
"@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0", "@react-navigation/native": "^7.0.0",
@@ -53,7 +55,6 @@
"expo-router": "~4.0.0", "expo-router": "~4.0.0",
"expo-secure-store": "~14.0.0", "expo-secure-store": "~14.0.0",
"expo-status-bar": "~2.0.0", "expo-status-bar": "~2.0.0",
"posthog-react-native": "^4.17.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-native": "0.76.9", "react-native": "0.76.9",
+61 -35
View File
@@ -1,7 +1,31 @@
import PostHog from "posthog-react-native"; import { getApp } from "@react-native-firebase/app";
import {
getAnalytics,
logEvent as firebaseLogEvent,
setUserId as firebaseSetUserId,
setUserProperty,
setAnalyticsCollectionEnabled,
resetAnalyticsData,
FirebaseAnalyticsTypes,
} from "@react-native-firebase/analytics";
let posthog: PostHog | null = null;
let isInitialized = false; let isInitialized = false;
let analyticsInstance: FirebaseAnalyticsTypes.Module | null = null;
function getAnalyticsInstance(): FirebaseAnalyticsTypes.Module | null {
if (analyticsInstance) {
return analyticsInstance;
}
try {
const app = getApp();
analyticsInstance = getAnalytics(app);
return analyticsInstance;
} catch (error) {
console.warn("[Analytics] Failed to get analytics instance:", error);
return null;
}
}
export async function initAnalytics() { export async function initAnalytics() {
if (isInitialized) { if (isInitialized) {
@@ -9,30 +33,19 @@ export async function initAnalytics() {
} }
// Skip initialization in non-RN environments (e.g., during tsc/Node.js) // Skip initialization in non-RN environments (e.g., during tsc/Node.js)
// AsyncStorage requires window to be defined
if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) { if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
return; return;
} }
const apiKey = process.env.EXPO_PUBLIC_POSTHOG_API_KEY;
const host = process.env.EXPO_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com";
if (!apiKey) {
console.warn("[Analytics] PostHog API key not configured");
return;
}
try { try {
posthog = new PostHog(apiKey, { const analytics = getAnalyticsInstance();
host, if (analytics) {
// Disable automatic capture in production for privacy await setAnalyticsCollectionEnabled(analytics, true);
captureAppLifecycleEvents: true, isInitialized = true;
}); console.log("[Analytics] Firebase Analytics initialized");
}
isInitialized = true;
console.log("[Analytics] PostHog initialized");
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to initialize PostHog:", error); console.warn("[Analytics] Failed to initialize Firebase Analytics:", error);
} }
} }
@@ -43,7 +56,10 @@ export function logEvent(
) { ) {
try { try {
if (__DEV__) console.log("[Analytics] Event:", name, params); if (__DEV__) console.log("[Analytics] Event:", name, params);
posthog?.capture(name, params); const analytics = getAnalyticsInstance();
if (analytics) {
firebaseLogEvent(analytics, name, params);
}
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to log event:", error); console.warn("[Analytics] Failed to log event:", error);
} }
@@ -53,7 +69,10 @@ export function logEvent(
export function setUserId(userId: string) { export function setUserId(userId: string) {
try { try {
if (__DEV__) console.log("[Analytics] Set user ID:", userId); if (__DEV__) console.log("[Analytics] Set user ID:", userId);
posthog?.identify(userId); const analytics = getAnalyticsInstance();
if (analytics) {
firebaseSetUserId(analytics, userId);
}
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to set user ID:", error); console.warn("[Analytics] Failed to set user ID:", error);
} }
@@ -63,7 +82,11 @@ export function setUserId(userId: string) {
export function clearUserId() { export function clearUserId() {
try { try {
if (__DEV__) console.log("[Analytics] Cleared user ID"); if (__DEV__) console.log("[Analytics] Cleared user ID");
posthog?.reset(); const analytics = getAnalyticsInstance();
if (analytics) {
firebaseSetUserId(analytics, null);
resetAnalyticsData(analytics);
}
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to clear user ID:", error); console.warn("[Analytics] Failed to clear user ID:", error);
} }
@@ -75,14 +98,13 @@ export function setUserProperties(
) { ) {
try { try {
if (__DEV__) console.log("[Analytics] Set user properties:", properties); if (__DEV__) console.log("[Analytics] Set user properties:", properties);
// Filter out null values for PostHog const analytics = getAnalyticsInstance();
const filteredProps: Record<string, string> = {}; if (analytics) {
for (const [key, value] of Object.entries(properties)) { // Firebase Analytics requires setting user properties one at a time
if (value !== null) { for (const [key, value] of Object.entries(properties)) {
filteredProps[key] = value; setUserProperty(analytics, key, value);
} }
} }
posthog?.capture("$set", { $set: filteredProps });
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to set user properties:", error); console.warn("[Analytics] Failed to set user properties:", error);
} }
@@ -92,7 +114,13 @@ export function setUserProperties(
export function logScreenView(screenName: string, screenClass?: string) { export function logScreenView(screenName: string, screenClass?: string) {
try { try {
if (__DEV__) console.log("[Analytics] Screen view:", screenName); if (__DEV__) console.log("[Analytics] Screen view:", screenName);
posthog?.screen(screenName, { screen_class: screenClass ?? screenName }); const analytics = getAnalyticsInstance();
if (analytics) {
firebaseLogEvent(analytics, "screen_view", {
screen_name: screenName,
screen_class: screenClass ?? screenName,
});
}
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to log screen view:", error); console.warn("[Analytics] Failed to log screen view:", error);
} }
@@ -100,11 +128,9 @@ export function logScreenView(screenName: string, screenClass?: string) {
// Flush events (useful before app backgrounding) // Flush events (useful before app backgrounding)
export function flush() { export function flush() {
try { // Firebase Analytics batches and sends events automatically
posthog?.flush(); // No manual flush needed, but we keep the function for API compatibility
} catch (error) { if (__DEV__) console.log("[Analytics] Flush called (no-op for Firebase)");
console.warn("[Analytics] Failed to flush:", error);
}
} }
// Pre-defined event helpers for common actions // Pre-defined event helpers for common actions
+11 -12
View File
@@ -120,16 +120,15 @@ jest.mock('base64-arraybuffer', () => ({
decode: jest.fn(() => new ArrayBuffer(0)), decode: jest.fn(() => new ArrayBuffer(0)),
})); }));
jest.mock('posthog-react-native', () => { jest.mock('@react-native-firebase/app', () => ({
return class PostHog { getApp: jest.fn(() => ({})),
capture = jest.fn(); }));
identify = jest.fn();
reset = jest.fn();
screen = jest.fn();
flush = jest.fn();
constructor() { jest.mock('@react-native-firebase/analytics', () => ({
// no-op getAnalytics: jest.fn(() => ({})),
} logEvent: jest.fn(),
}; setUserId: jest.fn(),
}); setUserProperty: jest.fn(),
setAnalyticsCollectionEnabled: jest.fn(() => Promise.resolve()),
resetAnalyticsData: jest.fn(),
}));
+896 -223
View File
File diff suppressed because it is too large. Load diff