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:
@@ -10,7 +10,7 @@ struct JoinResidenceView: View {
|
||||
@FocusState private var isCodeFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
WarmGradientBackground()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user