Add push notification support for Android and iOS
- Integrated Firebase Cloud Messaging (FCM) for Android - Integrated Apple Push Notification Service (APNs) for iOS - Created shared notification models and API client - Added device registration and token management - Added notification permission handling for Android - Created PushNotificationManager for iOS with AppDelegate - Added placeholder google-services.json (needs to be replaced with actual config) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
76
iosApp/iosApp/PushNotifications/AppDelegate.swift
Normal file
76
iosApp/iosApp/PushNotifications/AppDelegate.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import ComposeApp
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
// Set notification delegate
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
// Request notification permission
|
||||
Task { @MainActor in
|
||||
await PushNotificationManager.shared.requestNotificationPermission()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Remote Notifications
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
||||
) {
|
||||
Task { @MainActor in
|
||||
PushNotificationManager.shared.didRegisterForRemoteNotifications(withDeviceToken: deviceToken)
|
||||
}
|
||||
}
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFailToRegisterForRemoteNotificationsWithError error: Error
|
||||
) {
|
||||
Task { @MainActor in
|
||||
PushNotificationManager.shared.didFailToRegisterForRemoteNotifications(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
|
||||
// Called when notification is received while app is in foreground
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
print("📬 Notification received in foreground: \(userInfo)")
|
||||
|
||||
Task { @MainActor in
|
||||
PushNotificationManager.shared.handleNotification(userInfo: userInfo)
|
||||
}
|
||||
|
||||
// Show notification even when app is in foreground
|
||||
completionHandler([.banner, .sound, .badge])
|
||||
}
|
||||
|
||||
// Called when user taps on notification
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
print("👆 User tapped notification: \(userInfo)")
|
||||
|
||||
Task { @MainActor in
|
||||
PushNotificationManager.shared.handleNotification(userInfo: userInfo)
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
206
iosApp/iosApp/PushNotifications/PushNotificationManager.swift
Normal file
206
iosApp/iosApp/PushNotifications/PushNotificationManager.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import ComposeApp
|
||||
|
||||
@MainActor
|
||||
class PushNotificationManager: NSObject, ObservableObject {
|
||||
static let shared = PushNotificationManager()
|
||||
|
||||
@Published var deviceToken: String?
|
||||
@Published var notificationPermissionGranted = false
|
||||
|
||||
private let notificationApi = NotificationApi()
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Permission Request
|
||||
|
||||
func requestNotificationPermission() async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
|
||||
do {
|
||||
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
notificationPermissionGranted = granted
|
||||
|
||||
if granted {
|
||||
print("✅ Notification permission granted")
|
||||
// Register for remote notifications on main thread
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
} else {
|
||||
print("❌ Notification permission denied")
|
||||
}
|
||||
|
||||
return granted
|
||||
} catch {
|
||||
print("❌ Error requesting notification permission: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Token Management
|
||||
|
||||
func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) {
|
||||
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
|
||||
self.deviceToken = tokenString
|
||||
print("📱 APNs device token: \(tokenString)")
|
||||
|
||||
// Register with backend
|
||||
Task {
|
||||
await registerDeviceWithBackend(token: tokenString)
|
||||
}
|
||||
}
|
||||
|
||||
func didFailToRegisterForRemoteNotifications(withError error: Error) {
|
||||
print("❌ Failed to register for remote notifications: \(error)")
|
||||
}
|
||||
|
||||
// MARK: - Backend Registration
|
||||
|
||||
private func registerDeviceWithBackend(token: String) async {
|
||||
guard let authToken = TokenStorage.shared.getToken() else {
|
||||
print("⚠️ No auth token available, will register device after login")
|
||||
return
|
||||
}
|
||||
|
||||
let request = DeviceRegistrationRequest(
|
||||
registrationId: token,
|
||||
platform: "ios"
|
||||
)
|
||||
|
||||
let result = await notificationApi.registerDevice(token: authToken, request: request)
|
||||
|
||||
switch result {
|
||||
case let success as ApiResultSuccess<DeviceRegistrationResponse>:
|
||||
print("✅ Device registered successfully: \(success.data)")
|
||||
case let error as ApiResultError:
|
||||
print("❌ Failed to register device: \(error.message)")
|
||||
default:
|
||||
print("⚠️ Unexpected result type from device registration")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handle Notifications
|
||||
|
||||
func handleNotification(userInfo: [AnyHashable: Any]) {
|
||||
print("📬 Received notification: \(userInfo)")
|
||||
|
||||
// Extract notification data
|
||||
if let notificationId = userInfo["notification_id"] as? String {
|
||||
print("Notification ID: \(notificationId)")
|
||||
|
||||
// Mark as read when user taps notification
|
||||
Task {
|
||||
await markNotificationAsRead(notificationId: notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
if let type = userInfo["type"] as? String {
|
||||
print("Notification type: \(type)")
|
||||
handleNotificationType(type: type, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
print("Task notification for task ID: \(taskId)")
|
||||
// TODO: Navigate to task detail
|
||||
}
|
||||
|
||||
case "residence_shared":
|
||||
if let residenceId = userInfo["residence_id"] as? String {
|
||||
print("Residence shared notification for residence ID: \(residenceId)")
|
||||
// TODO: Navigate to residence detail
|
||||
}
|
||||
|
||||
case "warranty_expiring":
|
||||
if let documentId = userInfo["document_id"] as? String {
|
||||
print("Warranty expiring notification for document ID: \(documentId)")
|
||||
// TODO: Navigate to document detail
|
||||
}
|
||||
|
||||
default:
|
||||
print("Unknown notification type: \(type)")
|
||||
}
|
||||
}
|
||||
|
||||
private func markNotificationAsRead(notificationId: String) async {
|
||||
guard let authToken = TokenStorage.shared.getToken(),
|
||||
let notificationIdInt = Int32(notificationId) else {
|
||||
return
|
||||
}
|
||||
|
||||
let result = await notificationApi.markNotificationAsRead(
|
||||
token: authToken,
|
||||
notificationId: notificationIdInt
|
||||
)
|
||||
|
||||
switch result {
|
||||
case is ApiResultSuccess<Notification>:
|
||||
print("✅ Notification marked as read")
|
||||
case let error as ApiResultError:
|
||||
print("❌ Failed to mark notification as read: \(error.message)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Preferences
|
||||
|
||||
func updateNotificationPreferences(_ preferences: UpdateNotificationPreferencesRequest) async -> Bool {
|
||||
guard let authToken = TokenStorage.shared.getToken() else {
|
||||
print("⚠️ No auth token available")
|
||||
return false
|
||||
}
|
||||
|
||||
let result = await notificationApi.updateNotificationPreferences(
|
||||
token: authToken,
|
||||
request: preferences
|
||||
)
|
||||
|
||||
switch result {
|
||||
case is ApiResultSuccess<NotificationPreference>:
|
||||
print("✅ Notification preferences updated")
|
||||
return true
|
||||
case let error as ApiResultError:
|
||||
print("❌ Failed to update preferences: \(error.message)")
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func getNotificationPreferences() async -> NotificationPreference? {
|
||||
guard let authToken = TokenStorage.shared.getToken() else {
|
||||
print("⚠️ No auth token available")
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = await notificationApi.getNotificationPreferences(token: authToken)
|
||||
|
||||
switch result {
|
||||
case let success as ApiResultSuccess<NotificationPreference>:
|
||||
return success.data
|
||||
case let error as ApiResultError:
|
||||
print("❌ Failed to get preferences: \(error.message)")
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Badge Management
|
||||
|
||||
func clearBadge() {
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
}
|
||||
|
||||
func setBadge(count: Int) {
|
||||
UIApplication.shared.applicationIconBadgeNumber = count
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user