Add task completion animations, subscription trials, and quiet debug console

- Completion animations: play user-selected animation on task card after completing,
  with DataManager guard to prevent race condition during animation playback.
  Works in both AllTasksView and ResidenceDetailView. Animation preference
  persisted via @AppStorage and configurable from Settings.
- Subscription: add trial fields (trialStart, trialEnd, trialActive) and
  subscriptionSource to model, cross-platform purchase guard, trial banner
  in upgrade prompt, and platform-aware subscription management in profile.
- Analytics: disable PostHog SDK debug logging and remove console print
  statements to reduce debug console noise.
- Documents: remove redundant nested do-catch blocks in ViewModel wrapper.
- Widgets: add debounced timeline reloads and thread-safe file I/O queue.
- Onboarding: fix animation leak on disappear, remove unused state vars.
- Remove unused files (ContentView, StateFlowExtensions, CustomView).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-05 11:35:08 -06:00
parent c5f2bee83f
commit 98dbacdea0
73 changed files with 1770 additions and 529 deletions

View File

@@ -10,7 +10,7 @@ struct JoinResidenceView: View {
@FocusState private var isCodeFocused: Bool
var body: some View {
NavigationView {
NavigationStack {
ZStack {
WarmGradientBackground()

View File

@@ -16,10 +16,10 @@ struct ManageUsersView: View {
@State private var errorMessage: String?
@State private var isGeneratingCode = false
@State private var shareFileURL: URL?
@StateObject private var sharingManager = ResidenceSharingManager.shared
@ObservedObject private var sharingManager = ResidenceSharingManager.shared
var body: some View {
NavigationView {
NavigationStack {
ZStack {
WarmGradientBackground()

View File

@@ -28,6 +28,13 @@ struct ResidenceDetailView: View {
@State private var selectedTaskForCancel: TaskResponse?
@State private var showCancelConfirmation = false
// Completion animation state
@StateObject private var animationPreference = AnimationPreference.shared
@State private var animatingTaskId: Int32? = nil
@State private var animationPhase: AnimationPhase = .idle
@State private var pendingCompletedTask: TaskResponse? = nil
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var hasAppeared = false
@State private var showReportAlert = false
@State private var showReportConfirmation = false
@@ -105,14 +112,17 @@ struct ResidenceDetailView: View {
EditTaskView(task: task, isPresented: $showEditTask)
}
}
.sheet(item: $selectedTaskForComplete) { task in
.sheet(item: $selectedTaskForComplete, onDismiss: {
if let task = pendingCompletedTask {
startCompletionAnimation(for: task)
} else {
taskViewModel.isAnimatingCompletion = false
loadResidenceTasks(forceRefresh: true)
}
}) { task in
CompleteTaskView(task: task) { updatedTask in
print("DEBUG: onComplete callback called")
print("DEBUG: updatedTask is nil: \(updatedTask == nil)")
if let updatedTask = updatedTask {
print("DEBUG: updatedTask.id = \(updatedTask.id)")
print("DEBUG: updatedTask.kanbanColumn = \(updatedTask.kanbanColumn ?? "nil")")
updateTaskInKanban(updatedTask)
pendingCompletedTask = updatedTask
}
selectedTaskForComplete = nil
}
@@ -248,6 +258,9 @@ private extension ResidenceDetailView {
showArchiveConfirmation: $showArchiveConfirmation,
selectedTaskForCancel: $selectedTaskForCancel,
showCancelConfirmation: $showCancelConfirmation,
animatingTaskId: animatingTaskId,
animationPhase: animationPhase,
animationType: animationPreference.selectedAnimation,
reloadTasks: { loadResidenceTasks(forceRefresh: true) }
)
} else if isLoadingTasks {
@@ -422,6 +435,37 @@ private extension ResidenceDetailView {
taskViewModel.updateTaskInKanban(updatedTask)
}
func startCompletionAnimation(for updatedTask: TaskResponse) {
let duration = animationPreference.animationDuration(reduceMotion: reduceMotion)
guard duration > 0 else {
taskViewModel.isAnimatingCompletion = false
updateTaskInKanban(updatedTask)
pendingCompletedTask = nil
return
}
animatingTaskId = updatedTask.id
withAnimation {
animationPhase = .exiting
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation {
animationPhase = .complete
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
taskViewModel.isAnimatingCompletion = false
updateTaskInKanban(updatedTask)
animatingTaskId = nil
animationPhase = .idle
pendingCompletedTask = nil
}
}
func deleteResidence() {
guard TokenStorage.shared.getToken() != nil else { return }
@@ -500,6 +544,11 @@ private struct TasksSectionContainer: View {
@Binding var selectedTaskForCancel: TaskResponse?
@Binding var showCancelConfirmation: Bool
// Completion animation state
var animatingTaskId: Int32? = nil
var animationPhase: AnimationPhase = .idle
var animationType: TaskAnimationType = .none
let reloadTasks: () -> Void
var body: some View {
@@ -526,6 +575,7 @@ private struct TasksSectionContainer: View {
}
},
onCompleteTask: { task in
taskViewModel.isAnimatingCompletion = true
selectedTaskForComplete = task
},
onArchiveTask: { task in
@@ -536,7 +586,10 @@ private struct TasksSectionContainer: View {
taskViewModel.unarchiveTask(id: taskId) { _ in
reloadTasks()
}
}
},
animatingTaskId: animatingTaskId,
animationPhase: animationPhase,
animationType: animationType
)
}
}

View File

@@ -66,7 +66,9 @@ class ResidenceSharingManager: ObservableObject {
let jsonContent = CaseraShareCodec.shared.encodeSharedResidence(sharedResidence: sharedResidence)
guard let jsonData = jsonContent.data(using: .utf8) else {
#if DEBUG
print("ResidenceSharingManager: Failed to encode residence package as UTF-8")
#endif
errorMessage = "Failed to create share file"
return nil
}
@@ -80,7 +82,9 @@ class ResidenceSharingManager: ObservableObject {
AnalyticsManager.shared.track(.residenceShared(method: "file"))
return tempURL
} catch {
#if DEBUG
print("ResidenceSharingManager: Failed to write .casera file: \(error)")
#endif
errorMessage = "Failed to save share file"
return nil
}

View File

@@ -197,35 +197,27 @@ class ResidenceViewModel: ObservableObject {
Task {
do {
print("🏠 ResidenceVM: Calling API...")
let result = try await APILayer.shared.createResidence(request: request)
print("🏠 ResidenceVM: Got result: \(String(describing: result))")
await MainActor.run {
if let success = result as? ApiResultSuccess<ResidenceResponse> {
print("🏠 ResidenceVM: Is ApiResultSuccess, data = \(String(describing: success.data))")
if let residence = success.data {
print("🏠 ResidenceVM: Got residence with id \(residence.id)")
self.isLoading = false
completion(residence)
} else {
print("🏠 ResidenceVM: success.data is nil")
self.isLoading = false
completion(nil)
}
} else if let error = ApiResultBridge.error(from: result) {
print("🏠 ResidenceVM: Is ApiResultError: \(error.message ?? "nil")")
self.errorMessage = ErrorMessageParser.parse(error.message)
self.isLoading = false
completion(nil)
} else {
print("🏠 ResidenceVM: Unknown result type: \(type(of: result))")
self.isLoading = false
completion(nil)
}
}
} catch {
print("🏠 ResidenceVM: Exception: \(error)")
await MainActor.run {
self.errorMessage = ErrorMessageParser.parse(error.localizedDescription)
self.isLoading = false