@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.
- package/README.md +257 -12
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +82 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +93 -17
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusThemeConfig.kt +12 -1
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +135 -56
- package/ios/OctopusReactNativeSdk.mm +8 -0
- package/ios/OctopusReactNativeSdk.swift +21 -1
- package/ios/OctopusSDKInitializer.swift +95 -24
- package/ios/OctopusUIManager.swift +106 -11
- package/lib/module/initialize.js +50 -1
- package/lib/module/initialize.js.map +1 -1
- package/lib/module/internals/colorSchemeManager.js +105 -0
- package/lib/module/internals/colorSchemeManager.js.map +1 -0
- package/lib/module/internals/fontParser.js +48 -0
- package/lib/module/internals/fontParser.js.map +1 -0
- package/lib/typescript/src/initialize.d.ts +78 -10
- package/lib/typescript/src/initialize.d.ts.map +1 -1
- package/lib/typescript/src/internals/colorSchemeManager.d.ts +39 -0
- package/lib/typescript/src/internals/colorSchemeManager.d.ts.map +1 -0
- package/lib/typescript/src/internals/fontParser.d.ts +18 -0
- package/lib/typescript/src/internals/fontParser.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/initialize.ts +117 -12
- package/src/internals/colorSchemeManager.ts +113 -0
- package/src/internals/fontParser.ts +64 -0
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt
CHANGED
|
@@ -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
|
-
//
|
|
83
|
-
if (themeConfig
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
122
|
+
// Handle logo loading using built-in Android capabilities
|
|
123
|
+
var logoPainter by remember { mutableStateOf<Painter?>(null) }
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
135
|
+
let fontsMap = themeMap["fonts"] as? [String: Any] else {
|
|
65
136
|
return nil
|
|
66
137
|
}
|
|
67
138
|
|
|
68
|
-
//
|
|
69
|
-
if let
|
|
70
|
-
return
|
|
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
|
|
25
|
+
if let _ = theme {
|
|
21
26
|
// Apply theme immediately (without logo) to avoid delay
|
|
22
|
-
hostingController.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme,
|
|
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:
|
|
31
|
-
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 =
|
|
92
|
+
if let image = loadImageFromLocalPath(uri) {
|
|
74
93
|
completion(image)
|
|
75
94
|
} else {
|
|
76
|
-
|
|
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
|
}
|