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,85 @@
package com.mycrib.shared.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class DeviceRegistrationRequest(
@SerialName("registration_id")
val registrationId: String,
val platform: String // "android" or "ios"
)
@Serializable
data class DeviceRegistrationResponse(
val id: Int,
@SerialName("registration_id")
val registrationId: String,
val platform: String,
val active: Boolean,
@SerialName("date_created")
val dateCreated: String
)
@Serializable
data class NotificationPreference(
val id: Int,
@SerialName("task_due_soon")
val taskDueSoon: Boolean = true,
@SerialName("task_overdue")
val taskOverdue: Boolean = true,
@SerialName("task_completed")
val taskCompleted: Boolean = true,
@SerialName("task_assigned")
val taskAssigned: Boolean = true,
@SerialName("residence_shared")
val residenceShared: Boolean = true,
@SerialName("warranty_expiring")
val warrantyExpiring: Boolean = true,
@SerialName("created_at")
val createdAt: String,
@SerialName("updated_at")
val updatedAt: String
)
@Serializable
data class UpdateNotificationPreferencesRequest(
@SerialName("task_due_soon")
val taskDueSoon: Boolean? = null,
@SerialName("task_overdue")
val taskOverdue: Boolean? = null,
@SerialName("task_completed")
val taskCompleted: Boolean? = null,
@SerialName("task_assigned")
val taskAssigned: Boolean? = null,
@SerialName("residence_shared")
val residenceShared: Boolean? = null,
@SerialName("warranty_expiring")
val warrantyExpiring: Boolean? = null
)
@Serializable
data class Notification(
val id: Int,
@SerialName("notification_type")
val notificationType: String,
val title: String,
val body: String,
val data: Map<String, String> = emptyMap(),
val sent: Boolean,
@SerialName("sent_at")
val sentAt: String? = null,
val read: Boolean,
@SerialName("read_at")
val readAt: String? = null,
@SerialName("created_at")
val createdAt: String,
@SerialName("task_id")
val taskId: Int? = null
)
@Serializable
data class UnreadCountResponse(
@SerialName("unread_count")
val unreadCount: Int
)

View File

@@ -0,0 +1,186 @@
package com.mycrib.shared.network
import com.mycrib.shared.models.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
private val baseUrl = ApiClient.getBaseUrl()
/**
* Register a device for push notifications
*/
suspend fun registerDevice(
token: String,
request: DeviceRegistrationRequest
): ApiResult<DeviceRegistrationResponse> {
return try {
val response = client.post("$baseUrl/notifications/devices/register/") {
header("Authorization", "Token $token")
contentType(ContentType.Application.Json)
setBody(request)
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
val errorBody = try {
response.body<Map<String, String>>()
} catch (e: Exception) {
mapOf("error" to "Device registration failed")
}
ApiResult.Error(errorBody["error"] ?: "Device registration failed", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Unregister a device
*/
suspend fun unregisterDevice(
token: String,
registrationId: String
): ApiResult<Unit> {
return try {
val response = client.post("$baseUrl/notifications/devices/unregister/") {
header("Authorization", "Token $token")
contentType(ContentType.Application.Json)
setBody(mapOf("registration_id" to registrationId))
}
if (response.status.isSuccess()) {
ApiResult.Success(Unit)
} else {
ApiResult.Error("Device unregistration failed", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Get user's notification preferences
*/
suspend fun getNotificationPreferences(token: String): ApiResult<NotificationPreference> {
return try {
val response = client.get("$baseUrl/notifications/preferences/my_preferences/") {
header("Authorization", "Token $token")
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to get preferences", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Update notification preferences
*/
suspend fun updateNotificationPreferences(
token: String,
request: UpdateNotificationPreferencesRequest
): ApiResult<NotificationPreference> {
return try {
val response = client.put("$baseUrl/notifications/preferences/update_preferences/") {
header("Authorization", "Token $token")
contentType(ContentType.Application.Json)
setBody(request)
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to update preferences", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Get notification history
*/
suspend fun getNotificationHistory(token: String): ApiResult<List<Notification>> {
return try {
val response = client.get("$baseUrl/notifications/history/") {
header("Authorization", "Token $token")
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to get notification history", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Mark a notification as read
*/
suspend fun markNotificationAsRead(
token: String,
notificationId: Int
): ApiResult<Notification> {
return try {
val response = client.post("$baseUrl/notifications/history/$notificationId/mark_as_read/") {
header("Authorization", "Token $token")
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to mark notification as read", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Mark all notifications as read
*/
suspend fun markAllNotificationsAsRead(token: String): ApiResult<Map<String, Int>> {
return try {
val response = client.post("$baseUrl/notifications/history/mark_all_as_read/") {
header("Authorization", "Token $token")
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to mark all notifications as read", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Get unread notification count
*/
suspend fun getUnreadCount(token: String): ApiResult<UnreadCountResponse> {
return try {
val response = client.get("$baseUrl/notifications/history/unread_count/") {
header("Authorization", "Token $token")
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to get unread count", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
}