@octopus-community/react-native 1.0.0 → 1.0.2

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 CHANGED
@@ -49,7 +49,18 @@ Due to a bug in XCode 15, you might need to set `ENABLE_USER_SCRIPT_SANDBOXING`
49
49
 
50
50
  ### Expo
51
51
 
52
- An expo plugin will be available shortly.
52
+ Configure `use_frameworks` (static or dynamic) with `expo-build-properties`:
53
+
54
+ ```json
55
+ [
56
+ "expo-build-properties",
57
+ {
58
+ "ios": {
59
+ "useFrameworks": "static"
60
+ }
61
+ }
62
+ ]
63
+ ```
53
64
 
54
65
  ## Usage
55
66
 
@@ -89,12 +100,23 @@ import {
89
100
  closeUI,
90
101
  } from '@octopus-community/react-native';
91
102
 
92
- // Initialize with SSO mode
103
+ // Initialize with SSO mode and custom theme
93
104
  await initialize({
94
105
  apiKey: 'YOUR_OCTOPUS_API_KEY',
95
106
  connectionMode: {
96
107
  type: 'sso',
97
108
  appManagedFields: ['username', 'profilePicture', 'biography']
109
+ },
110
+ theme: {
111
+ colors: {
112
+ primary: '#FF6B35', // Your brand's primary color
113
+ primaryLowContrast: '#FF8C69', // Lighter variation
114
+ primaryHighContrast: '#CC4A1A', // Darker variation
115
+ onPrimary: '#FFFFFF', // Text color on primary background
116
+ },
117
+ logo: {
118
+ image: Image.resolveAssetSource(require('./assets/logo.png')), // Your custom logo
119
+ },
98
120
  }
99
121
  });
100
122
 
@@ -144,6 +166,44 @@ Show the Octopus home screen with the [`openUI()`](./docs/api/functions/openUI.m
144
166
 
145
167
  Future versions of this React Native module will let you show the UI in your React components. Please reach out if you need this prioritized.
146
168
 
169
+ ### Theme Customization
170
+
171
+ You can customize the Octopus UI to match your app's branding by providing a theme configuration during initialization:
172
+
173
+ ```ts
174
+ import { Image } from 'react-native';
175
+
176
+ await initialize({
177
+ apiKey: 'YOUR_OCTOPUS_API_KEY',
178
+ connectionMode: { type: 'octopus' },
179
+ theme: {
180
+ colors: {
181
+ primary: '#FF6B35', // Main brand color
182
+ primaryLowContrast: '#FF8C69', // Lighter variation of primary
183
+ primaryHighContrast: '#CC4A1A', // Darker variation for high contrast
184
+ onPrimary: '#FFFFFF', // Text color on primary background
185
+ },
186
+ logo: {
187
+ // Use Image.resolveAssetSource() for bundled image assets
188
+ image: Image.resolveAssetSource(require('./assets/logo.png')),
189
+ },
190
+ }
191
+ });
192
+ ```
193
+
194
+ **Supported customizations:**
195
+ - **Colors**: Primary, low contrast, high contrast, and text colors (iOS & Android)
196
+ - **Logo**: Custom logo using bundled images (iOS & Android)
197
+ - **Color Format**: Hex strings with or without # prefix (e.g., `#FF6B35` or `FF6B35`)
198
+ - **Supported Hex Formats**: 3-digit (`#F63`), 6-digit (`#FF6633`), 8-digit (`#FF6633FF`)
199
+ - **Zero Dependencies**: Implementation uses only built-in native APIs
200
+
201
+ **Logo Options:**
202
+ - **Bundled Images**: Use `Image.resolveAssetSource(require('./assets/logo.png'))` for local assets
203
+ - **Platform Support**: Both iOS and Android support local image resources
204
+
205
+ **Note**: All theme properties are optional. If not provided, the default Octopus theme will be used.
206
+
147
207
  ### API docs
148
208
 
149
209
  For details about the Typescript API, head to the [API docs](./docs/api/README.md).
@@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReactApplicationContext
5
5
  import com.facebook.react.bridge.ReadableMap
6
6
  import com.octopuscommunity.sdk.OctopusSDK
7
7
  import com.octopuscommunity.sdk.domain.model.ConnectionMode
8
+ import android.graphics.Color
8
9
 
9
10
  class OctopusSDKInitializer {
10
11
 
@@ -17,6 +18,11 @@ class OctopusSDKInitializer {
17
18
 
18
19
  try {
19
20
  val connectionMode = parseConnectionMode(options)
21
+
22
+ // Store theme configuration for later use in UI
23
+ val themeConfig = parseThemeConfig(options)
24
+ OctopusThemeManager.setThemeConfig(themeConfig)
25
+
20
26
  OctopusSDK.initialize(
21
27
  context = context,
22
28
  apiKey = apiKey,
@@ -29,6 +35,72 @@ class OctopusSDKInitializer {
29
35
  promise.reject("INITIALIZE_ERROR", "Failed to initialize Octopus SDK", e)
30
36
  }
31
37
  }
38
+
39
+ private fun parseThemeConfig(options: ReadableMap): OctopusThemeConfig? {
40
+ val themeMap = options.getMap("theme") ?: return null
41
+
42
+ val colorsMap = themeMap.getMap("colors")
43
+ val logoMap = themeMap.getMap("logo")
44
+
45
+ var primaryColor: String? = null
46
+ var primaryLowContrastColor: String? = null
47
+ var primaryHighContrastColor: String? = null
48
+ var onPrimaryColor: String? = null
49
+ var logoSource: ReadableMap? = null
50
+
51
+ colorsMap?.let { colors ->
52
+ primaryColor = parseColor(colors.getString("primary"))
53
+ primaryLowContrastColor = parseColor(colors.getString("primaryLowContrast"))
54
+ primaryHighContrastColor = parseColor(colors.getString("primaryHighContrast"))
55
+ onPrimaryColor = parseColor(colors.getString("onPrimary"))
56
+ }
57
+
58
+ // Handle logo - only support Image.resolveAssetSource() approach
59
+ logoMap?.let { logo ->
60
+ val imageSource = logo.getMap("image")
61
+ if (imageSource != null) {
62
+ logoSource = imageSource
63
+ }
64
+ }
65
+
66
+ // Only create theme config if we have at least one customization
67
+ if (primaryColor != null || logoSource != null) {
68
+ return OctopusThemeConfig(
69
+ primaryColor = primaryColor,
70
+ primaryLowContrastColor = primaryLowContrastColor,
71
+ primaryHighContrastColor = primaryHighContrastColor,
72
+ onPrimaryColor = onPrimaryColor,
73
+ logoSource = logoSource
74
+ )
75
+ }
76
+
77
+ return null
78
+ }
79
+
80
+ private fun extractResourceNameFromUri(uri: String): String? {
81
+ // Extract resource name from React Native image URI
82
+ // Examples: "logo.png" -> "logo", "images/logo.png" -> "logo"
83
+ return try {
84
+ val fileName = uri.substringAfterLast("/")
85
+ fileName.substringBeforeLast(".")
86
+ } catch (e: Exception) {
87
+ null
88
+ }
89
+ }
90
+
91
+ private fun parseColor(colorString: String?): String? {
92
+ if (colorString == null) return null
93
+
94
+ return try {
95
+ // Validate that the color string is a valid hex color
96
+ Color.parseColor(colorString)
97
+ // Return the original string if parsing succeeds
98
+ colorString
99
+ } catch (e: IllegalArgumentException) {
100
+ // Invalid color format - return null to skip this color
101
+ null
102
+ }
103
+ }
32
104
 
33
105
  private fun parseConnectionMode(options: ReadableMap): ConnectionMode {
34
106
  val connectionModeMap = options.getMap("connectionMode")
@@ -0,0 +1,11 @@
1
+ package com.octopuscommunity.octopusreactnativesdk
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+
5
+ data class OctopusThemeConfig(
6
+ val primaryColor: String?,
7
+ val primaryLowContrastColor: String?,
8
+ val primaryHighContrastColor: String?,
9
+ val onPrimaryColor: String?,
10
+ val logoSource: ReadableMap?
11
+ )
@@ -0,0 +1,11 @@
1
+ package com.octopuscommunity.octopusreactnativesdk
2
+
3
+ object OctopusThemeManager {
4
+ private var themeConfig: OctopusThemeConfig? = null
5
+
6
+ fun setThemeConfig(config: OctopusThemeConfig?) {
7
+ themeConfig = config
8
+ }
9
+
10
+ fun getThemeConfig(): OctopusThemeConfig? = themeConfig
11
+ }
@@ -4,68 +4,185 @@ import android.content.BroadcastReceiver
4
4
  import android.content.Context
5
5
  import android.content.Intent
6
6
  import android.content.IntentFilter
7
+ import android.content.res.Configuration
8
+ import android.graphics.BitmapFactory
7
9
  import android.os.Build
8
10
  import android.os.Bundle
11
+ import android.util.Log
9
12
  import androidx.activity.ComponentActivity
10
13
  import androidx.activity.compose.setContent
11
14
  import androidx.compose.foundation.layout.Box
12
15
  import androidx.compose.foundation.layout.fillMaxSize
13
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
14
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
15
28
  import androidx.navigation.compose.NavHost
29
+ import androidx.navigation.compose.composable
16
30
  import androidx.navigation.compose.rememberNavController
17
- import com.octopuscommunity.sdk.ui.OctopusDestination
31
+ import com.octopuscommunity.sdk.ui.OctopusDrawablesDefaults
32
+ import com.octopuscommunity.sdk.ui.OctopusTheme
33
+ import com.octopuscommunity.sdk.ui.home.OctopusHomeScreen
18
34
  import com.octopuscommunity.sdk.ui.octopusComposables
35
+ import com.octopuscommunity.sdk.ui.octopusDarkColorScheme
36
+ import com.octopuscommunity.sdk.ui.octopusLightColorScheme
37
+ import kotlinx.coroutines.Dispatchers
38
+ import kotlinx.coroutines.withContext
39
+ import java.net.URL
19
40
 
20
41
  class OctopusUIActivity : ComponentActivity() {
21
- private val closeUIReceiver = object : BroadcastReceiver() {
22
- override fun onReceive(context: Context?, intent: Intent?) {
23
- finish()
42
+ private val closeUIReceiver = object : BroadcastReceiver() {
43
+ override fun onReceive(context: Context?, intent: Intent?) {
44
+ finish()
45
+ }
46
+ }
47
+
48
+ override fun onCreate(savedInstanceState: Bundle?) {
49
+ super.onCreate(savedInstanceState)
50
+ registerCloseUIReceiver()
51
+ setContent {
52
+ OctopusUI(onBack = { finish() })
53
+ }
54
+ }
55
+
56
+ override fun onDestroy() {
57
+ super.onDestroy()
58
+ unregisterReceiver(closeUIReceiver)
24
59
  }
25
- }
26
60
 
27
- override fun onCreate(savedInstanceState: Bundle?) {
28
- super.onCreate(savedInstanceState)
29
- registerCloseUIReceiver()
30
- setContent {
31
- OctopusUI()
61
+ private fun registerCloseUIReceiver() {
62
+ val intentFilter = IntentFilter(OctopusUIController.CLOSE_UI_ACTION)
63
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
64
+ registerReceiver(closeUIReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
65
+ } else {
66
+ @Suppress("UnspecifiedRegisterReceiverFlag")
67
+ registerReceiver(closeUIReceiver, intentFilter)
68
+ }
69
+ }
70
+ }
71
+
72
+ @Composable
73
+ private fun OctopusUI(onBack: () -> Unit) {
74
+ val themeConfig = OctopusThemeManager.getThemeConfig()
75
+ val context = LocalContext.current
76
+
77
+ // Function to detect if system is in dark mode
78
+ fun isSystemInDarkTheme(): Boolean {
79
+ return (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
32
80
  }
33
- }
34
81
 
35
- override fun onDestroy() {
36
- super.onDestroy()
37
- unregisterReceiver(closeUIReceiver)
38
- }
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
+ }
105
+
106
+ // Handle logo loading using built-in Android capabilities
107
+ var logoPainter by remember { mutableStateOf<Painter?>(null) }
108
+
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
+ }
117
+ }
118
+ }
119
+
120
+ // Create drawables based on theme config
121
+ val drawables = if (logoPainter != null) {
122
+ OctopusDrawablesDefaults.drawables(logo = logoPainter)
123
+ } else {
124
+ OctopusDrawablesDefaults.drawables()
125
+ }
39
126
 
40
- private fun registerCloseUIReceiver() {
41
- val intentFilter = IntentFilter(OctopusUIController.CLOSE_UI_ACTION)
42
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
43
- registerReceiver(closeUIReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
127
+ // Apply custom theme
128
+ OctopusTheme(
129
+ colorScheme = colorScheme,
130
+ drawables = drawables
131
+ ) {
132
+ OctopusUIContent(onBack = onBack)
133
+ }
44
134
  } else {
45
- @Suppress("UnspecifiedRegisterReceiverFlag")
46
- registerReceiver(closeUIReceiver, intentFilter)
135
+ // No theme config - let the native SDK inherit the React Native app's theme colors
136
+ OctopusUIContent(onBack = onBack)
47
137
  }
48
- }
49
138
  }
50
139
 
51
140
  @Composable
52
- private fun OctopusUI() {
53
- Box(modifier = Modifier.fillMaxSize()) {
54
- val navController = rememberNavController()
55
-
56
- NavHost(
57
- navController = navController,
58
- startDestination = OctopusDestination.Home
59
- ) {
60
- octopusComposables(
61
- navController = navController,
62
- onNavigateToLogin = {
63
- OctopusEventEmitter.instance?.emitLoginRequired()
64
- },
65
- onNavigateToProfileEdit = { fieldToEdit ->
66
- OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
141
+ private fun OctopusUIContent(onBack: () -> Unit) {
142
+ Box(modifier = Modifier.fillMaxSize()) {
143
+ val navController = rememberNavController()
144
+
145
+ NavHost(
146
+ navController = navController,
147
+ startDestination = "OctopusHome"
148
+ ) {
149
+ composable(route = "OctopusHome") {
150
+ OctopusHomeScreen(
151
+ navController = navController,
152
+ backIcon = true,
153
+ onBack = onBack
154
+ )
155
+ }
156
+ octopusComposables(
157
+ navController = navController,
158
+ onNavigateToLogin = {
159
+ OctopusEventEmitter.instance?.emitLoginRequired()
160
+ },
161
+ onNavigateToProfileEdit = { fieldToEdit ->
162
+ OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
163
+ }
164
+ )
67
165
  }
68
- )
69
166
  }
70
- }
71
167
  }
168
+
169
+ private suspend fun loadImageFromUri(uri: String): Painter? {
170
+ return withContext(Dispatchers.IO) {
171
+ try {
172
+ val url = URL(uri)
173
+ val inputStream = url.openStream()
174
+ val bitmap = BitmapFactory.decodeStream(inputStream)
175
+ inputStream.close()
176
+
177
+ if (bitmap != null) {
178
+ BitmapPainter(bitmap.asImageBitmap())
179
+ } else {
180
+ Log.w("OctopusUI", "Failed to decode bitmap from URI: $uri")
181
+ null
182
+ }
183
+ } catch (e: Exception) {
184
+ Log.e("OctopusUI", "Error loading image from URI: $uri", e)
185
+ null
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,44 @@
1
+ import UIKit
2
+
3
+ /// A utility class for creating UIColor instances from hex strings.
4
+ /// This avoids conflicts with other modules that might also extend UIColor.
5
+ internal class OctopusColorUtility {
6
+
7
+ /// Creates a UIColor from a hex string.
8
+ /// - Parameter hex: A hex color string (e.g., "#FF0000", "FF0000", "#FFF", etc.)
9
+ /// - Returns: A UIColor instance, or nil if the hex string is invalid
10
+ static func color(fromHex hex: String) -> UIColor? {
11
+ // Remove any whitespace and convert to lowercase
12
+ let hex = hex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
13
+
14
+ // Remove # prefix if present
15
+ let cleanHex = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex
16
+
17
+ // Validate hex string contains only valid characters
18
+ guard cleanHex.range(of: "^[0-9a-f]+$", options: .regularExpression) != nil else {
19
+ return nil
20
+ }
21
+
22
+ var int: UInt64 = 0
23
+ Scanner(string: cleanHex).scanHexInt64(&int)
24
+
25
+ let a, r, g, b: UInt64
26
+ switch cleanHex.count {
27
+ case 3: // RGB (12-bit)
28
+ (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
29
+ case 6: // RGB (24-bit)
30
+ (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
31
+ case 8: // ARGB (32-bit)
32
+ (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
33
+ default:
34
+ return nil
35
+ }
36
+
37
+ return UIColor(
38
+ red: CGFloat(r) / 255,
39
+ green: CGFloat(g) / 255,
40
+ blue: CGFloat(b) / 255,
41
+ alpha: CGFloat(a) / 255
42
+ )
43
+ }
44
+ }
@@ -5,37 +5,41 @@ import UIKit
5
5
 
6
6
  @objc(OctopusReactNativeSdk)
7
7
  class OctopusReactNativeSdk: RCTEventEmitter {
8
-
8
+
9
9
  // MARK: - Properties
10
-
10
+
11
11
  private var octopusSDK: OctopusSDK?
12
12
  private lazy var uiManager = OctopusUIManager()
13
13
  private lazy var eventManager = OctopusEventManager(eventEmitter: self)
14
14
  private let sdkInitializer = OctopusSDKInitializer()
15
15
  private var ssoAuthenticator: OctopusSSOAuthenticator?
16
-
16
+ private var theme: OctopusTheme?
17
+ private var logoSource: [String: Any]?
18
+
17
19
  // MARK: - Initialization
18
-
20
+
19
21
  @objc(initialize:withResolver:withRejecter:)
20
22
  func initialize(options: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
21
23
  do {
22
24
  self.octopusSDK = try sdkInitializer.initialize(options: options, eventManager: eventManager)
23
25
  self.ssoAuthenticator = OctopusSSOAuthenticator(octopusSDK: self.octopusSDK!, eventManager: eventManager)
26
+ self.theme = sdkInitializer.parseTheme(from: options)
27
+ self.logoSource = sdkInitializer.getLogoSource(from: options)
24
28
  resolve(nil)
25
29
  } catch {
26
30
  reject("INITIALIZE_ERROR", "Failed to initialize Octopus SDK: \(error.localizedDescription)", error)
27
31
  }
28
32
  }
29
-
33
+
30
34
  // MARK: - User authentication
31
-
35
+
32
36
  @objc(connectUser:withResolver:withRejecter:)
33
37
  func connectUser(params: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
34
38
  guard let authenticator = ssoAuthenticator else {
35
39
  reject("CONNECT_USER_ERROR", "SDK not initialized. Call initialize() first.", nil)
36
40
  return
37
41
  }
38
-
42
+
39
43
  Task {
40
44
  do {
41
45
  try await authenticator.connectUser(params: params)
@@ -44,66 +48,66 @@ class OctopusReactNativeSdk: RCTEventEmitter {
44
48
  reject("CONNECT_USER_ERROR", "Failed to connect user: \(error.localizedDescription)", error)
45
49
  }
46
50
  }
47
-
51
+
48
52
  }
49
-
53
+
50
54
  @objc(completeUserTokenRequest:withToken:withResolver:withRejecter:)
51
55
  func completeUserTokenRequest(requestId: String, token: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
52
56
  guard let authenticator = ssoAuthenticator else {
53
57
  reject("PROVIDE_TOKEN_ERROR", "SDK not initialized", nil)
54
58
  return
55
59
  }
56
-
60
+
57
61
  authenticator.completeTokenRequest(requestId: requestId, token: token)
58
62
  resolve(nil)
59
63
  }
60
-
64
+
61
65
  @objc(cancelUserTokenRequest:withResolver:withRejecter:)
62
66
  func cancelUserTokenRequest(requestId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
63
67
  guard let authenticator = ssoAuthenticator else {
64
68
  reject("PROVIDE_TOKEN_ERROR", "SDK not initialized", nil)
65
69
  return
66
70
  }
67
-
71
+
68
72
  authenticator.cancelTokenRequest(requestId: requestId)
69
73
  resolve(nil)
70
74
  }
71
-
75
+
72
76
  @objc(disconnectUser:withRejecter:)
73
77
  func disconnectUser(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
74
78
  guard let authenticator = ssoAuthenticator else {
75
79
  reject("DISCONNECT_USER_ERROR", "SDK not initialized. Call initialize() first.", nil)
76
80
  return
77
81
  }
78
-
82
+
79
83
  do {
80
84
  try authenticator.disconnectUser()
81
85
  resolve(nil)
82
86
  } catch {
83
87
  reject("DISCONNECT_USER_ERROR", "Failed to disconnect user: \(error.localizedDescription)", error)
84
88
  }
85
-
89
+
86
90
  }
87
-
91
+
88
92
  // MARK: - UI management
89
-
93
+
90
94
  @objc(openUI:withRejecter:)
91
95
  func openUI(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
92
96
  guard let octopus = octopusSDK else {
93
97
  reject("OPEN_UI_ERROR", "SDK not initialized. Call initialize() first.", nil)
94
98
  return
95
99
  }
96
-
100
+
97
101
  DispatchQueue.main.async {
98
102
  do {
99
- try self.uiManager.openUI(octopus: octopus)
103
+ try self.uiManager.openUI(octopus: octopus, theme: self.theme, logoSource: self.logoSource)
100
104
  resolve(nil)
101
105
  } catch {
102
106
  reject("OPEN_UI_ERROR", error.localizedDescription, error)
103
107
  }
104
108
  }
105
109
  }
106
-
110
+
107
111
  @objc(closeUI:withRejecter:)
108
112
  func closeUI(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
109
113
  DispatchQueue.main.async {
@@ -115,33 +119,33 @@ class OctopusReactNativeSdk: RCTEventEmitter {
115
119
  }
116
120
  }
117
121
  }
118
-
122
+
119
123
  // MARK: - Lifecycle management
120
-
124
+
121
125
  deinit {
122
126
  cleanup()
123
127
  }
124
-
128
+
125
129
  private func cleanup() {
126
130
  uiManager.cleanup()
127
131
  octopusSDK = nil
128
132
  }
129
-
133
+
130
134
  @objc override func invalidate() {
131
135
  cleanup()
132
136
  super.invalidate()
133
137
  }
134
-
138
+
135
139
  // MARK: - RCTEventEmitter overrides
136
-
140
+
137
141
  @objc override func supportedEvents() -> [String]! {
138
142
  return eventManager.supportedEvents()
139
143
  }
140
-
144
+
141
145
  @objc override func startObserving() {
142
146
  eventManager.startObserving()
143
147
  }
144
-
148
+
145
149
  @objc override func stopObserving() {
146
150
  eventManager.stopObserving()
147
151
  }
@@ -1,22 +1,81 @@
1
1
  import Octopus
2
+ import OctopusUI
3
+ import SwiftUI
2
4
 
3
5
  class OctopusSDKInitializer {
4
6
  func initialize(options: [String: Any], eventManager: OctopusEventManager) throws -> OctopusSDK {
5
7
  guard let apiKey = options["apiKey"] as? String else {
6
8
  throw InitializationError.missingAPIKey
7
9
  }
8
-
10
+
9
11
  guard let connectionModeMap = options["connectionMode"] as? [String: Any] else {
10
12
  throw InitializationError.missingConnectionMode
11
13
  }
12
-
14
+
13
15
  let connectionMode = try parseConnectionMode(from: connectionModeMap, eventManager: eventManager)
14
16
  return try OctopusSDK(apiKey: apiKey, connectionMode: connectionMode)
15
17
  }
16
18
 
19
+ func parseTheme(from options: [String: Any]) -> OctopusTheme? {
20
+ guard let themeMap = options["theme"] as? [String: Any] else {
21
+ return nil
22
+ }
23
+
24
+ var colors: OctopusTheme.Colors?
25
+
26
+ // Parse colors
27
+ if let colorsMap = themeMap["colors"] as? [String: Any] {
28
+ var primarySet: OctopusTheme.Colors.ColorSet?
29
+
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
+ )
46
+ }
47
+ }
48
+
49
+ // 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 {
52
+ return nil
53
+ }
54
+
55
+ return OctopusTheme(
56
+ colors: colors,
57
+ fonts: OctopusTheme.Fonts(),
58
+ assets: OctopusTheme.Assets()
59
+ )
60
+ }
61
+
62
+ func getLogoSource(from options: [String: Any]) -> [String: Any]? {
63
+ guard let themeMap = options["theme"] as? [String: Any],
64
+ let logoMap = themeMap["logo"] as? [String: Any] else {
65
+ return nil
66
+ }
67
+
68
+ // Only support Image.resolveAssetSource() approach
69
+ if let imageSource = logoMap["image"] as? [String: Any] {
70
+ return imageSource
71
+ }
72
+
73
+ return nil
74
+ }
75
+
17
76
  private func parseConnectionMode(from connectionModeMap: [String: Any], eventManager: OctopusEventManager) throws -> ConnectionMode {
18
77
  let connectionModeType = connectionModeMap["type"] as? String
19
-
78
+
20
79
  switch connectionModeType {
21
80
  case "sso":
22
81
  return try createSSOConnectionMode(from: connectionModeMap, eventManager: eventManager)
@@ -26,10 +85,10 @@ class OctopusSDKInitializer {
26
85
  throw InitializationError.invalidConnectionModeType
27
86
  }
28
87
  }
29
-
88
+
30
89
  private func createSSOConnectionMode(from connectionModeMap: [String: Any], eventManager: OctopusEventManager) throws -> ConnectionMode {
31
90
  let appManagedFields = ProfileFieldMapper.fromReactNativeArray(connectionModeMap["appManagedFields"] as? [String])
32
-
91
+
33
92
  return .sso(
34
93
  .init(
35
94
  appManagedFields: appManagedFields,
@@ -48,7 +107,7 @@ enum InitializationError: Error, LocalizedError {
48
107
  case missingAPIKey
49
108
  case missingConnectionMode
50
109
  case invalidConnectionModeType
51
-
110
+
52
111
  var errorDescription: String? {
53
112
  switch self {
54
113
  case .missingAPIKey:
@@ -6,31 +6,92 @@ import React
6
6
 
7
7
  class OctopusUIManager {
8
8
  private weak var presentedViewController: UIViewController?
9
-
10
- func openUI(octopus: OctopusSDK) throws {
9
+
10
+ func openUI(octopus: OctopusSDK, theme: OctopusUI.OctopusTheme?, logoSource: [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
15
  let octopusHomeScreen = OctopusHomeScreen(octopus: octopus)
16
- let hostingController = UIHostingController(rootView: octopusHomeScreen)
16
+ let hostingController = UIHostingController(rootView: AnyView(octopusHomeScreen))
17
17
  hostingController.modalPresentationStyle = .fullScreen
18
-
18
+
19
+ // Apply theme if provided
20
+ if let theme = theme {
21
+ // Apply theme immediately (without logo) to avoid delay
22
+ hostingController.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, theme))
23
+
24
+ // Then load logo asynchronously and update theme if logo loads
25
+ if let logoSource = logoSource {
26
+ loadLogo(from: logoSource) { [weak hostingController] logoImage in
27
+ DispatchQueue.main.async {
28
+ if let logoImage = logoImage {
29
+ let updatedTheme = OctopusUI.OctopusTheme(
30
+ colors: theme.colors,
31
+ fonts: theme.fonts,
32
+ assets: OctopusUI.OctopusTheme.Assets(logo: logoImage)
33
+ )
34
+ hostingController?.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, updatedTheme))
35
+ } else {
36
+ // Theme is already applied, no need to do anything
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+
19
43
  presentedViewController = hostingController
20
-
44
+
21
45
  presentingViewController.present(hostingController, animated: true)
22
46
  }
23
47
 
48
+ private func loadLogo(from source: [String: Any], completion: @escaping (UIImage?) -> Void) {
49
+ // Handle React Native image source with URI (from Image.resolveAssetSource)
50
+ guard let uri = source["uri"] as? String else {
51
+ completion(nil)
52
+ return
53
+ }
54
+
55
+ // Remote URL
56
+ if uri.hasPrefix("http") {
57
+ guard let url = URL(string: uri) else {
58
+ completion(nil)
59
+ return
60
+ }
61
+
62
+ URLSession.shared.dataTask(with: url) { data, response, error in
63
+ guard let data = data, error == nil else {
64
+ completion(nil)
65
+ return
66
+ }
67
+
68
+ let image = UIImage(data: data)
69
+ completion(image)
70
+ }.resume()
71
+ } else {
72
+ // Local file path (from Image.resolveAssetSource)
73
+ if let image = UIImage(contentsOfFile: uri) {
74
+ completion(image)
75
+ } 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)
81
+ }
82
+ }
83
+ }
84
+
24
85
  func closeUI() throws {
25
86
  guard let presentedVC = presentedViewController else {
26
87
  throw NSError(domain: "CLOSE_UI_ERROR", code: 0, userInfo: [NSLocalizedDescriptionKey: "No UI is currently presented"])
27
88
  }
28
-
89
+
29
90
  presentedVC.dismiss(animated: true) {
30
91
  self.presentedViewController = nil
31
92
  }
32
93
  }
33
-
94
+
34
95
  func cleanup() {
35
96
  if let presentedVC = presentedViewController {
36
97
  presentedVC.dismiss(animated: false, completion: nil)
@@ -2,6 +2,10 @@
2
2
 
3
3
  import { OctopusReactNativeSdk } from "./internals/nativeModule.js";
4
4
 
5
+ /**
6
+ * Theme configuration for customizing the Octopus UI appearance.
7
+ */
8
+
5
9
  /**
6
10
  * Configuration params for initializing the Octopus SDK.
7
11
  */
@@ -1 +1 @@
1
- {"version":3,"names":["OctopusReactNativeSdk","initialize","params"],"sourceRoot":"../../src","sources":["initialize.ts"],"mappings":";;AAAA,SAASA,qBAAqB,QAAQ,6BAA0B;;AAGhE;AACA;AACA;;AAsBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAACC,MAAwB,EAAiB;EAClE,OAAOF,qBAAqB,CAACC,UAAU,CAACC,MAAM,CAAC;AACjD","ignoreList":[]}
1
+ {"version":3,"names":["OctopusReactNativeSdk","initialize","params"],"sourceRoot":"../../src","sources":["initialize.ts"],"mappings":";;AAAA,SAASA,qBAAqB,QAAQ,6BAA0B;;AAIhE;AACA;AACA;;AAoBA;AACA;AACA;;AAwBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAACC,MAAwB,EAAiB;EAClE,OAAOF,qBAAqB,CAACC,UAAU,CAACC,MAAM,CAAC;AACjD","ignoreList":[]}
@@ -1,4 +1,26 @@
1
1
  import type { UserProfileField } from './types/userProfileField';
2
+ import type { ImageResolvedAssetSource } from 'react-native';
3
+ /**
4
+ * Theme configuration for customizing the Octopus UI appearance.
5
+ */
6
+ export interface OctopusTheme {
7
+ /** Color customization options */
8
+ colors?: {
9
+ /** Primary color set for branding (hex format: #FF6B35 or FF6B35) */
10
+ primary?: string;
11
+ /** Primary low contrast color (lighter variation of primary) (hex format: #FF6B35 or FF6B35) */
12
+ primaryLowContrast?: string;
13
+ /** High contrast variation of primary color (hex format: #FF6B35 or FF6B35) */
14
+ primaryHighContrast?: string;
15
+ /** Color for content displayed over the primary color (hex format: #FF6B35 or FF6B35) */
16
+ onPrimary?: string;
17
+ };
18
+ /** Logo customization */
19
+ logo?: {
20
+ /** Local image resource - use Image.resolveAssetSource(require('./path/to/image.png')) */
21
+ image?: ImageResolvedAssetSource;
22
+ };
23
+ }
2
24
  /**
3
25
  * Configuration params for initializing the Octopus SDK.
4
26
  */
@@ -19,6 +41,8 @@ export interface InitializeParams {
19
41
  /** Octopus-managed authentication mode */
20
42
  type: 'octopus';
21
43
  };
44
+ /** Optional theme customization for the Octopus UI */
45
+ theme?: OctopusTheme;
22
46
  }
23
47
  /**
24
48
  * Initializes the Octopus SDK with the provided configuration.
@@ -1 +1 @@
1
- {"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../../../src/initialize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,EACV;QACE,6BAA6B;QAC7B,IAAI,EAAE,KAAK,CAAC;QACZ,iEAAiE;QACjE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;KACtC,GACD;QACE,0CAA0C;QAC1C,IAAI,EAAE,SAAS,CAAC;KACjB,CAAC;CACP;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE"}
1
+ {"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../../../src/initialize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,MAAM,CAAC,EAAE;QACP,qEAAqE;QACrE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gGAAgG;QAChG,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,+EAA+E;QAC/E,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,yFAAyF;QACzF,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,yBAAyB;IACzB,IAAI,CAAC,EAAE;QACL,0FAA0F;QAC1F,KAAK,CAAC,EAAE,wBAAwB,CAAC;KAClC,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,EACV;QACE,6BAA6B;QAC7B,IAAI,EAAE,KAAK,CAAC;QACZ,iEAAiE;QACjE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;KACtC,GACD;QACE,0CAA0C;QAC1C,IAAI,EAAE,SAAS,CAAC;KACjB,CAAC;IACN,sDAAsD;IACtD,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octopus-community/react-native",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "React Native module for the Octopus Community SDK",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
package/src/initialize.ts CHANGED
@@ -1,5 +1,28 @@
1
1
  import { OctopusReactNativeSdk } from './internals/nativeModule';
2
2
  import type { UserProfileField } from './types/userProfileField';
3
+ import type { ImageResolvedAssetSource } from 'react-native';
4
+
5
+ /**
6
+ * Theme configuration for customizing the Octopus UI appearance.
7
+ */
8
+ export interface OctopusTheme {
9
+ /** Color customization options */
10
+ colors?: {
11
+ /** Primary color set for branding (hex format: #FF6B35 or FF6B35) */
12
+ primary?: string;
13
+ /** Primary low contrast color (lighter variation of primary) (hex format: #FF6B35 or FF6B35) */
14
+ primaryLowContrast?: string;
15
+ /** High contrast variation of primary color (hex format: #FF6B35 or FF6B35) */
16
+ primaryHighContrast?: string;
17
+ /** Color for content displayed over the primary color (hex format: #FF6B35 or FF6B35) */
18
+ onPrimary?: string;
19
+ };
20
+ /** Logo customization */
21
+ logo?: {
22
+ /** Local image resource - use Image.resolveAssetSource(require('./path/to/image.png')) */
23
+ image?: ImageResolvedAssetSource;
24
+ };
25
+ }
3
26
 
4
27
  /**
5
28
  * Configuration params for initializing the Octopus SDK.
@@ -23,6 +46,8 @@ export interface InitializeParams {
23
46
  /** Octopus-managed authentication mode */
24
47
  type: 'octopus';
25
48
  };
49
+ /** Optional theme customization for the Octopus UI */
50
+ theme?: OctopusTheme;
26
51
  }
27
52
 
28
53
  /**