Skip to content

Commit 1a43567

Browse files
author
Rodrigo Gomez Palacio
committed
Update IAM manager & backend service with retry logic, optional headers
Motivation: the IAM fetch call (`listInAppMessages`) will include the rywToken, retryCount, & secondsSinceAppOpen (tracked on backend) We update the request & related code here. Handle retry logic
1 parent be10255 commit 1a43567

File tree

5 files changed

+612
-452
lines changed

5 files changed

+612
-452
lines changed

OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import android.app.AlertDialog
44
import com.onesignal.common.AndroidUtils
55
import com.onesignal.common.IDManager
66
import com.onesignal.common.JSONUtils
7+
import com.onesignal.common.consistency.IamFetchReadyCondition
8+
import com.onesignal.common.consistency.models.IConsistencyManager
79
import com.onesignal.common.events.EventProducer
810
import com.onesignal.common.exceptions.BackendException
911
import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler
@@ -66,6 +68,7 @@ internal class InAppMessagesManager(
6668
private val _lifecycle: IInAppLifecycleService,
6769
private val _languageContext: ILanguageContext,
6870
private val _time: ITime,
71+
private val _consistencyManager: IConsistencyManager,
6972
) : IInAppMessagesManager,
7073
IStartableService,
7174
ISubscriptionChangedHandler,
@@ -149,7 +152,13 @@ internal class InAppMessagesManager(
149152
}
150153

151154
// attempt to fetch messages from the backend (if we have the pre-requisite data already)
152-
fetchMessages()
155+
val onesignalId = _userManager.onesignalId
156+
val updateConditionDeferred =
157+
_consistencyManager.registerCondition(IamFetchReadyCondition(onesignalId))
158+
val rywToken = updateConditionDeferred.await()
159+
if (rywToken != null) {
160+
fetchMessages(rywToken)
161+
}
153162
}
154163
}
155164

@@ -181,18 +190,14 @@ internal class InAppMessagesManager(
181190
return
182191
}
183192

184-
suspendifyOnThread {
185-
fetchMessages()
186-
}
193+
fetchMessagesWhenConditionIsMet()
187194
}
188195

189196
override fun onModelReplaced(
190197
model: ConfigModel,
191198
tag: String,
192199
) {
193-
suspendifyOnThread {
194-
fetchMessages()
195-
}
200+
fetchMessagesWhenConditionIsMet()
196201
}
197202

198203
override fun onSubscriptionAdded(subscription: ISubscription) { }
@@ -207,27 +212,36 @@ internal class InAppMessagesManager(
207212
return
208213
}
209214

210-
suspendifyOnThread {
211-
fetchMessages()
212-
}
215+
fetchMessagesWhenConditionIsMet()
213216
}
214217

215218
override fun onSessionStarted() {
216219
for (redisplayInAppMessage in redisplayedInAppMessages) {
217220
redisplayInAppMessage.isDisplayedInSession = false
218221
}
219222

220-
suspendifyOnThread {
221-
fetchMessages()
222-
}
223+
fetchMessagesWhenConditionIsMet()
223224
}
224225

225226
override fun onSessionActive() { }
226227

227228
override fun onSessionEnded(duration: Long) { }
228229

230+
private fun fetchMessagesWhenConditionIsMet() {
231+
suspendifyOnThread {
232+
val onesignalId = _userManager.onesignalId
233+
val iamFetchCondition =
234+
_consistencyManager.registerCondition(IamFetchReadyCondition(onesignalId))
235+
val rywToken = iamFetchCondition.await()
236+
237+
if (rywToken != null) {
238+
fetchMessages(rywToken)
239+
}
240+
}
241+
}
242+
229243
// called when a new push subscription is added, or the app id is updated, or a new session starts
230-
private suspend fun fetchMessages() {
244+
private suspend fun fetchMessages(rywToken: String?) {
231245
// We only want to fetch IAMs if we know the app is in the
232246
// foreground, as we don't want to do this for background
233247
// events (such as push received), wasting resources for
@@ -252,7 +266,9 @@ internal class InAppMessagesManager(
252266
lastTimeFetchedIAMs = now
253267
}
254268

255-
val newMessages = _backend.listInAppMessages(appId, subscriptionId)
269+
// lambda so that it is updated on each potential retry
270+
val sessionDurationProvider = { _time.currentTimeMillis - _sessionService.startTime }
271+
val newMessages = _backend.listInAppMessages(appId, subscriptionId, rywToken, sessionDurationProvider)
256272

257273
if (newMessages != null) {
258274
this.messages = newMessages as MutableList<InAppMessage>
@@ -517,7 +533,9 @@ internal class InAppMessagesManager(
517533
if (triggerModel != null) {
518534
triggerModel.value = value
519535
} else {
520-
triggerModel = com.onesignal.inAppMessages.internal.triggers.TriggerModel()
536+
triggerModel =
537+
com.onesignal.inAppMessages.internal.triggers
538+
.TriggerModel()
521539
triggerModel.id = key
522540
triggerModel.key = key
523541
triggerModel.value = value
@@ -782,13 +800,15 @@ internal class InAppMessagesManager(
782800
private fun logInAppMessagePreviewActions(action: InAppMessageClickResult) {
783801
if (action.tags != null) {
784802
Logging.debug(
785-
"InAppMessagesManager.logInAppMessagePreviewActions: Tags detected inside of the action click payload, ignoring because action came from IAM preview:: " + action.tags.toString(),
803+
"InAppMessagesManager.logInAppMessagePreviewActions: Tags detected inside of the action click payload, ignoring because action came from IAM preview:: " +
804+
action.tags.toString(),
786805
)
787806
}
788807

789808
if (action.outcomes.size > 0) {
790809
Logging.debug(
791-
"InAppMessagesManager.logInAppMessagePreviewActions: Outcomes detected inside of the action click payload, ignoring because action came from IAM preview: " + action.outcomes.toString(),
810+
"InAppMessagesManager.logInAppMessagePreviewActions: Outcomes detected inside of the action click payload, ignoring because action came from IAM preview: " +
811+
action.outcomes.toString(),
792812
)
793813
}
794814

@@ -890,7 +910,8 @@ internal class InAppMessagesManager(
890910
) {
891911
val messageTitle = _applicationService.appContext.getString(R.string.location_permission_missing_title)
892912
val message = _applicationService.appContext.getString(R.string.location_permission_missing_message)
893-
AlertDialog.Builder(_applicationService.current)
913+
AlertDialog
914+
.Builder(_applicationService.current)
894915
.setTitle(messageTitle)
895916
.setMessage(message)
896917
.setPositiveButton(android.R.string.ok) { _, _ -> suspendifyOnThread { showMultiplePrompts(inAppMessage, prompts) } }

OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/IInAppBackendService.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ internal interface IInAppBackendService {
1313
*
1414
* @param appId The ID of the application that the IAM will be retrieved from.
1515
* @param subscriptionId The specific subscription within the [appId] the IAM will be delivered to.
16+
* @param rywToken Used for read your write consistency
17+
* @param sessionDurationProvider Lambda to calculate the session duration at the time of the request
1618
*
1719
* @return The list of IAMs associated to the subscription, or null if the IAMs could not be retrieved.
1820
*/
1921
suspend fun listInAppMessages(
2022
appId: String,
2123
subscriptionId: String,
24+
rywToken: String?,
25+
sessionDurationProvider: () -> Long,
2226
): List<InAppMessage>?
2327

2428
/**

OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import com.onesignal.common.NetworkUtils
44
import com.onesignal.common.exceptions.BackendException
55
import com.onesignal.core.internal.device.IDeviceService
66
import com.onesignal.core.internal.http.IHttpClient
7+
import com.onesignal.core.internal.http.impl.OptionalHeaders
78
import com.onesignal.debug.internal.logging.Logging
89
import com.onesignal.inAppMessages.internal.InAppMessage
910
import com.onesignal.inAppMessages.internal.InAppMessageContent
1011
import com.onesignal.inAppMessages.internal.backend.GetIAMDataResponse
1112
import com.onesignal.inAppMessages.internal.backend.IInAppBackendService
1213
import com.onesignal.inAppMessages.internal.hydrators.InAppHydrator
14+
import kotlinx.coroutines.delay
1315
import org.json.JSONObject
1416

1517
internal class InAppBackendService(
@@ -22,24 +24,11 @@ internal class InAppBackendService(
2224
override suspend fun listInAppMessages(
2325
appId: String,
2426
subscriptionId: String,
27+
rywToken: String?,
28+
sessionDurationProvider: () -> Long,
2529
): List<InAppMessage>? {
26-
// Retrieve any in app messages that might exist
27-
val response = _httpClient.get("apps/$appId/subscriptions/$subscriptionId/iams")
28-
29-
if (response.isSuccess) {
30-
val jsonResponse = JSONObject(response.payload)
31-
32-
if (jsonResponse.has("in_app_messages")) {
33-
val iamMessagesAsJSON = jsonResponse.getJSONArray("in_app_messages")
34-
// TODO: Outstanding question on whether we still want to cache this. Only used when
35-
// hard start of the app, but within 30 seconds of it being killed (i.e. same session startup).
36-
// Cache copy for quick cold starts
37-
// _prefs.savedIAMs = iamMessagesAsJSON.toString()
38-
return _hydrator.hydrateIAMMessages(iamMessagesAsJSON)
39-
}
40-
}
41-
42-
return null
30+
val baseUrl = "apps/$appId/subscriptions/$subscriptionId/iams"
31+
return attemptFetchWithRetries(baseUrl, rywToken, sessionDurationProvider)
4332
}
4433

4534
override suspend fun getIAMData(
@@ -51,7 +40,7 @@ internal class InAppBackendService(
5140
htmlPathForMessage(messageId, variantId, appId)
5241
?: return GetIAMDataResponse(null, false)
5342

54-
val response = _httpClient.get(htmlPath, null)
43+
val response = _httpClient.get(htmlPath)
5544

5645
if (response.isSuccess) {
5746
// Successful request, reset count
@@ -81,7 +70,7 @@ internal class InAppBackendService(
8170
): InAppMessageContent? {
8271
val htmlPath = "in_app_messages/device_preview?preview_id=$previewUUID&app_id=$appId"
8372

84-
val response = _httpClient.get(htmlPath, null)
73+
val response = _httpClient.get(htmlPath)
8574

8675
return if (response.isSuccess) {
8776
val jsonResponse = JSONObject(response.payload!!)
@@ -209,4 +198,75 @@ internal class InAppBackendService(
209198
) {
210199
Logging.error("Encountered a $statusCode error while attempting in-app message $requestType request: $response")
211200
}
201+
202+
private suspend fun attemptFetchWithRetries(
203+
baseUrl: String,
204+
rywToken: String?,
205+
sessionDurationProvider: () -> Long,
206+
): List<InAppMessage>? {
207+
var attempts = 1
208+
var retryLimit: Int? = null // Retry limit will be determined dynamically
209+
210+
while (retryLimit == null || attempts <= retryLimit + 1) {
211+
val retryCount = if (attempts > 1) attempts - 1 else null
212+
val values =
213+
OptionalHeaders(
214+
rywToken = rywToken,
215+
sessionDuration = sessionDurationProvider(),
216+
retryCount = retryCount,
217+
)
218+
val response = _httpClient.get(baseUrl, values)
219+
220+
if (response.isSuccess) {
221+
val jsonResponse = response.payload?.let { JSONObject(it) }
222+
return jsonResponse?.let { hydrateInAppMessages(it) }
223+
} else if (response.statusCode == 425 || response.statusCode == 429) {
224+
// Dynamically update the retry limit from response
225+
retryLimit = response.retryLimit ?: retryLimit
226+
227+
// Apply the Retry-After delay if present, otherwise proceed without delay
228+
val retryAfter = response.retryAfterSeconds
229+
if (retryAfter != null) {
230+
delay(retryAfter * 1_000L)
231+
}
232+
} else if (response.statusCode in 500..599) {
233+
return null
234+
} else {
235+
return null
236+
}
237+
238+
attempts++
239+
}
240+
241+
// Final attempt without the RYW token if retries fail
242+
return fetchInAppMessagesWithoutRywToken(baseUrl, sessionDurationProvider)
243+
}
244+
245+
private suspend fun fetchInAppMessagesWithoutRywToken(
246+
url: String,
247+
sessionDurationProvider: () -> Long,
248+
): List<InAppMessage>? {
249+
val response =
250+
_httpClient.get(
251+
url,
252+
OptionalHeaders(
253+
sessionDuration = sessionDurationProvider(),
254+
),
255+
)
256+
257+
if (response.isSuccess) {
258+
val jsonResponse = response.payload?.let { JSONObject(it) }
259+
return jsonResponse?.let { hydrateInAppMessages(it) }
260+
} else {
261+
return null
262+
}
263+
}
264+
265+
private fun hydrateInAppMessages(jsonResponse: JSONObject): List<InAppMessage>? =
266+
if (jsonResponse.has("in_app_messages")) {
267+
val iamMessagesAsJSON = jsonResponse.getJSONArray("in_app_messages")
268+
_hydrator.hydrateIAMMessages(iamMessagesAsJSON)
269+
} else {
270+
null
271+
}
212272
}

OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.onesignal.inAppMessages.internal
22

3+
import com.onesignal.common.consistency.models.IConsistencyManager
34
import com.onesignal.core.internal.config.ConfigModelStore
45
import com.onesignal.inAppMessages.internal.backend.IInAppBackendService
56
import com.onesignal.inAppMessages.internal.display.IInAppDisplayer
@@ -52,6 +53,7 @@ class InAppMessagesManagerTests : FunSpec({
5253
mockk<IInAppLifecycleService>(),
5354
MockHelper.languageContext(),
5455
MockHelper.time(1000),
56+
mockk<IConsistencyManager>(),
5557
)
5658

5759
// When

0 commit comments

Comments
 (0)