-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFaith_JournalApp.swift
More file actions
522 lines (470 loc) · 24.9 KB
/
Copy pathFaith_JournalApp.swift
File metadata and controls
522 lines (470 loc) · 24.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
// Faith_JournalApp.swift
// Faith Journal
//
// Created by Ronell Bradley on 6/29/25.
//
import SwiftUI
import SwiftData
import UserNotifications
#if os(iOS)
import UIKit
import GoogleMobileAds
#endif
@main
@available(iOS 17.0, *)
struct Faith_JournalApp: App {
#if os(iOS)
@UIApplicationDelegateAdaptor(PushNotificationAppDelegate.self) private var appDelegate
#endif
// CRITICAL: Use lazy initialization for singletons to avoid crashes during app startup
// Accessing .shared during app initialization can cause crashes if services aren't ready
private var notificationService: NotificationService {
NotificationService.shared
}
private var promptManager: PromptManager {
PromptManager.shared
}
init() {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"
print("✅ [BUILD] Faith Journal \(version) (\(build)) stamp=\(BuildInfo.stamp)")
// CRITICAL: Fix corrupted UserDefaults values BEFORE any views are created
// This prevents crashes when @AppStorage tries to load arrays/dictionaries that are stored as Strings
print("🔧 [FIX] Checking for corrupted UserDefaults values at app init...")
// Fix bookmarkedVerses - if stored as String, remove it
if let value = UserDefaults.standard.object(forKey: "bookmarkedVerses"),
value is String {
print("⚠️ [FIX] Found corrupted bookmarkedVerses (String), removing...")
UserDefaults.standard.removeObject(forKey: "bookmarkedVerses")
}
// Fix verseNotes - if stored as String, remove it
if let value = UserDefaults.standard.object(forKey: "verseNotes"),
value is String {
print("⚠️ [FIX] Found corrupted verseNotes (String), removing...")
UserDefaults.standard.removeObject(forKey: "verseNotes")
}
// Fix bibleRecentSearches - if stored as String, remove it
if let value = UserDefaults.standard.object(forKey: "bibleRecentSearches"),
value is String {
print("⚠️ [FIX] Found corrupted bibleRecentSearches (String), removing...")
UserDefaults.standard.removeObject(forKey: "bibleRecentSearches")
}
print("✅ [FIX] UserDefaults corruption check complete")
// CRITICAL: Configure Firebase as early as possible (before any Auth usage).
// Some singletons/views may touch FirebaseAuth during initialization; if Firebase
// isn't configured yet, the app will crash with:
// “The default FirebaseApp instance must be configured…”
FirebaseInitializer.shared.initialize()
#if os(iOS)
// CRITICAL: Initialize Google Mobile Ads before any ad views load.
// Required to prevent GADApplicationVerifyPublisherInitializedCorrectly crash.
MobileAds.shared.start(completionHandler: nil)
#endif
}
// Add defensive logging for TestFlight debugging
// Using local-only storage with Firebase for sync
// CRITICAL: Schema must be defined as a static property to prevent it from "disappearing"
static let appSchema = Schema([
JournalEntry.self,
PrayerRequest.self,
UserProfile.self,
MoodEntry.self,
MoodGoal.self,
MoodAchievement.self,
BibleVerseOfTheDay.self,
LiveSession.self,
LiveSessionParticipant.self,
Subscription.self,
ChatMessage.self,
SessionInvitation.self,
FaithFriend.self,
// SessionTemplate.self,
// SessionRating.self,
// SessionPlaylist.self,
// SessionNote.self,
// SessionClip.self,
BookmarkedVerse.self,
BibleHighlight.self,
BibleNote.self,
BibleReadingHistory.self,
ReadingPlan.self,
JournalPrompt.self,
BibleStudyTopic.self,
StatisticAchievement.self
])
var sharedModelContainer: ModelContainer = {
print("🚀 [LAUNCH] Starting ModelContainer initialization...")
print("🚀 [LAUNCH] Thread: \(Thread.isMainThread ? "Main" : "Background")")
print("🚀 [LAUNCH] Bundle: \(Bundle.main.bundleIdentifier ?? "unknown")")
let schema = Faith_JournalApp.appSchema
// Using Firebase for backend sync, not CloudKit
// SwiftData will use local storage only
print("🚀 [LAUNCH] Creating ModelContainer with local storage (Firebase for sync)...")
print("🚀 [STORAGE] Using SwiftData local storage")
print("🚀 [STORAGE] Firebase will handle cross-device synchronization")
// Use local-only storage (no CloudKit)
let modelConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false
// No cloudKitDatabase - using Firebase for sync instead
)
do {
print("🚀 [LAUNCH] Attempting ModelContainer creation with local storage...")
let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
// Log successful initialization for debugging
print("✅ [LAUNCH] ModelContainer initialized successfully")
print("✅ [LAUNCH] Using local SwiftData storage")
print("✅ [LAUNCH] Schema includes \(schema.entities.count) models")
print("✅ [LAUNCH] Schema entities: \(schema.entities.map { $0.name }.joined(separator: ", "))")
print("ℹ️ [STORAGE] Firebase will handle data synchronization")
return container
} catch let error as NSError {
// Check if this is a migration error (CoreData error 134140)
if error.domain == "NSCocoaErrorDomain" && error.code == 134140 {
print("❌ [MIGRATION] CoreData migration failed (Code 134140)")
print("❌ [MIGRATION] This usually means the schema changed in an incompatible way")
print("❌ [MIGRATION] Error details: \(error.localizedDescription)")
let userInfo = error.userInfo
print("❌ [MIGRATION] UserInfo: \(userInfo)")
if let reason = userInfo["reason"] as? String {
print("❌ [MIGRATION] Reason: \(reason)")
}
// In DEBUG mode, try to delete the old database and start fresh
#if DEBUG
print("⚠️ [MIGRATION] DEBUG MODE: Attempting to reset database...")
let fileManager = FileManager.default
// SwiftData stores databases in Application Support directory
if let appSupportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
let defaultStoreURL = appSupportURL.appendingPathComponent("default.store")
let defaultStoreShmURL = appSupportURL.appendingPathComponent("default.store-shm")
let defaultStoreWalURL = appSupportURL.appendingPathComponent("default.store-wal")
var deleted = false
for storeFile in [defaultStoreURL, defaultStoreShmURL, defaultStoreWalURL] {
if fileManager.fileExists(atPath: storeFile.path) {
do {
try fileManager.removeItem(at: storeFile)
print("✅ [MIGRATION] Deleted: \(storeFile.lastPathComponent)")
deleted = true
} catch {
print("⚠️ [MIGRATION] Could not delete \(storeFile.lastPathComponent): \(error)")
}
}
}
if deleted {
print("🔄 [MIGRATION] Retrying ModelContainer creation after database reset...")
do {
let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
print("✅ [MIGRATION] ModelContainer created successfully after reset")
return container
} catch {
print("❌ [MIGRATION] Still failed after reset: \(error)")
}
} else {
print("⚠️ [MIGRATION] No database files found to delete")
}
} else {
print("⚠️ [MIGRATION] Could not find Application Support directory")
}
#else
print("⚠️ [MIGRATION] PRODUCTION MODE: Migration error detected")
print("⚠️ [MIGRATION] To fix: Reset simulator data or reinstall app")
#endif
}
// In production, log the error and use local storage as fallback
print("❌ CRITICAL: Could not create ModelContainer: \(error)")
print("❌ CRITICAL: Error type: \(type(of: error))")
print("❌ CRITICAL: Error description: \(error.localizedDescription)")
print("❌ CRITICAL: Error domain: \(error.domain)")
print("❌ CRITICAL: Error code: \(error.code)")
print("❌ CRITICAL: Error userInfo: \(error.userInfo)")
print("Error details: \(error.localizedDescription)")
print("⚠️ [LAUNCH] Attempting in-memory fallback...")
// Try to create an in-memory container as fallback for NSError
#if DEBUG
let fallbackConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: true
)
do {
print("⚠️ Attempting fallback to in-memory storage (DEBUG ONLY)...")
return try ModelContainer(for: schema, configurations: [fallbackConfiguration])
} catch {
print("❌ CRITICAL: In-memory fallback also failed: \(error)")
fatalError("Failed to initialize data storage: \(error.localizedDescription)")
}
#else
// In production, try minimal configuration
let minimalConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false
)
do {
print("⚠️ Using minimal configuration...")
return try ModelContainer(for: schema, configurations: [minimalConfiguration])
} catch {
print("❌ CRITICAL: Minimal configuration failed: \(error)")
// Last resort: in-memory only
let emergencyConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: true
)
do {
return try ModelContainer(for: schema, configurations: [emergencyConfiguration])
} catch {
// Absolute last resort: empty schema
return try! ModelContainer(for: Schema([]), configurations: [ModelConfiguration(isStoredInMemoryOnly: true)])
}
}
#endif
} catch {
// Handle non-NSError errors
print("❌ CRITICAL: Could not create ModelContainer: \(error)")
print("❌ CRITICAL: Error type: \(type(of: error))")
print("❌ CRITICAL: Error description: \(error.localizedDescription)")
print("⚠️ [LAUNCH] Attempting in-memory fallback...")
// Try to create an in-memory container as fallback
#if DEBUG
let fallbackConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: true
)
do {
print("⚠️ Attempting fallback to in-memory storage (DEBUG ONLY)...")
return try ModelContainer(for: schema, configurations: [fallbackConfiguration])
} catch {
// Last resort: fatal error (but with better logging)
let errorMessage = """
Failed to initialize data storage.
This may be due to:
1. Schema migration problems
2. Insufficient device storage
3. Database corruption
Error: \(error.localizedDescription)
"""
print("❌ FATAL: \(errorMessage)")
fatalError(errorMessage)
}
#else
// In production, show error screen instead of crashing
// Create a minimal in-memory container to prevent crash
// User will see an error message in the UI
print("❌ CRITICAL: Failed to initialize ModelContainer in production")
print("Error: \(error.localizedDescription)")
print("⚠️ Attempting minimal fallback configuration...")
// Try minimal configuration
let minimalConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false
)
do {
print("⚠️ Using minimal configuration...")
return try ModelContainer(for: schema, configurations: [minimalConfiguration])
} catch {
// Last resort: in-memory only (data won't persist, but app won't crash)
print("❌ CRITICAL: Even minimal configuration failed")
print("⚠️ Using in-memory storage as last resort - data will not persist")
let emergencyConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: true
)
do {
return try ModelContainer(for: schema, configurations: [emergencyConfiguration])
} catch {
// CRITICAL: Never crash in production - create a minimal working container
// Even if this fails, we'll try with an empty schema to at least let the app start
print("❌ CRITICAL: Emergency configuration failed: \(error)")
print("⚠️ Attempting absolute minimal configuration...")
// Try with just the essential models
let essentialSchema = Schema([
JournalEntry.self,
PrayerRequest.self,
UserProfile.self
])
let absoluteMinimalConfig = ModelConfiguration(
schema: essentialSchema,
isStoredInMemoryOnly: true
)
do {
print("⚠️ Using essential models only (in-memory)...")
return try ModelContainer(for: essentialSchema, configurations: [absoluteMinimalConfig])
} catch {
// Last absolute resort - this should never happen, but if it does,
// we'll create an empty container to prevent crash
print("❌ CRITICAL: All initialization attempts failed")
print("⚠️ Creating empty container to prevent crash")
// Create a minimal empty container - app will work but data won't persist
// This is better than crashing
// CRITICAL: Never crash in production - try one final time
// Log extensively for debugging but allow app to continue
print("❌ CRITICAL: Unable to create even empty container")
print("⚠️ App will continue with limited functionality")
// Try one more time with absolute minimal setup
do {
print("⚠️ FINAL FALLBACK: Attempting emergency container creation")
return try ModelContainer(for: Schema([]), configurations: [ModelConfiguration(isStoredInMemoryOnly: true)])
} catch {
// Last resort: Create a dummy container that won't crash
// The app will show an error to the user but won't crash
print("❌ FINAL FALLBACK: Emergency container creation failed: \(error)")
print("⚠️ Using force unwrap as absolute last resort - app must start")
// Return a container with empty schema - app can still run
// This try! is acceptable as it's the absolute last resort
return try! ModelContainer(for: Schema([]), configurations: [ModelConfiguration(isStoredInMemoryOnly: true)])
}
}
}
}
#endif
}
}()
var body: some Scene {
WindowGroup {
ZStack {
// Ensure window background fills entire screen - no safe areas
Color.purple.opacity(0.8)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
AppRootView()
.environmentObject(notificationService)
.environmentObject(promptManager)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
#if os(iOS)
.background(FullScreenWindowConfigurator())
#endif
.task {
print("🚀 [LAUNCH] App body task started")
// Use task instead of onAppear for async initialization
// Initialize services safely on main actor
await MainActor.run {
print("🚀 [LAUNCH] MainActor.run block started")
// CRITICAL: Fix corrupted UserDefaults values BEFORE any views load
// This prevents crashes when @AppStorage tries to load arrays/dictionaries that are stored as Strings
print("🔧 [FIX] Checking for corrupted UserDefaults values...")
// Fix bookmarkedVerses - if stored as String, remove it
if let value = UserDefaults.standard.object(forKey: "bookmarkedVerses"),
value is String {
print("⚠️ [FIX] Found corrupted bookmarkedVerses (String), removing...")
UserDefaults.standard.removeObject(forKey: "bookmarkedVerses")
}
// Fix verseNotes - if stored as String, remove it
if let value = UserDefaults.standard.object(forKey: "verseNotes"),
value is String {
print("⚠️ [FIX] Found corrupted verseNotes (String), removing...")
UserDefaults.standard.removeObject(forKey: "verseNotes")
}
// Fix bibleRecentSearches - if stored as String, remove it
if let value = UserDefaults.standard.object(forKey: "bibleRecentSearches"),
value is String {
print("⚠️ [FIX] Found corrupted bibleRecentSearches (String), removing...")
UserDefaults.standard.removeObject(forKey: "bibleRecentSearches")
}
// Fix any other array/dictionary AppStorage values that might be corrupted
// Check for common keys that should be arrays but might be strings
let arrayKeys = ["bookmarkedVerses", "bibleRecentSearches", "recentSearches"]
for key in arrayKeys {
if let value = UserDefaults.standard.object(forKey: key),
value is String {
print("⚠️ [FIX] Found corrupted \(key) (String), removing...")
UserDefaults.standard.removeObject(forKey: key)
}
}
// Check for dictionary keys that might be strings
let dictKeys = ["verseNotes", "highlightedVerses"]
for key in dictKeys {
if let value = UserDefaults.standard.object(forKey: key),
value is String {
print("⚠️ [FIX] Found corrupted \(key) (String), removing...")
UserDefaults.standard.removeObject(forKey: key)
}
}
print("✅ [FIX] UserDefaults corruption check complete")
// Initialize prompt manager lazily (will initialize on first access)
_ = promptManager
print("🚀 [LAUNCH] PromptManager accessed")
// Clear badge when app opens
notificationService.clearBadge()
print("🚀 [LAUNCH] Badge cleared")
// Enable background sync (already registered in init, just schedule it)
// Temporarily disabled until BackgroundSyncService compilation is resolved
// if #available(iOS 17.0, *) {
// BackgroundSyncService.shared.scheduleBackgroundSync()
// print("🚀 [LAUNCH] Background sync enabled")
// }
}
// Setup notifications asynchronously
await setupNotifications()
print("🚀 [LAUNCH] Notifications setup complete")
}
#if os(iOS)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
Task { @MainActor in
notificationService.clearBadge()
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
Task { @MainActor in
notificationService.clearBadge()
if #available(iOS 17.0, *) {
// Get ModelContext from environment and check for sync
}
}
}
#endif
}
.modelContainer(sharedModelContainer)
}
private func setupNotifications() async {
// Request notification authorization
let granted = await notificationService.requestAuthorization()
#if os(iOS)
if granted {
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}
#endif
// Schedule daily prompt notification at 9 AM
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
if let scheduledDate = Calendar.current.date(from: dateComponents) {
await MainActor.run {
notificationService.scheduleDailyPromptNotification(time: scheduledDate)
}
}
}
}
// MARK: - Full Screen Window Configurator (iOS only)
#if os(iOS)
struct FullScreenWindowConfigurator: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let controller = UIViewController()
controller.edgesForExtendedLayout = .all
controller.extendedLayoutIncludesOpaqueBars = true
controller.view.backgroundColor = .clear
controller.view.clipsToBounds = false
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
DispatchQueue.main.async {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
for window in windowScene.windows {
// Ensure window fills entire screen
window.frame = windowScene.screen.bounds
window.backgroundColor = .clear
// Configure root view controller for full screen
if let rootViewController = window.rootViewController {
rootViewController.edgesForExtendedLayout = .all
rootViewController.extendedLayoutIncludesOpaqueBars = true
rootViewController.view.backgroundColor = .clear
rootViewController.view.frame = window.bounds
rootViewController.view.clipsToBounds = false
}
}
}
}
}
}
#endif