@octopus-community/react-native 1.0.8 → 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 (108) 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 +22 -9
  14. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +5 -15
  15. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt +17 -2
  16. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIViewManager.kt +63 -0
  17. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt +2 -2
  18. package/ios/OctopusEventManager.swift +27 -0
  19. package/ios/OctopusEventSerializer.swift +271 -0
  20. package/ios/OctopusReactNativeSdk.mm +26 -1
  21. package/ios/OctopusReactNativeSdk.swift +216 -2
  22. package/ios/OctopusSSOAuthenticator.swift +1 -5
  23. package/ios/OctopusUIManager.swift +83 -0
  24. package/ios/OctopusUIViewManager.m +7 -0
  25. package/ios/OctopusUIViewManager.swift +37 -0
  26. package/lib/module/OctopusUIView.js +39 -0
  27. package/lib/module/OctopusUIView.js.map +1 -0
  28. package/lib/module/addHasAccessToCommunityListener.js +33 -0
  29. package/lib/module/addHasAccessToCommunityListener.js.map +1 -0
  30. package/lib/module/addNavigateToUrlListener.js +41 -0
  31. package/lib/module/addNavigateToUrlListener.js.map +1 -0
  32. package/lib/module/addNotSeenNotificationsCountListener.js +30 -0
  33. package/lib/module/addNotSeenNotificationsCountListener.js.map +1 -0
  34. package/lib/module/addSDKEventListener.js +48 -0
  35. package/lib/module/addSDKEventListener.js.map +1 -0
  36. package/lib/module/connectUser.js +24 -3
  37. package/lib/module/connectUser.js.map +1 -1
  38. package/lib/module/index.js +12 -0
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/initialize.js +9 -12
  41. package/lib/module/initialize.js.map +1 -1
  42. package/lib/module/openUI.js +23 -2
  43. package/lib/module/openUI.js.map +1 -1
  44. package/lib/module/overrideCommunityAccess.js +36 -0
  45. package/lib/module/overrideCommunityAccess.js.map +1 -0
  46. package/lib/module/overrideDefaultLocale.js +75 -0
  47. package/lib/module/overrideDefaultLocale.js.map +1 -0
  48. package/lib/module/trackCommunityAccess.js +33 -0
  49. package/lib/module/trackCommunityAccess.js.map +1 -0
  50. package/lib/module/trackCustomEvent.js +36 -0
  51. package/lib/module/trackCustomEvent.js.map +1 -0
  52. package/lib/module/types/sdkEvents.js +2 -0
  53. package/lib/module/types/sdkEvents.js.map +1 -0
  54. package/lib/module/types/urlOpeningStrategy.js +23 -0
  55. package/lib/module/types/urlOpeningStrategy.js.map +1 -0
  56. package/lib/module/updateNotSeenNotificationsCount.js +33 -0
  57. package/lib/module/updateNotSeenNotificationsCount.js.map +1 -0
  58. package/lib/typescript/src/OctopusUIView.d.ts +32 -0
  59. package/lib/typescript/src/OctopusUIView.d.ts.map +1 -0
  60. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts +27 -0
  61. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts.map +1 -0
  62. package/lib/typescript/src/addNavigateToUrlListener.d.ts +31 -0
  63. package/lib/typescript/src/addNavigateToUrlListener.d.ts.map +1 -0
  64. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts +24 -0
  65. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts.map +1 -0
  66. package/lib/typescript/src/addSDKEventListener.d.ts +43 -0
  67. package/lib/typescript/src/addSDKEventListener.d.ts.map +1 -0
  68. package/lib/typescript/src/connectUser.d.ts +24 -8
  69. package/lib/typescript/src/connectUser.d.ts.map +1 -1
  70. package/lib/typescript/src/index.d.ts +13 -0
  71. package/lib/typescript/src/index.d.ts.map +1 -1
  72. package/lib/typescript/src/initialize.d.ts +9 -12
  73. package/lib/typescript/src/initialize.d.ts.map +1 -1
  74. package/lib/typescript/src/openUI.d.ts +28 -1
  75. package/lib/typescript/src/openUI.d.ts.map +1 -1
  76. package/lib/typescript/src/overrideCommunityAccess.d.ts +30 -0
  77. package/lib/typescript/src/overrideCommunityAccess.d.ts.map +1 -0
  78. package/lib/typescript/src/overrideDefaultLocale.d.ts +37 -0
  79. package/lib/typescript/src/overrideDefaultLocale.d.ts.map +1 -0
  80. package/lib/typescript/src/trackCommunityAccess.d.ts +27 -0
  81. package/lib/typescript/src/trackCommunityAccess.d.ts.map +1 -0
  82. package/lib/typescript/src/trackCustomEvent.d.ts +30 -0
  83. package/lib/typescript/src/trackCustomEvent.d.ts.map +1 -0
  84. package/lib/typescript/src/types/sdkEvents.d.ts +222 -0
  85. package/lib/typescript/src/types/sdkEvents.d.ts.map +1 -0
  86. package/lib/typescript/src/types/urlOpeningStrategy.d.ts +20 -0
  87. package/lib/typescript/src/types/urlOpeningStrategy.d.ts.map +1 -0
  88. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts +27 -0
  89. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts.map +1 -0
  90. package/package.json +2 -1
  91. package/src/OctopusUIView.tsx +57 -0
  92. package/src/addHasAccessToCommunityListener.ts +38 -0
  93. package/src/addNavigateToUrlListener.ts +54 -0
  94. package/src/addNotSeenNotificationsCountListener.ts +35 -0
  95. package/src/addSDKEventListener.ts +49 -0
  96. package/src/connectUser.ts +24 -8
  97. package/src/index.ts +13 -0
  98. package/src/initialize.ts +9 -12
  99. package/src/openUI.ts +32 -2
  100. package/src/overrideCommunityAccess.ts +33 -0
  101. package/src/overrideDefaultLocale.ts +88 -0
  102. package/src/trackCommunityAccess.ts +30 -0
  103. package/src/trackCustomEvent.ts +36 -0
  104. package/src/types/sdkEvents.ts +315 -0
  105. package/src/types/urlOpeningStrategy.ts +20 -0
  106. package/src/updateNotSeenNotificationsCount.ts +30 -0
  107. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +0 -155
  108. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +0 -434
@@ -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 {
@@ -18,6 +19,7 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
18
19
  private var logoSource: [String: Any]?
19
20
  private var fontConfiguration: [String: Any]?
20
21
  private var uiConfiguration: OctopusUIConfiguration?
22
+ private var cancellables = Set<AnyCancellable>()
21
23
 
22
24
  // MARK: - RCTBridgeModule
23
25
 
@@ -42,6 +44,10 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
42
44
  self.logoSource = sdkInitializer.getLogoSource(from: options)
43
45
  self.fontConfiguration = sdkInitializer.getFontConfiguration(from: options)
44
46
  self.uiConfiguration = sdkInitializer.parseUIConfiguration(from: options)
47
+
48
+ // Start observing reactive events after SDK initialization
49
+ startObservingReactiveEvents()
50
+
45
51
  resolve(nil)
46
52
  } catch {
47
53
  reject("INITIALIZE_ERROR", "Failed to initialize Octopus SDK: \(error.localizedDescription)", error)
@@ -108,15 +114,25 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
108
114
 
109
115
  // MARK: - UI management
110
116
 
111
- @objc(openUI:withRejecter:)
112
- 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 {
113
119
  guard let octopus = octopusSDK else {
114
120
  reject("OPEN_UI_ERROR", "SDK not initialized. Call initialize() first.", nil)
115
121
  return
116
122
  }
117
123
 
124
+ let interceptUrls = (options as? [String: Any])?["interceptUrls"] as? Bool ?? false
125
+
118
126
  DispatchQueue.main.async {
119
127
  do {
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
+ }
120
136
  try self.uiManager.openUI(
121
137
  octopus: octopus,
122
138
  theme: self.theme,
@@ -131,6 +147,15 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
131
147
  }
132
148
  }
133
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
+
134
159
  @objc(closeUI:withRejecter:)
135
160
  func closeUI(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
136
161
  DispatchQueue.main.async {
@@ -143,6 +168,28 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
143
168
  }
144
169
  }
145
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
+
146
193
  @objc(updateColorScheme:withResolver:withRejecter:)
147
194
  func updateColorScheme(colorScheme: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
148
195
  // iOS uses adaptive colors that automatically respond to system appearance changes
@@ -160,6 +207,134 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
160
207
  }
161
208
  }
162
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
+ }
163
338
 
164
339
  // MARK: - Lifecycle management
165
340
 
@@ -168,9 +343,48 @@ class OctopusReactNativeSdk: NSObject, RCTBridgeModule {
168
343
  }
169
344
 
170
345
  private func cleanup() {
346
+ cancellables.removeAll()
171
347
  uiManager.cleanup()
172
348
  octopusSDK = nil
173
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
+ }
174
388
 
175
389
  @objc func invalidate() {
176
390
  cleanup()
@@ -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
 
@@ -162,6 +162,76 @@ class OctopusUIManager {
162
162
  }
163
163
  }
164
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
+
165
235
  private func makeHomeScreenView(octopus: OctopusSDK, theme: OctopusUI.OctopusTheme?, bottomSafeAreaInset: CGFloat?) -> AnyView {
166
236
  if let inset = bottomSafeAreaInset {
167
237
  if let theme = theme {
@@ -235,3 +305,16 @@ class OctopusUIManager {
235
305
  }
236
306
 
237
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":[]}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ import { eventEmitter } from "./internals/eventEmitter.js";
4
+ import { OctopusReactNativeSdk } from "./internals/nativeModule.js";
5
+ import { UrlOpeningStrategy as UrlOpeningStrategyEnum } from "./types/urlOpeningStrategy.js";
6
+ /**
7
+ * Adds a listener for URL navigation events from the Octopus Community UI.
8
+ *
9
+ * Only has an effect when the UI was opened with `openUI({ interceptUrls: true })`.
10
+ * When the user taps a link, this callback is invoked with the URL. Return
11
+ * `handledByApp` if your app handles the URL (e.g. in-app web view), or
12
+ * `handledByOctopus` to let the SDK open it in the system browser.
13
+ *
14
+ * @param callback - Function called with the tapped URL. Can be async.
15
+ * Return `UrlOpeningStrategy.handledByApp` or `UrlOpeningStrategy.handledByOctopus`.
16
+ * @returns A subscription object with a `remove()` method to unsubscribe.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const subscription = addNavigateToUrlListener(async (url) => {
21
+ * if (url.startsWith('https://myapp.com/')) {
22
+ * // Handle deep link in-app
23
+ * Linking.openURL(url);
24
+ * return UrlOpeningStrategy.handledByApp;
25
+ * }
26
+ * return UrlOpeningStrategy.handledByOctopus; // Open in system browser
27
+ * });
28
+ *
29
+ * // Later, to unsubscribe:
30
+ * subscription.remove();
31
+ * ```
32
+ */
33
+ export function addNavigateToUrlListener(callback) {
34
+ return eventEmitter.addListener('navigateToUrl', async data => {
35
+ const strategy = await Promise.resolve(callback(data.url));
36
+ if (strategy === UrlOpeningStrategyEnum.handledByOctopus) {
37
+ OctopusReactNativeSdk.handleUrlStrategy(data.url, UrlOpeningStrategyEnum.handledByOctopus);
38
+ }
39
+ });
40
+ }
41
+ //# sourceMappingURL=addNavigateToUrlListener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["eventEmitter","OctopusReactNativeSdk","UrlOpeningStrategy","UrlOpeningStrategyEnum","addNavigateToUrlListener","callback","addListener","data","strategy","Promise","resolve","url","handledByOctopus","handleUrlStrategy"],"sourceRoot":"../../src","sources":["addNavigateToUrlListener.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,6BAA0B;AACvD,SAASC,qBAAqB,QAAQ,6BAA0B;AAChE,SAEEC,kBAAkB,IAAIC,sBAAsB,QACvC,+BAA4B;AAMnC;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;AACA,OAAO,SAASC,wBAAwBA,CACtCC,QAAuC,EACvC;EACA,OAAOL,YAAY,CAACM,WAAW,CAC7B,eAAe,EACf,MAAOC,IAAqB,IAAK;IAC/B,MAAMC,QAAQ,GAAG,MAAMC,OAAO,CAACC,OAAO,CAACL,QAAQ,CAACE,IAAI,CAACI,GAAG,CAAC,CAAC;IAC1D,IAAIH,QAAQ,KAAKL,sBAAsB,CAACS,gBAAgB,EAAE;MACxDX,qBAAqB,CAACY,iBAAiB,CACrCN,IAAI,CAACI,GAAG,EACRR,sBAAsB,CAACS,gBACzB,CAAC;IACH;EACF,CACF,CAAC;AACH","ignoreList":[]}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ import { eventEmitter } from "./internals/eventEmitter.js";
4
+ /**
5
+ * Adds a listener for not seen notifications count changes.
6
+ *
7
+ * This listener is triggered whenever the count of unseen notifications changes.
8
+ * The count is automatically updated by the SDK, but can also be manually refreshed
9
+ * using `updateNotSeenNotificationsCount()`.
10
+ *
11
+ * @param callback - Function called when the notification count changes
12
+ * @returns A subscription object with a `remove()` method to unsubscribe
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const subscription = addNotSeenNotificationsCountListener((count) => {
17
+ * console.log(`Unseen notifications: ${count}`);
18
+ * // Update your app's badge or UI
19
+ * });
20
+ *
21
+ * // Later, to unsubscribe:
22
+ * subscription.remove();
23
+ * ```
24
+ */
25
+ export function addNotSeenNotificationsCountListener(callback) {
26
+ return eventEmitter.addListener('notSeenNotificationsCountChanged', data => {
27
+ callback(data.count);
28
+ });
29
+ }
30
+ //# sourceMappingURL=addNotSeenNotificationsCountListener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["eventEmitter","addNotSeenNotificationsCountListener","callback","addListener","data","count"],"sourceRoot":"../../src","sources":["addNotSeenNotificationsCountListener.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,OAAO,SAASC,oCAAoCA,CAClDC,QAAmD,EACnD;EACA,OAAOF,YAAY,CAACG,WAAW,CAC7B,kCAAkC,EACjCC,IAAuB,IAAK;IAC3BF,QAAQ,CAACE,IAAI,CAACC,KAAK,CAAC;EACtB,CACF,CAAC;AACH","ignoreList":[]}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ import { eventEmitter } from "./internals/eventEmitter.js";
4
+ /**
5
+ * Adds a listener for SDK events.
6
+ *
7
+ * This listener receives all SDK events including:
8
+ * - Content creation (posts, comments, replies)
9
+ * - Content deletion
10
+ * - Reactions and interactions
11
+ * - Gamification events
12
+ * - Screen navigation
13
+ * - Profile modifications
14
+ * - Session events
15
+ * - And more...
16
+ *
17
+ * Use TypeScript type guards to narrow down specific event types:
18
+ *
19
+ * @param callback - Function called when any SDK event occurs
20
+ * @returns A subscription object with a `remove()` method to unsubscribe
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const subscription = addSDKEventListener((event) => {
25
+ * switch (event.type) {
26
+ * case 'postCreated':
27
+ * console.log(`Post created: ${event.postId}`);
28
+ * break;
29
+ * case 'reactionModified':
30
+ * console.log(`Reaction changed on ${event.contentId}`);
31
+ * break;
32
+ * case 'gamificationPointsGained':
33
+ * console.log(`Gained ${event.points} points for ${event.action}`);
34
+ * break;
35
+ * // ... handle other event types
36
+ * }
37
+ * });
38
+ *
39
+ * // Later, to unsubscribe:
40
+ * subscription.remove();
41
+ * ```
42
+ */
43
+ export function addSDKEventListener(callback) {
44
+ return eventEmitter.addListener('sdkEvent', data => {
45
+ callback(data);
46
+ });
47
+ }
48
+ //# sourceMappingURL=addSDKEventListener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["eventEmitter","addSDKEventListener","callback","addListener","data"],"sourceRoot":"../../src","sources":["addSDKEventListener.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,6BAA0B;AAKvD;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,mBAAmBA,CAACC,QAAkC,EAAE;EACtE,OAAOF,YAAY,CAACG,WAAW,CAAC,UAAU,EAAGC,IAAc,IAAK;IAC9DF,QAAQ,CAACE,IAAI,CAAC;EAChB,CAAC,CAAC;AACJ","ignoreList":[]}