From cb55774874b9fc37a255876077029333862ce5ec Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Sun, 25 Oct 2026 06:48:00 +0000 Subject: [PATCH 1/2] fix: treat time since last in-app as unbounded when no in-app was dismissed --- .../triggers/impl/DynamicTriggerController.kt | 4 +- .../impl/DynamicTriggerControllerTests.kt | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt index b7dd35afd7..82ae3e7273 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt @@ -149,8 +149,8 @@ internal class DynamicTriggerController( companion object { private const val REQUIRED_ACCURACY = 0.3 - // Assume last time an In-App Message was displayed a very very long time ago. - private const val DEFAULT_LAST_IN_APP_TIME_AGO: Long = 999999 + // If no In-App Message has been dismissed yet, treat the last one as infinitely long ago. + private const val DEFAULT_LAST_IN_APP_TIME_AGO: Long = Long.MAX_VALUE } override fun subscribe(handler: ITriggerHandler) = events.subscribe(handler) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt new file mode 100644 index 0000000000..83cccc91a3 --- /dev/null +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt @@ -0,0 +1,49 @@ +package com.onesignal.inAppMessages.internal.triggers.impl + +import com.onesignal.inAppMessages.internal.Trigger +import com.onesignal.inAppMessages.internal.state.InAppStateService +import com.onesignal.mocks.MockHelper +import com.onesignal.session.internal.session.ISessionService +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.json.JSONObject + +private fun timeSinceLastInAppTrigger(seconds: Int): Trigger { + val json = JSONObject() + json.put("id", "trigger-id") + json.put("kind", Trigger.OSTriggerKind.TIME_SINCE_LAST_IN_APP.toString()) + json.put("operator", Trigger.OSTriggerOperator.GREATER_THAN.toString()) + json.put("value", seconds) + return Trigger(json) +} + +class DynamicTriggerControllerTests : FunSpec({ + val oneHourInSeconds = 3600 + + test("time since last in-app trigger fires when no in-app message was dismissed yet") { + // Given + val state = InAppStateService() + val controller = DynamicTriggerController(state, mockk(relaxed = true), MockHelper.time(1_000_000)) + + // When + val result = controller.dynamicTriggerShouldFire(timeSinceLastInAppTrigger(oneHourInSeconds)) + + // Then + result shouldBe true + } + + test("time since last in-app trigger does not fire before the interval has elapsed") { + // Given + val now = 10_000_000L + val state = InAppStateService() + state.lastTimeInAppDismissed = now - 60_000L + val controller = DynamicTriggerController(state, mockk(relaxed = true), MockHelper.time(now)) + + // When + val result = controller.dynamicTriggerShouldFire(timeSinceLastInAppTrigger(oneHourInSeconds)) + + // Then + result shouldBe false + } +}) From 4783cc728dd7b943141c4a42458e3d0f7995ebd0 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:05:52 +0000 Subject: [PATCH 2/2] fix: compare trigger intervals before computing the timer offset With no in-app message dismissed, the time since the last one is Long.MAX_VALUE, so a negative required interval overflowed the offset into a large positive delay and Timer.schedule threw. Return before computing the offset when the interval has already passed. --- .../triggers/impl/DynamicTriggerController.kt | 6 ++++-- .../impl/DynamicTriggerControllerTests.kt | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt index 82ae3e7273..a37aa6a402 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerController.kt @@ -75,10 +75,12 @@ internal class DynamicTriggerController( return true } - val offset = requiredTimeInterval - currentTimeInterval - if (offset <= 0L) { + // Compare before subtracting: with an unbounded time since the last in-app + // message, a negative required interval would overflow the offset. + if (currentTimeInterval >= requiredTimeInterval) { return false } + val offset = requiredTimeInterval - currentTimeInterval // Prevents re-scheduling timers for messages that we're already waiting on if (scheduledMessages.contains(triggerId)) { diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt index 83cccc91a3..5dee068eb2 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/DynamicTriggerControllerTests.kt @@ -9,11 +9,14 @@ import io.kotest.matchers.shouldBe import io.mockk.mockk import org.json.JSONObject -private fun timeSinceLastInAppTrigger(seconds: Int): Trigger { +private fun timeSinceLastInAppTrigger( + seconds: Number, + operator: Trigger.OSTriggerOperator = Trigger.OSTriggerOperator.GREATER_THAN +): Trigger { val json = JSONObject() json.put("id", "trigger-id") json.put("kind", Trigger.OSTriggerKind.TIME_SINCE_LAST_IN_APP.toString()) - json.put("operator", Trigger.OSTriggerOperator.GREATER_THAN.toString()) + json.put("operator", operator.toString()) json.put("value", seconds) return Trigger(json) } @@ -46,4 +49,16 @@ class DynamicTriggerControllerTests : FunSpec({ // Then result shouldBe false } + + test("time since last in-app trigger with a negative less-than value does not fire when no in-app message was dismissed yet") { + // Given + val state = InAppStateService() + val controller = DynamicTriggerController(state, mockk(relaxed = true), MockHelper.time(1_000_000)) + + // When + val result = controller.dynamicTriggerShouldFire(timeSinceLastInAppTrigger(-1, Trigger.OSTriggerOperator.LESS_THAN)) + + // Then + result shouldBe false + } })