diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 6d7dc583..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 = 328; + 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 = 328; + CURRENT_PROJECT_VERSION = 331; DEVELOPMENT_TEAM = 7KWQK5S4K6; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; 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"), 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();