Add actionable push notifications for iOS and Android
iOS: - Add notification categories with action buttons (complete, view, cancel, etc.) - Handle notification actions in AppDelegate with API calls - Add navigation to specific task from notification tap - Register UNNotificationCategory for each task state Android: - Add NotificationActionReceiver BroadcastReceiver for handling actions - Update MyFirebaseMessagingService to show action buttons - Add deep link handling in MainActivity for task navigation - Register receiver in AndroidManifest.xml Shared: - Add navigateToTaskId parameter to App for cross-platform navigation - Add notification observers in MainTabView/AllTasksView for refresh 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -163,12 +163,103 @@ class PushNotificationManager: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when user taps the notification body (not an action button)
|
||||
func handleNotificationTap(userInfo: [AnyHashable: Any]) {
|
||||
print("📬 Handling notification tap")
|
||||
|
||||
// Mark as read
|
||||
if let notificationId = userInfo["notification_id"] as? String {
|
||||
Task {
|
||||
await markNotificationAsRead(notificationId: notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
// Check subscription status to determine navigation
|
||||
let isPremium = SubscriptionCacheWrapper.shared.currentTier == "pro" ||
|
||||
!(SubscriptionCacheWrapper.shared.currentSubscription?.limitationsEnabled ?? true)
|
||||
|
||||
if isPremium {
|
||||
// Premium user - navigate to task detail
|
||||
if let taskIdStr = userInfo["task_id"] as? String, let taskId = Int(taskIdStr) {
|
||||
navigateToTask(taskId: taskId)
|
||||
} else if let taskId = userInfo["task_id"] as? Int {
|
||||
navigateToTask(taskId: taskId)
|
||||
}
|
||||
} else {
|
||||
// Free user - navigate to home
|
||||
navigateToHome()
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when user selects an action button on the notification
|
||||
func handleNotificationAction(actionIdentifier: String, userInfo: [AnyHashable: Any]) {
|
||||
print("🔘 Handling notification action: \(actionIdentifier)")
|
||||
|
||||
// Mark as read
|
||||
if let notificationId = userInfo["notification_id"] as? String {
|
||||
Task {
|
||||
await markNotificationAsRead(notificationId: notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
// Check subscription status
|
||||
let isPremium = SubscriptionCacheWrapper.shared.currentTier == "pro" ||
|
||||
!(SubscriptionCacheWrapper.shared.currentSubscription?.limitationsEnabled ?? true)
|
||||
|
||||
guard isPremium else {
|
||||
// Free user shouldn't see actions, but if they somehow do, go to home
|
||||
navigateToHome()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract task ID
|
||||
var taskId: Int?
|
||||
if let taskIdStr = userInfo["task_id"] as? String {
|
||||
taskId = Int(taskIdStr)
|
||||
} else if let id = userInfo["task_id"] as? Int {
|
||||
taskId = id
|
||||
}
|
||||
|
||||
guard let taskId = taskId else {
|
||||
print("❌ No task_id found in notification")
|
||||
return
|
||||
}
|
||||
|
||||
// Handle action
|
||||
switch actionIdentifier {
|
||||
case NotificationActionID.viewTask.rawValue:
|
||||
navigateToTask(taskId: taskId)
|
||||
|
||||
case NotificationActionID.completeTask.rawValue:
|
||||
performCompleteTask(taskId: taskId)
|
||||
|
||||
case NotificationActionID.markInProgress.rawValue:
|
||||
performMarkInProgress(taskId: taskId)
|
||||
|
||||
case NotificationActionID.cancelTask.rawValue:
|
||||
performCancelTask(taskId: taskId)
|
||||
|
||||
case NotificationActionID.uncancelTask.rawValue:
|
||||
performUncancelTask(taskId: taskId)
|
||||
|
||||
case NotificationActionID.editTask.rawValue:
|
||||
navigateToEditTask(taskId: taskId)
|
||||
|
||||
default:
|
||||
print("⚠️ Unknown action: \(actionIdentifier)")
|
||||
navigateToTask(taskId: taskId)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleNotificationType(type: String, userInfo: [AnyHashable: Any]) {
|
||||
switch type {
|
||||
case "task_due_soon", "task_overdue", "task_completed", "task_assigned":
|
||||
if let taskId = userInfo["task_id"] as? String {
|
||||
if let taskIdStr = userInfo["task_id"] as? String, let taskId = Int(taskIdStr) {
|
||||
print("Task notification for task ID: \(taskId)")
|
||||
// TODO: Navigate to task detail
|
||||
navigateToTask(taskId: taskId)
|
||||
} else if let taskId = userInfo["task_id"] as? Int {
|
||||
print("Task notification for task ID: \(taskId)")
|
||||
navigateToTask(taskId: taskId)
|
||||
}
|
||||
|
||||
case "residence_shared":
|
||||
@@ -188,6 +279,123 @@ class PushNotificationManager: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Task Actions
|
||||
|
||||
private func performCompleteTask(taskId: Int) {
|
||||
print("✅ Completing task \(taskId) from notification action")
|
||||
Task {
|
||||
do {
|
||||
// Quick complete without photos/notes
|
||||
let request = TaskCompletionCreateRequest(
|
||||
taskId: Int32(taskId),
|
||||
completedAt: nil,
|
||||
notes: nil,
|
||||
actualCost: nil,
|
||||
rating: nil,
|
||||
imageUrls: nil
|
||||
)
|
||||
let result = try await APILayer.shared.createTaskCompletion(request: request)
|
||||
|
||||
if result is ApiResultSuccess<TaskCompletionResponse> {
|
||||
print("✅ Task \(taskId) completed successfully")
|
||||
// Post notification for UI refresh
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
||||
}
|
||||
} else if let error = result as? ApiResultError {
|
||||
print("❌ Failed to complete task: \(error.message)")
|
||||
}
|
||||
} catch {
|
||||
print("❌ Error completing task: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performMarkInProgress(taskId: Int) {
|
||||
print("🔄 Marking task \(taskId) as in progress from notification action")
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.markInProgress(taskId: Int32(taskId))
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
print("✅ Task \(taskId) marked as in progress")
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
||||
}
|
||||
} else if let error = result as? ApiResultError {
|
||||
print("❌ Failed to mark task in progress: \(error.message)")
|
||||
}
|
||||
} catch {
|
||||
print("❌ Error marking task in progress: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performCancelTask(taskId: Int) {
|
||||
print("🚫 Cancelling task \(taskId) from notification action")
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.cancelTask(taskId: Int32(taskId))
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
print("✅ Task \(taskId) cancelled")
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
||||
}
|
||||
} else if let error = result as? ApiResultError {
|
||||
print("❌ Failed to cancel task: \(error.message)")
|
||||
}
|
||||
} catch {
|
||||
print("❌ Error cancelling task: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performUncancelTask(taskId: Int) {
|
||||
print("↩️ Uncancelling task \(taskId) from notification action")
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.uncancelTask(taskId: Int32(taskId))
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
print("✅ Task \(taskId) uncancelled")
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
||||
}
|
||||
} else if let error = result as? ApiResultError {
|
||||
print("❌ Failed to uncancel task: \(error.message)")
|
||||
}
|
||||
} catch {
|
||||
print("❌ Error uncancelling task: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
private func navigateToTask(taskId: Int) {
|
||||
print("📱 Navigating to task \(taskId)")
|
||||
NotificationCenter.default.post(
|
||||
name: .navigateToTask,
|
||||
object: nil,
|
||||
userInfo: ["taskId": taskId]
|
||||
)
|
||||
}
|
||||
|
||||
private func navigateToEditTask(taskId: Int) {
|
||||
print("✏️ Navigating to edit task \(taskId)")
|
||||
NotificationCenter.default.post(
|
||||
name: .navigateToEditTask,
|
||||
object: nil,
|
||||
userInfo: ["taskId": taskId]
|
||||
)
|
||||
}
|
||||
|
||||
private func navigateToHome() {
|
||||
print("🏠 Navigating to home")
|
||||
NotificationCenter.default.post(name: .navigateToHome, object: nil)
|
||||
}
|
||||
|
||||
private func markNotificationAsRead(notificationId: String) async {
|
||||
guard TokenStorage.shared.getToken() != nil,
|
||||
let notificationIdInt = Int32(notificationId) else {
|
||||
|
||||
Reference in New Issue
Block a user