diff --git a/Tiny/Core/Services/Audio/HeartbeatSoundManager.swift b/Tiny/Core/Services/Audio/HeartbeatSoundManager.swift index cd516ce..002510e 100644 --- a/Tiny/Core/Services/Audio/HeartbeatSoundManager.swift +++ b/Tiny/Core/Services/Audio/HeartbeatSoundManager.swift @@ -58,7 +58,15 @@ enum HeartbeatFilterMode { class HeartbeatSoundManager: NSObject, ObservableObject { var currentUserRole: UserRole? // Add this after currentRoomCode - var syncManager: HeartbeatSyncManager? + var syncManager: HeartbeatSyncManager? { + didSet { + cancellables.removeAll() + syncManager?.$isSyncing + .receive(on: DispatchQueue.main) + .assign(to: \.isSyncing, on: self) + .store(in: &cancellables) + } + } var currentUserId: String? var currentRoomCode: String? @@ -84,6 +92,7 @@ class HeartbeatSoundManager: NSObject, ObservableObject { @Published var isPlaying = false @Published var isRunning = false @Published var isRecording = false + @Published var isSyncing = false @Published var lastRecording: Recording? @Published var savedRecordings: [Recording] = [] @Published var savedMoments: [Moment] = [] @@ -106,6 +115,7 @@ class HeartbeatSoundManager: NSObject, ObservableObject { private let heartbeatDetector = HeartbeatDetector() private var syncTimer: Timer? + private var cancellables = Set() override init() { super.init() @@ -131,6 +141,12 @@ class HeartbeatSoundManager: NSObject, ObservableObject { // Stop any existing timer syncTimer?.invalidate() + print("🔍 Checking periodic sync requirements:") + print(" - Role: \(currentUserRole?.rawValue ?? "nil")") + print(" - Room Code: \(currentRoomCode ?? "nil")") + print(" - SyncManager: \(syncManager == nil ? "nil" : "present")") + print(" - ModelContext: \(modelContext == nil ? "nil" : "present")") + // Only start periodic sync for fathers (moms upload directly) guard currentUserRole == .father, let roomCode = currentRoomCode, diff --git a/Tiny/Core/Services/Authentication/AuthenticationService.swift b/Tiny/Core/Services/Authentication/AuthenticationService.swift index 145fca0..d4f9027 100644 --- a/Tiny/Core/Services/Authentication/AuthenticationService.swift +++ b/Tiny/Core/Services/Authentication/AuthenticationService.swift @@ -13,10 +13,13 @@ import AuthenticationServices import CryptoKit internal import Combine +// swiftlint:disable type_body_length @MainActor class AuthenticationService: ObservableObject { @Published var currentUser: User? @Published var isAuthenticated = false + @Published var isLoading = false + @Published var partnerPregnancyWeeks: Int? private let auth = Auth.auth() private let database = Firestore.firestore() @@ -34,6 +37,9 @@ class AuthenticationService: ObservableObject { } func signInWithApple(authorization: ASAuthorization) async throws { + isLoading = true + defer { isLoading = false } + guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { throw NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid Credentials"]) } @@ -92,6 +98,9 @@ class AuthenticationService: ObservableObject { } func updateUserRole(role: UserRole, pregnancyWeeks: Int? = nil, roomCode: String? = nil) async throws { + isLoading = true + defer { isLoading = false } + guard let userId = auth.currentUser?.uid else { return } @@ -111,6 +120,9 @@ class AuthenticationService: ObservableObject { } func updateUserName(name: String) async throws { + isLoading = true + defer { isLoading = false } + guard let userId = auth.currentUser?.uid else { return } @@ -120,6 +132,9 @@ class AuthenticationService: ObservableObject { } func createRoom() async throws -> String { + isLoading = true + defer { isLoading = false } + guard let userId = auth.currentUser?.uid, currentUser?.role == .mother else { throw NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Only mothers can create rooms!"]) @@ -147,8 +162,11 @@ class AuthenticationService: ObservableObject { currentUser = nil isAuthenticated = false } - + // swiftlint:disable cyclomatic_complexity func deleteAccount() async throws { + isLoading = true + defer { isLoading = false } + guard let user = auth.currentUser else { throw NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "No user is currently signed in"]) } @@ -158,94 +176,99 @@ class AuthenticationService: ObservableObject { do { print("đŸ—‘ī¸ Starting account deletion for user: \(userId)") - // 1. Delete all heartbeat recordings from Firestore and Storage - print("đŸ—‘ī¸ Deleting heartbeat recordings...") - - // Query by motherUserId (the field used in your schema) - var heartbeatsSnapshot = try await database.collection("heartbeats") - .whereField("motherUserId", isEqualTo: userId) - .getDocuments() - - print(" Found \(heartbeatsSnapshot.documents.count) heartbeats by motherUserId") - - // Also check for any heartbeats with userId field (legacy/fallback) - let legacyHeartbeats = try await database.collection("heartbeats") - .whereField("userId", isEqualTo: userId) - .getDocuments() - - if !legacyHeartbeats.documents.isEmpty { - print(" Found \(legacyHeartbeats.documents.count) heartbeats by userId (legacy)") - } - - // Combine both results - var allHeartbeatDocs = heartbeatsSnapshot.documents - allHeartbeatDocs.append(contentsOf: legacyHeartbeats.documents) - - for document in allHeartbeatDocs { - let data = document.data() + // Only delete shared data (heartbeats/moments) if user is NOT a father + if currentUser?.role != .father { + // 1. Delete all heartbeat recordings from Firestore and Storage + print("đŸ—‘ī¸ Deleting heartbeat recordings...") + + // Query by motherUserId (the field used in your schema) + var heartbeatsSnapshot = try await database.collection("heartbeats") + .whereField("motherUserId", isEqualTo: userId) + .getDocuments() + + print(" Found \(heartbeatsSnapshot.documents.count) heartbeats by motherUserId") - // Delete from Firebase Storage if audioURL exists - if let audioURL = data["firebaseStorageURL"] as? String, !audioURL.isEmpty { - do { - let storageRef = Storage.storage().reference(forURL: audioURL) - try await storageRef.delete() - print(" ✅ Deleted audio file from Storage: \(document.documentID)") - } catch { - print(" âš ī¸ Failed to delete audio file \(document.documentID): \(error.localizedDescription)") + // Also check for any heartbeats with userId field (legacy/fallback) + let legacyHeartbeats = try await database.collection("heartbeats") + .whereField("userId", isEqualTo: userId) + .getDocuments() + + if !legacyHeartbeats.documents.isEmpty { + print(" Found \(legacyHeartbeats.documents.count) heartbeats by userId (legacy)") + } + + // Combine both results + var allHeartbeatDocs = heartbeatsSnapshot.documents + allHeartbeatDocs.append(contentsOf: legacyHeartbeats.documents) + + for document in allHeartbeatDocs { + let data = document.data() + + // Delete from Firebase Storage if audioURL exists + if let audioURL = data["firebaseStorageURL"] as? String, !audioURL.isEmpty { + do { + let storageRef = Storage.storage().reference(forURL: audioURL) + try await storageRef.delete() + print(" ✅ Deleted audio file from Storage: \(document.documentID)") + } catch { + print(" âš ī¸ Failed to delete audio file \(document.documentID): \(error.localizedDescription)") + } } + + // Delete from Firestore + try await database.collection("heartbeats").document(document.documentID).delete() + print(" ✅ Deleted heartbeat document: \(document.documentID)") } - // Delete from Firestore - try await database.collection("heartbeats").document(document.documentID).delete() - print(" ✅ Deleted heartbeat document: \(document.documentID)") - } - - print("đŸ—‘ī¸ Deleted \(allHeartbeatDocs.count) heartbeat recordings") - - // 2. Delete all moments from Firestore and Storage - print("đŸ—‘ī¸ Deleting moments...") - - // Query by motherUserId (the field used in your schema) - var momentsSnapshot = try await database.collection("moments") - .whereField("motherUserId", isEqualTo: userId) - .getDocuments() - - print(" Found \(momentsSnapshot.documents.count) moments by motherUserId") - - // Also check for any moments with userId field (legacy/fallback) - let legacyMoments = try await database.collection("moments") - .whereField("userId", isEqualTo: userId) - .getDocuments() - - if !legacyMoments.documents.isEmpty { - print(" Found \(legacyMoments.documents.count) moments by userId (legacy)") - } - - // Combine both results - var allMomentDocs = momentsSnapshot.documents - allMomentDocs.append(contentsOf: legacyMoments.documents) - - for document in allMomentDocs { - let data = document.data() + print("đŸ—‘ī¸ Deleted \(allHeartbeatDocs.count) heartbeat recordings") + + // 2. Delete all moments from Firestore and Storage + print("đŸ—‘ī¸ Deleting moments...") + + // Query by motherUserId (the field used in your schema) + var momentsSnapshot = try await database.collection("moments") + .whereField("motherUserId", isEqualTo: userId) + .getDocuments() + + print(" Found \(momentsSnapshot.documents.count) moments by motherUserId") + + // Also check for any moments with userId field (legacy/fallback) + let legacyMoments = try await database.collection("moments") + .whereField("userId", isEqualTo: userId) + .getDocuments() + + if !legacyMoments.documents.isEmpty { + print(" Found \(legacyMoments.documents.count) moments by userId (legacy)") + } + + // Combine both results + var allMomentDocs = momentsSnapshot.documents + allMomentDocs.append(contentsOf: legacyMoments.documents) - // Delete from Firebase Storage if imageURL exists - if let imageURL = data["firebaseStorageURL"] as? String, !imageURL.isEmpty { - do { - let storageRef = Storage.storage().reference(forURL: imageURL) - try await storageRef.delete() - print(" ✅ Deleted moment image from Storage: \(document.documentID)") - } catch { - print(" âš ī¸ Failed to delete moment image \(document.documentID): \(error.localizedDescription)") + for document in allMomentDocs { + let data = document.data() + + // Delete from Firebase Storage if imageURL exists + if let imageURL = data["firebaseStorageURL"] as? String, !imageURL.isEmpty { + do { + let storageRef = Storage.storage().reference(forURL: imageURL) + try await storageRef.delete() + print(" ✅ Deleted moment image from Storage: \(document.documentID)") + } catch { + print(" âš ī¸ Failed to delete moment image \(document.documentID): \(error.localizedDescription)") + } } + + // Delete from Firestore + try await database.collection("moments").document(document.documentID).delete() + print(" ✅ Deleted moment document: \(document.documentID)") } - // Delete from Firestore - try await database.collection("moments").document(document.documentID).delete() - print(" ✅ Deleted moment document: \(document.documentID)") + print("đŸ—‘ī¸ Deleted \(allMomentDocs.count) moments") + } else { + print("â„šī¸ User is father, skipping deletion of shared heartbeats and moments") } - print("đŸ—‘ī¸ Deleted \(allMomentDocs.count) moments") - // 3. Handle room cleanup if let roomCode = currentUser?.roomCode { print("đŸ—‘ī¸ Cleaning up room: \(roomCode)") @@ -303,7 +326,8 @@ class AuthenticationService: ObservableObject { throw error } } - + // swiftlint:enable cyclomatic_complexity + private func fetchUserData(userId: String) { database.collection("users").document(userId).addSnapshotListener { [weak self] snapshot, error in guard let snapshot = snapshot, snapshot.exists, let data = snapshot.data() else { @@ -323,6 +347,43 @@ class AuthenticationService: ObservableObject { ) self?.currentUser = user + + // If user is a father, fetch the mother's pregnancy weeks via the room + if user.role == .father, let roomCode = user.roomCode { + self?.fetchPartnerPregnancyWeeks(roomCode: roomCode) + } + } + } + + private func fetchPartnerPregnancyWeeks(roomCode: String) { + Task { + do { + // 1. Find the room + let roomSnapshot = try await database.collection("rooms") + .whereField("code", isEqualTo: roomCode) + .limit(to: 1) + .getDocuments() + + guard let roomDoc = roomSnapshot.documents.first else { return } + let roomData = roomDoc.data() + + // 2. Get mother's ID + guard let motherUserId = roomData["motherUserId"] as? String else { return } + + // 3. Fetch mother's user doc + let motherDoc = try await database.collection("users").document(motherUserId).getDocument() + guard let motherData = motherDoc.data() else { return } + + // 4. Get pregnancy weeks + if let weeks = motherData["pregnancyWeeks"] as? Int { + await MainActor.run { + self.partnerPregnancyWeeks = weeks + print("✅ Fetched partner's pregnancy weeks: \(weeks)") + } + } + } catch { + print("❌ Error fetching partner pregnancy weeks: \(error)") + } } } @@ -357,6 +418,9 @@ class AuthenticationService: ObservableObject { } func joinRoom(roomCode: String) async throws { + isLoading = true + defer { isLoading = false } + guard let userId = auth.currentUser?.uid else { throw NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "User not authenticated"]) } @@ -391,3 +455,4 @@ class AuthenticationService: ObservableObject { } } +// swiftlint:enable type_body_length diff --git a/Tiny/Core/Services/Storage/HeartbeatSyncManager.swift b/Tiny/Core/Services/Storage/HeartbeatSyncManager.swift index bb0b8da..36207ed 100644 --- a/Tiny/Core/Services/Storage/HeartbeatSyncManager.swift +++ b/Tiny/Core/Services/Storage/HeartbeatSyncManager.swift @@ -87,6 +87,9 @@ class HeartbeatSyncManager: ObservableObject { /// Marks a heartbeat as shared so the father can see it func shareHeartbeat(_ heartbeat: SavedHeartbeat) async throws { + isSyncing = true + defer { isSyncing = false } + guard let firebaseId = heartbeat.firebaseId else { throw SyncError.notSynced } @@ -104,6 +107,9 @@ class HeartbeatSyncManager: ObservableObject { /// Updates the display name of a heartbeat in Firestore func updateHeartbeatName(_ heartbeat: SavedHeartbeat, newName: String) async throws { + isSyncing = true + defer { isSyncing = false } + guard let firebaseId = heartbeat.firebaseId else { throw SyncError.notSynced } @@ -126,6 +132,9 @@ class HeartbeatSyncManager: ObservableObject { /// Marks a heartbeat as not shared func unshareHeartbeat(_ heartbeat: SavedHeartbeat) async throws { + isSyncing = true + defer { isSyncing = false } + guard let firebaseId = heartbeat.firebaseId else { throw SyncError.notSynced } @@ -243,6 +252,9 @@ class HeartbeatSyncManager: ObservableObject { /// Deletes a heartbeat from both Storage and Firestore func deleteHeartbeat(_ heartbeat: SavedHeartbeat) async throws { + isSyncing = true + defer { isSyncing = false } + // Delete from Storage if synced if let storageURL = heartbeat.firebaseStorageURL { try await storageService.deleteHeartbeat(downloadURL: storageURL) @@ -262,6 +274,9 @@ class HeartbeatSyncManager: ObservableObject { modelContext: ModelContext, isMother: Bool = true ) async throws -> [SavedHeartbeat] { + isSyncing = true + defer { isSyncing = false } + print("🔄 Syncing heartbeats from cloud...") print(" Room Code: \(roomCode)") print(" Is Mother: \(isMother)") @@ -392,6 +407,9 @@ class HeartbeatSyncManager: ObservableObject { } func deleteMoment(_ moment: SavedMoment) async throws { + isSyncing = true + defer { isSyncing = false } + // Delete from Storage if synced if let storageURL = moment.firebaseStorageURL { try await storageService.deleteMomentImage(downloadURL: storageURL) @@ -440,6 +458,9 @@ class HeartbeatSyncManager: ObservableObject { roomCode: String, modelContext: ModelContext ) async throws -> [SavedMoment] { + isSyncing = true + defer { isSyncing = false } + print("🔄 Syncing moments from cloud...") let metadataList = try await fetchAllMomentsForRoom(roomCode: roomCode) diff --git a/Tiny/Features/Authentication/Views/SignInView.swift b/Tiny/Features/Authentication/Views/SignInView.swift index 1f8ba0e..c12489a 100644 --- a/Tiny/Features/Authentication/Views/SignInView.swift +++ b/Tiny/Features/Authentication/Views/SignInView.swift @@ -56,30 +56,45 @@ struct SignInView: View { .cornerRadius(8) .padding(.horizontal) } - SignInWithAppleButton( - onRequest: { request in - request.requestedScopes = [.email, .fullName] - request.nonce = authService.startSignInWithAppleFlow() - }, - onCompletion: { result in - switch result { - case .success(let authorization): - Task { - do { - try await authService.signInWithApple(authorization: authorization) - } catch { - errorMessage = error.localizedDescription + if authService.isLoading { + HStack(spacing: 12) { + ProgressView() + .tint(.black) + Text("Signing in...") + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(.black) + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(Color.white) + .cornerRadius(12) + .padding(.horizontal, 40) + } else { + SignInWithAppleButton( + onRequest: { request in + request.requestedScopes = [.email, .fullName] + request.nonce = authService.startSignInWithAppleFlow() + }, + onCompletion: { result in + switch result { + case .success(let authorization): + Task { + do { + try await authService.signInWithApple(authorization: authorization) + } catch { + errorMessage = error.localizedDescription + } } + case .failure(let error): + errorMessage = error.localizedDescription } - case .failure(let error): - errorMessage = error.localizedDescription } - } - ) - .signInWithAppleButtonStyle(.white) - .frame(height: 50) - .cornerRadius(12) - .padding(.horizontal, 40) + ) + .signInWithAppleButtonStyle(.white) + .frame(height: 50) + .cornerRadius(12) + .padding(.horizontal, 40) + } } } } diff --git a/Tiny/Features/LiveListen/ViewModels/HeartbeatMainViewModel.swift b/Tiny/Features/LiveListen/ViewModels/HeartbeatMainViewModel.swift index f68fc2e..e09521c 100644 --- a/Tiny/Features/LiveListen/ViewModels/HeartbeatMainViewModel.swift +++ b/Tiny/Features/LiveListen/ViewModels/HeartbeatMainViewModel.swift @@ -22,6 +22,11 @@ class HeartbeatMainViewModel: ObservableObject { roomCode: String?, userRole: UserRole? ) { + print("đŸ› ī¸ setupManager called with:") + print(" - UserId: \(userId ?? "nil")") + print(" - RoomCode: \(roomCode ?? "nil")") + print(" - UserRole: \(userRole?.rawValue ?? "nil")") + heartbeatSoundManager.modelContext = modelContext heartbeatSoundManager.syncManager = syncManager heartbeatSoundManager.currentUserId = userId diff --git a/Tiny/Features/LiveListen/Views/HeartbeatMainView.swift b/Tiny/Features/LiveListen/Views/HeartbeatMainView.swift index d25f32a..d7607bf 100644 --- a/Tiny/Features/LiveListen/Views/HeartbeatMainView.swift +++ b/Tiny/Features/LiveListen/Views/HeartbeatMainView.swift @@ -33,7 +33,7 @@ struct HeartbeatMainView: View { viewModel.allowTabViewSwipe = !disable }, isMother: isMother, - inputWeek: authService.currentUser?.pregnancyWeeks + inputWeek: isMother ? authService.currentUser?.pregnancyWeeks : authService.partnerPregnancyWeeks ) .tag(0) .transition(.asymmetric( diff --git a/Tiny/Features/Profile/Views/ProfileView.swift b/Tiny/Features/Profile/Views/ProfileView.swift index c3e28fa..c66b158 100644 --- a/Tiny/Features/Profile/Views/ProfileView.swift +++ b/Tiny/Features/Profile/Views/ProfileView.swift @@ -15,6 +15,7 @@ struct ProfileView: View { @State private var showingDeleteAccountConfirmation = false @State private var showingDeleteError = false @State private var deleteErrorMessage = "" + @State private var isDeletingAccount = false @Environment(\.modelContext) private var modelContext @EnvironmentObject var authService: AuthenticationService @@ -50,6 +51,24 @@ struct ProfileView: View { // SETTINGS LIST settingsList } + if isDeletingAccount { + Color.black.opacity(0.7) + .ignoresSafeArea() + .zIndex(100) + + VStack(spacing: 20) { + ProgressView() + .scaleEffect(1.5) + .tint(.white) + Text("Deleting Account...") + .font(.headline) + .foregroundColor(.white) + } + .frame(width: 200, height: 160) + .background(Color.black.opacity(0.8)) + .cornerRadius(20) + .zIndex(101) + } } .onAppear { // Initialize only once @@ -222,11 +241,14 @@ struct ProfileView: View { Text("This will permanently delete your account and all associated data. This action cannot be undone.") } } + .disabled(authService.isLoading) } private func deleteAccount() async { + isDeletingAccount = true + defer { isDeletingAccount = false } + do { - try await authService.deleteAccount() viewModel.manager.deleteAllData() @@ -245,10 +267,15 @@ struct ProfileView: View { viewModel.signIn() } label: { HStack { - Image(systemName: "applelogo") - .font(.system(size: 20, weight: .medium)) - Text("Sign in with Apple") - .font(.system(size: 17, weight: .semibold)) + if authService.isLoading { + ProgressView() + .tint(.black) + } else { + Image(systemName: "applelogo") + .font(.system(size: 20, weight: .medium)) + Text("Sign in with Apple") + .font(.system(size: 17, weight: .semibold)) + } } .frame(maxWidth: .infinity) .frame(height: 44) @@ -257,6 +284,7 @@ struct ProfileView: View { .cornerRadius(8) } .buttonStyle(.plain) + .disabled(authService.isLoading) } .padding(.vertical, 8) } @@ -405,7 +433,13 @@ struct ProfilePhotoDetailView: View { dismiss() } } - .disabled(tempUserName.trimmingCharacters(in: .whitespaces).isEmpty) + .disabled(tempUserName.trimmingCharacters(in: .whitespaces).isEmpty || authService.isLoading) + .opacity(authService.isLoading ? 0.5 : 1.0) + .overlay { + if authService.isLoading { + ProgressView() + } + } } } .onAppear { diff --git a/Tiny/Features/Timeline/Views/PregnancyTimelineView.swift b/Tiny/Features/Timeline/Views/PregnancyTimelineView.swift index 72cd15a..0bd7f62 100644 --- a/Tiny/Features/Timeline/Views/PregnancyTimelineView.swift +++ b/Tiny/Features/Timeline/Views/PregnancyTimelineView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import SwiftData struct PregnancyTimelineView: View { @ObservedObject var heartbeatSoundManager: HeartbeatSoundManager @@ -81,6 +80,24 @@ struct PregnancyTimelineView: View { Spacer() + if heartbeatSoundManager.isSyncing { + HStack(spacing: 6) { + ProgressView() + .scaleEffect(0.7) + .tint(.white) + Text("Syncing") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white.opacity(0.9)) + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.black.opacity(0.3)) + .clipShape(Capsule()) + .transition(.opacity) + } + + Spacer() + if selectedWeek == nil { Button { isProfilePresented = true @@ -179,63 +196,45 @@ struct PregnancyTimelineView: View { let raw = heartbeatSoundManager.savedRecordings print("📊 Grouping \(raw.count) recordings") + guard let initialPregnancyWeek = inputWeek else { + print("âš ī¸ No pregnancy week available, showing empty timeline") + groupedData = [] + return + } + let calendar = Calendar.current let now = Date() - // Try to get pregnancy start date from UserDefaults first (set by synced heartbeats) - var pregnancyStartDate: Date? - - if let storedDate = UserDefaults.standard.object(forKey: "pregnancyStartDate") as? Date { - pregnancyStartDate = calendar.startOfDay(for: storedDate) - print("📅 Using stored pregnancy start date: \(pregnancyStartDate!)") - } else if let initialPregnancyWeek = inputWeek { - // If no stored date but we have inputWeek (for mothers), calculate and store it - pregnancyStartDate = calendar.date(byAdding: .weekOfYear, value: -initialPregnancyWeek, to: now)! - pregnancyStartDate = calendar.startOfDay(for: pregnancyStartDate!) + // Store initial pregnancy week and date in UserDefaults if not already stored + if UserDefaults.standard.object(forKey: "pregnancyStartDate") == nil { + let pregnancyStartDate = calendar.date(byAdding: .weekOfYear, value: -initialPregnancyWeek, to: now)! UserDefaults.standard.set(pregnancyStartDate, forKey: "pregnancyStartDate") UserDefaults.standard.set(initialPregnancyWeek, forKey: "initialPregnancyWeek") - print("💾 Stored new pregnancy start date: \(pregnancyStartDate!)") - } else if !raw.isEmpty { - // For fathers: Try to infer from the earliest synced heartbeat's pregnancy week - if let earliestRecording = raw.min(by: { $0.createdAt < $1.createdAt }) { - // Get the pregnancy week from the heartbeat metadata if available - if let modelContext = heartbeatSoundManager.modelContext { - do { - let results = try modelContext.fetch(FetchDescriptor()) - if let savedHeartbeat = results.first(where: { $0.timestamp == earliestRecording.createdAt }), - let pregnancyWeeks = savedHeartbeat.pregnancyWeeks { - // Calculate pregnancy start date from this heartbeat - let weeksSinceStart = calendar.dateComponents([.weekOfYear], from: earliestRecording.createdAt, to: now).weekOfYear ?? 0 - let estimatedCurrentWeek = pregnancyWeeks + weeksSinceStart - pregnancyStartDate = calendar.date(byAdding: .weekOfYear, value: -estimatedCurrentWeek, to: now)! - pregnancyStartDate = calendar.startOfDay(for: pregnancyStartDate!) - UserDefaults.standard.set(pregnancyStartDate, forKey: "pregnancyStartDate") - print("💾 Inferred pregnancy start date from heartbeat: \(pregnancyStartDate!)") - } - } catch { - print("âš ī¸ Error fetching heartbeat data: \(error)") - } - } - } + print("💾 Stored pregnancy start date: \(pregnancyStartDate)") } - guard let startDate = pregnancyStartDate else { - print("âš ī¸ No pregnancy data available, showing empty timeline") + // Get the stored pregnancy start date + guard let storedDate = UserDefaults.standard.object(forKey: "pregnancyStartDate") as? Date else { + print("âš ī¸ Could not get pregnancy start date") groupedData = [] return } + // Normalize to start of day to avoid time-based drift + let pregnancyStartDate = calendar.startOfDay(for: storedDate) // Calculate CURRENT pregnancy week based on time elapsed since pregnancy start - let weeksSinceStart = calendar.dateComponents([.weekOfYear], from: startDate, to: now).weekOfYear ?? 0 + let weeksSinceStart = calendar.dateComponents([.weekOfYear], from: pregnancyStartDate, to: now).weekOfYear ?? 0 let currentPregnancyWeek = weeksSinceStart - print("📅 Pregnancy started: \(startDate)") + print("📅 Pregnancy started: \(pregnancyStartDate)") + print("📅 Initial pregnancy week: \(initialPregnancyWeek)") print("📅 Current pregnancy week (calculated): \(currentPregnancyWeek)") + print("📅 Weeks elapsed: \(currentPregnancyWeek - initialPregnancyWeek)") // Group recordings by pregnancy week let grouped = Dictionary(grouping: raw) { recording -> Int in // Calculate how many weeks since pregnancy started - let weeksSinceStart = calendar.dateComponents([.weekOfYear], from: startDate, to: recording.createdAt).weekOfYear ?? 0 + let weeksSinceStart = calendar.dateComponents([.weekOfYear], from: pregnancyStartDate, to: recording.createdAt).weekOfYear ?? 0 let pregnancyWeek = weeksSinceStart print(" Recording from \(recording.createdAt) -> Pregnancy week \(pregnancyWeek)") return pregnancyWeek diff --git a/Tiny/Resources/Localizable.xcstrings b/Tiny/Resources/Localizable.xcstrings index 05d2f51..d80c5b6 100644 --- a/Tiny/Resources/Localizable.xcstrings +++ b/Tiny/Resources/Localizable.xcstrings @@ -227,6 +227,10 @@ }, "Deleted." : { + }, + "Deleting Account..." : { + "comment" : "A message displayed while deleting an account.", + "isCommentAutoGenerated" : true }, "Detection Confidence" : { "comment" : "A section header describing the confidence of the heartbeat data analysis.", @@ -621,6 +625,10 @@ "comment" : "A section header for the signal quality trends in the heartbeat analysis tab.", "isCommentAutoGenerated" : true }, + "Signing in..." : { + "comment" : "A text that appears when signing in with Apple.", + "isCommentAutoGenerated" : true + }, "Silent" : { "comment" : "A label displayed next to the left end of a slider in the \"Noise Gate\" section of the Enhanced Live Listen view.", "isCommentAutoGenerated" : true @@ -677,6 +685,10 @@ "comment" : "A label indicating that the playback has been stopped.", "isCommentAutoGenerated" : true }, + "Syncing" : { + "comment" : "A text indicating that the app is syncing with the server.", + "isCommentAutoGenerated" : true + }, "Take Photo" : { "comment" : "The text for a button that allows the user to take a photo.", "isCommentAutoGenerated" : true @@ -798,4 +810,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file