Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,29 +55,39 @@ internal class MindboxSdkLifecycleListener private constructor(

private var activityEventListener: ActivityEventListener? = null
private var reactInstanceEventListener: ReactInstanceEventListener? = null
private var reactInstanceEventListenerActivity: Activity? = null

private fun onReactContextAvailable(reactContext: ReactContext, activity: Activity) {
Mindbox.writeLog("[RN] ReactContext ready", Level.INFO)
addActivityEventListener(reactContext)
subscriber.onEvent(MindboxSdkLifecycleEvent.ActivityCreated(reactContext, activity))
}

private fun registerReactContextListener(onReady: (ReactContext) -> Unit) {
private fun registerReactContextListener(activity: Activity, onReady: (ReactContext) -> Unit) {
val host = getReactHost(application) ?: run {
Mindbox.writeLog(
"[RN] registerReactContextListener: ReactHost is null, skip listener.",
Level.WARN
)
return
}
reactInstanceEventListener?.let { host.removeReactInstanceEventListener(it) }
reactInstanceEventListener?.let { previousListener ->
host.removeReactInstanceEventListener(previousListener)
}
reactInstanceEventListenerActivity = null
val listener = object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
Mindbox.writeLog("[RN] ReactContext initialized (listener)", Level.INFO)
host.removeReactInstanceEventListener(this)
if (reactInstanceEventListener === this) {
reactInstanceEventListener = null
reactInstanceEventListenerActivity = null
}
onReady(context)
}
}
reactInstanceEventListener = listener
reactInstanceEventListenerActivity = activity
host.addReactInstanceEventListener(listener)
}

Expand All @@ -86,7 +96,7 @@ internal class MindboxSdkLifecycleListener private constructor(

val hasConsumedReactContext = AtomicBoolean(false)

registerReactContextListener { reactContext ->
registerReactContextListener(activity) { reactContext ->
if (hasConsumedReactContext.compareAndSet(false, true)) {
onReactContextAvailable(reactContext, activity)
}
Expand Down Expand Up @@ -127,13 +137,14 @@ internal class MindboxSdkLifecycleListener private constructor(

override fun onActivityDestroyed(activity: Activity) {
if (!isMainActivity(activity)) return
subscriber.onEvent(MindboxSdkLifecycleEvent.ActivityDestroyed(activity))
getReactContext()?.removeActivityEventListener(activityEventListener)
activityEventListener = null
reactInstanceEventListener?.let { listener ->
getReactHost(application)?.removeReactInstanceEventListener(listener)
if (reactInstanceEventListenerActivity === activity) {
reactInstanceEventListener?.let { listener ->
getReactHost(application)?.removeReactInstanceEventListener(listener)
}
reactInstanceEventListener = null
reactInstanceEventListenerActivity = null
}
reactInstanceEventListener = null
subscriber.onEvent(MindboxSdkLifecycleEvent.ActivityDestroyed(activity))
}

override fun onActivityStarted(activity: Activity) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
package com.exampleapp

import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
import cloud.mindbox.mobile_sdk.Mindbox
import cloud.mindbox.mobile_sdk.pushes.MindboxRemoteMessage
import cloud.mindbox.mindbox_firebase.MindboxFirebase
import cloud.mindbox.mindbox_huawei.MindboxHuawei
import cloud.mindbox.mobile_sdk.Mindbox
import com.exampleapp.NotificationPackage
import cloud.mindbox.mindbox_rustore.MindboxRuStore
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import cloud.mindbox.mindbox_rustore.MindboxRuStore

class MainApplication : Application(), ReactApplication {

Expand Down Expand Up @@ -55,31 +46,8 @@ class MainApplication : Application(), ReactApplication {
}
}

private val gson = Gson()

fun saveNotification(message: MindboxRemoteMessage) {
val sharedPreferences = getSharedPreferences("notifications", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
val notificationsJson = sharedPreferences.getString("notifications", "[]")
val type = object : TypeToken<MutableList<String>>() {}.type
val notifications: MutableList<String> = gson.fromJson(notificationsJson, type)
notifications.add(gson.toJson(message))
editor.putString("notifications", gson.toJson(notifications))
editor.apply()
notifyJS()
}

private fun notifyJS() {
val handler = Handler(Looper.getMainLooper())
handler.post {
try {
val reactInstanceManager = reactNativeHost.reactInstanceManager
reactInstanceManager.currentReactContext
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
?.emit("newNotification", null)
} catch (e: Exception) {
Log.e("MainApplication", "Error notifying React Native", e)
}
}
NotificationStorage.saveNotification(this, message)
NotificationModule.emitNotificationCenterUpdatedFromExternal()
}
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
package com.exampleapp

import android.content.Context
import android.content.SharedPreferences
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.module.annotations.ReactModule

class NotificationModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
@ReactModule(name = NativeNotificationModuleSpec.NAME)
class NotificationModule(
private val reactContext: ReactApplicationContext
) : NativeNotificationModuleSpec(reactContext) {

private val sharedPreferences: SharedPreferences =
reactContext.getSharedPreferences("notifications", Context.MODE_PRIVATE)
companion object {
@Volatile
private var activeModule: NotificationModule? = null

override fun getName(): String {
return "NotificationModule"
}

@ReactMethod
fun addListener(eventName: String?) {
fun emitNotificationCenterUpdatedFromExternal() {
activeModule?.emitNotificationCenterUpdated()
}
}

@ReactMethod
fun removeListeners(count: Integer?) {
init {
activeModule = this
}

@ReactMethod
fun getNotifications(promise: Promise) {
override fun getNotifications(promise: Promise) {
try {
val notificationsJson = sharedPreferences.getString("notifications", "[]")
val notificationsJson: String = NotificationStorage.getNotificationsJson(reactContext)
promise.resolve(notificationsJson)
} catch (e: Exception) {
promise.reject("Error", e)
} catch (error: Throwable) {
promise.reject("Error", error)
}
}

@ReactMethod
fun clearNotifications(promise: Promise) {
override fun clearNotifications(promise: Promise) {
try {
val editor = sharedPreferences.edit()
editor.putString("notifications", "[]")
editor.apply()
NotificationStorage.clearNotifications(reactContext)
promise.resolve(null)
} catch (e: Exception) {
promise.reject("Error", e)
} catch (error: Throwable) {
promise.reject("Error", error)
}
}

override fun invalidate() {
if (activeModule === this) {
activeModule = null
}
super.invalidate()
}

private fun emitNotificationCenterUpdated() {
emitOnNotificationCenterUpdated(Arguments.createMap())
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
package com.exampleapp

import com.facebook.react.ReactPackage
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.turbomodule.core.interfaces.TurboModule

class NotificationPackage : ReactPackage {
class NotificationPackage : TurboReactPackage() {

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == NativeNotificationModuleSpec.NAME) {
NotificationModule(reactContext)
} else {
null
}
}

override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(NotificationModule(reactContext))
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
return ReactModuleInfoProvider {
mapOf(
NativeNotificationModuleSpec.NAME to ReactModuleInfo(
NativeNotificationModuleSpec.NAME,
NativeNotificationModuleSpec.NAME,
false,
false,
false,
false,
TurboModule::class.java.isAssignableFrom(NotificationModule::class.java)
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.exampleapp

import android.content.Context
import cloud.mindbox.mobile_sdk.pushes.MindboxRemoteMessage
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type

object NotificationStorage {
private const val PREFERENCES_NAME: String = "notifications"
private const val NOTIFICATIONS_KEY: String = "notifications"
private const val EMPTY_NOTIFICATIONS_JSON: String = "[]"
private val gson: Gson = Gson()
private val notificationListType: Type = object : TypeToken<MutableList<String>>() {}.type

@Synchronized
fun saveNotification(context: Context, message: MindboxRemoteMessage) {
val notifications: MutableList<String> = readNotifications(context).toMutableList()
notifications.add(gson.toJson(message))
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
.edit()
.putString(NOTIFICATIONS_KEY, gson.toJson(notifications))
.apply()
}

@Synchronized
fun getNotificationsJson(context: Context): String {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
.getString(NOTIFICATIONS_KEY, EMPTY_NOTIFICATIONS_JSON)
?: EMPTY_NOTIFICATIONS_JSON
}

@Synchronized
fun clearNotifications(context: Context) {
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
.edit()
.putString(NOTIFICATIONS_KEY, EMPTY_NOTIFICATIONS_JSON)
.apply()
}

private fun readNotifications(context: Context): List<String> {
val notificationsJson: String = getNotificationsJson(context)
return runCatching {
gson.fromJson<MutableList<String>>(notificationsJson, notificationListType)
}.getOrNull() ?: emptyList()
}
}
15 changes: 4 additions & 11 deletions example/exampleApp/ios/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,16 @@ class AppDelegate: RCTAppDelegate, UNUserNotificationCenterDelegate {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func notifyReactNative() {
if let eventEmitter = bridge?.module(for: NotificationModule.self) as? NotificationModule {
eventEmitter.notifyReactNative()
}
func notifyReactNativeAboutNotificationCenterUpdate() {
NotificationCenter.default.post(name: NotificationCenterStorage.notificationCenterUpdatedName, object: nil)
}

// Handling remote notification fetch completion
override func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Mindbox.shared.application(application, performFetchWithCompletionHandler: completionHandler)
notifyReactNative()
notifyReactNativeAboutNotificationCenterUpdate()
}

// Updating APNS token in Mindbox
Expand All @@ -63,7 +61,7 @@ class AppDelegate: RCTAppDelegate, UNUserNotificationCenterDelegate {

// Displaying notifications when the app is active
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
notifyReactNative()
notifyReactNativeAboutNotificationCenterUpdate()
completionHandler([.alert, .sound, .badge])
}

Expand All @@ -80,11 +78,6 @@ class AppDelegate: RCTAppDelegate, UNUserNotificationCenterDelegate {
override func sourceURL(for bridge: RCTBridge!) -> URL! {
bundleURL()
}

override func extraModules(for bridge: RCTBridge!) -> [RCTBridgeModule] {
[NotificationModule()]
}

override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import MindboxNotifications
// https://developers.mindbox.ru/docs/ios-send-rich-push-react-native
class NotificationService: UNNotificationServiceExtension {

static let suiteName = "group.cloud.Mindbox.com.mindbox.exampleRN"
static let suiteName = "group.cloud.Mindbox.mindbox.RN.Example"
// Lazy initialization of MindboxNotificationService
lazy var mindboxService = MindboxNotificationService()

Expand Down
28 changes: 28 additions & 0 deletions example/exampleApp/ios/NotificationCenterStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

@objc(NotificationCenterStorage)
final class NotificationCenterStorage: NSObject {
static let suiteName: String = "group.cloud.Mindbox.mindbox.RN.Example"
static let notificationCenterUpdatedRawName: String = "NotificationCenterUpdated"
static let notificationCenterUpdatedName: Notification.Name = Notification.Name(notificationCenterUpdatedRawName)
private static let notificationsKey: String = "notifications"
private static let emptyNotificationsJson: String = "[]"

@objc static func getNotificationCenterUpdatedName() -> String {
return notificationCenterUpdatedRawName
}

@objc static func getNotificationsJson() -> String {
guard let userDefaults: UserDefaults = UserDefaults(suiteName: suiteName) else {
return emptyNotificationsJson
}
return userDefaults.string(forKey: notificationsKey) ?? emptyNotificationsJson
}

@objc static func clearNotifications() {
let userDefaults: UserDefaults? = UserDefaults(suiteName: suiteName)
userDefaults?.set(emptyNotificationsJson, forKey: notificationsKey)
userDefaults?.synchronize()
}

}
Loading
Loading