@capillarytech/react-native-hydra-lib 1.7.0-beta.7 → 1.7.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/android/build.gradle +8 -7
  2. package/android/src/main/java/com/hydralib/delegates/NonHydraNotificationActivity.kt +22 -10
  3. package/android/src/main/java/com/hydralib/eventemitter/HydraEventEmitter.kt +34 -4
  4. package/android/src/main/java/com/hydralib/gamification/GamificationBannerViewManager.kt +34 -3
  5. package/android/src/main/java/com/hydralib/gamification/GamificationEmbedViewManager.kt +25 -4
  6. package/android/src/main/java/com/hydralib/gamification/GamificationLoyaltyViewManager.kt +41 -6
  7. package/android/src/main/java/com/hydralib/hydrasdk/HydraCore.kt +60 -5
  8. package/android/src/main/java/com/hydralib/hydrasdk/HydraInAppMessages.kt +68 -41
  9. package/android/src/main/java/com/hydralib/hydrasdk/HydraInbox.kt +2 -2
  10. package/android/src/main/java/com/hydralib/hydrasdk/HydraPushNotification.kt +1 -1
  11. package/android/src/newarch/HydraLibModule.kt +8 -0
  12. package/android/src/oldarch/HydraLibModule.kt +10 -0
  13. package/ios/Delegates/HydraApplicationDelegate.swift +8 -0
  14. package/ios/GamificationViews/GamificationBannerViewComponentView.mm +39 -2
  15. package/ios/GamificationViews/GamificationEmbedViewComponentView.mm +33 -2
  16. package/ios/GamificationViews/GamificationLoyaltyViewComponentView.mm +41 -1
  17. package/ios/HydraLib.mm +8 -0
  18. package/ios/HydraSDK/HydraInAppMessages.swift +35 -8
  19. package/ios/HydraSDK/HydraInbox.swift +36 -9
  20. package/ios/HydraSDK/HydraInboxUI.swift +5 -1
  21. package/ios/HydraSDK/HydraPushNotification.swift +2 -2
  22. package/ios/HydraSDK/HydraSDKCore.swift +156 -82
  23. package/ios/Utils/HydraExtensions.swift +1 -1
  24. package/lib/commonjs/NativeHydraLib.ts +2 -0
  25. package/lib/commonjs/hydra-apis/Hydra.js +7 -0
  26. package/lib/commonjs/hydra-apis/Hydra.js.map +1 -1
  27. package/lib/commonjs/index.js +7 -0
  28. package/lib/commonjs/index.js.map +1 -1
  29. package/lib/commonjs/models/ThemeMode.js +14 -0
  30. package/lib/commonjs/models/ThemeMode.js.map +1 -0
  31. package/lib/module/NativeHydraLib.ts +2 -0
  32. package/lib/module/hydra-apis/Hydra.js +7 -0
  33. package/lib/module/hydra-apis/Hydra.js.map +1 -1
  34. package/lib/module/index.js +2 -1
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/models/ThemeMode.js +10 -0
  37. package/lib/module/models/ThemeMode.js.map +1 -0
  38. package/lib/typescript/commonjs/src/NativeHydraLib.d.ts +2 -0
  39. package/lib/typescript/commonjs/src/NativeHydraLib.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/src/hydra-apis/Hydra.d.ts +3 -0
  41. package/lib/typescript/commonjs/src/hydra-apis/Hydra.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/src/index.d.ts +2 -1
  43. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/src/models/ThemeMode.d.ts +7 -0
  45. package/lib/typescript/commonjs/src/models/ThemeMode.d.ts.map +1 -0
  46. package/lib/typescript/module/src/NativeHydraLib.d.ts +2 -0
  47. package/lib/typescript/module/src/NativeHydraLib.d.ts.map +1 -1
  48. package/lib/typescript/module/src/hydra-apis/Hydra.d.ts +3 -0
  49. package/lib/typescript/module/src/hydra-apis/Hydra.d.ts.map +1 -1
  50. package/lib/typescript/module/src/index.d.ts +2 -1
  51. package/lib/typescript/module/src/index.d.ts.map +1 -1
  52. package/lib/typescript/module/src/models/ThemeMode.d.ts +7 -0
  53. package/lib/typescript/module/src/models/ThemeMode.d.ts.map +1 -0
  54. package/package.json +1 -1
  55. package/sbom.spdx.json +4 -4
  56. package/scripts/HydraPods.rb +1 -1
  57. package/src/NativeHydraLib.ts +2 -0
  58. package/src/hydra-apis/Hydra.ts +10 -0
  59. package/src/index.tsx +2 -0
  60. package/src/models/ThemeMode.ts +6 -0
@@ -164,6 +164,7 @@ android {
164
164
  }
165
165
 
166
166
  repositories {
167
+ mavenLocal()
167
168
  mavenCentral()
168
169
  google()
169
170
  }
@@ -178,14 +179,14 @@ dependencies {
178
179
  //noinspection GradleDependency
179
180
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
180
181
 
181
- implementation "com.capillarytech:hydra-core:1.9.22-beta07"
182
- implementation "com.capillarytech:hydra-pushbase:1.9.22-beta07"
183
- implementation "com.capillarytech:hydra-firebase:1.9.22-beta07"
184
- implementation "com.capillarytech:hydra-richnotification:1.9.22-beta07"
182
+ implementation "com.capillarytech:hydra-core:1.9.22-beta09"
183
+ implementation "com.capillarytech:hydra-pushbase:1.9.22-beta09"
184
+ implementation "com.capillarytech:hydra-firebase:1.9.22-beta09"
185
+ implementation "com.capillarytech:hydra-richnotification:1.9.22-beta09"
185
186
 
186
- implementation "com.capillarytech:hydra-inbox-core:1.9.22-beta07"
187
- implementation "com.capillarytech:hydra-inbox-ui:1.9.22-beta07"
188
- implementation "com.capillarytech:hydra-inapp:1.9.22-beta07"
187
+ implementation "com.capillarytech:hydra-inbox-core:1.9.22-beta09"
188
+ implementation "com.capillarytech:hydra-inbox-ui:1.9.22-beta09"
189
+ implementation "com.capillarytech:hydra-inapp:1.9.22-beta09"
189
190
  implementation "com.google.firebase:firebase-messaging-ktx:24.1.0"
190
191
 
191
192
  implementation "com.google.code.gson:gson:2.11.0"
@@ -5,6 +5,7 @@ import androidx.annotation.RestrictTo
5
5
  import androidx.appcompat.app.AppCompatActivity
6
6
  import com.capillary.hydra.android.sdk.utils.Logger
7
7
  import com.google.gson.Gson
8
+ import com.google.gson.JsonSyntaxException
8
9
  import com.google.gson.reflect.TypeToken
9
10
  import com.hydralib.eventemitter.HydraEventEmitter
10
11
  import com.hydralib.eventemitter.HydraEventType
@@ -27,17 +28,28 @@ class NonHydraNotificationActivity : AppCompatActivity() {
27
28
  val remoteMessage = intent.getStringExtra(EXTRA_REMOTE_MESSAGE)
28
29
 
29
30
  Logger.d(TAG, "Remote Message: $remoteMessage")
30
- val gson = Gson()
31
- val type = object : TypeToken<Map<String, Any>>() {}.type
32
- val remoteMessageMap = gson.fromJson<Map<String, Any>>(remoteMessage, type)
33
31
 
34
- // Emit event to React Native
35
- HydraEventEmitter.sendEvent(
36
- HydraEventType.ON_NON_HYDRA_NOTIFICATION_CLICKED,
37
- remoteMessageMap
38
- )
39
-
40
- Logger.d(TAG, "Event emitted to React Native: ON_NON_HYDRA_NOTIFICATION_CLICKED")
32
+ if (remoteMessage.isNullOrEmpty()) {
33
+ Logger.e(TAG, "EXTRA_REMOTE_MESSAGE missing from intent, cannot emit click event")
34
+ finish()
35
+ return
36
+ }
37
+
38
+ try {
39
+ val gson = Gson()
40
+ val type = object : TypeToken<Map<String, Any>>() {}.type
41
+ val remoteMessageMap = gson.fromJson<Map<String, Any>>(remoteMessage, type)
42
+
43
+ // Emit event to React Native
44
+ HydraEventEmitter.sendEvent(
45
+ HydraEventType.ON_NON_HYDRA_NOTIFICATION_CLICKED,
46
+ remoteMessageMap
47
+ )
48
+
49
+ Logger.d(TAG, "Event emitted to React Native: ON_NON_HYDRA_NOTIFICATION_CLICKED")
50
+ } catch (e: JsonSyntaxException) {
51
+ Logger.e(TAG, "Failed to parse remote message JSON: ${e.message}\n${e.stackTraceToString()}")
52
+ }
41
53
  finish()
42
54
  }
43
55
  }
@@ -60,6 +60,11 @@ object HydraEventEmitter {
60
60
  if(callback != null) {
61
61
  eventsWithPendingCallback.add(PendingEvent(body, callback))
62
62
  }
63
+ } else {
64
+ Logger.w(
65
+ "HydraEventEmitter.sendEvent",
66
+ "No active listener for eventType = ${eventType.event}, dropping event. body = $body"
67
+ )
63
68
  }
64
69
  }
65
70
  }
@@ -82,8 +87,23 @@ object HydraEventEmitter {
82
87
  "HydraEventEmitter.addListener",
83
88
  "addListener called for eventName = $eventName"
84
89
  )
85
- val eventType = HydraEventType.from(eventName!!)
86
- if (!activeEventTypes.contains(eventType!!)) {
90
+ if (eventName == null) {
91
+ Logger.e("HydraEventEmitter.addListener", "eventName is null, ignoring addListener call")
92
+ return
93
+ }
94
+ val eventType = try {
95
+ // HydraEventType.from is declared as returning HydraEventType?, but it never actually
96
+ // returns null — it throws IllegalArgumentException for an unmapped name — so the null
97
+ // branch below is unreachable in practice; kept only to satisfy the nullable return type.
98
+ HydraEventType.from(eventName) ?: run {
99
+ Logger.e("HydraEventEmitter.addListener", "Unknown eventName = $eventName, ignoring addListener call")
100
+ return
101
+ }
102
+ } catch (e: IllegalArgumentException) {
103
+ Logger.e("HydraEventEmitter.addListener", "Unknown eventName = $eventName, ignoring addListener call")
104
+ return
105
+ }
106
+ if (!activeEventTypes.contains(eventType)) {
87
107
  Logger.d(
88
108
  "HydraEventEmitter.addListener",
89
109
  "adding event for eventName = $eventName"
@@ -107,13 +127,23 @@ object HydraEventEmitter {
107
127
  "removeListener called for eventName = $eventName"
108
128
  )
109
129
 
110
- if (activeEventTypes.contains(HydraEventType.from(eventName!!))) {
130
+ if (eventName == null) {
131
+ Logger.e("HydraEventEmitter.removeListener", "eventName is null, ignoring removeListener call")
132
+ return
133
+ }
134
+ val eventType = try {
135
+ HydraEventType.from(eventName)
136
+ } catch (e: IllegalArgumentException) {
137
+ Logger.e("HydraEventEmitter.removeListener", "Unknown eventName = $eventName, ignoring removeListener call")
138
+ return
139
+ }
140
+ if (eventType != null && activeEventTypes.contains(eventType)) {
111
141
  Logger.d(
112
142
  "HydraEventEmitter.removeListener",
113
143
  "removing event for eventName = $eventName"
114
144
  )
115
145
 
116
- activeEventTypes.remove(HydraEventType.from(eventName)!!)
146
+ activeEventTypes.remove(eventType)
117
147
  }
118
148
  }
119
149
 
@@ -9,6 +9,7 @@ import com.facebook.react.module.annotations.ReactModule
9
9
  import com.facebook.react.uimanager.SimpleViewManager
10
10
  import com.facebook.react.uimanager.ThemedReactContext
11
11
  import com.facebook.react.uimanager.annotations.ReactProp
12
+ import com.capillary.hydra.android.sdk.utils.Logger
12
13
  import com.hydralib.eventemitter.HydraEventEmitter
13
14
  import com.hydralib.eventemitter.HydraEventType
14
15
  import com.hydralib.hydrasdk.HydraCore
@@ -23,11 +24,21 @@ class GamificationBannerViewManager : SimpleViewManager<BannerView>() {
23
24
 
24
25
  @ReactProp(name = "bannerId")
25
26
  fun setBannerId(view: BannerView, bannerId: String?) {
26
- if (bannerId.isNullOrEmpty()) return
27
+ if (bannerId.isNullOrEmpty()) {
28
+ Logger.e("GamificationBannerViewManager.setBannerId", "bannerId prop is null or empty, skipping bind")
29
+ return
30
+ }
31
+ // Guard: don't re-bind if already bound to this bannerId. Without this, every RN
32
+ // re-render (triggered by the height-changed event) calls bindWithLayout again and
33
+ // re-increments the SDK's persistent show-count for this entry point, exhausting its
34
+ // frequency cap in half the real number of screen visits.
35
+ if (view.tag == bannerId) return
36
+ // findEntryPointByBannerId already logs the reason on failure
27
37
  val data = findEntryPointByBannerId(bannerId) ?: return
28
38
 
29
39
  // Defer bind until Yoga has measured and laid out the view
30
40
  if (view.width > 0 && view.height > 0) {
41
+ view.tag = bannerId
31
42
  bindWithLayout(view, data, bannerId)
32
43
  } else {
33
44
  view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
@@ -37,6 +48,7 @@ class GamificationBannerViewManager : SimpleViewManager<BannerView>() {
37
48
  ) {
38
49
  if (right - left > 0 && bottom - top > 0) {
39
50
  view.removeOnLayoutChangeListener(this)
51
+ view.tag = bannerId
40
52
  bindWithLayout(view, data, bannerId)
41
53
  }
42
54
  }
@@ -57,6 +69,14 @@ class GamificationBannerViewManager : SimpleViewManager<BannerView>() {
57
69
  private fun bindWithLayout(view: BannerView, data: EntryPointData, bannerId: String) {
58
70
  val w = view.width
59
71
  val activity = HydraCore.curReactActivity
72
+ if (activity == null) {
73
+ // Not a fatal bind failure — the banner still renders — but entry point clicks
74
+ // will silently no-op below for the life of this view, so surface it.
75
+ Logger.e(
76
+ "GamificationBannerViewManager.bindWithLayout",
77
+ "curReactActivity is null, entry point clicks will not be handled for bannerId = $bannerId"
78
+ )
79
+ }
60
80
 
61
81
  // Set height to 0 so BannerView.bind() enters SDK-managed path
62
82
  // and calculates height from dashboard container.height (dp)
@@ -110,9 +130,20 @@ class GamificationBannerViewManager : SimpleViewManager<BannerView>() {
110
130
  }
111
131
 
112
132
  private fun findEntryPointByBannerId(bannerId: String): EntryPointData? {
113
- val flowManager = HydraCore.getFlowManager() ?: return null
133
+ val flowManager = HydraCore.getFlowManager()
134
+ if (flowManager == null) {
135
+ Logger.e("GamificationBannerViewManager.findEntryPointByBannerId", "FlowManager is null, cannot resolve bannerId = $bannerId")
136
+ return null
137
+ }
114
138
  val entryPoints = flowManager.getFilteredEntryPoints(null)
115
- return entryPoints.find { it.mobile?.container?.bannerId == bannerId }
139
+ val entryPoint = entryPoints.find { it.mobile?.container?.bannerId == bannerId }
140
+ if (entryPoint == null) {
141
+ Logger.e(
142
+ "GamificationBannerViewManager.findEntryPointByBannerId",
143
+ "No matching entry point for bannerId = $bannerId"
144
+ )
145
+ }
146
+ return entryPoint
116
147
  }
117
148
 
118
149
  companion object {
@@ -8,6 +8,7 @@ import com.facebook.react.module.annotations.ReactModule
8
8
  import com.facebook.react.uimanager.SimpleViewManager
9
9
  import com.facebook.react.uimanager.ThemedReactContext
10
10
  import com.facebook.react.uimanager.annotations.ReactProp
11
+ import com.capillary.hydra.android.sdk.utils.Logger
11
12
  import com.hydralib.eventemitter.HydraEventEmitter
12
13
  import com.hydralib.eventemitter.HydraEventType
13
14
  import com.hydralib.hydrasdk.HydraCore
@@ -29,11 +30,15 @@ class GamificationEmbedViewManager : SimpleViewManager<EmbedWebView>() {
29
30
 
30
31
  @ReactProp(name = "embedId")
31
32
  fun setEmbedId(view: EmbedWebView, embedId: String?) {
32
- if (embedId.isNullOrEmpty()) return
33
+ if (embedId.isNullOrEmpty()) {
34
+ Logger.e("GamificationEmbedViewManager.setEmbedId", "embedId prop is null or empty, skipping bind")
35
+ return
36
+ }
33
37
  // Guard: don't re-bind if already bound to this embedId. Without this, every RN
34
38
  // re-render (triggered by the height-changed event) calls bindWithLayout again and
35
39
  // accumulates an additional OnPreDrawListener, producing an infinite emit loop.
36
40
  if (view.tag == embedId) return
41
+ // findEntryPointByEmbedId already logs the reason on failure
37
42
  val data = findEntryPointByEmbedId(embedId) ?: return
38
43
 
39
44
  // Defer bind until Yoga has measured and laid out the view
@@ -63,7 +68,11 @@ class GamificationEmbedViewManager : SimpleViewManager<EmbedWebView>() {
63
68
  private fun bindWithLayout(view: EmbedWebView, data: EntryPointData, embedId: String) {
64
69
  val w = view.width
65
70
  val h = view.height
66
- val activity = HydraCore.curReactActivity ?: return
71
+ val activity = HydraCore.curReactActivity
72
+ if (activity == null) {
73
+ Logger.e("GamificationEmbedViewManager.bindWithLayout", "curReactActivity is null, cannot bind embed view")
74
+ return
75
+ }
67
76
 
68
77
  // Sync layoutParams with Yoga's actual pixel dimensions
69
78
  // so SDK sees a positive height and takes host-controlled path
@@ -136,12 +145,24 @@ class GamificationEmbedViewManager : SimpleViewManager<EmbedWebView>() {
136
145
  }
137
146
 
138
147
  private fun findEntryPointByEmbedId(embedId: String): EntryPointData? {
139
- val flowManager = HydraCore.getFlowManager() ?: return null
148
+ val flowManager = HydraCore.getFlowManager()
149
+ if (flowManager == null) {
150
+ Logger.e("GamificationEmbedViewManager.findEntryPointByEmbedId", "FlowManager is null, cannot resolve embedId = $embedId")
151
+ return null
152
+ }
140
153
  val entryPoints = flowManager.getFilteredEntryPoints(null)
141
- return entryPoints.find { it.mobile?.container?.bannerId == embedId }
154
+ val entryPoint = entryPoints.find { it.mobile?.container?.bannerId == embedId }
155
+ if (entryPoint == null) {
156
+ Logger.e(
157
+ "GamificationEmbedViewManager.findEntryPointByEmbedId",
158
+ "No matching entry point for embedId = $embedId"
159
+ )
160
+ }
161
+ return entryPoint
142
162
  }
143
163
 
144
164
  override fun onDropViewInstance(view: EmbedWebView) {
165
+ Logger.d("GamificationEmbedViewManager.onDropViewInstance", "Destroying embed view instance")
145
166
  preDrawListeners.remove(view)?.let { view.viewTreeObserver.removeOnPreDrawListener(it) }
146
167
  view.destroy()
147
168
  super.onDropViewInstance(view)
@@ -7,6 +7,7 @@ import com.facebook.react.module.annotations.ReactModule
7
7
  import com.facebook.react.uimanager.SimpleViewManager
8
8
  import com.facebook.react.uimanager.ThemedReactContext
9
9
  import com.facebook.react.uimanager.annotations.ReactProp
10
+ import com.capillary.hydra.android.sdk.utils.Logger
10
11
  import com.hydralib.hydrasdk.HydraCore
11
12
  import kotlinx.coroutines.CoroutineScope
12
13
  import kotlinx.coroutines.Dispatchers
@@ -22,11 +23,19 @@ class GamificationLoyaltyViewManager : SimpleViewManager<LoyaltyEmbedView>() {
22
23
 
23
24
  @ReactProp(name = "loyaltyId")
24
25
  fun setLoyaltyId(view: LoyaltyEmbedView, loyaltyId: String?) {
25
- if (loyaltyId.isNullOrEmpty()) return
26
+ if (loyaltyId.isNullOrEmpty()) {
27
+ Logger.e("GamificationLoyaltyViewManager.setLoyaltyId", "loyaltyId prop is null or empty, skipping bind")
28
+ return
29
+ }
30
+ // Guard: don't re-bind if already bound to this loyaltyId. Without this, every RN
31
+ // re-render calls bindWithLayout again, re-binding the WebView unnecessarily.
32
+ if (view.tag == loyaltyId) return
33
+ // findLoyaltyUrlById already logs the reason on failure (missing FlowManager or unmatched id)
26
34
  val loyaltyUrl = findLoyaltyUrlById(loyaltyId) ?: return
27
35
 
28
36
  // Defer bind until Yoga has measured and laid out the view
29
37
  if (view.width > 0 && view.height > 0) {
38
+ view.tag = loyaltyId
30
39
  bindWithLayout(view, loyaltyUrl)
31
40
  } else {
32
41
  view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
@@ -36,6 +45,7 @@ class GamificationLoyaltyViewManager : SimpleViewManager<LoyaltyEmbedView>() {
36
45
  ) {
37
46
  if (right - left > 0 && bottom - top > 0) {
38
47
  view.removeOnLayoutChangeListener(this)
48
+ view.tag = loyaltyId
39
49
  bindWithLayout(view, loyaltyUrl)
40
50
  }
41
51
  }
@@ -51,8 +61,16 @@ class GamificationLoyaltyViewManager : SimpleViewManager<LoyaltyEmbedView>() {
51
61
  private fun bindWithLayout(view: LoyaltyEmbedView, loyaltyUrl: LoyaltyUrl) {
52
62
  val w = view.width
53
63
  val h = view.height
54
- val activity = HydraCore.curReactActivity ?: return
55
- val flowManager = HydraCore.getFlowManager() ?: return
64
+ val activity = HydraCore.curReactActivity
65
+ if (activity == null) {
66
+ Logger.e("GamificationLoyaltyViewManager.bindWithLayout", "curReactActivity is null, cannot bind loyalty view")
67
+ return
68
+ }
69
+ val flowManager = HydraCore.getFlowManager()
70
+ if (flowManager == null) {
71
+ Logger.e("GamificationLoyaltyViewManager.bindWithLayout", "FlowManager is null, cannot bind loyalty view")
72
+ return
73
+ }
56
74
 
57
75
  // Sync layoutParams with Yoga's actual pixel dimensions
58
76
  view.layoutParams?.let { lp ->
@@ -62,13 +80,18 @@ class GamificationLoyaltyViewManager : SimpleViewManager<LoyaltyEmbedView>() {
62
80
 
63
81
  // Resync after game played
64
82
  view.onGamePlayed = {
83
+ Logger.d("GamificationLoyaltyViewManager.onGamePlayed", "Game played, resyncing flow manager")
65
84
  CoroutineScope(Dispatchers.Main).launch {
66
85
  HydraCore.getFlowManager()?.resync()
67
86
  }
68
87
  }
69
88
 
70
89
  val capToken = flowManager.getCapToken()
71
- view.bind(loyaltyUrl.url, capToken, activity)
90
+ view.bind(loyaltyUrl.embedId, loyaltyUrl.url, capToken, activity)
91
+ Logger.d(
92
+ "GamificationLoyaltyViewManager.bindWithLayout",
93
+ "Bound loyalty view, url = ${loyaltyUrl.url}, size = ${w}x$h"
94
+ )
72
95
 
73
96
  // Force measure/layout of children — RN parent swallows requestLayout()
74
97
  // so LoyaltyEmbedView's internal WebView never gets measured otherwise
@@ -80,11 +103,23 @@ class GamificationLoyaltyViewManager : SimpleViewManager<LoyaltyEmbedView>() {
80
103
  }
81
104
 
82
105
  private fun findLoyaltyUrlById(loyaltyId: String): LoyaltyUrl? {
83
- val flowManager = HydraCore.getFlowManager() ?: return null
84
- return flowManager.getLoyaltyUrls().find { it.embedId == loyaltyId }
106
+ val flowManager = HydraCore.getFlowManager()
107
+ if (flowManager == null) {
108
+ Logger.e("GamificationLoyaltyViewManager.findLoyaltyUrlById", "FlowManager is null, cannot resolve loyaltyId = $loyaltyId")
109
+ return null
110
+ }
111
+ val loyaltyUrl = flowManager.getLoyaltyUrls().find { it.embedId == loyaltyId }
112
+ if (loyaltyUrl == null) {
113
+ Logger.e(
114
+ "GamificationLoyaltyViewManager.findLoyaltyUrlById",
115
+ "No matching loyalty URL for loyaltyId = $loyaltyId"
116
+ )
117
+ }
118
+ return loyaltyUrl
85
119
  }
86
120
 
87
121
  override fun onDropViewInstance(view: LoyaltyEmbedView) {
122
+ Logger.d("GamificationLoyaltyViewManager.onDropViewInstance", "Destroying loyalty view instance")
88
123
  view.destroy()
89
124
  super.onDropViewInstance(view)
90
125
  }
@@ -17,6 +17,7 @@ import com.capillary.hydra.android.sdk.utils.Logger
17
17
  import com.capillary.hydra.gamification.GamificationCallback
18
18
  import com.capillary.hydra.gamification.GamificationFlowManager
19
19
  import com.capillary.hydra.gamification.GamificationRegion
20
+ import com.capillary.hydra.gamification.ThemeMode
20
21
  import com.capillary.hydra.gamification.ui.GamificationUIManager
21
22
  import com.facebook.react.ReactActivity
22
23
  import com.facebook.react.bridge.Arguments
@@ -246,8 +247,12 @@ object HydraCore {
246
247
  gamificationWriteKey(cgWriteKey)
247
248
  try {
248
249
  gamificationRegion(GamificationRegion.valueOf(cgRegion.uppercase()))
249
- } catch (_: IllegalArgumentException) {
250
-
250
+ } catch (e: IllegalArgumentException) {
251
+ Log.e(
252
+ "HydraCore.initHydraInstance",
253
+ "Invalid gamification region \"$cgRegion\", does not match any GamificationRegion enum value",
254
+ e
255
+ )
251
256
  }
252
257
  }
253
258
  if (cgBaseUrl.isNotEmpty()) {
@@ -761,11 +766,13 @@ object HydraCore {
761
766
  if (hydraAPI == null) {
762
767
  Logger.e("HydraCore.reportUserEvent", "Hydra instance not initialized")
763
768
  promise?.reject("Error Reporting user event", "Hydra instance not initialized", Throwable())
769
+ return
764
770
  }
765
771
 
766
772
  if (eventName == null) {
767
773
  Logger.e("HydraCore.reportUserEvent", "Event name cannot be null")
768
774
  promise?.reject("Error Reporting user event", "Event name cannot be null", Throwable())
775
+ return
769
776
  }
770
777
 
771
778
  hydraAPI!!.pushEvent(eventName!!, attributes?.toMap())
@@ -798,7 +805,10 @@ object HydraCore {
798
805
  Logger.d("HydraCore.getCurrentUserState", "Current user state: $stateString")
799
806
  promise?.resolve(stateString)
800
807
  } catch (e: Exception) {
801
- Logger.e("HydraCore.getCurrentUserState", "Error getting current user state: ${e.message}")
808
+ Logger.e(
809
+ "HydraCore.getCurrentUserState",
810
+ "Error getting current user state: ${e.message}\n${e.stackTraceToString()}"
811
+ )
802
812
  promise?.reject("Error", "Failed to get current user state", e)
803
813
  }
804
814
  }
@@ -815,7 +825,10 @@ object HydraCore {
815
825
  promise?.resolve(null)
816
826
  }
817
827
  } catch (e: Exception) {
818
- Logger.e("HydraCore.getVisitorDetails", "Error getting visitor details: ${e.message}")
828
+ Logger.e(
829
+ "HydraCore.getVisitorDetails",
830
+ "Error getting visitor details: ${e.message}\n${e.stackTraceToString()}"
831
+ )
819
832
  promise?.reject("Error", "Failed to get visitor details", e)
820
833
  }
821
834
  }
@@ -830,7 +843,10 @@ object HydraCore {
830
843
  Logger.d("HydraCore.isVisitorFlowEnabled", "Visitor flow enabled: $isEnabled")
831
844
  promise?.resolve(isEnabled)
832
845
  } catch (e: Exception) {
833
- Logger.e("HydraCore.isVisitorFlowEnabled", "Error checking visitor flow: ${e.message}")
846
+ Logger.e(
847
+ "HydraCore.isVisitorFlowEnabled",
848
+ "Error checking visitor flow: ${e.message}\n${e.stackTraceToString()}"
849
+ )
834
850
  promise?.reject("Error", "Failed to check visitor flow", e)
835
851
  }
836
852
  }
@@ -905,4 +921,43 @@ object HydraCore {
905
921
  promise?.resolve(hydraAPI!!.isGamificationSuppressed())
906
922
  }
907
923
 
924
+ /** Case/whitespace-tolerant lookup, avoids relying on ThemeMode.valueOf()'s exact-match reflection. */
925
+ private fun parseThemeMode(mode: String): ThemeMode? =
926
+ when (mode.trim().uppercase()) {
927
+ "NONE" -> ThemeMode.NONE
928
+ "AUTO" -> ThemeMode.AUTO
929
+ "LIGHT" -> ThemeMode.LIGHT
930
+ "DARK" -> ThemeMode.DARK
931
+ else -> null
932
+ }
933
+
934
+ fun setThemeMode(mode: String?, promise: Promise?) {
935
+ if (mode == null) {
936
+ Logger.e("HydraCore.setThemeMode", "Theme mode is null")
937
+ promise?.reject("Error setting theme mode", "Theme mode is null", Throwable())
938
+ return
939
+ }
940
+
941
+ if (hydraAPI == null) {
942
+ Logger.e("HydraCore.setThemeMode", "HydraAPI is null")
943
+ promise?.reject("Error setting theme mode", "HydraAPI is null", Throwable())
944
+ return
945
+ }
946
+
947
+ val themeMode = parseThemeMode(mode)
948
+ if (themeMode == null) {
949
+ Logger.e("HydraCore.setThemeMode", "Invalid theme mode: $mode")
950
+ promise?.reject("Error setting theme mode", "Invalid theme mode: $mode", Throwable())
951
+ return
952
+ }
953
+
954
+ hydraAPI!!.setThemeMode(themeMode)
955
+ promise?.resolve(null)
956
+ }
957
+
958
+ fun getThemeMode(promise: Promise?) {
959
+ val mode = hydraAPI?.getThemeMode() ?: ThemeMode.NONE
960
+ promise?.resolve(mode.name)
961
+ }
962
+
908
963
  }
@@ -66,33 +66,46 @@ object HydraInAppMessages {
66
66
  message: ReadableMap?,
67
67
  promise: Promise?
68
68
  ) {
69
-
70
- message?.let {
71
- val messageMap = message.toMap()
72
- messageMap["nativeJsonPayload"]?.let {
73
- val jsonString = Gson().toJson(it)
74
- val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
75
- HydraInAppUIHelper.getInstance()
76
- .reportInAppMessageIsClicked(HydraCore.appContext!!, payloadFromJson)
77
- promise?.resolve(true)
78
- }
69
+ if (message == null) {
70
+ Logger.e("HydraInAppMessages.reportMessageClicked", "message is null, cannot report click")
71
+ promise?.reject("Error", "message is null")
72
+ return
73
+ }
74
+ val messageMap = message.toMap()
75
+ val nativeJsonPayload = messageMap["nativeJsonPayload"]
76
+ if (nativeJsonPayload == null) {
77
+ Logger.e("HydraInAppMessages.reportMessageClicked", "nativeJsonPayload missing in message, cannot report click")
78
+ promise?.reject("Error", "nativeJsonPayload missing in message")
79
+ return
79
80
  }
81
+ val jsonString = Gson().toJson(nativeJsonPayload)
82
+ val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
83
+ HydraInAppUIHelper.getInstance()
84
+ .reportInAppMessageIsClicked(HydraCore.appContext!!, payloadFromJson)
85
+ promise?.resolve(true)
80
86
  }
81
87
 
82
88
  fun reportMessageDismissed(
83
89
  message: ReadableMap?,
84
90
  promise: Promise?
85
91
  ) {
86
- message?.let {
87
- val messageMap = message.toMap()
88
- messageMap["nativeJsonPayload"]?.let {
89
- val jsonString = Gson().toJson(it)
90
- val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
91
- HydraInAppUIHelper.getInstance()
92
- .reportInAppMessageDismissed(HydraCore.appContext!!, payloadFromJson)
93
- promise?.resolve(true)
94
- }
92
+ if (message == null) {
93
+ Logger.e("HydraInAppMessages.reportMessageDismissed", "message is null, cannot report dismissal")
94
+ promise?.reject("Error", "message is null")
95
+ return
96
+ }
97
+ val messageMap = message.toMap()
98
+ val nativeJsonPayload = messageMap["nativeJsonPayload"]
99
+ if (nativeJsonPayload == null) {
100
+ Logger.e("HydraInAppMessages.reportMessageDismissed", "nativeJsonPayload missing in message, cannot report dismissal")
101
+ promise?.reject("Error", "nativeJsonPayload missing in message")
102
+ return
95
103
  }
104
+ val jsonString = Gson().toJson(nativeJsonPayload)
105
+ val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
106
+ HydraInAppUIHelper.getInstance()
107
+ .reportInAppMessageDismissed(HydraCore.appContext!!, payloadFromJson)
108
+ promise?.resolve(true)
96
109
  }
97
110
 
98
111
  fun reportMessageRating(
@@ -100,16 +113,23 @@ object HydraInAppMessages {
100
113
  rating: Double,
101
114
  promise: Promise?
102
115
  ) {
103
- message?.let {
104
- val messageMap = message.toMap()
105
- messageMap["nativeJsonPayload"]?.let {
106
- val jsonString = Gson().toJson(it)
107
- val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
108
- HydraInAppUIHelper.getInstance()
109
- .reportInAppMessageRating(HydraCore.appContext!!, payloadFromJson, rating.toInt())
110
- promise?.resolve(true)
111
- }
116
+ if (message == null) {
117
+ Logger.e("HydraInAppMessages.reportMessageRating", "message is null, cannot report rating")
118
+ promise?.reject("Error", "message is null")
119
+ return
120
+ }
121
+ val messageMap = message.toMap()
122
+ val nativeJsonPayload = messageMap["nativeJsonPayload"]
123
+ if (nativeJsonPayload == null) {
124
+ Logger.e("HydraInAppMessages.reportMessageRating", "nativeJsonPayload missing in message, cannot report rating")
125
+ promise?.reject("Error", "nativeJsonPayload missing in message")
126
+ return
112
127
  }
128
+ val jsonString = Gson().toJson(nativeJsonPayload)
129
+ val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
130
+ HydraInAppUIHelper.getInstance()
131
+ .reportInAppMessageRating(HydraCore.appContext!!, payloadFromJson, rating.toInt())
132
+ promise?.resolve(true)
113
133
  }
114
134
 
115
135
  fun reportMessageFeedback(
@@ -118,19 +138,26 @@ object HydraInAppMessages {
118
138
  feedbackOptions: ReadableArray?,
119
139
  promise: Promise?
120
140
  ) {
121
- message?.let {
122
- val messageMap = message.toMap()
123
- messageMap["nativeJsonPayload"]?.let {
124
- val jsonString = Gson().toJson(it)
125
- val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
126
- val feedbackOptionsList = feedbackOptions?.toList()?.mapNotNull { option -> option as? Int } ?: listOf()
127
- HydraInAppUIHelper.getInstance().reportInAppMessageFeedback(
128
- HydraCore.appContext!!,
129
- payloadFromJson,
130
- feedbackOptionsList
131
- )
132
- promise?.resolve(true)
133
- }
141
+ if (message == null) {
142
+ Logger.e("HydraInAppMessages.reportMessageFeedback", "message is null, cannot report feedback")
143
+ promise?.reject("Error", "message is null")
144
+ return
145
+ }
146
+ val messageMap = message.toMap()
147
+ val nativeJsonPayload = messageMap["nativeJsonPayload"]
148
+ if (nativeJsonPayload == null) {
149
+ Logger.e("HydraInAppMessages.reportMessageFeedback", "nativeJsonPayload missing in message, cannot report feedback")
150
+ promise?.reject("Error", "nativeJsonPayload missing in message")
151
+ return
134
152
  }
153
+ val jsonString = Gson().toJson(nativeJsonPayload)
154
+ val payloadFromJson = Gson().fromJson(jsonString, CampaignPayload::class.java)
155
+ val feedbackOptionsList = feedbackOptions?.toList()?.mapNotNull { option -> option as? Int } ?: listOf()
156
+ HydraInAppUIHelper.getInstance().reportInAppMessageFeedback(
157
+ HydraCore.appContext!!,
158
+ payloadFromJson,
159
+ feedbackOptionsList
160
+ )
161
+ promise?.resolve(true)
135
162
  }
136
163
  }
@@ -43,7 +43,7 @@ object HydraInbox {
43
43
  Logger.d("HydraInbox.markInboxMessageAsRead", "Inbox message marked read successfully")
44
44
  return
45
45
  }
46
- Logger.d("HydraInbox.markInboxMessageAsRead", "Invalid inbox message details")
46
+ Logger.e("HydraInbox.markInboxMessageAsRead", "Invalid inbox message details, input message = $inboxMessage")
47
47
  promise?.resolve(false)
48
48
  }
49
49
 
@@ -56,7 +56,7 @@ object HydraInbox {
56
56
  Logger.d("HydraInbox.deleteInboxMessage", "Inbox message deleted successfully")
57
57
  return
58
58
  }
59
- Logger.d("HydraInbox.deleteInboxMessage", "Invalid inbox message details")
59
+ Logger.e("HydraInbox.deleteInboxMessage", "Invalid inbox message details, input message = $inboxMessage")
60
60
  promise?.resolve(false)
61
61
  }
62
62
 
@@ -218,7 +218,7 @@ object HydraPushNotification {
218
218
 
219
219
  fun getFCMToken(promise: Promise?) {
220
220
  if (fcmToken == null) {
221
- Logger.d("HydraPushNotification.getFCMToken", "Token is not available, try later")
221
+ Logger.e("HydraPushNotification.getFCMToken", "Token is not available, try later")
222
222
  promise?.reject("TokenNotSet", "Token is not available, try later")
223
223
  return
224
224
  }