Add push notification deep linking and sharing subscription checks
- Add deep link navigation from push notifications to specific task column on kanban board - Fix subscription check in push notification handler to allow navigation when limitations disabled - Add pendingNavigationTaskId to handle notifications when app isn't ready - Add ScrollViewReader to AllTasksView for programmatic scrolling to task column - Add canShareResidence() and canShareContractor() subscription checks (iOS & Android) - Add test APNS file for simulator push notification testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ struct AllTasksView: View {
|
||||
|
||||
// Deep link task ID to open (from push notification)
|
||||
@State private var pendingTaskId: Int32?
|
||||
// Column index to scroll to (for deep link navigation)
|
||||
@State private var scrollToColumnIndex: Int?
|
||||
|
||||
// Use ViewModel's computed properties
|
||||
private var totalTaskCount: Int { taskViewModel.totalTaskCount }
|
||||
@@ -100,6 +102,11 @@ struct AllTasksView: View {
|
||||
.onAppear {
|
||||
PostHogAnalytics.shared.screen(AnalyticsEvents.taskScreenShown)
|
||||
|
||||
// Check for pending navigation from push notification (app launched from notification)
|
||||
if let taskId = PushNotificationManager.shared.pendingNavigationTaskId {
|
||||
pendingTaskId = Int32(taskId)
|
||||
}
|
||||
|
||||
// Check if widget completed a task - force refresh if dirty
|
||||
if WidgetDataManager.shared.areTasksDirty() {
|
||||
WidgetDataManager.shared.clearDirtyFlag()
|
||||
@@ -111,27 +118,38 @@ struct AllTasksView: View {
|
||||
}
|
||||
// Handle push notification deep links
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToTask)) { notification in
|
||||
print("📬 AllTasksView received .navigateToTask notification")
|
||||
if let userInfo = notification.userInfo,
|
||||
let taskId = userInfo["taskId"] as? Int {
|
||||
print("📬 Setting pendingTaskId to \(taskId)")
|
||||
pendingTaskId = Int32(taskId)
|
||||
// If tasks are already loaded, try to navigate immediately
|
||||
if let response = tasksResponse {
|
||||
print("📬 Tasks already loaded, attempting immediate navigation")
|
||||
navigateToTaskInKanban(taskId: Int32(taskId), response: response)
|
||||
}
|
||||
} else {
|
||||
print("📬 Failed to extract taskId from notification userInfo: \(notification.userInfo ?? [:])")
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToEditTask)) { notification in
|
||||
print("📬 AllTasksView received .navigateToEditTask notification")
|
||||
if let userInfo = notification.userInfo,
|
||||
let taskId = userInfo["taskId"] as? Int {
|
||||
print("📬 Setting pendingTaskId to \(taskId)")
|
||||
pendingTaskId = Int32(taskId)
|
||||
// If tasks are already loaded, try to navigate immediately
|
||||
if let response = tasksResponse {
|
||||
print("📬 Tasks already loaded, attempting immediate navigation")
|
||||
navigateToTaskInKanban(taskId: Int32(taskId), response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
// When tasks load and we have a pending task ID, open the edit sheet
|
||||
// When tasks load and we have a pending task ID, scroll to column and open the edit sheet
|
||||
.onChange(of: tasksResponse) { response in
|
||||
print("📬 tasksResponse changed, pendingTaskId=\(pendingTaskId?.description ?? "nil")")
|
||||
if let taskId = pendingTaskId, let response = response {
|
||||
// Find the task in all columns
|
||||
let allTasks = response.columns.flatMap { $0.tasks }
|
||||
if let task = allTasks.first(where: { $0.id == taskId }) {
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
pendingTaskId = nil
|
||||
}
|
||||
navigateToTaskInKanban(taskId: taskId, response: response)
|
||||
}
|
||||
}
|
||||
// Check dirty flag when app returns from background (widget may have completed a task)
|
||||
@@ -209,59 +227,73 @@ struct AllTasksView: View {
|
||||
}
|
||||
.padding()
|
||||
} else {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHGrid(rows: [
|
||||
GridItem(.flexible(), spacing: 16)
|
||||
], spacing: 16) {
|
||||
// Dynamically create columns from response
|
||||
ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in
|
||||
DynamicTaskColumnView(
|
||||
column: column,
|
||||
onEditTask: { task in
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: { task in
|
||||
selectedTaskForCancel = task
|
||||
showCancelConfirmation = true
|
||||
},
|
||||
onUncancelTask: { taskId in
|
||||
taskViewModel.uncancelTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onMarkInProgress: { taskId in
|
||||
taskViewModel.markInProgress(id: taskId) { success in
|
||||
if success {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHGrid(rows: [
|
||||
GridItem(.flexible(), spacing: 16)
|
||||
], spacing: 16) {
|
||||
// Dynamically create columns from response
|
||||
ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in
|
||||
DynamicTaskColumnView(
|
||||
column: column,
|
||||
onEditTask: { task in
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: { task in
|
||||
selectedTaskForCancel = task
|
||||
showCancelConfirmation = true
|
||||
},
|
||||
onUncancelTask: { taskId in
|
||||
taskViewModel.uncancelTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onMarkInProgress: { taskId in
|
||||
taskViewModel.markInProgress(id: taskId) { success in
|
||||
if success {
|
||||
loadAllTasks()
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
},
|
||||
onArchiveTask: { task in
|
||||
selectedTaskForArchive = task
|
||||
showArchiveConfirmation = true
|
||||
},
|
||||
onUnarchiveTask: { taskId in
|
||||
taskViewModel.unarchiveTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
},
|
||||
onArchiveTask: { task in
|
||||
selectedTaskForArchive = task
|
||||
showArchiveConfirmation = true
|
||||
},
|
||||
onUnarchiveTask: { taskId in
|
||||
taskViewModel.unarchiveTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
)
|
||||
.frame(width: 350)
|
||||
.id(index) // Add ID for ScrollViewReader
|
||||
.scrollTransition { content, phase in
|
||||
content
|
||||
.opacity(phase.isIdentity ? 1 : 0.8)
|
||||
.scaleEffect(phase.isIdentity ? 1 : 0.95)
|
||||
}
|
||||
)
|
||||
.frame(width: 350)
|
||||
.scrollTransition { content, phase in
|
||||
content
|
||||
.opacity(phase.isIdentity ? 1 : 0.8)
|
||||
.scaleEffect(phase.isIdentity ? 1 : 0.95)
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
.padding(16)
|
||||
}
|
||||
.scrollTargetBehavior(.viewAligned)
|
||||
.onChange(of: scrollToColumnIndex) { columnIndex in
|
||||
if let columnIndex = columnIndex {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
proxy.scrollTo(columnIndex, anchor: .leading)
|
||||
}
|
||||
// Clear after scrolling
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
scrollToColumnIndex = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
.padding(16)
|
||||
}
|
||||
.scrollTargetBehavior(.viewAligned)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,6 +342,32 @@ struct AllTasksView: View {
|
||||
private func updateTaskInKanban(_ updatedTask: TaskResponse) {
|
||||
taskViewModel.updateTaskInKanban(updatedTask)
|
||||
}
|
||||
|
||||
private func navigateToTaskInKanban(taskId: Int32, response: TaskColumnsResponse) {
|
||||
print("📬 navigateToTaskInKanban called with taskId=\(taskId)")
|
||||
|
||||
// Find which column contains the task
|
||||
for (index, column) in response.columns.enumerated() {
|
||||
if column.tasks.contains(where: { $0.id == taskId }) {
|
||||
print("📬 Found task in column \(index) '\(column.name)'")
|
||||
|
||||
// Clear pending
|
||||
pendingTaskId = nil
|
||||
PushNotificationManager.shared.clearPendingNavigation()
|
||||
|
||||
// Small delay to ensure view is ready, then scroll
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
self.scrollToColumnIndex = index
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Task not found
|
||||
print("📬 Task with id=\(taskId) not found")
|
||||
pendingTaskId = nil
|
||||
PushNotificationManager.shared.clearPendingNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to apply corner radius to specific corners
|
||||
|
||||
Reference in New Issue
Block a user