Skip to content

Commit e3d72a5

Browse files
abdulraqeeb33fadi-georgeAR Abdul Azeezsherwinski
authored
fix: [SDK-4388] subscription state permanently stuck at "Never Subscribed" when login() is called before requestPermission() (#2627)
Co-authored-by: Fadi George <fadii925@gmail.com> Co-authored-by: AR Abdul Azeez <abdul@onesignal.com> Co-authored-by: sherwinski <sherwin@onesignal.com>
1 parent 547027f commit e3d72a5

2 files changed

Lines changed: 444 additions & 13 deletions

File tree

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ internal class SubscriptionOperationExecutor(
5757
val startingOp = operations.first()
5858

5959
return if (startingOp is CreateSubscriptionOperation) {
60-
createSubscription(startingOp, operations)
60+
// If the subscription already exists on the backend (non-local id), POSTing
61+
// to /subscriptions with that id is silently a server-side no-op and drops
62+
// the merged enabled/status payload. Dispatch as an update instead.
63+
if (!IDManager.isLocalId(startingOp.subscriptionId)) {
64+
updateExistingSubscriptionFromCreate(startingOp, operations)
65+
} else {
66+
createSubscription(startingOp, operations)
67+
}
6168
} else if (operations.any { it is DeleteSubscriptionOperation }) {
6269
if (operations.size > 1) {
6370
throw Exception("Only supports one operation! Attempted operations:\n$operations")
@@ -76,6 +83,30 @@ internal class SubscriptionOperationExecutor(
7683
}
7784
}
7885

86+
private suspend fun updateExistingSubscriptionFromCreate(
87+
createOperation: CreateSubscriptionOperation,
88+
operations: List<Operation>,
89+
): ExecutionResponse {
90+
// if there are any deletes all operations should be tossed, nothing to do.
91+
if (operations.any { it is DeleteSubscriptionOperation }) {
92+
return ExecutionResponse(ExecutionResult.SUCCESS)
93+
}
94+
95+
val lastUpdateOperation = operations.lastOrNull { it is UpdateSubscriptionOperation } as UpdateSubscriptionOperation?
96+
val patchOp =
97+
UpdateSubscriptionOperation(
98+
createOperation.appId,
99+
createOperation.onesignalId,
100+
createOperation.externalId,
101+
createOperation.subscriptionId,
102+
createOperation.type,
103+
lastUpdateOperation?.enabled ?: createOperation.enabled,
104+
lastUpdateOperation?.address ?: createOperation.address,
105+
lastUpdateOperation?.status ?: createOperation.status,
106+
)
107+
return updateSubscription(patchOp, listOf(patchOp))
108+
}
109+
79110
private suspend fun createSubscription(
80111
createOperation: CreateSubscriptionOperation,
81112
operations: List<Operation>,
@@ -91,12 +122,11 @@ internal class SubscriptionOperationExecutor(
91122
val enabled = lastUpdateOperation?.enabled ?: createOperation.enabled
92123
val address = lastUpdateOperation?.address ?: createOperation.address
93124
val status = lastUpdateOperation?.status ?: createOperation.status
94-
val subId = if (!IDManager.isLocalId(createOperation.subscriptionId)) createOperation.subscriptionId else null
95125

96126
try {
97127
val subscription =
98128
SubscriptionObject(
99-
id = subId,
129+
id = null,
100130
convert(createOperation.type),
101131
address,
102132
enabled,
@@ -220,7 +250,30 @@ internal class SubscriptionOperationExecutor(
220250
) {
221251
return ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds)
222252
}
223-
// toss this, but create an identical CreateSubscriptionOperation to re-create the subscription being updated.
253+
// The original update target no longer exists on the backend, so issue
254+
// a fresh local-id Create. A non-local id here would re-route through
255+
// updateExistingSubscriptionFromCreate -> PATCH -> 404 indefinitely because
256+
// execute() dispatches non-local-id Creates as PATCHes; only a local id
257+
// forces the POST/rebuild-user recovery path that can actually re-create
258+
// the subscription.
259+
val recoveryLocalId = IDManager.createLocalId()
260+
val staleSubscriptionId = lastOperation.subscriptionId
261+
262+
// Rewrite the cached SubscriptionModel id (and pushSubscriptionId, if it
263+
// pointed at the stale id) to the new local id. This keeps the recovery
264+
// Create and the rebuild-user path's getRebuildOperationsIfCurrentUser
265+
// emitting Creates with the same subscriptionId, so they dedupe instead
266+
// of producing two POST /users subscription rows. HYDRATE prevents the
267+
// SubscriptionModelStoreListener from enqueuing follow-on operations.
268+
_subscriptionModelStore.get(staleSubscriptionId)?.setStringProperty(
269+
SubscriptionModel::id.name,
270+
recoveryLocalId,
271+
ModelChangeTags.HYDRATE,
272+
)
273+
if (_configModelStore.model.pushSubscriptionId == staleSubscriptionId) {
274+
_configModelStore.model.pushSubscriptionId = recoveryLocalId
275+
}
276+
224277
ExecutionResponse(
225278
ExecutionResult.FAIL_NORETRY,
226279
operations =
@@ -229,7 +282,7 @@ internal class SubscriptionOperationExecutor(
229282
lastOperation.appId,
230283
lastOperation.onesignalId,
231284
lastOperation.externalId,
232-
lastOperation.subscriptionId,
285+
recoveryLocalId,
233286
lastOperation.type,
234287
lastOperation.enabled,
235288
lastOperation.address,

0 commit comments

Comments
 (0)