Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions ios/App/CapApp-SPM/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down
23 changes: 22 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -656,6 +656,27 @@ function App() {
};
}, []);

if (!isSupabaseConfigured) {
return (
<div className="min-h-screen bg-[#0A0A0A] flex items-center justify-center px-6 text-center">
<div className="max-w-md space-y-4">
<h1 className="text-2xl font-semibold text-white">Configuration Error</h1>
<p className="text-white/60 leading-relaxed">
This build is missing required configuration. Please reinstall the app
or try again later.
</p>
<button
onClick={() => window.location.reload()}
className="px-8 py-3 rounded-xl font-semibold text-black"
style={{ background: 'linear-gradient(90deg,#A68B3C,#C5A55A,#D9BE6C,#C5A55A,#A68B3C)' }}
>
Reload
</button>
</div>
</div>
);
}

return (
<ErrorBoundary FallbackComponent={VeteranErrorBoundary}>
<MotionConfig reducedMotion="user">
Expand Down
15 changes: 9 additions & 6 deletions src/lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
);
}
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 20 additions & 8 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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(
<React.StrictMode>
Expand All @@ -157,6 +167,8 @@ if (!container) {
);
initNativeFeatures();
} catch (error: unknown) {
showFatalError('Synchronous Boot Crash', error);
showFatalError('Boot Crash', error);
}
}

boot();
Loading