diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore
index c945982..05bdf7c 100755
--- a/apps/mobile/.gitignore
+++ b/apps/mobile/.gitignore
@@ -5,3 +5,4 @@
expo-env.d.ts
# @end expo-cli
*.aab
+*.ipa
\ No newline at end of file
diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile
new file mode 100644
index 0000000..7515cd5
--- /dev/null
+++ b/apps/mobile/Gemfile
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
+
+# gem "rails"
+
+gem "fastlane", "~> 2.230"
diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock
new file mode 100644
index 0000000..bd9102c
--- /dev/null
+++ b/apps/mobile/Gemfile.lock
@@ -0,0 +1,233 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.9)
+ abbrev (0.1.2)
+ addressable (2.8.8)
+ public_suffix (>= 2.0.2, < 8.0)
+ artifactory (3.0.17)
+ atomos (0.1.3)
+ aws-eventstream (1.3.2)
+ aws-partitions (1.1109.0)
+ aws-sdk-core (3.224.1)
+ aws-eventstream (~> 1, >= 1.3.0)
+ aws-partitions (~> 1, >= 1.992.0)
+ aws-sigv4 (~> 1.9)
+ base64
+ jmespath (~> 1, >= 1.6.1)
+ logger
+ aws-sdk-kms (1.101.0)
+ aws-sdk-core (~> 3, >= 3.216.0)
+ aws-sigv4 (~> 1.5)
+ aws-sdk-s3 (1.188.0)
+ aws-sdk-core (~> 3, >= 3.224.1)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.5)
+ aws-sigv4 (1.11.0)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ base64 (0.2.0)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ csv (3.3.5)
+ declarative (0.0.20)
+ digest-crc (0.7.0)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.5.20190701)
+ unf (>= 0.0.5, < 1.0.0)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ excon (0.109.0)
+ faraday (1.10.4)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-cookie_jar (0.0.8)
+ faraday (>= 0.8.0)
+ http-cookie (>= 1.0.0)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.1)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.2.0)
+ multipart-post (~> 2.0)
+ faraday-net_http (1.0.2)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ faraday_middleware (1.2.1)
+ faraday (~> 1.0)
+ fastimage (2.4.0)
+ fastlane (2.230.0)
+ CFPropertyList (>= 2.3, < 4.0.0)
+ abbrev (~> 0.1.2)
+ addressable (>= 2.8, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
+ base64 (~> 0.2.0)
+ bundler (>= 1.12.0, < 3.0.0)
+ colored (~> 1.2)
+ commander (~> 4.6)
+ csv (~> 3.3)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 1.0)
+ faraday-cookie_jar (~> 0.0.6)
+ faraday_middleware (~> 1.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ fastlane-sirp (>= 1.0.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.3)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-env (>= 1.6.0, < 2.0.0)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ http-cookie (~> 1.0.5)
+ json (< 3.0.0)
+ jwt (>= 2.1.0, < 3)
+ logger (>= 1.6, < 2.0)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multipart-post (>= 2.0.0, < 3.0.0)
+ mutex_m (~> 0.3.0)
+ naturally (~> 2.2)
+ nkf (~> 0.2.0)
+ optparse (>= 0.1.1, < 1.0.0)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
+ security (= 0.1.5)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
+ terminal-table (~> 3)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.4.1)
+ xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+ fastlane-sirp (1.0.0)
+ sysrandom (~> 1.0)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.54.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-core (0.11.3)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.16.2, < 2.a)
+ httpclient (>= 2.8.1, < 3.a)
+ mini_mime (~> 1.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.a)
+ rexml
+ google-apis-iamcredentials_v1 (0.17.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.13.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-storage_v1 (0.29.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-cloud-core (1.6.1)
+ google-cloud-env (>= 1.0, < 3.a)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
+ google-cloud-errors (1.3.1)
+ google-cloud-storage (1.45.0)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.29.0)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
+ googleauth (1.8.1)
+ faraday (>= 0.17.3, < 3.a)
+ jwt (>= 1.4, < 3.0)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.0.8)
+ domain_name (~> 0.5)
+ httpclient (2.9.0)
+ mutex_m
+ jmespath (1.6.2)
+ json (2.7.6)
+ jwt (2.10.2)
+ base64
+ logger (1.7.0)
+ mini_magick (4.13.2)
+ mini_mime (1.1.5)
+ multi_json (1.15.0)
+ multipart-post (2.4.1)
+ mutex_m (0.3.0)
+ nanaimo (0.4.0)
+ naturally (2.3.0)
+ nkf (0.2.0)
+ optparse (0.8.1)
+ os (1.1.4)
+ plist (3.7.2)
+ public_suffix (5.1.1)
+ rake (13.3.1)
+ representable (3.2.0)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (3.1.2)
+ rexml (3.4.4)
+ rouge (3.28.0)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.4.1)
+ security (0.1.5)
+ signet (0.18.0)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 3.0)
+ multi_json (~> 1.10)
+ simctl (1.6.10)
+ CFPropertyList
+ naturally
+ sysrandom (1.0.5)
+ terminal-notifier (2.0.0)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ trailblazer-option (0.1.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.2)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unf (0.2.0)
+ unicode-display_width (2.6.0)
+ word_wrap (1.0.0)
+ xcodeproj (1.27.0)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.4.0)
+ rexml (>= 3.3.6, < 4.0)
+ xcpretty (0.4.1)
+ rouge (~> 3.28.0)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ fastlane (~> 2.230)
+
+BUNDLED WITH
+ 1.17.2
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
index 07bd381..2ad7fc2 100755
--- a/apps/mobile/app/_layout.tsx
+++ b/apps/mobile/app/_layout.tsx
@@ -1,54 +1,54 @@
-// Polyfills must be imported FIRST before any other React Native imports
-import "../src/polyfills/web";
-import "../src/polyfills/crypto";
-
-// Initialize Sentry early to capture all errors
-import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
-initSentry();
-
-// Initialize Analytics
-import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
-initAnalytics();
-
-// Track cold start immediately
-Events.appOpened("cold");
-
-// Import push notification utilities
-import {
- clearPushToken,
- setupNotificationResponseListener,
- getInitialNotification,
- handleNotificationResponse,
-} from "../src/lib/notifications";
-
-import React, { useEffect, useRef } from "react";
-import { Platform, View, AppState, AppStateStatus } from "react-native";
-
-// Suppress useLayoutEffect warning on server-side rendering
-if (Platform.OS === "web" && typeof globalThis.window === "undefined") {
- React.useLayoutEffect = useEffect;
- // Also silence the warning in case some libraries ignore the monkey-patch
- const originalError = console.error;
- console.error = (...args) => {
- if (typeof args[0] === "string" && args[0].includes("useLayoutEffect does nothing on the server")) {
- return;
- }
- originalError(...args);
- };
-}
-
-import { Stack, router, usePathname } from "expo-router";
-import { StatusBar } from "expo-status-bar";
-// GestureHandlerRootView is only needed on native platforms
-// On web, we use a simple View to avoid findNodeHandle errors
-const GestureHandlerRootView = Platform.OS === "web"
- ? View
- : require("react-native-gesture-handler").GestureHandlerRootView;
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import * as Linking from "expo-linking";
-import { useAuthStore } from "../src/store";
-import { supabase } from "../src/lib/supabase";
-import { EncryptionKeyInitializer } from "../src/components/EncryptionKeyInitializer";
+// Polyfills must be imported FIRST before any other React Native imports
+import "../src/polyfills/web";
+import "../src/polyfills/crypto";
+
+// Initialize Sentry early to capture all errors
+import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
+initSentry();
+
+// Initialize Analytics
+import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
+initAnalytics();
+
+// Track cold start immediately
+Events.appOpened("cold");
+
+// Import push notification utilities
+import {
+ clearPushToken,
+ setupNotificationResponseListener,
+ getInitialNotification,
+ handleNotificationResponse,
+} from "../src/lib/notifications";
+
+import React, { useEffect, useRef } from "react";
+import { Platform, View, AppState, AppStateStatus } from "react-native";
+
+// Suppress useLayoutEffect warning on server-side rendering
+if (Platform.OS === "web" && typeof globalThis.window === "undefined") {
+ React.useLayoutEffect = useEffect;
+ // Also silence the warning in case some libraries ignore the monkey-patch
+ const originalError = console.error;
+ console.error = (...args) => {
+ if (typeof args[0] === "string" && args[0].includes("useLayoutEffect does nothing on the server")) {
+ return;
+ }
+ originalError(...args);
+ };
+}
+
+import { Stack, router, usePathname } from "expo-router";
+import { StatusBar } from "expo-status-bar";
+// GestureHandlerRootView is only needed on native platforms
+// On web, we use a simple View to avoid findNodeHandle errors
+const GestureHandlerRootView = Platform.OS === "web"
+ ? View
+ : require("react-native-gesture-handler").GestureHandlerRootView;
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import * as Linking from "expo-linking";
+import { useAuthStore } from "../src/store";
+import { supabase } from "../src/lib/supabase";
+import { EncryptionKeyInitializer } from "../src/components/EncryptionKeyInitializer";
const queryClient = new QueryClient();
@@ -209,18 +209,18 @@ export default function RootLayout() {
}, []);
- return (
-
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+ );
}
diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json
index f4001c5..1f6031d 100644
--- a/apps/mobile/eas.json
+++ b/apps/mobile/eas.json
@@ -12,7 +12,17 @@
"distribution": "internal"
},
"production": {
- "autoIncrement": true
+ "autoIncrement": true,
+ "env": {
+ "EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
+ "EXPO_PUBLIC_SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNramNya2pwbWhxaGl1Y2lmdWt4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjcxMDc1ODEsImV4cCI6MjA4MjY4MzU4MX0.7FA_RmFXUjlmZF96O9xlAUfxHrQrSIZcJclNReLL4lI",
+ "EXPO_PUBLIC_SENTRY_DSN": "https://03a91f8a1b1ecdce2d5f38a4c23ca9d5@o4510613894070272.ingest.de.sentry.io/4510613896560720",
+ "EXPO_PUBLIC_REVENUECAT_IOS_API_KEY": "appl_lfPyYonFQtVytftPAfXIrGsMNFu",
+ "EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY": "goog_zoHtaRswsgvxtvYWIhZyvrPBGgh",
+ "EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID": "Sauci Pro",
+ "EXPO_PUBLIC_POSTHOG_API_KEY": "phc_dXEEZrPwH6lKDaKNpkFcA3qafg1qwqakHoU5UfdX5Ug",
+ "EXPO_PUBLIC_POSTHOG_HOST": "https://eu.i.posthog.com"
+ }
}
},
"submit": {
diff --git a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj
index bc5e700..4ee1cf1 100644
--- a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj
+++ b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj
@@ -389,7 +389,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = BXVPBATDZY;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -426,7 +426,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = BXVPBATDZY;
INFOPLIST_FILE = Sauci/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@@ -505,10 +505,7 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
- OTHER_LDFLAGS = (
- "$(inherited)",
- " ",
- );
+ OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -563,10 +560,7 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
- OTHER_LDFLAGS = (
- "$(inherited)",
- " ",
- );
+ OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
diff --git a/apps/mobile/ios/sentry.properties b/apps/mobile/ios/sentry.properties
index 68a02c4..2a03adb 100644
--- a/apps/mobile/ios/sentry.properties
+++ b/apps/mobile/ios/sentry.properties
@@ -1,4 +1,4 @@
defaults.url=https://sentry.io/
defaults.org=molten-prints
defaults.project=sauci
-# Using SENTRY_AUTH_TOKEN environment variable
\ No newline at end of file
+# auth.token is provided via SENTRY_AUTH_TOKEN environment variable
\ No newline at end of file
diff --git a/apps/mobile/jest.config.js b/apps/mobile/jest.config.js
index 9c6b0fa..49ae8b9 100644
--- a/apps/mobile/jest.config.js
+++ b/apps/mobile/jest.config.js
@@ -2,7 +2,10 @@
module.exports = {
preset: 'jest-expo',
clearMocks: true,
- setupFiles: ['../../node_modules/react-native-gesture-handler/jestSetup.js'],
+ setupFiles: [
+ '/src/test/setupEnv.js',
+ '../../node_modules/react-native-gesture-handler/jestSetup.js',
+ ],
setupFilesAfterEnv: [
'@testing-library/jest-native/extend-expect',
'/src/test/setup.ts',
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 8169791..c661935 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -12,7 +12,11 @@
"typecheck": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
- "test:ci": "jest --runInBand --ci"
+ "test:ci": "jest --runInBand --ci",
+ "build:ios": "dotenv -e .env -- eas build --platform ios --profile production",
+ "build:ios:local": "dotenv -e .env -- eas build --platform ios --profile production --local",
+ "build:android": "dotenv -e .env -- eas build --platform android --profile production",
+ "build:android:local": "dotenv -e .env -- eas build --platform android --profile production --local"
},
"dependencies": {
"@expo/ngrok": "^4.1.0",
@@ -71,6 +75,7 @@
"@types/jest": "^30.0.0",
"@types/react": "~18.3.12",
"@types/react-test-renderer": "^19.1.0",
+ "dotenv-cli": "^11.0.0",
"jest": "^29.7.0",
"jest-expo": "~52.0.0",
"react-test-renderer": "^18.3.1",
diff --git a/apps/mobile/src/hooks/useDecryptedMedia.ts b/apps/mobile/src/hooks/useDecryptedMedia.ts
index 3935df4..6efe135 100644
--- a/apps/mobile/src/hooks/useDecryptedMedia.ts
+++ b/apps/mobile/src/hooks/useDecryptedMedia.ts
@@ -14,6 +14,8 @@ import { useEncryptionKeys } from './useEncryptionKeys';
import { getCachedSignedUrl, getStoragePath } from '../lib/imageCache';
import { supabase } from '../lib/supabase';
import { reencryptPendingMessages } from '../lib/encryption/reencryptPendingMessages';
+// Import shared cache from mediaCache to avoid circular dependency
+import { decryptedMediaCache, clearDecryptedMediaCache } from '../lib/mediaCache';
export type DecryptedMediaErrorCode =
| 'E2EE_NOT_SUPPORTED'
@@ -22,8 +24,8 @@ export type DecryptedMediaErrorCode =
| 'E2EE_PENDING_RECIPIENT_KEY'
| 'E2EE_DECRYPT_FAILED';
-// In-memory cache to avoid re-decrypting the same media
-const decryptedMediaCache = new Map();
+// Re-export clearDecryptedMediaCache for backwards compatibility
+export { clearDecryptedMediaCache };
interface DecryptedMediaState {
/** Local file URI to the decrypted media (or null if not ready) */
@@ -396,13 +398,3 @@ export function useDecryptedMedia({
};
}
-/**
- * Clear the decrypted media cache
- * Call this when logging out or when memory is low
- */
-export async function clearDecryptedMediaCache(): Promise {
- for (const uri of decryptedMediaCache.values()) {
- await cleanupDecryptedMedia(uri);
- }
- decryptedMediaCache.clear();
-}
diff --git a/apps/mobile/src/hooks/useEncryptionKeys.ts b/apps/mobile/src/hooks/useEncryptionKeys.ts
index 4bf42a6..b76d48f 100644
--- a/apps/mobile/src/hooks/useEncryptionKeys.ts
+++ b/apps/mobile/src/hooks/useEncryptionKeys.ts
@@ -22,7 +22,8 @@ import {
} from '../lib/encryption';
import type { RSAPublicKeyJWK } from '../lib/encryption';
import { supabase } from '../lib/supabase';
-import { useAuthStore } from '../store';
+// Import directly from authStore to avoid circular dependency with store/index.ts
+import { useAuthStore } from '../store/authStore';
interface UseEncryptionKeysReturn {
/** RSA private key in JWK format */
diff --git a/apps/mobile/src/lib/authErrorHandler.ts b/apps/mobile/src/lib/authErrorHandler.ts
index 6b92758..1c12b0e 100644
--- a/apps/mobile/src/lib/authErrorHandler.ts
+++ b/apps/mobile/src/lib/authErrorHandler.ts
@@ -1,6 +1,7 @@
import { router } from "expo-router";
import { supabase } from "./supabase";
-import { useAuthStore } from "../store";
+// Import directly from authStore to avoid circular dependency with store/index.ts
+import { useAuthStore } from "../store/authStore";
/**
* Handles authentication errors from edge function calls.
diff --git a/apps/mobile/src/lib/mediaCache.ts b/apps/mobile/src/lib/mediaCache.ts
new file mode 100644
index 0000000..af57753
--- /dev/null
+++ b/apps/mobile/src/lib/mediaCache.ts
@@ -0,0 +1,22 @@
+/**
+ * Media Cache Utilities
+ *
+ * Extracted from useDecryptedMedia to avoid circular dependencies.
+ * This module handles caching of decrypted media files.
+ */
+
+import { cleanupDecryptedMedia } from './encryption';
+
+// In-memory cache to avoid re-decrypting the same media
+export const decryptedMediaCache = new Map();
+
+/**
+ * Clears all cached decrypted media files from disk and memory.
+ * Call this on sign out to free up storage.
+ */
+export async function clearDecryptedMediaCache(): Promise {
+ for (const uri of decryptedMediaCache.values()) {
+ await cleanupDecryptedMedia(uri);
+ }
+ decryptedMediaCache.clear();
+}
diff --git a/apps/mobile/src/lib/supabase.ts b/apps/mobile/src/lib/supabase.ts
index 8f696c4..69a9ad2 100755
--- a/apps/mobile/src/lib/supabase.ts
+++ b/apps/mobile/src/lib/supabase.ts
@@ -47,10 +47,26 @@ const ExpoStorageAdapter = {
},
};
-const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!;
-const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;
+const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
+const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
-export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
+// Validate environment variables at startup (skip in test environment where mocks handle this)
+const isTestEnv = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
+if (!isTestEnv && (!supabaseUrl || !supabaseAnonKey)) {
+ const missing = [];
+ if (!supabaseUrl) missing.push('EXPO_PUBLIC_SUPABASE_URL');
+ if (!supabaseAnonKey) missing.push('EXPO_PUBLIC_SUPABASE_ANON_KEY');
+ const errorMsg = `Missing required environment variables: ${missing.join(', ')}. App cannot start.`;
+ console.error('[Supabase]', errorMsg);
+ // In production, this will be caught by Sentry before crashing
+ throw new Error(errorMsg);
+}
+
+// Use dummy values in test environment (supabase-js is mocked anyway)
+const finalUrl = supabaseUrl || (isTestEnv ? 'https://test.supabase.co' : '');
+const finalKey = supabaseAnonKey || (isTestEnv ? 'test-key' : '');
+
+export const supabase = createClient(finalUrl, finalKey, {
auth: {
storage: ExpoStorageAdapter,
autoRefreshToken: true,
diff --git a/apps/mobile/src/polyfills/crypto.ts b/apps/mobile/src/polyfills/crypto.ts
index 19c450e..6015753 100644
--- a/apps/mobile/src/polyfills/crypto.ts
+++ b/apps/mobile/src/polyfills/crypto.ts
@@ -1,9 +1,9 @@
/**
* Crypto polyfill for React Native
- *
+ *
* This file must be imported BEFORE any code that uses crypto.
* It installs react-native-quick-crypto as the global crypto implementation.
- *
+ *
* On web, we skip this since the native Web Crypto API is available.
*/
@@ -13,17 +13,17 @@ if (Platform.OS !== 'web') {
try {
// Import react-native-quick-crypto to install its polyfill
const QuickCrypto = require('react-native-quick-crypto');
-
+
// Install the polyfill globally if not already present
if (typeof global.crypto === 'undefined') {
(global as any).crypto = QuickCrypto;
}
-
+
// Also ensure subtle is available
if (global.crypto && !global.crypto.subtle) {
(global.crypto as any).subtle = QuickCrypto.subtle;
}
-
+
console.log('[Crypto] react-native-quick-crypto polyfill installed');
} catch (error) {
console.error('[Crypto] Failed to install crypto polyfill:', error);
diff --git a/apps/mobile/src/store/authStore.ts b/apps/mobile/src/store/authStore.ts
index 26220c4..716377c 100644
--- a/apps/mobile/src/store/authStore.ts
+++ b/apps/mobile/src/store/authStore.ts
@@ -2,7 +2,7 @@ import { create } from "zustand";
import { supabase } from "../lib/supabase";
import { Events } from "../lib/analytics";
import { clearKeys } from "../lib/encryption";
-import { clearDecryptedMediaCache } from "../hooks/useDecryptedMedia";
+import { clearDecryptedMediaCache } from "../lib/mediaCache";
import type { Profile, Couple } from "@/types";
import type { RSAPublicKeyJWK, RSAPrivateKeyJWK } from "../lib/encryption/types";
diff --git a/apps/mobile/src/test/setup.ts b/apps/mobile/src/test/setup.ts
index 87a4938..42beeac 100644
--- a/apps/mobile/src/test/setup.ts
+++ b/apps/mobile/src/test/setup.ts
@@ -1,20 +1,10 @@
///
+// Environment variables are set in setupEnv.js (runs before modules load)
+
// Some modules rely on this global existing in RN.
(global as any).__DEV__ = true;
-if (!process.env.EXPO_PUBLIC_SUPABASE_URL) {
- process.env.EXPO_PUBLIC_SUPABASE_URL = 'https://example.supabase.co';
-}
-
-if (!process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY) {
- process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY = 'test-anon-key';
-}
-
-if (!process.env.EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID) {
- process.env.EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID = 'pro';
-}
-
jest.mock('@supabase/supabase-js', () => {
const makeChannel = () => {
const channel: any = {
diff --git a/apps/mobile/src/test/setupEnv.js b/apps/mobile/src/test/setupEnv.js
new file mode 100644
index 0000000..a82b43f
--- /dev/null
+++ b/apps/mobile/src/test/setupEnv.js
@@ -0,0 +1,7 @@
+// Set test environment variables BEFORE modules are loaded
+// This must run in setupFiles (not setupFilesAfterEnv) to ensure
+// env vars are available when supabase.ts is imported
+
+process.env.EXPO_PUBLIC_SUPABASE_URL = 'https://example.supabase.co';
+process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY = 'test-anon-key';
+process.env.EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID = 'pro';
diff --git a/package-lock.json b/package-lock.json
index 9a66d8c..386c893 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -121,6 +121,7 @@
"@types/jest": "^30.0.0",
"@types/react": "~18.3.12",
"@types/react-test-renderer": "^19.1.0",
+ "dotenv-cli": "^11.0.0",
"jest": "^29.7.0",
"jest-expo": "~52.0.0",
"react-test-renderer": "^18.3.1",
@@ -10723,6 +10724,64 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/dotenv-cli": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-11.0.0.tgz",
+ "integrity": "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "dotenv": "^17.1.0",
+ "dotenv-expand": "^12.0.0",
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "dotenv": "cli.js"
+ }
+ },
+ "node_modules/dotenv-cli/node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dotenv-cli/node_modules/dotenv-expand": {
+ "version": "12.0.3",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
+ "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dotenv": "^16.4.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dotenv-cli/node_modules/dotenv-expand/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dotenv-expand": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
@@ -20737,6 +20796,7 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">= 6"
}