Skip to content

Commit b3aad11

Browse files
chore: SSE bug fixes (#639)
1 parent 126b67c commit b3aad11

File tree

3 files changed

+149
-13
lines changed

3 files changed

+149
-13
lines changed

messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ class GistModalActivity : AppCompatActivity(), ModalInAppMessageViewCallback, Tr
3535
private lateinit var binding: ActivityGistBinding
3636
private var messagePosition: MessagePosition = MessagePosition.CENTER
3737

38+
// Store the message that this activity is displaying to avoid dismissing wrong messages
39+
// when multiple modal activities exist during transitions (race condition fix)
40+
private var activityMessage: Message? = null
41+
3842
private val attributesListenerJob: MutableList<Job> = mutableListOf()
3943
private val elapsedTimer: ElapsedTimer = ElapsedTimer()
4044
private val logger = SDKComponent.logger
@@ -103,6 +107,9 @@ class GistModalActivity : AppCompatActivity(), ModalInAppMessageViewCallback, Tr
103107
}
104108

105109
private fun setupMessage(message: Message) {
110+
// Store the message this activity is displaying
111+
activityMessage = message
112+
106113
logger.debug("GistModelActivity onCreate: $message")
107114
elapsedTimer.start("Displaying modal for message: ${message.messageId}")
108115

@@ -190,25 +197,26 @@ class GistModalActivity : AppCompatActivity(), ModalInAppMessageViewCallback, Tr
190197
for (job in attributesListenerJob) {
191198
job.cancel()
192199
}
193-
// if the message has been cancelled, do not perform any further actions
194-
// to avoid sending any callbacks to the client app
195-
// If the message is not persistent, dismiss it and inform the callback
196200

197-
val state = currentMessageState
201+
// Only dispatch dismiss if THIS activity's message is still the currently displayed message.
202+
// This prevents a race condition where Activity1 finishes while Activity2 is already showing,
203+
// which would cause Activity1's onDestroy to incorrectly dismiss Activity2's message.
204+
val displayedMessage = activityMessage
205+
val messageInQueue = currentMessageState?.message
198206
val inAppManager = inAppMessagingManager
199-
if (state != null && inAppManager != null) {
200-
if (!isPersistentMessage()) {
201-
inAppManager.dispatch(InAppMessagingAction.DismissMessage(message = state.message))
207+
208+
if (displayedMessage != null && inAppManager != null && displayedMessage.queueId == messageInQueue?.queueId) {
209+
if (!isPersistentMessage(displayedMessage)) {
210+
inAppManager.dispatch(InAppMessagingAction.DismissMessage(message = displayedMessage))
202211
} else {
203-
inAppManager.dispatch(InAppMessagingAction.DismissMessage(message = state.message, shouldLog = false))
212+
inAppManager.dispatch(InAppMessagingAction.DismissMessage(message = displayedMessage, shouldLog = false))
204213
}
205214
}
206215
super.onDestroy()
207216
}
208217

209-
private fun isPersistentMessage(message: Message? = null): Boolean {
210-
val currentMessage = message ?: currentMessageState?.message
211-
return currentMessage?.gistProperties?.persistent ?: false
218+
private fun isPersistentMessage(message: Message?): Boolean {
219+
return message?.gistProperties?.persistent ?: false
212220
}
213221

214222
private fun onMessageShown(message: Message) {

messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingMiddlewares.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private fun handleMessageDismissal(logger: Logger, store: Store<InAppMessagingSt
8080

8181
// After the dismissal is processed, dispatch ProcessMessageQueue to show the next message
8282
// The dismissed message will be filtered out by processMessages() since its queueId is now in shownMessageQueueIds
83-
if (store.state.sseEnabled) {
83+
if (store.state.shouldUseSse) {
8484
SDKComponent.inAppSseLogger.logTryDisplayNextMessageAfterDismissal()
8585
store.dispatch(InAppMessagingAction.ProcessMessageQueue(store.state.messagesInQueue.toList()))
8686
}

messaginginapp/src/test/java/io/customer/messaginginapp/gist/presentation/GistModalActivityTest.kt

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,20 @@ import io.customer.messaginginapp.gist.GistEnvironment
2020
import io.customer.messaginginapp.gist.data.listeners.GistQueue
2121
import io.customer.messaginginapp.gist.data.model.Message
2222
import io.customer.messaginginapp.gist.data.model.MessagePosition
23+
import io.customer.messaginginapp.gist.utilities.ModalMessageExtras
2324
import io.customer.messaginginapp.gist.utilities.ModalMessageParser
2425
import io.customer.messaginginapp.state.InAppMessagingAction
2526
import io.customer.messaginginapp.state.InAppMessagingManager
27+
import io.customer.messaginginapp.state.MessageBuilderMock
28+
import io.customer.messaginginapp.state.ModalMessageState
2629
import io.customer.messaginginapp.testutils.core.IntegrationTest
2730
import io.customer.sdk.core.di.SDKComponent
2831
import io.customer.sdk.core.util.DispatchersProvider
2932
import io.customer.sdk.core.util.Logger
3033
import io.customer.sdk.core.util.ScopeProvider
3134
import io.customer.sdk.data.model.Region
3235
import io.mockk.coEvery
36+
import io.mockk.every
3337
import io.mockk.mockk
3438
import io.mockk.spyk
3539
import io.mockk.verify
@@ -171,7 +175,129 @@ class GistModalActivityTest : IntegrationTest() {
171175
scenario.close()
172176
}
173177

174-
private fun initializeModuleMessagingInApp() {
178+
// region onDestroy race condition prevention tests
179+
180+
@Test
181+
fun onDestroy_givenActivityMessageMatchesMessageInQueue_expectDismissDispatched() {
182+
// Initialize ModuleMessagingInApp
183+
val messagingManager = initializeModuleMessagingInApp()
184+
val testMessage = MessageBuilderMock.createMessage(
185+
queueId = "test-queue-id",
186+
persistent = false
187+
)
188+
189+
// Setup parser to return the test message
190+
coEvery { mockMessageParser.parseExtras(any()) } returns ModalMessageExtras(
191+
message = testMessage,
192+
messagePosition = MessagePosition.CENTER
193+
)
194+
195+
// Set the modal state to Displayed with the SAME message (matching queueId)
196+
every { messagingManager.getCurrentState() } returns mockk(relaxed = true) {
197+
every { modalMessageState } returns ModalMessageState.Displayed(testMessage)
198+
}
199+
200+
val intent = createActivityIntent(testMessage)
201+
val scenario = ActivityScenario.launch<GistModalActivity>(intent)
202+
flushCoroutines(scopeProviderStub.inAppLifecycleScope)
203+
204+
// Destroy the activity
205+
scenario.moveToState(Lifecycle.State.DESTROYED)
206+
207+
// Verify dismiss was dispatched since the activity's message matches the one in queue
208+
assertCalledOnce {
209+
messagingManager.dispatch(
210+
match<InAppMessagingAction.DismissMessage> {
211+
it.message == testMessage && it.shouldLog
212+
}
213+
)
214+
}
215+
216+
scenario.close()
217+
}
218+
219+
@Test
220+
fun onDestroy_givenActivityMessageDifferentFromMessageInQueue_expectDismissNotDispatched() {
221+
// Initialize ModuleMessagingInApp
222+
val messagingManager = initializeModuleMessagingInApp()
223+
val activityMessage = MessageBuilderMock.createMessage(
224+
queueId = "activity-queue-id",
225+
persistent = false
226+
)
227+
val queueMessage = MessageBuilderMock.createMessage(
228+
queueId = "different-queue-id",
229+
persistent = false
230+
)
231+
232+
// Setup parser to return the activity's message
233+
coEvery { mockMessageParser.parseExtras(any()) } returns ModalMessageExtras(
234+
message = activityMessage,
235+
messagePosition = MessagePosition.CENTER
236+
)
237+
238+
// Set the modal state to Displayed with a DIFFERENT message (different queueId)
239+
// This simulates the race condition where Activity2 is already showing
240+
every { messagingManager.getCurrentState() } returns mockk(relaxed = true) {
241+
every { modalMessageState } returns ModalMessageState.Displayed(queueMessage)
242+
}
243+
244+
val intent = createActivityIntent(activityMessage)
245+
val scenario = ActivityScenario.launch<GistModalActivity>(intent)
246+
flushCoroutines(scopeProviderStub.inAppLifecycleScope)
247+
248+
// Destroy the activity
249+
scenario.moveToState(Lifecycle.State.DESTROYED)
250+
251+
// Verify dismiss was NOT dispatched since the messages don't match (race condition prevention)
252+
verify(exactly = 0) {
253+
messagingManager.dispatch(match<InAppMessagingAction.DismissMessage> { true })
254+
}
255+
256+
scenario.close()
257+
}
258+
259+
@Test
260+
fun onDestroy_givenPersistentMessage_expectDismissDispatchedWithShouldLogFalse() {
261+
// Initialize ModuleMessagingInApp
262+
val messagingManager = initializeModuleMessagingInApp()
263+
val testMessage = MessageBuilderMock.createMessage(
264+
queueId = "test-queue-id",
265+
persistent = true // Persistent message
266+
)
267+
268+
// Setup parser to return the test message
269+
coEvery { mockMessageParser.parseExtras(any()) } returns ModalMessageExtras(
270+
message = testMessage,
271+
messagePosition = MessagePosition.CENTER
272+
)
273+
274+
// Set the modal state to Displayed with the SAME message
275+
every { messagingManager.getCurrentState() } returns mockk(relaxed = true) {
276+
every { modalMessageState } returns ModalMessageState.Displayed(testMessage)
277+
}
278+
279+
val intent = createActivityIntent(testMessage)
280+
val scenario = ActivityScenario.launch<GistModalActivity>(intent)
281+
flushCoroutines(scopeProviderStub.inAppLifecycleScope)
282+
283+
// Destroy the activity
284+
scenario.moveToState(Lifecycle.State.DESTROYED)
285+
286+
// Verify dismiss was dispatched with shouldLog = false for persistent message
287+
assertCalledOnce {
288+
messagingManager.dispatch(
289+
match<InAppMessagingAction.DismissMessage> {
290+
it.message == testMessage && !it.shouldLog
291+
}
292+
)
293+
}
294+
295+
scenario.close()
296+
}
297+
298+
// endregion
299+
300+
private fun initializeModuleMessagingInApp(): InAppMessagingManager {
175301
val moduleConfig = MessagingInAppModuleConfig.Builder(
176302
siteId = "test-site-id",
177303
region = Region.US
@@ -188,6 +314,8 @@ class GistModalActivityTest : IntegrationTest() {
188314
environment = GistEnvironment.LOCAL
189315
)
190316
).flushCoroutines(scopeProviderStub.inAppLifecycleScope)
317+
318+
return messagingManager
191319
}
192320

193321
private fun createTestMessage(): Message = Message(

0 commit comments

Comments
 (0)