From 58209b94efbeb0ac1cde8b4350d773643ebf1a87 Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sun, 4 Jan 2026 16:43:24 +0000 Subject: [PATCH] Fix EAS local builds and resolve require cycles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add dotenv-cli for EAS local builds to load env vars - Break circular dependencies in store/hooks imports - Add env var validation with clear error messages - Remove hardcoded Sentry auth token from sentry.properties - Add .ipa files to .gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/mobile/.gitignore | 1 + apps/mobile/Gemfile | 9 + apps/mobile/Gemfile.lock | 233 ++++++++++++++++++ apps/mobile/app/_layout.tsx | 130 +++++----- apps/mobile/eas.json | 12 +- .../ios/Sauci.xcodeproj/project.pbxproj | 14 +- apps/mobile/ios/sentry.properties | 2 +- apps/mobile/jest.config.js | 5 +- apps/mobile/package.json | 7 +- apps/mobile/src/hooks/useDecryptedMedia.ts | 16 +- apps/mobile/src/hooks/useEncryptionKeys.ts | 3 +- apps/mobile/src/lib/authErrorHandler.ts | 3 +- apps/mobile/src/lib/mediaCache.ts | 22 ++ apps/mobile/src/lib/supabase.ts | 22 +- apps/mobile/src/polyfills/crypto.ts | 10 +- apps/mobile/src/store/authStore.ts | 2 +- apps/mobile/src/test/setup.ts | 14 +- apps/mobile/src/test/setupEnv.js | 7 + package-lock.json | 60 +++++ 19 files changed, 458 insertions(+), 114 deletions(-) create mode 100644 apps/mobile/Gemfile create mode 100644 apps/mobile/Gemfile.lock create mode 100644 apps/mobile/src/lib/mediaCache.ts create mode 100644 apps/mobile/src/test/setupEnv.js 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" }