@octopus-community/react-native 1.0.4 → 1.0.6

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.
@@ -107,10 +107,12 @@ class OctopusSSOAuthenticator(private val eventEmitter: OctopusEventEmitter) {
107
107
  } else null
108
108
 
109
109
  val profilePicture = profileParams.getString("profilePicture")?.let { pictureUrl ->
110
- if (pictureUrl.startsWith("http://") || pictureUrl.startsWith("https://")) {
110
+ if (pictureUrl.isNotBlank() && (pictureUrl.startsWith("http://") || pictureUrl.startsWith("https://"))) {
111
111
  Image.Remote(pictureUrl)
112
- } else {
112
+ } else if (pictureUrl.isNotBlank()) {
113
113
  Image.Local(pictureUrl)
114
+ } else {
115
+ null
114
116
  }
115
117
  }
116
118
 
@@ -44,6 +44,12 @@ import com.octopuscommunity.sdk.ui.octopusLightColorScheme
44
44
  import kotlinx.coroutines.Dispatchers
45
45
  import kotlinx.coroutines.withContext
46
46
  import java.net.URL
47
+ import java.util.concurrent.ConcurrentHashMap
48
+
49
+ // Cache for loaded images to avoid reloading on recomposition
50
+ private val imageCache = ConcurrentHashMap<String, Painter?>()
51
+ // Cache for drawable resources list to avoid repeated reflection
52
+ private var cachedDrawableResources: List<String>? = null
47
53
 
48
54
  class OctopusUIActivity : ComponentActivity() {
49
55
  private val closeUIReceiver = object : BroadcastReceiver() {
@@ -79,7 +85,7 @@ class OctopusUIActivity : ComponentActivity() {
79
85
  @Composable
80
86
  private fun OctopusUI(onBack: () -> Unit) {
81
87
  val context = LocalContext.current
82
-
88
+
83
89
  // Get theme config once when UI is created - no need for polling
84
90
  val themeConfig = OctopusThemeManager.getThemeConfig()
85
91
 
@@ -122,19 +128,19 @@ private fun OctopusUI(onBack: () -> Unit) {
122
128
  // Handle logo loading using built-in Android capabilities
123
129
  var logoPainter by remember { mutableStateOf<Painter?>(null) }
124
130
 
125
- LaunchedEffect(themeConfig) {
126
- themeConfig?.logoSource?.let { logoSource ->
127
- val uri = logoSource.getString("uri")
128
- if (uri != null) {
129
- logoPainter = loadImageFromUri(uri)
130
- } else {
131
+ LaunchedEffect(themeConfig) {
132
+ themeConfig?.logoSource?.let { logoSource ->
133
+ val uri = logoSource.getString("uri")
134
+ if (uri != null) {
135
+ logoPainter = loadImageFromUri(uri, context)
136
+ } else {
137
+ logoPainter = null
138
+ }
139
+ } ?: run {
140
+ // No theme config or no logo source
131
141
  logoPainter = null
132
142
  }
133
- } ?: run {
134
- // No theme config or no logo source
135
- logoPainter = null
136
143
  }
137
- }
138
144
 
139
145
  // Create drawables based on theme config
140
146
  val drawables = if (logoPainter != null) {
@@ -158,14 +164,14 @@ private fun OctopusUI(onBack: () -> Unit) {
158
164
 
159
165
  private fun createCustomTypography(themeConfig: OctopusThemeConfig?): OctopusTypography {
160
166
  val defaultTypography = OctopusTypographyDefaults.typography()
161
-
167
+
162
168
  if (themeConfig?.fonts == null) {
163
169
  return defaultTypography
164
170
  }
165
-
171
+
166
172
  val fontsConfig = themeConfig.fonts!!
167
173
  val textStyles = fontsConfig.textStyles
168
-
174
+
169
175
  // Create custom typography based on new unified font configuration
170
176
  if (textStyles != null && textStyles.isNotEmpty()) {
171
177
  return OctopusTypographyDefaults.typography(
@@ -177,8 +183,8 @@ private fun createCustomTypography(themeConfig: OctopusThemeConfig?): OctopusTyp
177
183
  caption2 = createTextStyle(textStyles["caption2"], defaultTypography.caption2)
178
184
  )
179
185
  }
180
-
181
-
186
+
187
+
182
188
  return defaultTypography
183
189
  }
184
190
 
@@ -186,19 +192,19 @@ private fun createTextStyle(textStyleConfig: OctopusTextStyleConfig?, defaultSty
186
192
  if (textStyleConfig == null) {
187
193
  return defaultStyle
188
194
  }
189
-
195
+
190
196
  val fontFamily = when (textStyleConfig.fontType) {
191
197
  "serif" -> FontFamily.Serif
192
198
  "monospace" -> FontFamily.Monospace
193
199
  "default" -> FontFamily.Default
194
200
  else -> FontFamily.Default
195
201
  }
196
-
197
- val fontSize = textStyleConfig.fontSize?.let {
202
+
203
+ val fontSize = textStyleConfig.fontSize?.let {
198
204
  // Use points directly as sp (1 point ≈ 1 sp on Android)
199
205
  it.sp
200
206
  } ?: defaultStyle.fontSize
201
-
207
+
202
208
  return defaultStyle.copy(
203
209
  fontFamily = fontFamily,
204
210
  fontSize = fontSize
@@ -247,21 +253,170 @@ private fun OctopusUIContent(onBack: () -> Unit) {
247
253
  }
248
254
  }
249
255
 
250
- private suspend fun loadImageFromUri(uri: String): Painter? {
256
+ private suspend fun loadImageFromUri(uri: String, context: Context): Painter? {
257
+ // Check cache first
258
+ imageCache[uri]?.let { return it }
259
+
251
260
  return withContext(Dispatchers.IO) {
252
261
  try {
253
- val url = URL(uri)
254
- val inputStream = url.openStream()
255
- val bitmap = BitmapFactory.decodeStream(inputStream)
256
- inputStream.close()
257
-
258
- if (bitmap != null) {
259
- BitmapPainter(bitmap.asImageBitmap())
260
- } else {
261
- null
262
+ val result = when {
263
+ // Network URLs - load directly
264
+ uri.startsWith("http://", ignoreCase = true) ||
265
+ uri.startsWith("https://", ignoreCase = true) -> {
266
+ loadFromNetwork(uri)
267
+ }
268
+ // Everything else - try as React Native asset
269
+ else -> {
270
+ loadReactNativeAsset(uri, context)
271
+ }
262
272
  }
273
+
274
+ // Cache result (including null to avoid retrying failed loads)
275
+ imageCache[uri] = result
276
+ result
263
277
  } catch (e: Exception) {
278
+ Log.w("OctopusUIActivity", "Failed to load image from URI: $uri", e)
279
+ imageCache[uri] = null
264
280
  null
265
281
  }
266
282
  }
267
- }
283
+ }
284
+
285
+ private fun loadFromNetwork(url: String): Painter? {
286
+ return try {
287
+ URL(url).openStream().use { inputStream ->
288
+ val bitmap = BitmapFactory.decodeStream(inputStream)
289
+ bitmap?.let { BitmapPainter(it.asImageBitmap()) }
290
+ }
291
+ } catch (e: Exception) {
292
+ Log.w("OctopusUIActivity", "Failed to load image from network: $url", e)
293
+ null
294
+ }
295
+ }
296
+
297
+ private fun loadReactNativeAsset(assetName: String, context: Context): Painter? {
298
+ return try {
299
+ // First try: Direct drawable resource lookup (most common case)
300
+ val drawableResult = loadFromDrawableResources(assetName, context)
301
+ if (drawableResult != null) {
302
+ return drawableResult
303
+ }
304
+
305
+ // Second try: Assets folder with strategic path checking
306
+ loadFromAssetsFolder(assetName, context)
307
+ } catch (e: Exception) {
308
+ Log.w("OctopusUIActivity", "Failed to load asset: $assetName", e)
309
+ null
310
+ }
311
+ }
312
+
313
+ private fun loadFromAssetsFolder(assetName: String, context: Context): Painter? {
314
+ // Check if assetName already has extension
315
+ val hasExtension = assetName.contains(".")
316
+ val extensions = if (hasExtension) listOf("") else listOf("png", "jpg", "jpeg", "webp")
317
+
318
+ val folders = listOf(
319
+ "",
320
+ "drawable-mdpi",
321
+ "drawable-hdpi",
322
+ "drawable-xhdpi",
323
+ "drawable-xxhdpi",
324
+ "drawable-xxxhdpi",
325
+ "drawable"
326
+ )
327
+
328
+ for (folder in folders) {
329
+ for (ext in extensions) {
330
+ val filename = if (ext.isEmpty()) assetName else "$assetName.$ext"
331
+ val path = if (folder.isEmpty()) filename else "$folder/$filename"
332
+
333
+ try {
334
+ context.assets.open(path).use { inputStream ->
335
+ val bitmap = BitmapFactory.decodeStream(inputStream)
336
+ if (bitmap != null) {
337
+ return BitmapPainter(bitmap.asImageBitmap())
338
+ }
339
+ }
340
+ } catch (e: Exception) {
341
+ // Continue to next path
342
+ }
343
+ }
344
+ }
345
+
346
+ return null
347
+ }
348
+
349
+ private fun loadFromDrawableResources(assetName: String, context: Context): Painter? {
350
+ return try {
351
+ // Try exact match first
352
+ val resourceId = context.resources.getIdentifier(
353
+ assetName, "drawable", context.packageName
354
+ )
355
+ if (resourceId != 0) {
356
+ return loadBitmapFromResource(context, resourceId)
357
+ }
358
+
359
+ // Fallback: fuzzy search (only if exact match fails)
360
+ val drawableResources = getAllDrawableResources(context)
361
+ for (resourceName in drawableResources) {
362
+ if (isResourceNameMatch(resourceName, assetName)) {
363
+ val resId = context.resources.getIdentifier(
364
+ resourceName, "drawable", context.packageName
365
+ )
366
+ if (resId != 0) {
367
+ return loadBitmapFromResource(context, resId)
368
+ }
369
+ }
370
+ }
371
+
372
+ null
373
+ } catch (e: Exception) {
374
+ null
375
+ }
376
+ }
377
+
378
+ private fun loadBitmapFromResource(context: Context, resourceId: Int): Painter? {
379
+ return try {
380
+ val bitmap = BitmapFactory.decodeResource(context.resources, resourceId)
381
+ bitmap?.let { BitmapPainter(it.asImageBitmap()) }
382
+ } catch (e: Exception) {
383
+ null
384
+ }
385
+ }
386
+
387
+ private fun getAllDrawableResources(context: Context): List<String> {
388
+ // Return cached result if available
389
+ cachedDrawableResources?.let { return it }
390
+
391
+ return try {
392
+ val packageName = context.packageName
393
+ val drawableResources = mutableListOf<String>()
394
+
395
+ // Get all drawable resources by scanning the R.drawable class
396
+ val drawableClass = Class.forName("$packageName.R\$drawable")
397
+ val fields = drawableClass.declaredFields
398
+
399
+ for (field in fields) {
400
+ if (field.type == Int::class.javaPrimitiveType) {
401
+ drawableResources.add(field.name)
402
+ }
403
+ }
404
+
405
+ // Cache for future use
406
+ cachedDrawableResources = drawableResources
407
+ drawableResources
408
+ } catch (e: Exception) {
409
+ Log.w("OctopusUIActivity", "Failed to get drawable resources", e)
410
+ emptyList()
411
+ }
412
+ }
413
+
414
+
415
+ private fun isResourceNameMatch(resourceName: String, assetName: String): Boolean {
416
+ val normalizedResourceName = resourceName.lowercase()
417
+ val normalizedAssetName = assetName.lowercase()
418
+
419
+ // Only check if one contains the other - removed dangerous generic patterns
420
+ return normalizedResourceName.contains(normalizedAssetName) ||
421
+ normalizedAssetName.contains(normalizedResourceName)
422
+ }
@@ -2,16 +2,16 @@ import React
2
2
  import Octopus
3
3
 
4
4
  class OctopusEventManager {
5
- private weak var eventEmitter: RCTEventEmitter?
5
+ private weak var bridge: RCTBridge?
6
6
  private var hasListeners = false
7
7
 
8
- init(eventEmitter: RCTEventEmitter) {
9
- self.eventEmitter = eventEmitter
8
+ init(bridge: RCTBridge?) {
9
+ self.bridge = bridge
10
10
  }
11
11
 
12
12
  func emitLoginRequired() {
13
13
  if hasListeners {
14
- eventEmitter?.sendEvent(withName: "loginRequired", body: nil)
14
+ bridge?.eventDispatcher().sendAppEvent(withName: "loginRequired", body: nil)
15
15
  }
16
16
  }
17
17
 
@@ -19,14 +19,14 @@ class OctopusEventManager {
19
19
  if hasListeners {
20
20
  let fieldToEdit = ProfileFieldMapper.toReactNativeString(profileField)
21
21
  let eventBody = ["fieldToEdit": fieldToEdit]
22
- eventEmitter?.sendEvent(withName: "editUser", body: eventBody)
22
+ bridge?.eventDispatcher().sendAppEvent(withName: "editUser", body: eventBody)
23
23
  }
24
24
  }
25
25
 
26
26
  func emitUserTokenRequest(requestId: String) {
27
27
  if hasListeners {
28
28
  let eventBody = ["requestId": requestId]
29
- eventEmitter?.sendEvent(withName: "userTokenRequest", body: eventBody)
29
+ bridge?.eventDispatcher().sendAppEvent(withName: "userTokenRequest", body: eventBody)
30
30
  }
31
31
  }
32
32
 
@@ -37,8 +37,4 @@ class OctopusEventManager {
37
37
  func stopObserving() {
38
38
  hasListeners = false
39
39
  }
40
-
41
- func supportedEvents() -> [String] {
42
- return ["loginRequired", "editUser", "userTokenRequest"]
43
- }
44
40
  }
@@ -1,3 +1,2 @@
1
1
  #import <React/RCTBridgeModule.h>
2
2
  #import <React/RCTViewManager.h>
3
- #import <React/RCTEventEmitter.h>
@@ -1,7 +1,6 @@
1
1
  #import <React/RCTBridgeModule.h>
2
- #import <React/RCTEventEmitter.h>
3
2
 
4
- @interface RCT_EXTERN_MODULE(OctopusReactNativeSdk, RCTEventEmitter)
3
+ @interface RCT_EXTERN_MODULE(OctopusReactNativeSdk, NSObject)
5
4
 
6
5
  RCT_EXTERN_METHOD(initialize:(NSDictionary *)options
7
6
  withResolver:(RCTPromiseResolveBlock)resolve
@@ -37,6 +36,10 @@ RCT_EXTERN_METHOD(updateTheme:(NSDictionary *)themeOptions
37
36
  withResolver:(RCTPromiseResolveBlock)resolve
38
37
  withRejecter:(RCTPromiseRejectBlock)reject)
39
38
 
39
+ RCT_EXTERN_METHOD(addListener:(NSString *)eventName)
40
+
41
+ RCT_EXTERN_METHOD(removeListeners:(NSInteger)count)
42
+
40
43
  + (BOOL)requiresMainQueueSetup
41
44
  {
42
45
  return NO;
@@ -2,20 +2,33 @@ import Octopus
2
2
  import OctopusUI
3
3
  import SwiftUI
4
4
  import UIKit
5
+ import React
5
6
 
6
7
  @objc(OctopusReactNativeSdk)
7
- class OctopusReactNativeSdk: RCTEventEmitter {
8
+ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
8
9
 
9
10
  // MARK: - Properties
10
11
 
11
12
  private var octopusSDK: OctopusSDK?
12
13
  private lazy var uiManager = OctopusUIManager()
13
- private lazy var eventManager = OctopusEventManager(eventEmitter: self)
14
+ private lazy var eventManager = OctopusEventManager(bridge: bridge)
14
15
  private let sdkInitializer = OctopusSDKInitializer()
15
16
  private var ssoAuthenticator: OctopusSSOAuthenticator?
16
17
  private var theme: OctopusTheme?
17
18
  private var logoSource: [String: Any]?
18
19
  private var fontConfiguration: [String: Any]?
20
+
21
+ // MARK: - RCTBridgeModule
22
+
23
+ @objc var bridge: RCTBridge!
24
+
25
+ @objc static func moduleName() -> String! {
26
+ return "OctopusReactNativeSdk"
27
+ }
28
+
29
+ @objc static func requiresMainQueueSetup() -> Bool {
30
+ return false
31
+ }
19
32
 
20
33
  // MARK: - Initialization
21
34
 
@@ -151,22 +164,19 @@ class OctopusReactNativeSdk: RCTEventEmitter {
151
164
  octopusSDK = nil
152
165
  }
153
166
 
154
- @objc override func invalidate() {
167
+ @objc func invalidate() {
155
168
  cleanup()
156
- super.invalidate()
157
169
  }
158
-
159
- // MARK: - RCTEventEmitter overrides
160
-
161
- @objc override func supportedEvents() -> [String]! {
162
- return eventManager.supportedEvents()
163
- }
164
-
165
- @objc override func startObserving() {
170
+
171
+ // MARK: - Event Listener Support for NativeEventEmitter
172
+
173
+ @objc func addListener(_ eventName: String) {
174
+ // Start observing when listeners are added
166
175
  eventManager.startObserving()
167
176
  }
168
-
169
- @objc override func stopObserving() {
177
+
178
+ @objc func removeListeners(_ count: Int) {
179
+ // Stop observing when listeners are removed
170
180
  eventManager.stopObserving()
171
181
  }
172
182
  }
@@ -87,7 +87,7 @@ class OctopusSSOAuthenticator {
87
87
  let ageInformation: ClientUser.AgeInformation? = legalAgeReached != nil ? (legalAgeReached! ? .legalAgeReached : .underaged) : nil
88
88
 
89
89
  var profilePictureData: Data? = nil
90
- if let pictureUrl = profileParams["profilePicture"] as? String {
90
+ if let pictureUrl = profileParams["profilePicture"] as? String, !pictureUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
91
91
  profilePictureData = try await loadImageData(from: pictureUrl)
92
92
  }
93
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octopus-community/react-native",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "React Native module for the Octopus Community SDK",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -67,7 +67,7 @@
67
67
  "@evilmartians/lefthook": "^1.5.0",
68
68
  "@react-native/eslint-config": "^0.78.0",
69
69
  "@types/jest": "^29.5.5",
70
- "@types/react": "^19.0.0",
70
+ "@types/react": "^19.1.0",
71
71
  "commitlint": "^19.6.1",
72
72
  "del-cli": "^5.1.0",
73
73
  "eslint": "^9.22.0",
@@ -75,8 +75,8 @@
75
75
  "eslint-plugin-prettier": "^5.2.3",
76
76
  "jest": "^29.7.0",
77
77
  "prettier": "^3.0.3",
78
- "react": "19.0.0",
79
- "react-native": "0.78.2",
78
+ "react": "19.1.0",
79
+ "react-native": "^0.81.4",
80
80
  "react-native-builder-bob": "^0.40.12",
81
81
  "turbo": "^1.10.7",
82
82
  "typedoc": "^0.28.7",