@octopus-community/react-native 1.0.7 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/OctopusReactNativeSdk.podspec +1 -1
  2. package/README.md +40 -35
  3. package/android/build.gradle +2 -0
  4. package/android/gradle.properties +2 -2
  5. package/android/src/main/AndroidManifest.xml +2 -1
  6. package/android/src/main/AndroidManifestNew.xml +2 -1
  7. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusActivity.kt +56 -0
  8. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusContent.kt +396 -0
  9. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventEmitter.kt +22 -0
  10. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventSerializer.kt +339 -0
  11. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactModule.kt +326 -0
  12. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/{OctopusReactNativeSdkPackage.kt → OctopusReactPackage.kt} +3 -3
  13. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +53 -9
  14. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +5 -15
  15. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfiguration.kt +6 -0
  16. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfigurationManager.kt +12 -0
  17. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt +17 -2
  18. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIViewManager.kt +63 -0
  19. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt +2 -2
  20. package/ios/OctopusEventManager.swift +27 -0
  21. package/ios/OctopusEventSerializer.swift +271 -0
  22. package/ios/OctopusReactNativeSdk.mm +26 -1
  23. package/ios/OctopusReactNativeSdk.swift +225 -3
  24. package/ios/OctopusSDKInitializer.swift +32 -0
  25. package/ios/OctopusSSOAuthenticator.swift +1 -5
  26. package/ios/OctopusUIConfiguration.swift +6 -0
  27. package/ios/OctopusUIManager.swift +134 -10
  28. package/ios/OctopusUIViewManager.m +7 -0
  29. package/ios/OctopusUIViewManager.swift +37 -0
  30. package/lib/module/OctopusUIView.js +39 -0
  31. package/lib/module/OctopusUIView.js.map +1 -0
  32. package/lib/module/addHasAccessToCommunityListener.js +33 -0
  33. package/lib/module/addHasAccessToCommunityListener.js.map +1 -0
  34. package/lib/module/addNavigateToUrlListener.js +41 -0
  35. package/lib/module/addNavigateToUrlListener.js.map +1 -0
  36. package/lib/module/addNotSeenNotificationsCountListener.js +30 -0
  37. package/lib/module/addNotSeenNotificationsCountListener.js.map +1 -0
  38. package/lib/module/addSDKEventListener.js +48 -0
  39. package/lib/module/addSDKEventListener.js.map +1 -0
  40. package/lib/module/connectUser.js +24 -3
  41. package/lib/module/connectUser.js.map +1 -1
  42. package/lib/module/index.js +12 -0
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/initialize.js +13 -12
  45. package/lib/module/initialize.js.map +1 -1
  46. package/lib/module/openUI.js +23 -2
  47. package/lib/module/openUI.js.map +1 -1
  48. package/lib/module/overrideCommunityAccess.js +36 -0
  49. package/lib/module/overrideCommunityAccess.js.map +1 -0
  50. package/lib/module/overrideDefaultLocale.js +75 -0
  51. package/lib/module/overrideDefaultLocale.js.map +1 -0
  52. package/lib/module/trackCommunityAccess.js +33 -0
  53. package/lib/module/trackCommunityAccess.js.map +1 -0
  54. package/lib/module/trackCustomEvent.js +36 -0
  55. package/lib/module/trackCustomEvent.js.map +1 -0
  56. package/lib/module/types/sdkEvents.js +2 -0
  57. package/lib/module/types/sdkEvents.js.map +1 -0
  58. package/lib/module/types/urlOpeningStrategy.js +23 -0
  59. package/lib/module/types/urlOpeningStrategy.js.map +1 -0
  60. package/lib/module/updateNotSeenNotificationsCount.js +33 -0
  61. package/lib/module/updateNotSeenNotificationsCount.js.map +1 -0
  62. package/lib/typescript/src/OctopusUIView.d.ts +32 -0
  63. package/lib/typescript/src/OctopusUIView.d.ts.map +1 -0
  64. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts +27 -0
  65. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts.map +1 -0
  66. package/lib/typescript/src/addNavigateToUrlListener.d.ts +31 -0
  67. package/lib/typescript/src/addNavigateToUrlListener.d.ts.map +1 -0
  68. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts +24 -0
  69. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts.map +1 -0
  70. package/lib/typescript/src/addSDKEventListener.d.ts +43 -0
  71. package/lib/typescript/src/addSDKEventListener.d.ts.map +1 -0
  72. package/lib/typescript/src/connectUser.d.ts +24 -8
  73. package/lib/typescript/src/connectUser.d.ts.map +1 -1
  74. package/lib/typescript/src/index.d.ts +13 -0
  75. package/lib/typescript/src/index.d.ts.map +1 -1
  76. package/lib/typescript/src/initialize.d.ts +22 -12
  77. package/lib/typescript/src/initialize.d.ts.map +1 -1
  78. package/lib/typescript/src/openUI.d.ts +28 -1
  79. package/lib/typescript/src/openUI.d.ts.map +1 -1
  80. package/lib/typescript/src/overrideCommunityAccess.d.ts +30 -0
  81. package/lib/typescript/src/overrideCommunityAccess.d.ts.map +1 -0
  82. package/lib/typescript/src/overrideDefaultLocale.d.ts +37 -0
  83. package/lib/typescript/src/overrideDefaultLocale.d.ts.map +1 -0
  84. package/lib/typescript/src/trackCommunityAccess.d.ts +27 -0
  85. package/lib/typescript/src/trackCommunityAccess.d.ts.map +1 -0
  86. package/lib/typescript/src/trackCustomEvent.d.ts +30 -0
  87. package/lib/typescript/src/trackCustomEvent.d.ts.map +1 -0
  88. package/lib/typescript/src/types/sdkEvents.d.ts +222 -0
  89. package/lib/typescript/src/types/sdkEvents.d.ts.map +1 -0
  90. package/lib/typescript/src/types/urlOpeningStrategy.d.ts +20 -0
  91. package/lib/typescript/src/types/urlOpeningStrategy.d.ts.map +1 -0
  92. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts +27 -0
  93. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts.map +1 -0
  94. package/package.json +2 -1
  95. package/src/OctopusUIView.tsx +57 -0
  96. package/src/addHasAccessToCommunityListener.ts +38 -0
  97. package/src/addNavigateToUrlListener.ts +54 -0
  98. package/src/addNotSeenNotificationsCountListener.ts +35 -0
  99. package/src/addSDKEventListener.ts +49 -0
  100. package/src/connectUser.ts +24 -8
  101. package/src/index.ts +13 -0
  102. package/src/initialize.ts +23 -12
  103. package/src/openUI.ts +32 -2
  104. package/src/overrideCommunityAccess.ts +33 -0
  105. package/src/overrideDefaultLocale.ts +88 -0
  106. package/src/trackCommunityAccess.ts +30 -0
  107. package/src/trackCustomEvent.ts +36 -0
  108. package/src/types/sdkEvents.ts +315 -0
  109. package/src/types/urlOpeningStrategy.ts +20 -0
  110. package/src/updateNotSeenNotificationsCount.ts +30 -0
  111. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +0 -155
  112. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +0 -422
@@ -3,6 +3,7 @@ import OctopusUI
3
3
  import SwiftUI
4
4
  import UIKit
5
5
  import React
6
+ import Combine
6
7
 
7
8
  @objc(OctopusReactNativeSdk)
8
9
  class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
@@ -17,6 +18,8 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
17
18
  private var theme: OctopusTheme?
18
19
  private var logoSource: [String: Any]?
19
20
  private var fontConfiguration: [String: Any]?
21
+ private var uiConfiguration: OctopusUIConfiguration?
22
+ private var cancellables = Set<AnyCancellable>()
20
23
 
21
24
  // MARK: - RCTBridgeModule
22
25
 
@@ -40,6 +43,11 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
40
43
  self.theme = sdkInitializer.parseTheme(from: options)
41
44
  self.logoSource = sdkInitializer.getLogoSource(from: options)
42
45
  self.fontConfiguration = sdkInitializer.getFontConfiguration(from: options)
46
+ self.uiConfiguration = sdkInitializer.parseUIConfiguration(from: options)
47
+
48
+ // Start observing reactive events after SDK initialization
49
+ startObservingReactiveEvents()
50
+
43
51
  resolve(nil)
44
52
  } catch {
45
53
  reject("INITIALIZE_ERROR", "Failed to initialize Octopus SDK: \(error.localizedDescription)", error)
@@ -106,16 +114,32 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
106
114
 
107
115
  // MARK: - UI management
108
116
 
109
- @objc(openUI:withRejecter:)
110
- func openUI(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
117
+ @objc(openUI:withResolver:withRejecter:)
118
+ func openUI(options: NSDictionary?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
111
119
  guard let octopus = octopusSDK else {
112
120
  reject("OPEN_UI_ERROR", "SDK not initialized. Call initialize() first.", nil)
113
121
  return
114
122
  }
115
123
 
124
+ let interceptUrls = (options as? [String: Any])?["interceptUrls"] as? Bool ?? false
125
+
116
126
  DispatchQueue.main.async {
117
127
  do {
118
- try self.uiManager.openUI(octopus: octopus, theme: self.theme, logoSource: self.logoSource, fontConfiguration: self.fontConfiguration)
128
+ if interceptUrls {
129
+ octopus.set(onNavigateToURLCallback: { [weak self] url in
130
+ self?.eventManager.emitNavigateToUrl(url: url.absoluteString)
131
+ return .handledByApp
132
+ })
133
+ } else {
134
+ octopus.set(onNavigateToURLCallback: nil)
135
+ }
136
+ try self.uiManager.openUI(
137
+ octopus: octopus,
138
+ theme: self.theme,
139
+ logoSource: self.logoSource,
140
+ fontConfiguration: self.fontConfiguration,
141
+ uiConfiguration: self.uiConfiguration
142
+ )
119
143
  resolve(nil)
120
144
  } catch {
121
145
  reject("OPEN_UI_ERROR", error.localizedDescription, error)
@@ -123,6 +147,15 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
123
147
  }
124
148
  }
125
149
 
150
+ @objc(handleUrlStrategy:withStrategy:)
151
+ func handleUrlStrategy(url: String, strategy: String) -> Void {
152
+ guard strategy == "handledByOctopus" else { return }
153
+ guard let urlObj = URL(string: url) else { return }
154
+ DispatchQueue.main.async {
155
+ UIApplication.shared.open(urlObj)
156
+ }
157
+ }
158
+
126
159
  @objc(closeUI:withRejecter:)
127
160
  func closeUI(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
128
161
  DispatchQueue.main.async {
@@ -135,6 +168,28 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
135
168
  }
136
169
  }
137
170
 
171
+ /// Called by OctopusUIViewManager's container view to embed the native UI (non-fullscreen).
172
+ @objc func addEmbeddedView(containerView: UIView, interceptUrls: Bool) {
173
+ guard let octopus = octopusSDK else { return }
174
+ if interceptUrls {
175
+ octopus.set(onNavigateToURLCallback: { [weak self] url in
176
+ self?.eventManager.emitNavigateToUrl(url: url.absoluteString)
177
+ return .handledByApp
178
+ })
179
+ } else {
180
+ octopus.set(onNavigateToURLCallback: nil)
181
+ }
182
+ uiManager.addEmbeddedView(
183
+ to: containerView,
184
+ octopus: octopus,
185
+ theme: theme,
186
+ logoSource: logoSource,
187
+ fontConfiguration: fontConfiguration,
188
+ uiConfiguration: uiConfiguration,
189
+ interceptUrls: interceptUrls
190
+ )
191
+ }
192
+
138
193
  @objc(updateColorScheme:withResolver:withRejecter:)
139
194
  func updateColorScheme(colorScheme: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
140
195
  // iOS uses adaptive colors that automatically respond to system appearance changes
@@ -152,6 +207,134 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
152
207
  }
153
208
  }
154
209
 
210
+ // MARK: - Notification management
211
+
212
+ @objc(updateNotSeenNotificationsCount:withRejecter:)
213
+ func updateNotSeenNotificationsCount(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
214
+ guard let octopus = octopusSDK else {
215
+ reject("UPDATE_ERROR", "SDK not initialized. Call initialize() first.", nil)
216
+ return
217
+ }
218
+
219
+ Task {
220
+ do {
221
+ try await octopus.updateNotSeenNotificationsCount()
222
+ resolve(nil)
223
+ } catch {
224
+ reject("UPDATE_ERROR", error.localizedDescription, error)
225
+ }
226
+ }
227
+ }
228
+
229
+ // MARK: - Analytics (custom events)
230
+
231
+ @objc(trackCustomEvent:withProperties:withResolver:withRejecter:)
232
+ func trackCustomEvent(
233
+ name: String,
234
+ properties: NSDictionary?,
235
+ resolve: @escaping RCTPromiseResolveBlock,
236
+ reject: @escaping RCTPromiseRejectBlock
237
+ ) -> Void {
238
+ guard let octopus = octopusSDK else {
239
+ reject("TRACK_ERROR", "SDK not initialized. Call initialize() first.", nil)
240
+ return
241
+ }
242
+ let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
243
+ guard !trimmedName.isEmpty else {
244
+ reject("INVALID_ARGS", "name is required and must be non-empty", nil)
245
+ return
246
+ }
247
+ let props = dictionaryToStringMap(properties) ?? [:]
248
+ let customEvent = CustomEvent(
249
+ name: trimmedName,
250
+ properties: props.mapValues { CustomEvent.PropertyValue(value: $0) }
251
+ )
252
+ Task {
253
+ do {
254
+ try await octopus.track(customEvent: customEvent)
255
+ resolve(nil)
256
+ } catch {
257
+ reject("TRACK_ERROR", error.localizedDescription, error)
258
+ }
259
+ }
260
+ }
261
+
262
+ /// Converts NSDictionary to [String: String]. Only string values are included.
263
+ private func dictionaryToStringMap(_ dict: NSDictionary?) -> [String: String]? {
264
+ guard let dict = dict as? [String: Any] else { return nil }
265
+ var result: [String: String] = [:]
266
+ for (key, value) in dict {
267
+ if let str = value as? String {
268
+ result[key] = str
269
+ }
270
+ }
271
+ return result
272
+ }
273
+
274
+ // MARK: - Community access override
275
+
276
+ @objc(overrideCommunityAccess:withResolver:withRejecter:)
277
+ func overrideCommunityAccess(
278
+ hasAccess: Bool,
279
+ resolve: @escaping RCTPromiseResolveBlock,
280
+ reject: @escaping RCTPromiseRejectBlock
281
+ ) -> Void {
282
+ guard let octopus = octopusSDK else {
283
+ reject("OVERRIDE_ERROR", "SDK not initialized. Call initialize() first.", nil)
284
+ return
285
+ }
286
+ Task {
287
+ do {
288
+ try await octopus.overrideCommunityAccess(hasAccess)
289
+ resolve(nil)
290
+ } catch {
291
+ reject("OVERRIDE_ERROR", error.localizedDescription, error)
292
+ }
293
+ }
294
+ }
295
+
296
+ // MARK: - Track community access (analytics only)
297
+
298
+ @objc(trackCommunityAccess:withResolver:withRejecter:)
299
+ func trackCommunityAccess(
300
+ hasAccess: Bool,
301
+ resolve: @escaping RCTPromiseResolveBlock,
302
+ reject: @escaping RCTPromiseRejectBlock
303
+ ) -> Void {
304
+ guard let octopus = octopusSDK else {
305
+ reject("TRACK_ACCESS_ERROR", "SDK not initialized. Call initialize() first.", nil)
306
+ return
307
+ }
308
+ octopus.track(hasAccessToCommunity: hasAccess)
309
+ resolve(nil)
310
+ }
311
+
312
+ // MARK: - Locale override
313
+
314
+ @objc(overrideDefaultLocale:withCountryCode:withResolver:withRejecter:)
315
+ func overrideDefaultLocale(
316
+ languageCode: NSString?,
317
+ countryCode: NSString?,
318
+ resolve: @escaping RCTPromiseResolveBlock,
319
+ reject: @escaping RCTPromiseRejectBlock
320
+ ) -> Void {
321
+ guard let octopus = octopusSDK else {
322
+ reject("LOCALE_ERROR", "SDK not initialized. Call initialize() first.", nil)
323
+ return
324
+ }
325
+ let locale: Locale?
326
+ if let lang = languageCode as String? {
327
+ if let country = countryCode as String? {
328
+ locale = Locale(identifier: "\(lang)-\(country)")
329
+ } else {
330
+ locale = Locale(identifier: lang)
331
+ }
332
+ } else {
333
+ locale = nil
334
+ }
335
+ octopus.overrideDefaultLocale(with: locale)
336
+ resolve(nil)
337
+ }
155
338
 
156
339
  // MARK: - Lifecycle management
157
340
 
@@ -160,9 +343,48 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
160
343
  }
161
344
 
162
345
  private func cleanup() {
346
+ cancellables.removeAll()
163
347
  uiManager.cleanup()
164
348
  octopusSDK = nil
165
349
  }
350
+
351
+ private func startObservingReactiveEvents() {
352
+ startObservingNotSeenNotificationsCount()
353
+ startObservingHasAccessToCommunity()
354
+ startObservingEvents()
355
+ }
356
+
357
+ private func startObservingNotSeenNotificationsCount() {
358
+ guard let octopus = octopusSDK else { return }
359
+ octopus.$notSeenNotificationsCount
360
+ .receive(on: DispatchQueue.main)
361
+ .sink { [weak self] count in
362
+ self?.eventManager.emitNotSeenNotificationsCountChanged(count: count)
363
+ }
364
+ .store(in: &cancellables)
365
+ }
366
+
367
+ private func startObservingHasAccessToCommunity() {
368
+ guard let octopus = octopusSDK else { return }
369
+ octopus.$hasAccessToCommunity
370
+ .receive(on: DispatchQueue.main)
371
+ .sink { [weak self] hasAccess in
372
+ self?.eventManager.emitHasAccessToCommunityChanged(hasAccess: hasAccess)
373
+ }
374
+ .store(in: &cancellables)
375
+ }
376
+
377
+ private func startObservingEvents() {
378
+ guard let octopus = octopusSDK else { return }
379
+ octopus.eventPublisher
380
+ .receive(on: DispatchQueue.main)
381
+ .sink { [weak self] event in
382
+ if let eventData = OctopusEventSerializer.serializeEvent(event) {
383
+ self?.eventManager.emitSDKEvent(eventData: eventData)
384
+ }
385
+ }
386
+ .store(in: &cancellables)
387
+ }
166
388
 
167
389
  @objc func invalidate() {
168
390
  cleanup()
@@ -1,6 +1,7 @@
1
1
  import Octopus
2
2
  import OctopusUI
3
3
  import SwiftUI
4
+ import CoreGraphics
4
5
 
5
6
  class OctopusSDKInitializer {
6
7
  func initialize(options: [String: Any], eventManager: OctopusEventManager) throws -> OctopusSDK {
@@ -144,6 +145,37 @@ class OctopusSDKInitializer {
144
145
  return nil
145
146
  }
146
147
 
148
+ func parseUIConfiguration(from options: [String: Any]) -> OctopusUIConfiguration? {
149
+ guard let uiOptions = options["ui"] as? [String: Any] else {
150
+ return nil
151
+ }
152
+
153
+ let supportedKeys = [
154
+ "bottomSafeAreaInset",
155
+ "bottomPadding",
156
+ "contentPadding",
157
+ "contentPaddingBottom",
158
+ ]
159
+
160
+ var bottomInset: CGFloat?
161
+
162
+ for key in supportedKeys {
163
+ if let value = uiOptions[key] as? NSNumber {
164
+ bottomInset = CGFloat(truncating: value)
165
+ break
166
+ } else if let value = uiOptions[key] as? Double {
167
+ bottomInset = CGFloat(value)
168
+ break
169
+ }
170
+ }
171
+
172
+ guard let finalInset = bottomInset else {
173
+ return nil
174
+ }
175
+
176
+ return OctopusUIConfiguration(bottomSafeAreaInset: max(0, finalInset))
177
+ }
178
+
147
179
  private func parseConnectionMode(from connectionModeMap: [String: Any], eventManager: OctopusEventManager) throws -> ConnectionMode {
148
180
  let connectionModeType = connectionModeMap["type"] as? String
149
181
 
@@ -82,9 +82,6 @@ class OctopusSSOAuthenticator {
82
82
 
83
83
  let username = profileParams["username"] as? String
84
84
  let biography = profileParams["biography"] as? String
85
- let legalAgeReached = profileParams["legalAgeReached"] as? Bool
86
-
87
- let ageInformation: ClientUser.AgeInformation? = legalAgeReached != nil ? (legalAgeReached! ? .legalAgeReached : .underaged) : nil
88
85
 
89
86
  var profilePictureData: Data? = nil
90
87
  if let pictureUrl = profileParams["profilePicture"] as? String, !pictureUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
@@ -94,8 +91,7 @@ class OctopusSSOAuthenticator {
94
91
  return ClientUser.Profile(
95
92
  nickname: username,
96
93
  bio: biography,
97
- picture: profilePictureData,
98
- ageInformation: ageInformation
94
+ picture: profilePictureData
99
95
  )
100
96
  }
101
97
 
@@ -0,0 +1,6 @@
1
+ import CoreGraphics
2
+
3
+ struct OctopusUIConfiguration {
4
+ let bottomSafeAreaInset: CGFloat?
5
+ }
6
+
@@ -7,24 +7,35 @@ 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]?, fontConfiguration: [String: Any]?) throws {
10
+ func openUI(
11
+ octopus: OctopusSDK,
12
+ theme: OctopusUI.OctopusTheme?,
13
+ logoSource: [String: Any]?,
14
+ fontConfiguration: [String: Any]?,
15
+ uiConfiguration: OctopusUIConfiguration?
16
+ ) throws {
11
17
  guard let presentingViewController = RCTPresentedViewController() else {
12
18
  throw NSError(domain: "OPEN_UI_ERROR", code: 0, userInfo: [NSLocalizedDescriptionKey: "Could not find presenting view controller"])
13
19
  }
14
20
 
15
21
  // Create custom theme with font configuration
16
22
  let customTheme = createCustomTheme(baseTheme: theme, fontConfiguration: fontConfiguration)
17
-
18
- let octopusHomeScreen = OctopusHomeScreen(octopus: octopus)
19
- .environment(\.octopusTheme, customTheme)
20
-
21
- let hostingController = UIHostingController(rootView: AnyView(octopusHomeScreen))
22
- hostingController.modalPresentationStyle = .fullScreen
23
+ let bottomSafeAreaInset = uiConfiguration?.bottomSafeAreaInset
24
+ let initialTheme = (theme != nil || fontConfiguration != nil) ? customTheme : nil
23
25
 
26
+ let hostingController = UIHostingController(
27
+ rootView: makeHomeScreenView(octopus: octopus, theme: initialTheme, bottomSafeAreaInset: bottomSafeAreaInset)
28
+ )
29
+ hostingController.modalPresentationStyle = .fullScreen
30
+
24
31
  // Apply theme if provided
25
32
  if let _ = theme {
26
33
  // Apply theme immediately (without logo) to avoid delay
27
- hostingController.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, customTheme))
34
+ hostingController.rootView = makeHomeScreenView(
35
+ octopus: octopus,
36
+ theme: customTheme,
37
+ bottomSafeAreaInset: bottomSafeAreaInset
38
+ )
28
39
 
29
40
  // Then load logo asynchronously and update theme if logo loads
30
41
  if let logoSource = logoSource {
@@ -36,7 +47,11 @@ class OctopusUIManager {
36
47
  fonts: customTheme.fonts,
37
48
  assets: OctopusUI.OctopusTheme.Assets(logo: logoImage)
38
49
  )
39
- hostingController?.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, updatedTheme))
50
+ hostingController?.rootView = self.makeHomeScreenView(
51
+ octopus: octopus,
52
+ theme: updatedTheme,
53
+ bottomSafeAreaInset: bottomSafeAreaInset
54
+ )
40
55
  } else {
41
56
  // Theme is already applied, no need to do anything
42
57
  }
@@ -53,7 +68,11 @@ class OctopusUIManager {
53
68
  fonts: OctopusUI.OctopusTheme.Fonts(),
54
69
  assets: OctopusUI.OctopusTheme.Assets(logo: logoImage)
55
70
  )
56
- hostingController?.rootView = AnyView(OctopusHomeScreen(octopus: octopus).environment(\.octopusTheme, logoTheme))
71
+ hostingController?.rootView = self.makeHomeScreenView(
72
+ octopus: octopus,
73
+ theme: logoTheme,
74
+ bottomSafeAreaInset: bottomSafeAreaInset
75
+ )
57
76
  }
58
77
  }
59
78
  }
@@ -142,6 +161,98 @@ class OctopusUIManager {
142
161
  presentedViewController = nil
143
162
  }
144
163
  }
164
+
165
+ /// Embeds the Octopus UI into the given container view (for use in ViewManager / non-fullscreen).
166
+ /// Caller is responsible for setting octopus.set(onNavigateToURLCallback:) when interceptUrls is true.
167
+ func addEmbeddedView(
168
+ to containerView: UIView,
169
+ octopus: OctopusSDK,
170
+ theme: OctopusUI.OctopusTheme?,
171
+ logoSource: [String: Any]?,
172
+ fontConfiguration: [String: Any]?,
173
+ uiConfiguration: OctopusUIConfiguration?,
174
+ interceptUrls: Bool
175
+ ) {
176
+ let customTheme = createCustomTheme(baseTheme: theme, fontConfiguration: fontConfiguration)
177
+ let bottomSafeAreaInset = uiConfiguration?.bottomSafeAreaInset
178
+ let initialTheme = (theme != nil || fontConfiguration != nil) ? customTheme : nil
179
+
180
+ let hostingController = UIHostingController(
181
+ rootView: makeHomeScreenView(octopus: octopus, theme: initialTheme, bottomSafeAreaInset: bottomSafeAreaInset)
182
+ )
183
+ hostingController.view.backgroundColor = .clear
184
+ containerView.addSubview(hostingController.view)
185
+ hostingController.view.translatesAutoresizingMaskIntoConstraints = false
186
+ NSLayoutConstraint.activate([
187
+ hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
188
+ hostingController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
189
+ hostingController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
190
+ hostingController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
191
+ ])
192
+
193
+ if let parentVC = containerView.findViewController() {
194
+ parentVC.addChild(hostingController)
195
+ hostingController.didMove(toParent: parentVC)
196
+ }
197
+
198
+ if let _ = theme, let logoSource = logoSource {
199
+ loadLogo(from: logoSource) { [weak hostingController] logoImage in
200
+ DispatchQueue.main.async {
201
+ if let logoImage = logoImage {
202
+ let updatedTheme = OctopusUI.OctopusTheme(
203
+ colors: customTheme.colors,
204
+ fonts: customTheme.fonts,
205
+ assets: OctopusUI.OctopusTheme.Assets(logo: logoImage)
206
+ )
207
+ hostingController?.rootView = self.makeHomeScreenView(
208
+ octopus: octopus,
209
+ theme: updatedTheme,
210
+ bottomSafeAreaInset: bottomSafeAreaInset
211
+ )
212
+ }
213
+ }
214
+ }
215
+ } else if let logoSource = logoSource {
216
+ loadLogo(from: logoSource) { [weak hostingController] logoImage in
217
+ DispatchQueue.main.async {
218
+ if let logoImage = logoImage {
219
+ let logoTheme = OctopusUI.OctopusTheme(
220
+ colors: OctopusUI.OctopusTheme.Colors(),
221
+ fonts: OctopusUI.OctopusTheme.Fonts(),
222
+ assets: OctopusUI.OctopusTheme.Assets(logo: logoImage)
223
+ )
224
+ hostingController?.rootView = self.makeHomeScreenView(
225
+ octopus: octopus,
226
+ theme: logoTheme,
227
+ bottomSafeAreaInset: bottomSafeAreaInset
228
+ )
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ private func makeHomeScreenView(octopus: OctopusSDK, theme: OctopusUI.OctopusTheme?, bottomSafeAreaInset: CGFloat?) -> AnyView {
236
+ if let inset = bottomSafeAreaInset {
237
+ if let theme = theme {
238
+ return AnyView(
239
+ OctopusHomeScreen(octopus: octopus, bottomSafeAreaInset: inset)
240
+ .environment(\.octopusTheme, theme)
241
+ )
242
+ }
243
+
244
+ return AnyView(OctopusHomeScreen(octopus: octopus, bottomSafeAreaInset: inset))
245
+ }
246
+
247
+ if let theme = theme {
248
+ return AnyView(
249
+ OctopusHomeScreen(octopus: octopus)
250
+ .environment(\.octopusTheme, theme)
251
+ )
252
+ }
253
+
254
+ return AnyView(OctopusHomeScreen(octopus: octopus))
255
+ }
145
256
 
146
257
  private func createCustomTheme(baseTheme: OctopusUI.OctopusTheme?, fontConfiguration: [String: Any]?) -> OctopusUI.OctopusTheme {
147
258
  // If no font configuration, return the base theme or default
@@ -194,3 +305,16 @@ class OctopusUIManager {
194
305
  }
195
306
 
196
307
  }
308
+
309
+ // MARK: - Helper to find parent view controller for embedding
310
+ extension UIView {
311
+ func findViewController() -> UIViewController? {
312
+ if let nextResponder = self.next as? UIViewController {
313
+ return nextResponder
314
+ }
315
+ if let nextResponder = self.next as? UIView {
316
+ return nextResponder.findViewController()
317
+ }
318
+ return nil
319
+ }
320
+ }
@@ -0,0 +1,7 @@
1
+ #import <React/RCTViewManager.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(OctopusUIViewManager, RCTViewManager)
4
+
5
+ RCT_EXPORT_VIEW_PROPERTY(interceptUrls, BOOL)
6
+
7
+ @end
@@ -0,0 +1,37 @@
1
+ import React
2
+ import UIKit
3
+
4
+ /// Container view that embeds the native Octopus UI when added to a window.
5
+ /// The `interceptUrls` property is set by React Native via RCT_EXPORT_VIEW_PROPERTY (KVC on the view).
6
+ @objc final class OctopusEmbeddedContainerView: UIView {
7
+ weak var bridge: RCTBridge?
8
+ @objc var interceptUrls: Bool = false
9
+ private var hasEmbedded = false
10
+
11
+ override func didMoveToWindow() {
12
+ super.didMoveToWindow()
13
+ guard window != nil, !hasEmbedded else { return }
14
+ guard let bridge = bridge else { return }
15
+
16
+ guard let module = bridge.module(for: OctopusReactNativeSdk.self) as? OctopusReactNativeSdk else {
17
+ return
18
+ }
19
+ module.addEmbeddedView(containerView: self, interceptUrls: interceptUrls)
20
+ hasEmbedded = true
21
+ }
22
+ }
23
+
24
+ @objc(OctopusUIViewManager)
25
+ class OctopusUIViewManager: RCTViewManager {
26
+
27
+ override func view() -> UIView! {
28
+ let view = OctopusEmbeddedContainerView()
29
+ view.bridge = bridge
30
+ view.backgroundColor = .clear
31
+ return view
32
+ }
33
+
34
+ override static func moduleName() -> String! {
35
+ return "OctopusUIView"
36
+ }
37
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ import { requireNativeComponent, StyleSheet } from 'react-native';
4
+ import { jsx as _jsx } from "react/jsx-runtime";
5
+ const NativeOctopusUIView = requireNativeComponent('OctopusUIView');
6
+
7
+ /**
8
+ * Embeds the Octopus Community UI as a native view inside your screen.
9
+ * Use this when you want to keep your app navigation (e.g. bottom tab bar) visible
10
+ * instead of opening the SDK in fullscreen with `openUI()`.
11
+ *
12
+ * You must call `initialize()` before rendering this component.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * function CommunityTab() {
17
+ * return (
18
+ * <View style={{ flex: 1 }}>
19
+ * <OctopusUIView interceptUrls={true} style={StyleSheet.absoluteFill} />
20
+ * </View>
21
+ * );
22
+ * }
23
+ * ```
24
+ */
25
+ export function OctopusUIView({
26
+ interceptUrls = false,
27
+ style
28
+ }) {
29
+ return /*#__PURE__*/_jsx(NativeOctopusUIView, {
30
+ interceptUrls: interceptUrls,
31
+ style: StyleSheet.flatten([styles.default, style])
32
+ });
33
+ }
34
+ const styles = StyleSheet.create({
35
+ default: {
36
+ flex: 1
37
+ }
38
+ });
39
+ //# sourceMappingURL=OctopusUIView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["requireNativeComponent","StyleSheet","jsx","_jsx","NativeOctopusUIView","OctopusUIView","interceptUrls","style","flatten","styles","default","create","flex"],"sourceRoot":"../../src","sources":["OctopusUIView.tsx"],"mappings":";;AAAA,SACEA,sBAAsB,EACtBC,UAAU,QAGL,cAAc;AAAC,SAAAC,GAAA,IAAAC,IAAA;AActB,MAAMC,mBAAmB,GACvBJ,sBAAsB,CAAqB,eAAe,CAAC;;AAE7D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASK,aAAaA,CAAC;EAC5BC,aAAa,GAAG,KAAK;EACrBC;AACkB,CAAC,EAAE;EACrB,oBACEJ,IAAA,CAACC,mBAAmB;IAClBE,aAAa,EAAEA,aAAc;IAC7BC,KAAK,EAAEN,UAAU,CAACO,OAAO,CAAC,CAACC,MAAM,CAACC,OAAO,EAAEH,KAAK,CAAC;EAAE,CACpD,CAAC;AAEN;AAEA,MAAME,MAAM,GAAGR,UAAU,CAACU,MAAM,CAAC;EAC/BD,OAAO,EAAE;IACPE,IAAI,EAAE;EACR;AACF,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ import { eventEmitter } from "./internals/eventEmitter.js";
4
+ /**
5
+ * Adds a listener for community access changes.
6
+ *
7
+ * This listener receives the **Octopus-managed** access state: the cohort value that determines
8
+ * whether the user has access to the community (when the SDK manages the A/B logic). It is triggered
9
+ * when that state changes — e.g. after you call `overrideCommunityAccess`, or when the cohort is
10
+ * updated by Octopus. Use it to show or hide community entry points in your UI. If your app manages
11
+ * access itself (and only reports it via `trackCommunityAccess`), this listener is less relevant,
12
+ * since the SDK is not the source of the access decision.
13
+ *
14
+ * @param callback - Function called when the access status changes
15
+ * @returns A subscription object with a `remove()` method to unsubscribe
16
+ * @see {@link overrideCommunityAccess} – set the cohort when Octopus manages A/B.
17
+ * @see {@link trackCommunityAccess} – report access for analytics when your app manages access.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const subscription = addHasAccessToCommunityListener((hasAccess) => {
22
+ * console.log(`Has access to community: ${hasAccess}`);
23
+ * // Show or hide community features based on access
24
+ * });
25
+ * subscription.remove();
26
+ * ```
27
+ */
28
+ export function addHasAccessToCommunityListener(callback) {
29
+ return eventEmitter.addListener('hasAccessToCommunityChanged', data => {
30
+ callback(data.hasAccess);
31
+ });
32
+ }
33
+ //# sourceMappingURL=addHasAccessToCommunityListener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["eventEmitter","addHasAccessToCommunityListener","callback","addListener","data","hasAccess"],"sourceRoot":"../../src","sources":["addHasAccessToCommunityListener.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,6BAA0B;AAIvD;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,+BAA+BA,CAC7CC,QAA8C,EAC9C;EACA,OAAOF,YAAY,CAACG,WAAW,CAC7B,6BAA6B,EAC5BC,IAA4B,IAAK;IAChCF,QAAQ,CAACE,IAAI,CAACC,SAAS,CAAC;EAC1B,CACF,CAAC;AACH","ignoreList":[]}