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();