Integration: residence invite accept/decline APIs + wire notification actions

Adds acceptResidenceInvite / declineResidenceInvite to ResidenceApi
(POST /api/residences/{id}/invite/{accept|decline}) and exposes them via
APILayer. On accept success, myResidences is force-refreshed so the
newly-joined residence appears without a manual pull.

Wires NotificationActionReceiver's ACCEPT_INVITE / DECLINE_INVITE
handlers to the new APILayer calls, replacing the log-only TODOs left
behind by P4 Stream O. Notifications are now cleared only on API
success so a failed accept stays actionable.

Tests:
 - ResidenceApiInviteTest covers correct HTTP method/path + error surfacing.
 - NotificationActionReceiverTest invite cases updated to assert the new
   APILayer calls (were previously asserting the log-only path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 13:36:59 -05:00
parent 10b57aabaa
commit 485f70dfa1
5 changed files with 227 additions and 28 deletions

View File

@@ -415,6 +415,32 @@ object APILayer {
return result
}
/**
* Accept a residence invite (push-notification action button).
* Refreshes myResidences on success so the new residence appears in the
* cache without requiring a manual pull-to-refresh.
*/
suspend fun acceptResidenceInvite(residenceId: Int): ApiResult<Unit> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = residenceApi.acceptResidenceInvite(token, residenceId)
if (result is ApiResult.Success) {
// Residence list may have changed — force a refresh so any
// newly-joined residence shows up in the home screen.
getMyResidences(forceRefresh = true)
}
return result
}
/**
* Decline a residence invite (push-notification action button).
* Does not require cache refresh — pending-invites are not cached
* client-side; the next app-open will re-query the server anyway.
*/
suspend fun declineResidenceInvite(residenceId: Int): ApiResult<Unit> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
return residenceApi.declineResidenceInvite(token, residenceId)
}
suspend fun getResidence(id: Int, forceRefresh: Boolean = false): ApiResult<ResidenceResponse> {
// Check DataManager caches first - return cached if valid and not forcing refresh
if (!forceRefresh) {

View File

@@ -177,6 +177,50 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
/**
* Accept a residence invite (push-notification action button).
*
* Backend: `POST /api/residences/{id}/invite/accept`. No body.
* Parity: `NotificationCategories.swift` ACCEPT_INVITE on iOS.
*/
suspend fun acceptResidenceInvite(token: String, residenceId: Int): ApiResult<Unit> {
return try {
val response = client.post("$baseUrl/residences/$residenceId/invite/accept/") {
header("Authorization", "Token $token")
}
if (response.status.isSuccess()) {
ApiResult.Success(Unit)
} else {
val errorMessage = ErrorParser.parseError(response)
ApiResult.Error(errorMessage, response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Decline a residence invite. See [acceptResidenceInvite] for endpoint
* pattern. Backend: `POST /api/residences/{id}/invite/decline`.
*/
suspend fun declineResidenceInvite(token: String, residenceId: Int): ApiResult<Unit> {
return try {
val response = client.post("$baseUrl/residences/$residenceId/invite/decline/") {
header("Authorization", "Token $token")
}
if (response.status.isSuccess()) {
ApiResult.Success(Unit)
} else {
val errorMessage = ErrorParser.parseError(response)
ApiResult.Error(errorMessage, response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
suspend fun joinWithCode(token: String, code: String): ApiResult<JoinResidenceResponse> {
return try {
val response = client.post("$baseUrl/residences/join-with-code/") {