@@ -2,17 +2,21 @@ package io.customer.customer_io.messaginginapp
22
33import android.app.Activity
44import io.customer.customer_io.bridge.NativeModuleBridge
5+ import io.customer.customer_io.bridge.nativeMapArgs
56import io.customer.customer_io.bridge.nativeNoArgs
6- import io.customer.customer_io.messaginginapp.InlineInAppMessageViewFactory
77import io.customer.customer_io.utils.getAs
88import io.customer.messaginginapp.MessagingInAppModuleConfig
99import io.customer.messaginginapp.ModuleMessagingInApp
1010import io.customer.messaginginapp.di.inAppMessaging
11+ import io.customer.messaginginapp.gist.data.model.InboxMessage
12+ import io.customer.messaginginapp.gist.data.model.response.InboxMessageFactory
13+ import io.customer.messaginginapp.inbox.NotificationInbox
1114import io.customer.messaginginapp.type.InAppEventListener
1215import io.customer.messaginginapp.type.InAppMessage
1316import io.customer.sdk.CustomerIO
1417import io.customer.sdk.CustomerIOBuilder
1518import io.customer.sdk.core.di.SDKComponent
19+ import io.customer.sdk.core.util.Logger
1620import io.customer.sdk.data.model.Region
1721import io.flutter.embedding.engine.plugins.FlutterPlugin
1822import io.flutter.embedding.engine.plugins.activity.ActivityAware
@@ -31,13 +35,33 @@ internal class CustomerIOInAppMessaging(
3135 override val moduleName: String = " InAppMessaging"
3236 override val flutterCommunicationChannel: MethodChannel =
3337 MethodChannel (pluginBinding.binaryMessenger, " customer_io_messaging_in_app" )
38+ private val logger: Logger = SDKComponent .logger
39+ private val inAppMessagingModule: ModuleMessagingInApp ?
40+ get() = runCatching { CustomerIO .instance().inAppMessaging() }.getOrNull()
3441 private var activity: WeakReference <Activity >? = null
3542 private val binaryMessenger = pluginBinding.binaryMessenger
3643 private val platformViewRegistry = pluginBinding.platformViewRegistry
3744
45+ // Dedicated lock for inbox listener setup to avoid blocking other operations
46+ private val inboxListenerLock = Any ()
47+ private val inboxChangeListener = FlutterNotificationInboxChangeListener .instance
48+ private var isInboxChangeListenerSetup = false
49+
50+ /* *
51+ * Returns NotificationInbox instance if available, null otherwise, logging error on failure.
52+ * Note: Notification Inbox is only available after SDK is initialized.
53+ */
54+ private fun requireInboxInstance (): NotificationInbox ? {
55+ val inbox = inAppMessagingModule?.inbox()
56+ if (inbox == null ) {
57+ logger.error(" Notification Inbox is not available. Ensure CustomerIO SDK is initialized." )
58+ }
59+ return inbox
60+ }
61+
3862 override fun onAttachedToEngine () {
3963 super .onAttachedToEngine()
40-
64+
4165 // Register the platform view factory for inline in-app messages
4266 platformViewRegistry.registerViewFactory(
4367 " customer_io_inline_in_app_message_view" ,
@@ -61,15 +85,26 @@ internal class CustomerIOInAppMessaging(
6185 this .activity = null
6286 }
6387
88+ override fun onDetachedFromEngine () {
89+ clearInboxChangeListener()
90+ super .onDetachedFromEngine()
91+ }
92+
6493 override fun onMethodCall (call : MethodCall , result : MethodChannel .Result ) {
6594 when (call.method) {
6695 " dismissMessage" -> call.nativeNoArgs(result, ::dismissMessage)
96+ " subscribeToInboxMessages" -> setupInboxChangeListener(call, result)
97+ " getInboxMessages" -> getInboxMessages(call, result)
98+ " markInboxMessageOpened" -> call.nativeMapArgs(result, ::markInboxMessageOpened)
99+ " markInboxMessageUnopened" -> call.nativeMapArgs(result, ::markInboxMessageUnopened)
100+ " markInboxMessageDeleted" -> call.nativeMapArgs(result, ::markInboxMessageDeleted)
101+ " trackInboxMessageClicked" -> call.nativeMapArgs(result, ::trackInboxMessageClicked)
67102 else -> super .onMethodCall(call, result)
68103 }
69104 }
70105
71106 private fun dismissMessage () {
72- CustomerIO .instance().inAppMessaging() .dismissMessage()
107+ inAppMessagingModule? .dismissMessage()
73108 }
74109
75110 /* *
@@ -102,6 +137,135 @@ internal class CustomerIOInAppMessaging(
102137 )
103138 builder.addCustomerIOModule(module)
104139 }
140+
141+ /* *
142+ * Sets up the inbox change listener to receive real-time updates.
143+ * This method can be called multiple times safely and will only set up the listener once.
144+ * Note: Inbox must be available (SDK initialized) before this can succeed.
145+ */
146+ private fun setupInboxChangeListener (call : MethodCall , result : MethodChannel .Result ) {
147+ synchronized(inboxListenerLock) {
148+ // Only set up once to avoid duplicate listeners
149+ if (isInboxChangeListenerSetup) {
150+ result.success(true )
151+ return
152+ }
153+
154+ val inbox = requireInboxInstance() ? : run {
155+ result.error(" INBOX_NOT_AVAILABLE" , " Notification Inbox is not available. Ensure CustomerIO SDK is initialized." , null )
156+ return
157+ }
158+
159+ inboxChangeListener.setEventEmitter(
160+ emitter = { data ->
161+ runOnMainThread {
162+ flutterCommunicationChannel.invokeMethod(" inboxMessagesChanged" , data)
163+ }
164+ }
165+ )
166+ inbox.addChangeListener(inboxChangeListener)
167+ isInboxChangeListenerSetup = true
168+ logger.debug(" NotificationInboxChangeListener set up successfully" )
169+ result.success(true )
170+ }
171+ }
172+
173+ private fun clearInboxChangeListener () {
174+ synchronized(inboxListenerLock) {
175+ if (! isInboxChangeListenerSetup) {
176+ return
177+ }
178+ requireInboxInstance()?.removeChangeListener(inboxChangeListener)
179+ inboxChangeListener.clearEventEmitter()
180+ isInboxChangeListenerSetup = false
181+ }
182+ }
183+
184+ private fun getInboxMessages (call : MethodCall , result : MethodChannel .Result ) {
185+ val inbox = requireInboxInstance() ? : run {
186+ result.error(" INBOX_NOT_AVAILABLE" , " Notification Inbox is not available. Ensure CustomerIO SDK is initialized." , null )
187+ return
188+ }
189+
190+ // Extract topic parameter if provided
191+ val args = call.arguments as ? Map <String , Any >
192+ val topic = args?.get(" topic" ) as ? String
193+
194+ // Fetch messages with topic filter
195+ // Using async callback avoids blocking main thread (prevents ANR/deadlocks)
196+ inbox.fetchMessages(topic) { fetchResult ->
197+ runOnMainThread {
198+ fetchResult.onSuccess { messages ->
199+ result.success(messages.map { it.toMap() })
200+ }.onFailure { error ->
201+ logger.error(" Failed to get inbox messages: ${error.message} " )
202+ result.error(" FETCH_ERROR" , error.message, null )
203+ }
204+ }
205+ }
206+ }
207+
208+ private fun markInboxMessageOpened (params : Map <String , Any >) {
209+ val message = params.getAs<Map <String , Any >>(" message" )
210+
211+ performInboxMessageAction(message) { inbox, inboxMessage ->
212+ inbox.markMessageOpened(inboxMessage)
213+ }
214+ }
215+
216+ private fun markInboxMessageUnopened (params : Map <String , Any >) {
217+ val message = params.getAs<Map <String , Any >>(" message" )
218+
219+ performInboxMessageAction(message) { inbox, inboxMessage ->
220+ inbox.markMessageUnopened(inboxMessage)
221+ }
222+ }
223+
224+ private fun markInboxMessageDeleted (params : Map <String , Any >) {
225+ val message = params.getAs<Map <String , Any >>(" message" )
226+
227+ performInboxMessageAction(message) { inbox, inboxMessage ->
228+ inbox.markMessageDeleted(inboxMessage)
229+ }
230+ }
231+
232+ private fun trackInboxMessageClicked (params : Map <String , Any >) {
233+ val message = params.getAs<Map <String , Any >>(" message" )
234+ val actionName = params.getAs<String >(" actionName" )
235+
236+ performInboxMessageAction(message) { inbox, inboxMessage ->
237+ inbox.trackMessageClicked(inboxMessage, actionName)
238+ }
239+ }
240+
241+ /* *
242+ * Runs block on main/UI thread. Uses activity's runOnUiThread if available,
243+ * otherwise falls back to Handler(Looper.getMainLooper()).post
244+ */
245+ private fun runOnMainThread (block : () -> Unit ) {
246+ val currentActivity = activity?.get()
247+ if (currentActivity != null ) {
248+ currentActivity.runOnUiThread(block)
249+ } else {
250+ // Activity is null - use main looper directly
251+ android.os.Handler (android.os.Looper .getMainLooper()).post(block)
252+ }
253+ }
254+
255+ /* *
256+ * Helper to validate inbox instance and message data before performing a message action.
257+ * Throws exceptions if inbox is unavailable or message data is invalid.
258+ */
259+ private fun performInboxMessageAction (
260+ message : Map <String , Any >? ,
261+ action : (NotificationInbox , InboxMessage ) -> Unit ,
262+ ) {
263+ val inbox = requireInboxInstance()
264+ ? : throw IllegalStateException (" Notification Inbox is not available. Ensure CustomerIO SDK is initialized." )
265+ val inboxMessage = message?.let { InboxMessageFactory .fromMap(it) }
266+ ? : throw IllegalArgumentException (" Invalid message data: $message " )
267+ action(inbox, inboxMessage)
268+ }
105269}
106270
107271class CustomerIOInAppEventListener (private val invokeMethod : (String , Any? ) -> Unit ) :
@@ -142,4 +306,4 @@ class CustomerIOInAppEventListener(private val invokeMethod: (String, Any?) -> U
142306 )
143307 )
144308 }
145- }
309+ }
0 commit comments