From dfd7bda4aa0699f76e2549de82c2221beb5f097e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 14:07:10 +0000 Subject: [PATCH 1/3] Fix black screen: graceful degradation for missing env vars, bump to build 330 The black screen was caused by supabase.ts throwing at module evaluation time when env vars are missing, killing the app before React could mount. Three-layer fix: - supabase.ts: Replace fatal throw with console.warn + export isSupabaseConfigured flag so the module evaluates safely - App.tsx: Check isSupabaseConfigured and show a styled error screen instead of attempting to render with broken Supabase client - main.tsx: Dynamic import for App so the global error handler is registered before any app modules evaluate (defense-in-depth) Also bumps iOS build number to 330. https://claude.ai/code/session_01QF55kSiZi1wJ7HYAfqumWD --- ios/App/App.xcodeproj/project.pbxproj | 4 ++-- src/App.tsx | 23 +++++++++++++++++++++- src/lib/supabase.ts | 15 ++++++++------ src/main.tsx | 28 +++++++++++++++++++-------- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 6d7dc583..b69225b8 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -312,7 +312,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 328; + CURRENT_PROJECT_VERSION = 330; DEVELOPMENT_TEAM = 7KWQK5S4K6; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -337,7 +337,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 328; + CURRENT_PROJECT_VERSION = 330; DEVELOPMENT_TEAM = 7KWQK5S4K6; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; diff --git a/src/App.tsx b/src/App.tsx index a0bcbf7d..15b75c64 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,7 +31,7 @@ import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { PremiumGuard } from './components/PremiumGuard'; import { ensureFreshEntitlement } from './services/entitlements'; import { startSync, stopSync } from './services/syncEngine'; -import { supabase, getSharedSession } from './lib/supabase'; +import { supabase, getSharedSession, isSupabaseConfigured } from './lib/supabase'; import { logger } from './utils/logger'; import { initNativeOAuthListener } from './lib/nativeOAuth'; import { initializePurchases, loginPurchases, logoutPurchases } from './services/iap'; @@ -656,6 +656,27 @@ function App() { }; }, []); + if (!isSupabaseConfigured) { + return ( +
+
+

Configuration Error

+

+ This build is missing required configuration. Please reinstall the app + or try again later. +

+ +
+
+ ); + } + return ( diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 0cb53f3c..80c4f561 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -9,8 +9,11 @@ const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || ''; const isTestEnv = typeof import.meta.env.VITEST !== 'undefined'; const isMissingConfig = !supabaseUrl || !supabaseAnonKey || supabaseUrl.includes('placeholder'); +/** True when valid Supabase env vars were provided at build time (always true in tests). */ +export const isSupabaseConfigured = !isMissingConfig || isTestEnv; + if (isMissingConfig && !isTestEnv) { - throw new Error( + console.warn( 'Supabase configuration missing. Set VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY environment variables.' ); } @@ -64,12 +67,12 @@ if (isNativeApp) { } } -// In production the throw above guarantees real values. -// In test mode, placeholder values allow tests to import this module -// without requiring real Supabase credentials. +// When config is missing (test env or misconfigured build), use placeholder +// values so the module evaluates without crashing. API calls will fail +// gracefully; the UI checks isSupabaseConfigured and shows an error screen. export const supabase = createClient( - isTestEnv && !supabaseUrl ? 'https://placeholder.supabase.co' : supabaseUrl, - isTestEnv && !supabaseAnonKey ? 'placeholder-key' : supabaseAnonKey, + isMissingConfig ? 'https://placeholder.supabase.co' : supabaseUrl, + isMissingConfig ? 'placeholder-key' : supabaseAnonKey, { auth: { storage: secureAuthStorage, diff --git a/src/main.tsx b/src/main.tsx index ddd2c98f..9c163f57 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,12 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; import './index.css'; -import { initNativeFeatures } from './utils/capacitor'; import { logger } from './utils/logger'; // ============================================================ // Fatal error display — uses safe DOM APIs (no innerHTML XSS risk) +// MUST be registered BEFORE importing App, so module-evaluation +// errors (e.g. missing env vars) show a styled error page +// instead of a black screen. // ============================================================ const showFatalError = (label: string, err: unknown) => { // Log full details for debugging (only visible in browser dev tools) @@ -141,14 +142,23 @@ if (!isNative && 'serviceWorker' in navigator) { } // ============================================================ -// BOOT +// BOOT — Dynamic import so error handlers above catch any +// module-evaluation failures in App or its dependency tree. // ============================================================ -const container = document.getElementById('root'); +async function boot() { + const container = document.getElementById('root'); + + if (!container) { + showFatalError('Boot Failure', new Error('#root element missing in HTML')); + return; + } -if (!container) { - showFatalError('Boot Failure', new Error('#root element missing in HTML')); -} else { try { + const [{ default: App }, { initNativeFeatures }] = await Promise.all([ + import('./App'), + import('./utils/capacitor'), + ]); + const root = ReactDOM.createRoot(container); root.render( @@ -157,6 +167,8 @@ if (!container) { ); initNativeFeatures(); } catch (error: unknown) { - showFatalError('Synchronous Boot Crash', error); + showFatalError('Boot Crash', error); } } + +boot(); From c063ed416e56d3acc5545299def27850a3a68d97 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 14:27:25 +0000 Subject: [PATCH 2/3] Bump iOS build to 331 (330 was the broken black screen build) https://claude.ai/code/session_01QF55kSiZi1wJ7HYAfqumWD --- ios/App/App.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index b69225b8..a4910d31 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -312,7 +312,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 330; + CURRENT_PROJECT_VERSION = 331; DEVELOPMENT_TEAM = 7KWQK5S4K6; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -337,7 +337,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 330; + CURRENT_PROJECT_VERSION = 331; DEVELOPMENT_TEAM = 7KWQK5S4K6; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; From face29c7e35430ead585d6e0e1cd82256ca4330f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 14:32:48 +0000 Subject: [PATCH 3/3] Update Package.swift from cap sync for build 331 https://claude.ai/code/session_01QF55kSiZi1wJ7HYAfqumWD --- ios/App/CapApp-SPM/Package.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/App/CapApp-SPM/Package.swift b/ios/App/CapApp-SPM/Package.swift index b9d2b512..69764ec7 100644 --- a/ios/App/CapApp-SPM/Package.swift +++ b/ios/App/CapApp-SPM/Package.swift @@ -14,6 +14,7 @@ let package = Package( .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.1.0"), .package(name: "CapacitorApp", path: "../../../node_modules/@capacitor/app"), .package(name: "CapacitorBrowser", path: "../../../node_modules/@capacitor/browser"), + .package(name: "CapacitorClipboard", path: "../../../node_modules/@capacitor/clipboard"), .package(name: "CapacitorHaptics", path: "../../../node_modules/@capacitor/haptics"), .package(name: "CapacitorKeyboard", path: "../../../node_modules/@capacitor/keyboard"), .package(name: "CapacitorLocalNotifications", path: "../../../node_modules/@capacitor/local-notifications"), @@ -28,6 +29,7 @@ let package = Package( .product(name: "Cordova", package: "capacitor-swift-pm"), .product(name: "CapacitorApp", package: "CapacitorApp"), .product(name: "CapacitorBrowser", package: "CapacitorBrowser"), + .product(name: "CapacitorClipboard", package: "CapacitorClipboard"), .product(name: "CapacitorHaptics", package: "CapacitorHaptics"), .product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"), .product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications"),