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:
Trey t
2025-11-11 22:39:39 -06:00
parent 1a4b5d07bf
commit ec7c01e92d
14 changed files with 919 additions and 1 deletions

View File

@@ -0,0 +1,148 @@
package com.example.mycrib
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "New FCM token: $token")
// Store token locally for registration with backend
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putString(KEY_FCM_TOKEN, token)
.apply()
// Send token to backend API if user is logged in
// Note: In a real app, you might want to use WorkManager for reliable delivery
CoroutineScope(Dispatchers.IO).launch {
try {
val authToken = com.mycrib.storage.TokenStorage.getToken()
if (authToken != null) {
val notificationApi = com.mycrib.shared.network.NotificationApi()
val request = com.mycrib.shared.models.DeviceRegistrationRequest(
registrationId = token,
platform = "android"
)
when (val result = notificationApi.registerDevice(authToken, request)) {
is com.mycrib.shared.network.ApiResult.Success -> {
Log.d(TAG, "Device registered successfully with new token")
}
is com.mycrib.shared.network.ApiResult.Error -> {
Log.e(TAG, "Failed to register device with new token: ${result.message}")
}
is com.mycrib.shared.network.ApiResult.Loading,
is com.mycrib.shared.network.ApiResult.Idle -> {
// These states shouldn't occur for direct API calls
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error registering device with new token", e)
}
}
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
Log.d(TAG, "Message received from: ${message.from}")
// Check if message contains notification payload
message.notification?.let { notification ->
Log.d(TAG, "Notification: ${notification.title} - ${notification.body}")
sendNotification(
notification.title ?: "MyCrib",
notification.body ?: "",
message.data
)
}
// Check if message contains data payload
if (message.data.isNotEmpty()) {
Log.d(TAG, "Message data: ${message.data}")
// If there's no notification payload, create one from data
if (message.notification == null) {
val title = message.data["title"] ?: "MyCrib"
val body = message.data["body"] ?: ""
sendNotification(title, body, message.data)
}
}
}
private fun sendNotification(title: String, body: String, data: Map<String, String>) {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// Add data to intent for handling when notification is clicked
data.forEach { (key, value) ->
putExtra(key, value)
}
}
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_ONE_SHOT
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
pendingIntentFlags
)
val channelId = getString(R.string.default_notification_channel_id)
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Create notification channel for Android O and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"MyCrib Notifications",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications for tasks, residences, and warranties"
}
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
companion object {
private const val TAG = "FCMService"
private const val NOTIFICATION_ID = 0
private const val PREFS_NAME = "mycrib_prefs"
private const val KEY_FCM_TOKEN = "fcm_token"
fun getStoredToken(context: Context): String? {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString(KEY_FCM_TOKEN, null)
}
}
}