@octopus-community/react-native 1.0.5 → 1.0.7
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.
- package/README.md +4 -4
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +11 -11
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +21 -21
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +21 -2
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +186 -31
- package/ios/OctopusSSOAuthenticator.swift +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ await initialize({
|
|
|
115
115
|
onPrimary: '#FFFFFF', // Text color on primary background
|
|
116
116
|
},
|
|
117
117
|
logo: {
|
|
118
|
-
image: Image.resolveAssetSource(require('./assets/logo.png')), // Your custom logo
|
|
118
|
+
image: Image.resolveAssetSource(require('./assets/images/logo.png')), // Your custom logo
|
|
119
119
|
},
|
|
120
120
|
}
|
|
121
121
|
});
|
|
@@ -186,7 +186,7 @@ await initialize({
|
|
|
186
186
|
onPrimary: '#FFFFFF', // Text color on primary background
|
|
187
187
|
},
|
|
188
188
|
logo: {
|
|
189
|
-
image: Image.resolveAssetSource(require('./assets/logo.png')),
|
|
189
|
+
image: Image.resolveAssetSource(require('./assets/images/logo.png')),
|
|
190
190
|
},
|
|
191
191
|
}
|
|
192
192
|
});
|
|
@@ -351,7 +351,7 @@ await initialize({
|
|
|
351
351
|
},
|
|
352
352
|
// Custom logo
|
|
353
353
|
logo: {
|
|
354
|
-
image: Image.resolveAssetSource(require('./assets/logo.png')),
|
|
354
|
+
image: Image.resolveAssetSource(require('./assets/images/logo.png')),
|
|
355
355
|
},
|
|
356
356
|
}
|
|
357
357
|
});
|
|
@@ -408,7 +408,7 @@ const switchToGreenTheme = async () => {
|
|
|
408
408
|
- `caption1`: Small captions (default: 12pt)
|
|
409
409
|
- `caption2`: Extra small captions (default: 10pt)
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
|
|
412
412
|
|
|
413
413
|
**Supported Formats:**
|
|
414
414
|
- **Colors**: 3-digit (`#F63`), 6-digit (`#FF6633`), 8-digit (`#FF6633FF`) hex codes
|
|
@@ -12,7 +12,7 @@ class OctopusReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
12
12
|
private val sdkInitializer = OctopusSDKInitializer()
|
|
13
13
|
private val eventEmitter = OctopusEventEmitter(reactContext)
|
|
14
14
|
private val uiController = OctopusUIController(reactContext)
|
|
15
|
-
private val ssoAuthenticator = OctopusSSOAuthenticator(eventEmitter)
|
|
15
|
+
private val ssoAuthenticator = OctopusSSOAuthenticator(reactContext, eventEmitter)
|
|
16
16
|
|
|
17
17
|
override fun getName(): String = NAME
|
|
18
18
|
|
|
@@ -85,10 +85,10 @@ class OctopusReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
85
85
|
promise.reject("UPDATE_THEME_ERROR", "Failed to update theme", e)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
private fun parseColor(colorString: String?): String? {
|
|
90
90
|
if (colorString == null) return null
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
return try {
|
|
93
93
|
// Validate that the color string is a valid hex color
|
|
94
94
|
android.graphics.Color.parseColor(colorString)
|
|
@@ -99,31 +99,31 @@ class OctopusReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
99
99
|
null
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
private fun parseFontsConfig(fontsMap: ReadableMap): OctopusFontsConfig? {
|
|
104
104
|
// Use pre-processed configuration from TypeScript layer
|
|
105
105
|
val parsedConfig = fontsMap.getMap("parsedConfig")
|
|
106
106
|
if (parsedConfig != null) {
|
|
107
107
|
return parsePreProcessedFontsConfig(parsedConfig)
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
return null
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
private fun parsePreProcessedFontsConfig(parsedConfig: ReadableMap): OctopusFontsConfig? {
|
|
114
114
|
val textStylesMap = parsedConfig.getMap("textStyles")
|
|
115
115
|
val textStyles = mutableMapOf<String, OctopusTextStyleConfig>()
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
// Parse pre-processed font configuration from TypeScript layer
|
|
118
118
|
textStylesMap?.let { textStylesMap ->
|
|
119
119
|
val textStyleKeys = arrayOf("title1", "title2", "body1", "body2", "caption1", "caption2")
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
textStyleKeys.forEach { key ->
|
|
122
122
|
val textStyleMap = textStylesMap.getMap(key)
|
|
123
123
|
textStyleMap?.let { style ->
|
|
124
124
|
val fontType = style.getString("fontType")
|
|
125
125
|
val fontSize = if (style.hasKey("fontSize")) style.getDouble("fontSize") else Double.NaN
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
if (fontType != null || (!fontSize.isNaN() && fontSize > 0)) {
|
|
128
128
|
textStyles[key] = OctopusTextStyleConfig(
|
|
129
129
|
fontType = fontType,
|
|
@@ -133,14 +133,14 @@ class OctopusReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
// Only create fonts config if we have text styles
|
|
138
138
|
if (textStyles.isNotEmpty()) {
|
|
139
139
|
return OctopusFontsConfig(
|
|
140
140
|
textStyles = textStyles
|
|
141
141
|
)
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
return null
|
|
145
145
|
}
|
|
146
146
|
|
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt
CHANGED
|
@@ -18,11 +18,11 @@ class OctopusSDKInitializer {
|
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
20
|
val connectionMode = parseConnectionMode(options)
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
// Store theme configuration for later use in UI
|
|
23
23
|
val themeConfig = parseThemeConfig(options)
|
|
24
24
|
OctopusThemeManager.setThemeConfig(themeConfig)
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
OctopusSDK.initialize(
|
|
27
27
|
context = context,
|
|
28
28
|
apiKey = apiKey,
|
|
@@ -35,19 +35,19 @@ class OctopusSDKInitializer {
|
|
|
35
35
|
promise.reject("INITIALIZE_ERROR", "Failed to initialize Octopus SDK", e)
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
fun parseThemeConfig(options: ReadableMap): OctopusThemeConfig? {
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
val themeMap = options.getMap("theme")
|
|
42
42
|
val colorScheme = options.getString("colorScheme") // Get colorScheme from React Native
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
var primaryColor: String? = null
|
|
45
45
|
var primaryLowContrastColor: String? = null
|
|
46
46
|
var primaryHighContrastColor: String? = null
|
|
47
47
|
var onPrimaryColor: String? = null
|
|
48
48
|
var logoSource: ReadableMap? = null
|
|
49
49
|
var fontsConfig: OctopusFontsConfig? = null
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
// Parse colors from theme if available
|
|
52
52
|
themeMap?.let { theme ->
|
|
53
53
|
val colorsMap = theme.getMap("colors")
|
|
@@ -55,7 +55,7 @@ class OctopusSDKInitializer {
|
|
|
55
55
|
// Check if this is a dual-mode theme (has light and dark properties)
|
|
56
56
|
val lightColors = colors.getMap("light")
|
|
57
57
|
val darkColors = colors.getMap("dark")
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
if (lightColors != null && darkColors != null) {
|
|
60
60
|
// Dual-mode theme - select colors based on current color scheme
|
|
61
61
|
val selectedColors = if (colorScheme == "dark") darkColors else lightColors
|
|
@@ -71,7 +71,7 @@ class OctopusSDKInitializer {
|
|
|
71
71
|
onPrimaryColor = parseColor(if (colors.hasKey("onPrimary")) colors.getString("onPrimary") else null)
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
// Handle logo from theme
|
|
76
76
|
val logoMap = theme.getMap("logo")
|
|
77
77
|
logoMap?.let { logo ->
|
|
@@ -80,7 +80,7 @@ class OctopusSDKInitializer {
|
|
|
80
80
|
logoSource = imageSource
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
// Handle fonts from theme - use pre-processed configuration from TypeScript layer
|
|
85
85
|
val fontsMap = theme.getMap("fonts")
|
|
86
86
|
fontsMap?.let { fonts ->
|
|
@@ -90,7 +90,7 @@ class OctopusSDKInitializer {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
// Handle logo at root level (for backward compatibility)
|
|
95
95
|
if (logoSource == null) {
|
|
96
96
|
val rootLogoMap = options.getMap("logo")
|
|
@@ -101,7 +101,7 @@ class OctopusSDKInitializer {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
// Only create theme config if we have at least one customization
|
|
106
106
|
if (primaryColor != null || logoSource != null || colorScheme != null || fontsConfig != null) {
|
|
107
107
|
return OctopusThemeConfig(
|
|
@@ -114,24 +114,24 @@ class OctopusSDKInitializer {
|
|
|
114
114
|
fonts = fontsConfig
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
return null
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
private fun parsePreProcessedFontsConfig(parsedConfig: ReadableMap): OctopusFontsConfig? {
|
|
122
122
|
val textStylesMap = parsedConfig.getMap("textStyles")
|
|
123
123
|
val textStyles = mutableMapOf<String, OctopusTextStyleConfig>()
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
// Parse pre-processed font configuration from TypeScript layer
|
|
126
126
|
textStylesMap?.let { textStylesMap ->
|
|
127
127
|
val textStyleKeys = arrayOf("title1", "title2", "body1", "body2", "caption1", "caption2")
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
textStyleKeys.forEach { key ->
|
|
130
130
|
val textStyleMap = textStylesMap.getMap(key)
|
|
131
131
|
textStyleMap?.let { style ->
|
|
132
132
|
val fontType = style.getString("fontType")
|
|
133
133
|
val fontSize = if (style.hasKey("fontSize")) style.getDouble("fontSize") else Double.NaN
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
if (fontType != null || (!fontSize.isNaN() && fontSize > 0)) {
|
|
136
136
|
textStyles[key] = OctopusTextStyleConfig(
|
|
137
137
|
fontType = fontType,
|
|
@@ -141,18 +141,18 @@ class OctopusSDKInitializer {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
// Only create fonts config if we have text styles
|
|
146
146
|
if (textStyles.isNotEmpty()) {
|
|
147
147
|
return OctopusFontsConfig(
|
|
148
148
|
textStyles = textStyles
|
|
149
149
|
)
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
return null
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
private fun extractResourceNameFromUri(uri: String): String? {
|
|
157
157
|
// Extract resource name from React Native image URI
|
|
158
158
|
// Examples: "logo.png" -> "logo", "images/logo.png" -> "logo"
|
|
@@ -163,10 +163,10 @@ class OctopusSDKInitializer {
|
|
|
163
163
|
null
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
private fun parseColor(colorString: String?): String? {
|
|
168
168
|
if (colorString == null) return null
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
return try {
|
|
171
171
|
// Validate that the color string is a valid hex color
|
|
172
172
|
Color.parseColor(colorString)
|
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt
CHANGED
|
@@ -15,8 +15,13 @@ import java.util.UUID
|
|
|
15
15
|
import java.util.concurrent.ConcurrentHashMap
|
|
16
16
|
import kotlin.coroutines.resume
|
|
17
17
|
import kotlin.coroutines.resumeWithException
|
|
18
|
+
import android.content.Context
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
class OctopusSSOAuthenticator(
|
|
22
|
+
private val context: Context,
|
|
23
|
+
private val eventEmitter: OctopusEventEmitter
|
|
24
|
+
) {
|
|
20
25
|
|
|
21
26
|
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
22
27
|
private val pendingTokenRequests =
|
|
@@ -109,8 +114,22 @@ class OctopusSSOAuthenticator(private val eventEmitter: OctopusEventEmitter) {
|
|
|
109
114
|
val profilePicture = profileParams.getString("profilePicture")?.let { pictureUrl ->
|
|
110
115
|
if (pictureUrl.startsWith("http://") || pictureUrl.startsWith("https://")) {
|
|
111
116
|
Image.Remote(pictureUrl)
|
|
112
|
-
} else {
|
|
117
|
+
} else if (pictureUrl.startsWith("file") || pictureUrl.startsWith("android.resource")) {
|
|
113
118
|
Image.Local(pictureUrl)
|
|
119
|
+
} else if(pictureUrl.isNotBlank()) {
|
|
120
|
+
// Asset names like "assets_images_logo" need conversion
|
|
121
|
+
// Convert to drawable resource URI
|
|
122
|
+
val resourceId = context.resources.getIdentifier(pictureUrl, "drawable", context.packageName)
|
|
123
|
+
Image.Local(
|
|
124
|
+
if (resourceId != 0) {
|
|
125
|
+
"android.resource://${context.packageName}/$resourceId"
|
|
126
|
+
} else {
|
|
127
|
+
// Fallback: try assets folder
|
|
128
|
+
"file:///android_asset/$pictureUrl"
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
} else {
|
|
132
|
+
null
|
|
114
133
|
}
|
|
115
134
|
}
|
|
116
135
|
|
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt
CHANGED
|
@@ -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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|