@octopus-community/react-native 1.0.2 → 1.0.4

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.
@@ -30,6 +30,13 @@ import androidx.navigation.compose.composable
30
30
  import androidx.navigation.compose.rememberNavController
31
31
  import com.octopuscommunity.sdk.ui.OctopusDrawablesDefaults
32
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
33
40
  import com.octopuscommunity.sdk.ui.home.OctopusHomeScreen
34
41
  import com.octopuscommunity.sdk.ui.octopusComposables
35
42
  import com.octopuscommunity.sdk.ui.octopusDarkColorScheme
@@ -71,72 +78,134 @@ class OctopusUIActivity : ComponentActivity() {
71
78
 
72
79
  @Composable
73
80
  private fun OctopusUI(onBack: () -> Unit) {
74
- val themeConfig = OctopusThemeManager.getThemeConfig()
75
81
  val context = LocalContext.current
82
+
83
+ // Get theme config once when UI is created - no need for polling
84
+ val themeConfig = OctopusThemeManager.getThemeConfig()
76
85
 
77
86
  // Function to detect if system is in dark mode
78
87
  fun isSystemInDarkTheme(): Boolean {
79
88
  return (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
80
89
  }
81
90
 
82
- // Only apply custom theming if theme config is provided
83
- if (themeConfig != null) {
84
- // Create color scheme based on theme config
85
- val colorScheme = if (isSystemInDarkTheme()) {
86
- octopusDarkColorScheme()
87
- } else {
88
- octopusLightColorScheme()
89
- }.let { octopusColorScheme ->
90
- octopusColorScheme.copy(
91
- primary = themeConfig.primaryColor?.let {
92
- Color(android.graphics.Color.parseColor(it))
93
- } ?: octopusColorScheme.primary,
94
- primaryLow = themeConfig.primaryLowContrastColor?.let {
95
- Color(android.graphics.Color.parseColor(it))
96
- } ?: octopusColorScheme.primaryLow,
97
- primaryHigh = themeConfig.primaryHighContrastColor?.let {
98
- Color(android.graphics.Color.parseColor(it))
99
- } ?: octopusColorScheme.primaryHigh,
100
- onPrimary = themeConfig.onPrimaryColor?.let {
101
- Color(android.graphics.Color.parseColor(it))
102
- } ?: octopusColorScheme.onPrimary
103
- )
104
- }
91
+ // Determine base color scheme based on theme config or system theme
92
+ val baseColorScheme = if (themeConfig?.colorScheme == "dark") {
93
+ octopusDarkColorScheme()
94
+ } else if (themeConfig?.colorScheme == "light") {
95
+ octopusLightColorScheme()
96
+ } else if (isSystemInDarkTheme()) {
97
+ octopusDarkColorScheme()
98
+ } else {
99
+ octopusLightColorScheme()
100
+ }
101
+
102
+ // Apply custom colors if theme config is provided
103
+ val finalColorScheme = if (themeConfig != null) {
104
+ baseColorScheme.copy(
105
+ primary = themeConfig.primaryColor?.let {
106
+ Color(android.graphics.Color.parseColor(it))
107
+ } ?: baseColorScheme.primary,
108
+ primaryLow = themeConfig.primaryLowContrastColor?.let {
109
+ Color(android.graphics.Color.parseColor(it))
110
+ } ?: baseColorScheme.primaryLow,
111
+ primaryHigh = themeConfig.primaryHighContrastColor?.let {
112
+ Color(android.graphics.Color.parseColor(it))
113
+ } ?: baseColorScheme.primaryHigh,
114
+ onPrimary = themeConfig.onPrimaryColor?.let {
115
+ Color(android.graphics.Color.parseColor(it))
116
+ } ?: baseColorScheme.onPrimary
117
+ )
118
+ } else {
119
+ baseColorScheme
120
+ }
105
121
 
106
- // Handle logo loading using built-in Android capabilities
107
- var logoPainter by remember { mutableStateOf<Painter?>(null) }
122
+ // Handle logo loading using built-in Android capabilities
123
+ var logoPainter by remember { mutableStateOf<Painter?>(null) }
108
124
 
109
- LaunchedEffect(themeConfig.logoSource) {
110
- themeConfig.logoSource?.let { logoSource ->
111
- val uri = logoSource.getString("uri")
112
- if (uri != null) {
113
- logoPainter = loadImageFromUri(uri)
114
- } else {
115
- logoPainter = null
116
- }
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
+ logoPainter = null
117
132
  }
133
+ } ?: run {
134
+ // No theme config or no logo source
135
+ logoPainter = null
118
136
  }
137
+ }
119
138
 
120
- // Create drawables based on theme config
121
- val drawables = if (logoPainter != null) {
122
- OctopusDrawablesDefaults.drawables(logo = logoPainter)
123
- } else {
124
- OctopusDrawablesDefaults.drawables()
125
- }
126
-
127
- // Apply custom theme
128
- OctopusTheme(
129
- colorScheme = colorScheme,
130
- drawables = drawables
131
- ) {
132
- OctopusUIContent(onBack = onBack)
133
- }
139
+ // Create drawables based on theme config
140
+ val drawables = if (logoPainter != null) {
141
+ OctopusDrawablesDefaults.drawables(logo = logoPainter)
134
142
  } else {
135
- // No theme config - let the native SDK inherit the React Native app's theme colors
143
+ OctopusDrawablesDefaults.drawables()
144
+ }
145
+
146
+ // Create typography based on theme config
147
+ val typography = createCustomTypography(themeConfig)
148
+
149
+ // Apply theme with custom colors, logo, and typography
150
+ OctopusTheme(
151
+ colorScheme = finalColorScheme,
152
+ drawables = drawables,
153
+ typography = typography
154
+ ) {
136
155
  OctopusUIContent(onBack = onBack)
137
156
  }
138
157
  }
139
158
 
159
+ private fun createCustomTypography(themeConfig: OctopusThemeConfig?): OctopusTypography {
160
+ val defaultTypography = OctopusTypographyDefaults.typography()
161
+
162
+ if (themeConfig?.fonts == null) {
163
+ return defaultTypography
164
+ }
165
+
166
+ val fontsConfig = themeConfig.fonts!!
167
+ val textStyles = fontsConfig.textStyles
168
+
169
+ // Create custom typography based on new unified font configuration
170
+ if (textStyles != null && textStyles.isNotEmpty()) {
171
+ return OctopusTypographyDefaults.typography(
172
+ title1 = createTextStyle(textStyles["title1"], defaultTypography.title1),
173
+ title2 = createTextStyle(textStyles["title2"], defaultTypography.title2),
174
+ body1 = createTextStyle(textStyles["body1"], defaultTypography.body1),
175
+ body2 = createTextStyle(textStyles["body2"], defaultTypography.body2),
176
+ caption1 = createTextStyle(textStyles["caption1"], defaultTypography.caption1),
177
+ caption2 = createTextStyle(textStyles["caption2"], defaultTypography.caption2)
178
+ )
179
+ }
180
+
181
+
182
+ return defaultTypography
183
+ }
184
+
185
+ private fun createTextStyle(textStyleConfig: OctopusTextStyleConfig?, defaultStyle: TextStyle): TextStyle {
186
+ if (textStyleConfig == null) {
187
+ return defaultStyle
188
+ }
189
+
190
+ val fontFamily = when (textStyleConfig.fontType) {
191
+ "serif" -> FontFamily.Serif
192
+ "monospace" -> FontFamily.Monospace
193
+ "default" -> FontFamily.Default
194
+ else -> FontFamily.Default
195
+ }
196
+
197
+ val fontSize = textStyleConfig.fontSize?.let {
198
+ // Use points directly as sp (1 point ≈ 1 sp on Android)
199
+ it.sp
200
+ } ?: defaultStyle.fontSize
201
+
202
+ return defaultStyle.copy(
203
+ fontFamily = fontFamily,
204
+ fontSize = fontSize
205
+ )
206
+ }
207
+
208
+
140
209
  @Composable
141
210
  private fun OctopusUIContent(onBack: () -> Unit) {
142
211
  Box(modifier = Modifier.fillMaxSize()) {
@@ -147,11 +216,23 @@ private fun OctopusUIContent(onBack: () -> Unit) {
147
216
  startDestination = "OctopusHome"
148
217
  ) {
149
218
  composable(route = "OctopusHome") {
150
- OctopusHomeScreen(
151
- navController = navController,
152
- backIcon = true,
153
- onBack = onBack
154
- )
219
+ OctopusNavigationHandler(
220
+ navigateToLogin = {
221
+ OctopusEventEmitter.instance?.emitLoginRequired()
222
+ },
223
+ navigateToProfileEdit = { fieldToEdit ->
224
+ OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
225
+ },
226
+ navigateToClientObject = {
227
+ // TODO : Bridge Post react
228
+ }
229
+ ) {
230
+ OctopusHomeScreen(
231
+ navController = navController,
232
+ backIcon = true,
233
+ onBack = onBack
234
+ )
235
+ }
155
236
  }
156
237
  octopusComposables(
157
238
  navController = navController,
@@ -177,11 +258,9 @@ private suspend fun loadImageFromUri(uri: String): Painter? {
177
258
  if (bitmap != null) {
178
259
  BitmapPainter(bitmap.asImageBitmap())
179
260
  } else {
180
- Log.w("OctopusUI", "Failed to decode bitmap from URI: $uri")
181
261
  null
182
262
  }
183
263
  } catch (e: Exception) {
184
- Log.e("OctopusUI", "Error loading image from URI: $uri", e)
185
264
  null
186
265
  }
187
266
  }
@@ -29,6 +29,14 @@ RCT_EXTERN_METHOD(cancelUserTokenRequest:(NSString *)requestId
29
29
  RCT_EXTERN_METHOD(disconnectUser:(RCTPromiseResolveBlock)resolve
30
30
  withRejecter:(RCTPromiseRejectBlock)reject)
31
31
 
32
+ RCT_EXTERN_METHOD(updateColorScheme:(NSString *)colorScheme
33
+ withResolver:(RCTPromiseResolveBlock)resolve
34
+ withRejecter:(RCTPromiseRejectBlock)reject)
35
+
36
+ RCT_EXTERN_METHOD(updateTheme:(NSDictionary *)themeOptions
37
+ withResolver:(RCTPromiseResolveBlock)resolve
38
+ withRejecter:(RCTPromiseRejectBlock)reject)
39
+
32
40
  + (BOOL)requiresMainQueueSetup
33
41
  {
34
42
  return NO;
@@ -15,6 +15,7 @@ class OctopusReactNativeSdk: RCTEventEmitter {
15
15
  private var ssoAuthenticator: OctopusSSOAuthenticator?
16
16
  private var theme: OctopusTheme?
17
17
  private var logoSource: [String: Any]?
18
+ private var fontConfiguration: [String: Any]?
18
19
 
19
20
  // MARK: - Initialization
20
21
 
@@ -25,6 +26,7 @@ class OctopusReactNativeSdk: RCTEventEmitter {
25
26
  self.ssoAuthenticator = OctopusSSOAuthenticator(octopusSDK: self.octopusSDK!, eventManager: eventManager)
26
27
  self.theme = sdkInitializer.parseTheme(from: options)
27
28
  self.logoSource = sdkInitializer.getLogoSource(from: options)
29
+ self.fontConfiguration = sdkInitializer.getFontConfiguration(from: options)
28
30
  resolve(nil)
29
31
  } catch {
30
32
  reject("INITIALIZE_ERROR", "Failed to initialize Octopus SDK: \(error.localizedDescription)", error)
@@ -100,7 +102,7 @@ class OctopusReactNativeSdk: RCTEventEmitter {
100
102
 
101
103
  DispatchQueue.main.async {
102
104
  do {
103
- try self.uiManager.openUI(octopus: octopus, theme: self.theme, logoSource: self.logoSource)
105
+ try self.uiManager.openUI(octopus: octopus, theme: self.theme, logoSource: self.logoSource, fontConfiguration: self.fontConfiguration)
104
106
  resolve(nil)
105
107
  } catch {
106
108
  reject("OPEN_UI_ERROR", error.localizedDescription, error)
@@ -120,6 +122,24 @@ class OctopusReactNativeSdk: RCTEventEmitter {
120
122
  }
121
123
  }
122
124
 
125
+ @objc(updateColorScheme:withResolver:withRejecter:)
126
+ func updateColorScheme(colorScheme: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
127
+ // iOS uses adaptive colors that automatically respond to system appearance changes
128
+ // No manual updates needed - the theme is applied when UI opens
129
+ resolve(nil)
130
+ }
131
+
132
+ @objc(updateTheme:withResolver:withRejecter:)
133
+ func updateTheme(themeOptions: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
134
+ do {
135
+ self.theme = sdkInitializer.parseTheme(from: themeOptions)
136
+ resolve(nil)
137
+ } catch {
138
+ reject("UPDATE_THEME_ERROR", "Failed to update theme: \(error.localizedDescription)", error)
139
+ }
140
+ }
141
+
142
+
123
143
  // MARK: - Lifecycle management
124
144
 
125
145
  deinit {
@@ -22,52 +22,123 @@ class OctopusSDKInitializer {
22
22
  }
23
23
 
24
24
  var colors: OctopusTheme.Colors?
25
+ var fonts: OctopusTheme.Fonts?
25
26
 
26
27
  // Parse colors
27
28
  if let colorsMap = themeMap["colors"] as? [String: Any] {
28
29
  var primarySet: OctopusTheme.Colors.ColorSet?
29
30
 
30
- if let primary = colorsMap["primary"] as? String,
31
- let primaryColor = OctopusColorUtility.color(fromHex: primary) {
32
- let lowContrast = (colorsMap["primaryLowContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? primaryColor
33
- let highContrast = (colorsMap["primaryHighContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.black
34
- let onPrimary = (colorsMap["onPrimary"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.white
35
-
36
- primarySet = OctopusTheme.Colors.ColorSet(
37
- main: Color(uiColor: primaryColor),
38
- lowContrast: Color(uiColor: lowContrast),
39
- highContrast: Color(uiColor: highContrast)
40
- )
41
-
42
- colors = OctopusTheme.Colors(
43
- primarySet: primarySet,
44
- onPrimary: Color(uiColor: onPrimary)
45
- )
31
+ // Check if this is a dual-mode theme (has light and dark properties)
32
+ if let lightColors = colorsMap["light"] as? [String: Any],
33
+ let darkColors = colorsMap["dark"] as? [String: Any] {
34
+ // Dual-mode theme - create adaptive colors that automatically respond to system appearance
35
+ if let lightPrimary = lightColors["primary"] as? String,
36
+ let darkPrimary = darkColors["primary"] as? String,
37
+ let lightPrimaryColor = OctopusColorUtility.color(fromHex: lightPrimary),
38
+ let darkPrimaryColor = OctopusColorUtility.color(fromHex: darkPrimary) {
39
+
40
+ // Create adaptive colors using UIColor's dynamic color capabilities
41
+ let adaptivePrimary = UIColor { traitCollection in
42
+ traitCollection.userInterfaceStyle == .dark ? darkPrimaryColor : lightPrimaryColor
43
+ }
44
+
45
+ let lightLowContrast = (lightColors["primaryLowContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? lightPrimaryColor
46
+ let darkLowContrast = (darkColors["primaryLowContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? darkPrimaryColor
47
+ let adaptiveLowContrast = UIColor { traitCollection in
48
+ traitCollection.userInterfaceStyle == .dark ? darkLowContrast : lightLowContrast
49
+ }
50
+
51
+ let lightHighContrast = (lightColors["primaryHighContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.black
52
+ let darkHighContrast = (darkColors["primaryHighContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.black
53
+ let adaptiveHighContrast = UIColor { traitCollection in
54
+ traitCollection.userInterfaceStyle == .dark ? darkHighContrast : lightHighContrast
55
+ }
56
+
57
+ let lightOnPrimary = (lightColors["onPrimary"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.white
58
+ let darkOnPrimary = (darkColors["onPrimary"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.black
59
+ let adaptiveOnPrimary = UIColor { traitCollection in
60
+ traitCollection.userInterfaceStyle == .dark ? darkOnPrimary : lightOnPrimary
61
+ }
62
+
63
+ primarySet = OctopusTheme.Colors.ColorSet(
64
+ main: Color(uiColor: adaptivePrimary),
65
+ lowContrast: Color(uiColor: adaptiveLowContrast),
66
+ highContrast: Color(uiColor: adaptiveHighContrast)
67
+ )
68
+
69
+ colors = OctopusTheme.Colors(
70
+ primarySet: primarySet,
71
+ onPrimary: Color(uiColor: adaptiveOnPrimary)
72
+ )
73
+ }
74
+ } else {
75
+ // Single-mode theme (backward compatibility)
76
+ if let primary = colorsMap["primary"] as? String,
77
+ let primaryColor = OctopusColorUtility.color(fromHex: primary) {
78
+ let lowContrast = (colorsMap["primaryLowContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? primaryColor
79
+ let highContrast = (colorsMap["primaryHighContrast"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.black
80
+ let onPrimary = (colorsMap["onPrimary"] as? String).flatMap(OctopusColorUtility.color(fromHex:)) ?? UIColor.white
81
+
82
+ primarySet = OctopusTheme.Colors.ColorSet(
83
+ main: Color(uiColor: primaryColor),
84
+ lowContrast: Color(uiColor: lowContrast),
85
+ highContrast: Color(uiColor: highContrast)
86
+ )
87
+
88
+ colors = OctopusTheme.Colors(
89
+ primarySet: primarySet,
90
+ onPrimary: Color(uiColor: onPrimary)
91
+ )
92
+ }
46
93
  }
47
94
  }
48
95
 
96
+ // Parse fonts
97
+ if let fontsMap = themeMap["fonts"] as? [String: Any] {
98
+ // Font customization will be applied through OctopusTypography
99
+ // This is parsed here but applied separately in the UI manager
100
+ fonts = OctopusTheme.Fonts()
101
+ }
102
+
49
103
  // Note: Logo will be handled separately in UI manager due to async loading requirements
50
- // Only create theme if we have colors, otherwise return nil
51
- guard let colors = colors else {
104
+ // Only create theme if we have colors or fonts, otherwise return nil
105
+ guard colors != nil || fonts != nil else {
52
106
  return nil
53
107
  }
54
108
 
55
109
  return OctopusTheme(
56
- colors: colors,
57
- fonts: OctopusTheme.Fonts(),
110
+ colors: colors ?? OctopusTheme.Colors(),
111
+ fonts: fonts ?? OctopusTheme.Fonts(),
58
112
  assets: OctopusTheme.Assets()
59
113
  )
60
114
  }
61
115
 
62
116
  func getLogoSource(from options: [String: Any]) -> [String: Any]? {
117
+ // First try to get logo from theme
118
+ if let themeMap = options["theme"] as? [String: Any],
119
+ let logoMap = themeMap["logo"] as? [String: Any],
120
+ let imageSource = logoMap["image"] as? [String: Any] {
121
+ return imageSource
122
+ }
123
+
124
+ // Fallback: try to get logo from root level (for backward compatibility)
125
+ if let logoMap = options["logo"] as? [String: Any],
126
+ let imageSource = logoMap["image"] as? [String: Any] {
127
+ return imageSource
128
+ }
129
+
130
+ return nil
131
+ }
132
+
133
+ func getFontConfiguration(from options: [String: Any]) -> [String: Any]? {
63
134
  guard let themeMap = options["theme"] as? [String: Any],
64
- let logoMap = themeMap["logo"] as? [String: Any] else {
135
+ let fontsMap = themeMap["fonts"] as? [String: Any] else {
65
136
  return nil
66
137
  }
67
138
 
68
- // Only support Image.resolveAssetSource() approach
69
- if let imageSource = logoMap["image"] as? [String: Any] {
70
- return imageSource
139
+ // Use pre-processed configuration from TypeScript layer
140
+ if let parsedConfig = fontsMap["parsedConfig"] as? [String: Any] {
141
+ return ["parsedConfig": parsedConfig]
71
142
  }
72
143
 
73
144
  return nil
@@ -7,19 +7,24 @@ import React
7
7
  class OctopusUIManager {
8
8
  private weak var presentedViewController: UIViewController?
9
9
 
10
- func openUI(octopus: OctopusSDK, theme: OctopusUI.OctopusTheme?, logoSource: [String: Any]?) throws {
10
+ func openUI(octopus: OctopusSDK, theme: OctopusUI.OctopusTheme?, logoSource: [String: Any]?, fontConfiguration: [String: Any]?) throws {
11
11
  guard let presentingViewController = RCTPresentedViewController() else {
12
12
  throw NSError(domain: "OPEN_UI_ERROR", code: 0, userInfo: [NSLocalizedDescriptionKey: "Could not find presenting view controller"])
13
13
  }
14
14
 
15
+ // Create custom theme with font configuration
16
+ let customTheme = createCustomTheme(baseTheme: theme, fontConfiguration: fontConfiguration)
17
+
15
18
  let octopusHomeScreen = OctopusHomeScreen(octopus: octopus)
19
+ .environment(\.octopusTheme, customTheme)
20
+
16
21
  let hostingController = UIHostingController(rootView: AnyView(octopusHomeScreen))
17
22
  hostingController.modalPresentationStyle = .fullScreen
18
23
 
19
24
  // Apply theme if provided
20
- if let theme = theme {
25
+ if let _ = theme {
21
26
  // Apply theme immediately (without logo) to avoid delay
22
- hostingController.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, theme))
27
+ hostingController.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, customTheme))
23
28
 
24
29
  // Then load logo asynchronously and update theme if logo loads
25
30
  if let logoSource = logoSource {
@@ -27,8 +32,8 @@ class OctopusUIManager {
27
32
  DispatchQueue.main.async {
28
33
  if let logoImage = logoImage {
29
34
  let updatedTheme = OctopusUI.OctopusTheme(
30
- colors: theme.colors,
31
- fonts: theme.fonts,
35
+ colors: customTheme.colors,
36
+ fonts: customTheme.fonts,
32
37
  assets: OctopusUI.OctopusTheme.Assets(logo: logoImage)
33
38
  )
34
39
  hostingController?.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, updatedTheme))
@@ -38,6 +43,20 @@ class OctopusUIManager {
38
43
  }
39
44
  }
40
45
  }
46
+ } else if let logoSource = logoSource {
47
+ // No theme but there's a logo - load it and create a theme with just the logo
48
+ loadLogo(from: logoSource) { [weak hostingController] logoImage in
49
+ DispatchQueue.main.async {
50
+ if let logoImage = logoImage {
51
+ let logoTheme = OctopusUI.OctopusTheme(
52
+ colors: OctopusUI.OctopusTheme.Colors(),
53
+ fonts: OctopusUI.OctopusTheme.Fonts(),
54
+ assets: OctopusUI.OctopusTheme.Assets(logo: logoImage)
55
+ )
56
+ hostingController?.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, logoTheme))
57
+ }
58
+ }
59
+ }
41
60
  }
42
61
 
43
62
  presentedViewController = hostingController
@@ -70,18 +89,43 @@ class OctopusUIManager {
70
89
  }.resume()
71
90
  } else {
72
91
  // Local file path (from Image.resolveAssetSource)
73
- if let image = UIImage(contentsOfFile: uri) {
92
+ if let image = loadImageFromLocalPath(uri) {
74
93
  completion(image)
75
94
  } else {
76
- // Fallback: try to load from bundle using the filename
77
- let filename = (uri as NSString).lastPathComponent
78
- let nameWithoutExtension = (filename as NSString).deletingPathExtension
79
- let image = UIImage(named: nameWithoutExtension)
80
- completion(image)
95
+ completion(nil)
81
96
  }
82
97
  }
83
98
  }
84
99
 
100
+ private func loadImageFromLocalPath(_ path: String) -> UIImage? {
101
+ // Handle absolute file URLs (file://) first
102
+ if path.hasPrefix("file://"), let url = URL(string: path),
103
+ let image = UIImage(contentsOfFile: url.path) {
104
+ return image
105
+ }
106
+
107
+ // Try raw absolute path next
108
+ if path.hasPrefix("/"), FileManager.default.fileExists(atPath: path),
109
+ let image = UIImage(contentsOfFile: path) {
110
+ return image
111
+ }
112
+
113
+ // For React Native bundled assets the path is relative (e.g. assets/assets/logo.png)
114
+ let nsPath = path as NSString
115
+ let directory = nsPath.deletingLastPathComponent
116
+ let filename = nsPath.lastPathComponent
117
+ let nameWithoutExtension = (filename as NSString).deletingPathExtension
118
+ let fileExtension = (filename as NSString).pathExtension
119
+
120
+ if let bundlePath = Bundle.main.path(forResource: nameWithoutExtension, ofType: fileExtension.isEmpty ? nil : fileExtension, inDirectory: directory.isEmpty ? nil : directory) ,
121
+ let image = UIImage(contentsOfFile: bundlePath) {
122
+ return image
123
+ }
124
+
125
+ // Final fallback: let UIKit try to resolve it by name (works for assets catalog entries)
126
+ return UIImage(named: nameWithoutExtension)
127
+ }
128
+
85
129
  func closeUI() throws {
86
130
  guard let presentedVC = presentedViewController else {
87
131
  throw NSError(domain: "CLOSE_UI_ERROR", code: 0, userInfo: [NSLocalizedDescriptionKey: "No UI is currently presented"])
@@ -98,4 +142,55 @@ class OctopusUIManager {
98
142
  presentedViewController = nil
99
143
  }
100
144
  }
145
+
146
+ private func createCustomTheme(baseTheme: OctopusUI.OctopusTheme?, fontConfiguration: [String: Any]?) -> OctopusUI.OctopusTheme {
147
+ // If no font configuration, return the base theme or default
148
+ guard let fontConfig = fontConfiguration else {
149
+ return baseTheme ?? OctopusUI.OctopusTheme()
150
+ }
151
+
152
+ // Use pre-processed configuration from TypeScript layer
153
+ if let parsedConfig = fontConfig["parsedConfig"] as? [String: Any],
154
+ let textStyles = parsedConfig["textStyles"] as? [String: [String: Any]] {
155
+
156
+ let customFonts = OctopusUI.OctopusTheme.Fonts(
157
+ title1: createFontFromPreProcessedStyle(textStyles["title1"], defaultSize: 28),
158
+ title2: createFontFromPreProcessedStyle(textStyles["title2"], defaultSize: 24),
159
+ body1: createFontFromPreProcessedStyle(textStyles["body1"], defaultSize: 18),
160
+ body2: createFontFromPreProcessedStyle(textStyles["body2"], defaultSize: 16),
161
+ caption1: createFontFromPreProcessedStyle(textStyles["caption1"], defaultSize: 12),
162
+ caption2: createFontFromPreProcessedStyle(textStyles["caption2"], defaultSize: 10),
163
+ navBarItem: createFontFromPreProcessedStyle(textStyles["body1"], defaultSize: 17) // Use body1 style for nav
164
+ )
165
+
166
+ return OctopusUI.OctopusTheme(
167
+ colors: baseTheme?.colors ?? OctopusUI.OctopusTheme.Colors(),
168
+ fonts: customFonts,
169
+ assets: baseTheme?.assets ?? OctopusUI.OctopusTheme.Assets()
170
+ )
171
+ }
172
+
173
+ return baseTheme ?? OctopusUI.OctopusTheme()
174
+ }
175
+
176
+ private func createFontFromPreProcessedStyle(_ textStyle: [String: Any]?, defaultSize: CGFloat) -> Font {
177
+ guard let textStyle = textStyle else {
178
+ return Font.system(size: defaultSize)
179
+ }
180
+
181
+ let fontType = textStyle["fontType"] as? String
182
+ let fontSize = textStyle["fontSize"] as? Double ?? Double(defaultSize)
183
+
184
+ switch fontType {
185
+ case "serif":
186
+ return Font.system(size: CGFloat(fontSize), design: .serif)
187
+ case "monospace":
188
+ return Font.system(size: CGFloat(fontSize), design: .monospaced)
189
+ case "default":
190
+ return Font.system(size: CGFloat(fontSize))
191
+ default:
192
+ return Font.system(size: CGFloat(fontSize))
193
+ }
194
+ }
195
+
101
196
  }