@octopus-community/react-native 1.0.7 → 1.9.1

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 (112) hide show
  1. package/OctopusReactNativeSdk.podspec +1 -1
  2. package/README.md +40 -35
  3. package/android/build.gradle +2 -0
  4. package/android/gradle.properties +2 -2
  5. package/android/src/main/AndroidManifest.xml +2 -1
  6. package/android/src/main/AndroidManifestNew.xml +2 -1
  7. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusActivity.kt +56 -0
  8. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusContent.kt +396 -0
  9. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventEmitter.kt +22 -0
  10. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventSerializer.kt +339 -0
  11. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactModule.kt +326 -0
  12. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/{OctopusReactNativeSdkPackage.kt → OctopusReactPackage.kt} +3 -3
  13. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +53 -9
  14. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +5 -15
  15. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfiguration.kt +6 -0
  16. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfigurationManager.kt +12 -0
  17. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt +17 -2
  18. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIViewManager.kt +63 -0
  19. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt +2 -2
  20. package/ios/OctopusEventManager.swift +27 -0
  21. package/ios/OctopusEventSerializer.swift +271 -0
  22. package/ios/OctopusReactNativeSdk.mm +26 -1
  23. package/ios/OctopusReactNativeSdk.swift +225 -3
  24. package/ios/OctopusSDKInitializer.swift +32 -0
  25. package/ios/OctopusSSOAuthenticator.swift +1 -5
  26. package/ios/OctopusUIConfiguration.swift +6 -0
  27. package/ios/OctopusUIManager.swift +134 -10
  28. package/ios/OctopusUIViewManager.m +7 -0
  29. package/ios/OctopusUIViewManager.swift +37 -0
  30. package/lib/module/OctopusUIView.js +39 -0
  31. package/lib/module/OctopusUIView.js.map +1 -0
  32. package/lib/module/addHasAccessToCommunityListener.js +33 -0
  33. package/lib/module/addHasAccessToCommunityListener.js.map +1 -0
  34. package/lib/module/addNavigateToUrlListener.js +41 -0
  35. package/lib/module/addNavigateToUrlListener.js.map +1 -0
  36. package/lib/module/addNotSeenNotificationsCountListener.js +30 -0
  37. package/lib/module/addNotSeenNotificationsCountListener.js.map +1 -0
  38. package/lib/module/addSDKEventListener.js +48 -0
  39. package/lib/module/addSDKEventListener.js.map +1 -0
  40. package/lib/module/connectUser.js +24 -3
  41. package/lib/module/connectUser.js.map +1 -1
  42. package/lib/module/index.js +12 -0
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/initialize.js +13 -12
  45. package/lib/module/initialize.js.map +1 -1
  46. package/lib/module/openUI.js +23 -2
  47. package/lib/module/openUI.js.map +1 -1
  48. package/lib/module/overrideCommunityAccess.js +36 -0
  49. package/lib/module/overrideCommunityAccess.js.map +1 -0
  50. package/lib/module/overrideDefaultLocale.js +75 -0
  51. package/lib/module/overrideDefaultLocale.js.map +1 -0
  52. package/lib/module/trackCommunityAccess.js +33 -0
  53. package/lib/module/trackCommunityAccess.js.map +1 -0
  54. package/lib/module/trackCustomEvent.js +36 -0
  55. package/lib/module/trackCustomEvent.js.map +1 -0
  56. package/lib/module/types/sdkEvents.js +2 -0
  57. package/lib/module/types/sdkEvents.js.map +1 -0
  58. package/lib/module/types/urlOpeningStrategy.js +23 -0
  59. package/lib/module/types/urlOpeningStrategy.js.map +1 -0
  60. package/lib/module/updateNotSeenNotificationsCount.js +33 -0
  61. package/lib/module/updateNotSeenNotificationsCount.js.map +1 -0
  62. package/lib/typescript/src/OctopusUIView.d.ts +32 -0
  63. package/lib/typescript/src/OctopusUIView.d.ts.map +1 -0
  64. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts +27 -0
  65. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts.map +1 -0
  66. package/lib/typescript/src/addNavigateToUrlListener.d.ts +31 -0
  67. package/lib/typescript/src/addNavigateToUrlListener.d.ts.map +1 -0
  68. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts +24 -0
  69. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts.map +1 -0
  70. package/lib/typescript/src/addSDKEventListener.d.ts +43 -0
  71. package/lib/typescript/src/addSDKEventListener.d.ts.map +1 -0
  72. package/lib/typescript/src/connectUser.d.ts +24 -8
  73. package/lib/typescript/src/connectUser.d.ts.map +1 -1
  74. package/lib/typescript/src/index.d.ts +13 -0
  75. package/lib/typescript/src/index.d.ts.map +1 -1
  76. package/lib/typescript/src/initialize.d.ts +22 -12
  77. package/lib/typescript/src/initialize.d.ts.map +1 -1
  78. package/lib/typescript/src/openUI.d.ts +28 -1
  79. package/lib/typescript/src/openUI.d.ts.map +1 -1
  80. package/lib/typescript/src/overrideCommunityAccess.d.ts +30 -0
  81. package/lib/typescript/src/overrideCommunityAccess.d.ts.map +1 -0
  82. package/lib/typescript/src/overrideDefaultLocale.d.ts +37 -0
  83. package/lib/typescript/src/overrideDefaultLocale.d.ts.map +1 -0
  84. package/lib/typescript/src/trackCommunityAccess.d.ts +27 -0
  85. package/lib/typescript/src/trackCommunityAccess.d.ts.map +1 -0
  86. package/lib/typescript/src/trackCustomEvent.d.ts +30 -0
  87. package/lib/typescript/src/trackCustomEvent.d.ts.map +1 -0
  88. package/lib/typescript/src/types/sdkEvents.d.ts +222 -0
  89. package/lib/typescript/src/types/sdkEvents.d.ts.map +1 -0
  90. package/lib/typescript/src/types/urlOpeningStrategy.d.ts +20 -0
  91. package/lib/typescript/src/types/urlOpeningStrategy.d.ts.map +1 -0
  92. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts +27 -0
  93. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts.map +1 -0
  94. package/package.json +2 -1
  95. package/src/OctopusUIView.tsx +57 -0
  96. package/src/addHasAccessToCommunityListener.ts +38 -0
  97. package/src/addNavigateToUrlListener.ts +54 -0
  98. package/src/addNotSeenNotificationsCountListener.ts +35 -0
  99. package/src/addSDKEventListener.ts +49 -0
  100. package/src/connectUser.ts +24 -8
  101. package/src/index.ts +13 -0
  102. package/src/initialize.ts +23 -12
  103. package/src/openUI.ts +32 -2
  104. package/src/overrideCommunityAccess.ts +33 -0
  105. package/src/overrideDefaultLocale.ts +88 -0
  106. package/src/trackCommunityAccess.ts +30 -0
  107. package/src/trackCustomEvent.ts +36 -0
  108. package/src/types/sdkEvents.ts +315 -0
  109. package/src/types/urlOpeningStrategy.ts +20 -0
  110. package/src/updateNotSeenNotificationsCount.ts +30 -0
  111. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +0 -155
  112. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +0 -422
@@ -1,155 +0,0 @@
1
- package com.octopuscommunity.octopusreactnativesdk
2
-
3
- import com.facebook.react.bridge.ReactApplicationContext
4
- import com.facebook.react.bridge.ReactContextBaseJavaModule
5
- import com.facebook.react.bridge.ReactMethod
6
- import com.facebook.react.bridge.Promise
7
- import com.facebook.react.bridge.ReadableMap
8
-
9
- class OctopusReactNativeSdkModule(reactContext: ReactApplicationContext) :
10
- ReactContextBaseJavaModule(reactContext) {
11
-
12
- private val sdkInitializer = OctopusSDKInitializer()
13
- private val eventEmitter = OctopusEventEmitter(reactContext)
14
- private val uiController = OctopusUIController(reactContext)
15
- private val ssoAuthenticator = OctopusSSOAuthenticator(reactContext, eventEmitter)
16
-
17
- override fun getName(): String = NAME
18
-
19
- @ReactMethod
20
- fun initialize(options: ReadableMap, promise: Promise) {
21
- sdkInitializer.initialize(reactApplicationContext, options, promise)
22
- }
23
-
24
- @ReactMethod
25
- fun openUI(promise: Promise) {
26
- uiController.openUI(promise)
27
- }
28
-
29
- @ReactMethod
30
- fun closeUI(promise: Promise) {
31
- uiController.closeUI(promise)
32
- }
33
-
34
- @ReactMethod
35
- fun connectUser(params: ReadableMap, promise: Promise) {
36
- ssoAuthenticator.connectUser(params, promise)
37
- }
38
-
39
- @ReactMethod
40
- fun disconnectUser(promise: Promise) {
41
- ssoAuthenticator.disconnectUser(promise)
42
- }
43
-
44
- @ReactMethod
45
- fun completeUserTokenRequest(requestId: String, token: String, promise: Promise) {
46
- ssoAuthenticator.completeTokenRequest(requestId, token)
47
- promise.resolve(null)
48
- }
49
-
50
- @ReactMethod
51
- fun cancelUserTokenRequest(requestId: String, promise: Promise) {
52
- ssoAuthenticator.cancelTokenRequest(requestId)
53
- promise.resolve(null)
54
- }
55
-
56
- @ReactMethod
57
- fun addListener(eventName: String) {
58
- eventEmitter.addListener(eventName)
59
- }
60
-
61
- @ReactMethod
62
- fun removeListeners(count: Int) {
63
- eventEmitter.removeListeners(count)
64
- }
65
-
66
- @ReactMethod
67
- fun updateColorScheme(colorScheme: String?, promise: Promise) {
68
- // Update the theme config with the new color scheme
69
- val currentThemeConfig = OctopusThemeManager.getThemeConfig()
70
- if (currentThemeConfig != null) {
71
- val updatedThemeConfig = currentThemeConfig.copy(colorScheme = colorScheme)
72
- OctopusThemeManager.setThemeConfig(updatedThemeConfig)
73
- }
74
- promise.resolve(null)
75
- }
76
-
77
- @ReactMethod
78
- fun updateTheme(themeOptions: ReadableMap, promise: Promise) {
79
- try {
80
- // Parse theme using the standard initializer
81
- val themeConfig = sdkInitializer.parseThemeConfig(themeOptions)
82
- OctopusThemeManager.setThemeConfig(themeConfig)
83
- promise.resolve(null)
84
- } catch (e: Exception) {
85
- promise.reject("UPDATE_THEME_ERROR", "Failed to update theme", e)
86
- }
87
- }
88
-
89
- private fun parseColor(colorString: String?): String? {
90
- if (colorString == null) return null
91
-
92
- return try {
93
- // Validate that the color string is a valid hex color
94
- android.graphics.Color.parseColor(colorString)
95
- // Return the original string if parsing succeeds
96
- colorString
97
- } catch (e: IllegalArgumentException) {
98
- // Invalid color format - return null to skip this color
99
- null
100
- }
101
- }
102
-
103
- private fun parseFontsConfig(fontsMap: ReadableMap): OctopusFontsConfig? {
104
- // Use pre-processed configuration from TypeScript layer
105
- val parsedConfig = fontsMap.getMap("parsedConfig")
106
- if (parsedConfig != null) {
107
- return parsePreProcessedFontsConfig(parsedConfig)
108
- }
109
-
110
- return null
111
- }
112
-
113
- private fun parsePreProcessedFontsConfig(parsedConfig: ReadableMap): OctopusFontsConfig? {
114
- val textStylesMap = parsedConfig.getMap("textStyles")
115
- val textStyles = mutableMapOf<String, OctopusTextStyleConfig>()
116
-
117
- // Parse pre-processed font configuration from TypeScript layer
118
- textStylesMap?.let { textStylesMap ->
119
- val textStyleKeys = arrayOf("title1", "title2", "body1", "body2", "caption1", "caption2")
120
-
121
- textStyleKeys.forEach { key ->
122
- val textStyleMap = textStylesMap.getMap(key)
123
- textStyleMap?.let { style ->
124
- val fontType = style.getString("fontType")
125
- val fontSize = if (style.hasKey("fontSize")) style.getDouble("fontSize") else Double.NaN
126
-
127
- if (fontType != null || (!fontSize.isNaN() && fontSize > 0)) {
128
- textStyles[key] = OctopusTextStyleConfig(
129
- fontType = fontType,
130
- fontSize = if (fontSize.isNaN() || fontSize <= 0) null else fontSize
131
- )
132
- }
133
- }
134
- }
135
- }
136
-
137
- // Only create fonts config if we have text styles
138
- if (textStyles.isNotEmpty()) {
139
- return OctopusFontsConfig(
140
- textStyles = textStyles
141
- )
142
- }
143
-
144
- return null
145
- }
146
-
147
-
148
- companion object {
149
- const val NAME = "OctopusReactNativeSdk"
150
- }
151
-
152
- init {
153
- OctopusEventEmitter.instance = eventEmitter
154
- }
155
- }
@@ -1,422 +0,0 @@
1
- package com.octopuscommunity.octopusreactnativesdk
2
-
3
- import android.content.BroadcastReceiver
4
- import android.content.Context
5
- import android.content.Intent
6
- import android.content.IntentFilter
7
- import android.content.res.Configuration
8
- import android.graphics.BitmapFactory
9
- import android.os.Build
10
- import android.os.Bundle
11
- import android.util.Log
12
- import androidx.activity.ComponentActivity
13
- import androidx.activity.compose.setContent
14
- import androidx.compose.foundation.layout.Box
15
- import androidx.compose.foundation.layout.fillMaxSize
16
- import androidx.compose.runtime.Composable
17
- import androidx.compose.runtime.LaunchedEffect
18
- import androidx.compose.runtime.getValue
19
- import androidx.compose.runtime.mutableStateOf
20
- import androidx.compose.runtime.remember
21
- import androidx.compose.runtime.setValue
22
- import androidx.compose.ui.Modifier
23
- import androidx.compose.ui.graphics.Color
24
- import androidx.compose.ui.graphics.asImageBitmap
25
- import androidx.compose.ui.graphics.painter.BitmapPainter
26
- import androidx.compose.ui.graphics.painter.Painter
27
- import androidx.compose.ui.platform.LocalContext
28
- import androidx.navigation.compose.NavHost
29
- import androidx.navigation.compose.composable
30
- import androidx.navigation.compose.rememberNavController
31
- import com.octopuscommunity.sdk.ui.OctopusDrawablesDefaults
32
- import com.octopuscommunity.sdk.ui.OctopusTheme
33
- import com.octopuscommunity.sdk.ui.OctopusTypographyDefaults
34
- import com.octopuscommunity.sdk.ui.OctopusTypography
35
- import com.octopuscommunity.sdk.ui.components.OctopusNavigationHandler
36
- import androidx.compose.ui.text.TextStyle
37
- import androidx.compose.ui.text.font.FontFamily
38
- import androidx.compose.ui.text.font.FontWeight
39
- import androidx.compose.ui.unit.sp
40
- import com.octopuscommunity.sdk.ui.home.OctopusHomeScreen
41
- import com.octopuscommunity.sdk.ui.octopusComposables
42
- import com.octopuscommunity.sdk.ui.octopusDarkColorScheme
43
- import com.octopuscommunity.sdk.ui.octopusLightColorScheme
44
- import kotlinx.coroutines.Dispatchers
45
- import kotlinx.coroutines.withContext
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
53
-
54
- class OctopusUIActivity : ComponentActivity() {
55
- private val closeUIReceiver = object : BroadcastReceiver() {
56
- override fun onReceive(context: Context?, intent: Intent?) {
57
- finish()
58
- }
59
- }
60
-
61
- override fun onCreate(savedInstanceState: Bundle?) {
62
- super.onCreate(savedInstanceState)
63
- registerCloseUIReceiver()
64
- setContent {
65
- OctopusUI(onBack = { finish() })
66
- }
67
- }
68
-
69
- override fun onDestroy() {
70
- super.onDestroy()
71
- unregisterReceiver(closeUIReceiver)
72
- }
73
-
74
- private fun registerCloseUIReceiver() {
75
- val intentFilter = IntentFilter(OctopusUIController.CLOSE_UI_ACTION)
76
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
77
- registerReceiver(closeUIReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
78
- } else {
79
- @Suppress("UnspecifiedRegisterReceiverFlag")
80
- registerReceiver(closeUIReceiver, intentFilter)
81
- }
82
- }
83
- }
84
-
85
- @Composable
86
- private fun OctopusUI(onBack: () -> Unit) {
87
- val context = LocalContext.current
88
-
89
- // Get theme config once when UI is created - no need for polling
90
- val themeConfig = OctopusThemeManager.getThemeConfig()
91
-
92
- // Function to detect if system is in dark mode
93
- fun isSystemInDarkTheme(): Boolean {
94
- return (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
95
- }
96
-
97
- // Determine base color scheme based on theme config or system theme
98
- val baseColorScheme = if (themeConfig?.colorScheme == "dark") {
99
- octopusDarkColorScheme()
100
- } else if (themeConfig?.colorScheme == "light") {
101
- octopusLightColorScheme()
102
- } else if (isSystemInDarkTheme()) {
103
- octopusDarkColorScheme()
104
- } else {
105
- octopusLightColorScheme()
106
- }
107
-
108
- // Apply custom colors if theme config is provided
109
- val finalColorScheme = if (themeConfig != null) {
110
- baseColorScheme.copy(
111
- primary = themeConfig.primaryColor?.let {
112
- Color(android.graphics.Color.parseColor(it))
113
- } ?: baseColorScheme.primary,
114
- primaryLow = themeConfig.primaryLowContrastColor?.let {
115
- Color(android.graphics.Color.parseColor(it))
116
- } ?: baseColorScheme.primaryLow,
117
- primaryHigh = themeConfig.primaryHighContrastColor?.let {
118
- Color(android.graphics.Color.parseColor(it))
119
- } ?: baseColorScheme.primaryHigh,
120
- onPrimary = themeConfig.onPrimaryColor?.let {
121
- Color(android.graphics.Color.parseColor(it))
122
- } ?: baseColorScheme.onPrimary
123
- )
124
- } else {
125
- baseColorScheme
126
- }
127
-
128
- // Handle logo loading using built-in Android capabilities
129
- var logoPainter by remember { mutableStateOf<Painter?>(null) }
130
-
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
141
- logoPainter = null
142
- }
143
- }
144
-
145
- // Create drawables based on theme config
146
- val drawables = if (logoPainter != null) {
147
- OctopusDrawablesDefaults.drawables(logo = logoPainter)
148
- } else {
149
- OctopusDrawablesDefaults.drawables()
150
- }
151
-
152
- // Create typography based on theme config
153
- val typography = createCustomTypography(themeConfig)
154
-
155
- // Apply theme with custom colors, logo, and typography
156
- OctopusTheme(
157
- colorScheme = finalColorScheme,
158
- drawables = drawables,
159
- typography = typography
160
- ) {
161
- OctopusUIContent(onBack = onBack)
162
- }
163
- }
164
-
165
- private fun createCustomTypography(themeConfig: OctopusThemeConfig?): OctopusTypography {
166
- val defaultTypography = OctopusTypographyDefaults.typography()
167
-
168
- if (themeConfig?.fonts == null) {
169
- return defaultTypography
170
- }
171
-
172
- val fontsConfig = themeConfig.fonts!!
173
- val textStyles = fontsConfig.textStyles
174
-
175
- // Create custom typography based on new unified font configuration
176
- if (textStyles != null && textStyles.isNotEmpty()) {
177
- return OctopusTypographyDefaults.typography(
178
- title1 = createTextStyle(textStyles["title1"], defaultTypography.title1),
179
- title2 = createTextStyle(textStyles["title2"], defaultTypography.title2),
180
- body1 = createTextStyle(textStyles["body1"], defaultTypography.body1),
181
- body2 = createTextStyle(textStyles["body2"], defaultTypography.body2),
182
- caption1 = createTextStyle(textStyles["caption1"], defaultTypography.caption1),
183
- caption2 = createTextStyle(textStyles["caption2"], defaultTypography.caption2)
184
- )
185
- }
186
-
187
-
188
- return defaultTypography
189
- }
190
-
191
- private fun createTextStyle(textStyleConfig: OctopusTextStyleConfig?, defaultStyle: TextStyle): TextStyle {
192
- if (textStyleConfig == null) {
193
- return defaultStyle
194
- }
195
-
196
- val fontFamily = when (textStyleConfig.fontType) {
197
- "serif" -> FontFamily.Serif
198
- "monospace" -> FontFamily.Monospace
199
- "default" -> FontFamily.Default
200
- else -> FontFamily.Default
201
- }
202
-
203
- val fontSize = textStyleConfig.fontSize?.let {
204
- // Use points directly as sp (1 point ≈ 1 sp on Android)
205
- it.sp
206
- } ?: defaultStyle.fontSize
207
-
208
- return defaultStyle.copy(
209
- fontFamily = fontFamily,
210
- fontSize = fontSize
211
- )
212
- }
213
-
214
-
215
- @Composable
216
- private fun OctopusUIContent(onBack: () -> Unit) {
217
- Box(modifier = Modifier.fillMaxSize()) {
218
- val navController = rememberNavController()
219
-
220
- NavHost(
221
- navController = navController,
222
- startDestination = "OctopusHome"
223
- ) {
224
- composable(route = "OctopusHome") {
225
- OctopusNavigationHandler(
226
- navigateToLogin = {
227
- OctopusEventEmitter.instance?.emitLoginRequired()
228
- },
229
- navigateToProfileEdit = { fieldToEdit ->
230
- OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
231
- },
232
- navigateToClientObject = {
233
- // TODO : Bridge Post react
234
- }
235
- ) {
236
- OctopusHomeScreen(
237
- navController = navController,
238
- backIcon = true,
239
- onBack = onBack
240
- )
241
- }
242
- }
243
- octopusComposables(
244
- navController = navController,
245
- onNavigateToLogin = {
246
- OctopusEventEmitter.instance?.emitLoginRequired()
247
- },
248
- onNavigateToProfileEdit = { fieldToEdit ->
249
- OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
250
- }
251
- )
252
- }
253
- }
254
- }
255
-
256
- private suspend fun loadImageFromUri(uri: String, context: Context): Painter? {
257
- // Check cache first
258
- imageCache[uri]?.let { return it }
259
-
260
- return withContext(Dispatchers.IO) {
261
- try {
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
- }
272
- }
273
-
274
- // Cache result (including null to avoid retrying failed loads)
275
- imageCache[uri] = result
276
- result
277
- } catch (e: Exception) {
278
- Log.w("OctopusUIActivity", "Failed to load image from URI: $uri", e)
279
- imageCache[uri] = null
280
- null
281
- }
282
- }
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
- }