@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.
- package/OctopusReactNativeSdk.podspec +1 -1
- package/README.md +40 -35
- package/android/build.gradle +2 -0
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/AndroidManifestNew.xml +2 -1
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusActivity.kt +56 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusContent.kt +396 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventEmitter.kt +22 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventSerializer.kt +339 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactModule.kt +326 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/{OctopusReactNativeSdkPackage.kt → OctopusReactPackage.kt} +3 -3
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +53 -9
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +5 -15
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfiguration.kt +6 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfigurationManager.kt +12 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt +17 -2
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIViewManager.kt +63 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt +2 -2
- package/ios/OctopusEventManager.swift +27 -0
- package/ios/OctopusEventSerializer.swift +271 -0
- package/ios/OctopusReactNativeSdk.mm +26 -1
- package/ios/OctopusReactNativeSdk.swift +225 -3
- package/ios/OctopusSDKInitializer.swift +32 -0
- package/ios/OctopusSSOAuthenticator.swift +1 -5
- package/ios/OctopusUIConfiguration.swift +6 -0
- package/ios/OctopusUIManager.swift +134 -10
- package/ios/OctopusUIViewManager.m +7 -0
- package/ios/OctopusUIViewManager.swift +37 -0
- package/lib/module/OctopusUIView.js +39 -0
- package/lib/module/OctopusUIView.js.map +1 -0
- package/lib/module/addHasAccessToCommunityListener.js +33 -0
- package/lib/module/addHasAccessToCommunityListener.js.map +1 -0
- package/lib/module/addNavigateToUrlListener.js +41 -0
- package/lib/module/addNavigateToUrlListener.js.map +1 -0
- package/lib/module/addNotSeenNotificationsCountListener.js +30 -0
- package/lib/module/addNotSeenNotificationsCountListener.js.map +1 -0
- package/lib/module/addSDKEventListener.js +48 -0
- package/lib/module/addSDKEventListener.js.map +1 -0
- package/lib/module/connectUser.js +24 -3
- package/lib/module/connectUser.js.map +1 -1
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/initialize.js +13 -12
- package/lib/module/initialize.js.map +1 -1
- package/lib/module/openUI.js +23 -2
- package/lib/module/openUI.js.map +1 -1
- package/lib/module/overrideCommunityAccess.js +36 -0
- package/lib/module/overrideCommunityAccess.js.map +1 -0
- package/lib/module/overrideDefaultLocale.js +75 -0
- package/lib/module/overrideDefaultLocale.js.map +1 -0
- package/lib/module/trackCommunityAccess.js +33 -0
- package/lib/module/trackCommunityAccess.js.map +1 -0
- package/lib/module/trackCustomEvent.js +36 -0
- package/lib/module/trackCustomEvent.js.map +1 -0
- package/lib/module/types/sdkEvents.js +2 -0
- package/lib/module/types/sdkEvents.js.map +1 -0
- package/lib/module/types/urlOpeningStrategy.js +23 -0
- package/lib/module/types/urlOpeningStrategy.js.map +1 -0
- package/lib/module/updateNotSeenNotificationsCount.js +33 -0
- package/lib/module/updateNotSeenNotificationsCount.js.map +1 -0
- package/lib/typescript/src/OctopusUIView.d.ts +32 -0
- package/lib/typescript/src/OctopusUIView.d.ts.map +1 -0
- package/lib/typescript/src/addHasAccessToCommunityListener.d.ts +27 -0
- package/lib/typescript/src/addHasAccessToCommunityListener.d.ts.map +1 -0
- package/lib/typescript/src/addNavigateToUrlListener.d.ts +31 -0
- package/lib/typescript/src/addNavigateToUrlListener.d.ts.map +1 -0
- package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts +24 -0
- package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts.map +1 -0
- package/lib/typescript/src/addSDKEventListener.d.ts +43 -0
- package/lib/typescript/src/addSDKEventListener.d.ts.map +1 -0
- package/lib/typescript/src/connectUser.d.ts +24 -8
- package/lib/typescript/src/connectUser.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/initialize.d.ts +22 -12
- package/lib/typescript/src/initialize.d.ts.map +1 -1
- package/lib/typescript/src/openUI.d.ts +28 -1
- package/lib/typescript/src/openUI.d.ts.map +1 -1
- package/lib/typescript/src/overrideCommunityAccess.d.ts +30 -0
- package/lib/typescript/src/overrideCommunityAccess.d.ts.map +1 -0
- package/lib/typescript/src/overrideDefaultLocale.d.ts +37 -0
- package/lib/typescript/src/overrideDefaultLocale.d.ts.map +1 -0
- package/lib/typescript/src/trackCommunityAccess.d.ts +27 -0
- package/lib/typescript/src/trackCommunityAccess.d.ts.map +1 -0
- package/lib/typescript/src/trackCustomEvent.d.ts +30 -0
- package/lib/typescript/src/trackCustomEvent.d.ts.map +1 -0
- package/lib/typescript/src/types/sdkEvents.d.ts +222 -0
- package/lib/typescript/src/types/sdkEvents.d.ts.map +1 -0
- package/lib/typescript/src/types/urlOpeningStrategy.d.ts +20 -0
- package/lib/typescript/src/types/urlOpeningStrategy.d.ts.map +1 -0
- package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts +27 -0
- package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/OctopusUIView.tsx +57 -0
- package/src/addHasAccessToCommunityListener.ts +38 -0
- package/src/addNavigateToUrlListener.ts +54 -0
- package/src/addNotSeenNotificationsCountListener.ts +35 -0
- package/src/addSDKEventListener.ts +49 -0
- package/src/connectUser.ts +24 -8
- package/src/index.ts +13 -0
- package/src/initialize.ts +23 -12
- package/src/openUI.ts +32 -2
- package/src/overrideCommunityAccess.ts +33 -0
- package/src/overrideDefaultLocale.ts +88 -0
- package/src/trackCommunityAccess.ts +30 -0
- package/src/trackCustomEvent.ts +36 -0
- package/src/types/sdkEvents.ts +315 -0
- package/src/types/urlOpeningStrategy.ts +20 -0
- package/src/updateNotSeenNotificationsCount.ts +30 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +0 -155
- 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
|
-
|
|
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
|
|
|
@@ -7,24 +7,35 @@ import React
|
|
|
7
7
|
class OctopusUIManager {
|
|
8
8
|
private weak var presentedViewController: UIViewController?
|
|
9
9
|
|
|
10
|
-
func openUI(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,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":[]}
|