@octopus-community/react-native 1.0.5 → 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
+ }
@@ -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.5",
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",