@octopus-community/react-native 1.0.0
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/LICENSE.md +143 -0
- package/OctopusReactNativeSdk.podspec +33 -0
- package/README.md +161 -0
- package/android/build.gradle +92 -0
- package/android/gradle.properties +9 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/AndroidManifestNew.xml +8 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventEmitter.kt +48 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +73 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkPackage.kt +17 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +55 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +124 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +71 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt +34 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt +39 -0
- package/ios/OctopusEventManager.swift +44 -0
- package/ios/OctopusReactNativeSdk-Bridging-Header.h +3 -0
- package/ios/OctopusReactNativeSdk.mm +37 -0
- package/ios/OctopusReactNativeSdk.swift +148 -0
- package/ios/OctopusSDKInitializer.swift +62 -0
- package/ios/OctopusSSOAuthenticator.swift +157 -0
- package/ios/OctopusUIManager.swift +40 -0
- package/ios/ProfileFieldMapper.swift +43 -0
- package/lib/module/addEditUserListener.js +13 -0
- package/lib/module/addEditUserListener.js.map +1 -0
- package/lib/module/addLoginRequiredListener.js +14 -0
- package/lib/module/addLoginRequiredListener.js.map +1 -0
- package/lib/module/addUserTokenRequestListener.js +26 -0
- package/lib/module/addUserTokenRequestListener.js.map +1 -0
- package/lib/module/closeUI.js +11 -0
- package/lib/module/closeUI.js.map +1 -0
- package/lib/module/connectUser.js +14 -0
- package/lib/module/connectUser.js.map +1 -0
- package/lib/module/disconnectUser.js +7 -0
- package/lib/module/disconnectUser.js.map +1 -0
- package/lib/module/enums/LogLevel.enum.js +13 -0
- package/lib/module/enums/LogLevel.enum.js.map +1 -0
- package/lib/module/index.js +15 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/initialize.js +38 -0
- package/lib/module/initialize.js.map +1 -0
- package/lib/module/internals/eventEmitter.js +6 -0
- package/lib/module/internals/eventEmitter.js.map +1 -0
- package/lib/module/internals/logger.js +42 -0
- package/lib/module/internals/logger.js.map +1 -0
- package/lib/module/internals/nativeModule.js +13 -0
- package/lib/module/internals/nativeModule.js.map +1 -0
- package/lib/module/logger.js +4 -0
- package/lib/module/logger.js.map +1 -0
- package/lib/module/openUI.js +11 -0
- package/lib/module/openUI.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types/userProfileField.js +2 -0
- package/lib/module/types/userProfileField.js.map +1 -0
- package/lib/module/useUserTokenProvider.js +33 -0
- package/lib/module/useUserTokenProvider.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/addEditUserListener.d.ts +14 -0
- package/lib/typescript/src/addEditUserListener.d.ts.map +1 -0
- package/lib/typescript/src/addLoginRequiredListener.d.ts +10 -0
- package/lib/typescript/src/addLoginRequiredListener.d.ts.map +1 -0
- package/lib/typescript/src/addUserTokenRequestListener.d.ts +10 -0
- package/lib/typescript/src/addUserTokenRequestListener.d.ts.map +1 -0
- package/lib/typescript/src/closeUI.d.ts +5 -0
- package/lib/typescript/src/closeUI.d.ts.map +1 -0
- package/lib/typescript/src/connectUser.d.ts +30 -0
- package/lib/typescript/src/connectUser.d.ts.map +1 -0
- package/lib/typescript/src/disconnectUser.d.ts +2 -0
- package/lib/typescript/src/disconnectUser.d.ts.map +1 -0
- package/lib/typescript/src/enums/LogLevel.enum.d.ts +10 -0
- package/lib/typescript/src/enums/LogLevel.enum.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/initialize.d.ts +50 -0
- package/lib/typescript/src/initialize.d.ts.map +1 -0
- package/lib/typescript/src/internals/eventEmitter.d.ts +3 -0
- package/lib/typescript/src/internals/eventEmitter.d.ts.map +1 -0
- package/lib/typescript/src/internals/logger.d.ts +20 -0
- package/lib/typescript/src/internals/logger.d.ts.map +1 -0
- package/lib/typescript/src/internals/nativeModule.d.ts +2 -0
- package/lib/typescript/src/internals/nativeModule.d.ts.map +1 -0
- package/lib/typescript/src/logger.d.ts +3 -0
- package/lib/typescript/src/logger.d.ts.map +1 -0
- package/lib/typescript/src/openUI.d.ts +5 -0
- package/lib/typescript/src/openUI.d.ts.map +1 -0
- package/lib/typescript/src/types/userProfileField.d.ts +14 -0
- package/lib/typescript/src/types/userProfileField.d.ts.map +1 -0
- package/lib/typescript/src/useUserTokenProvider.d.ts +17 -0
- package/lib/typescript/src/useUserTokenProvider.d.ts.map +1 -0
- package/package.json +136 -0
- package/src/addEditUserListener.ts +19 -0
- package/src/addLoginRequiredListener.ts +16 -0
- package/src/addUserTokenRequestListener.ts +32 -0
- package/src/closeUI.ts +8 -0
- package/src/connectUser.ts +34 -0
- package/src/disconnectUser.ts +5 -0
- package/src/enums/LogLevel.enum.ts +9 -0
- package/src/index.ts +12 -0
- package/src/initialize.ts +56 -0
- package/src/internals/eventEmitter.ts +4 -0
- package/src/internals/logger.ts +60 -0
- package/src/internals/nativeModule.ts +18 -0
- package/src/logger.ts +7 -0
- package/src/openUI.ts +8 -0
- package/src/types/userProfileField.ts +13 -0
- package/src/useUserTokenProvider.ts +36 -0
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
package com.octopuscommunity.octopusreactnativesdk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.octopuscommunity.sdk.OctopusSDK
|
|
6
|
+
import com.octopuscommunity.sdk.domain.model.ClientUser
|
|
7
|
+
import com.octopuscommunity.sdk.domain.model.ClientUser.Profile.AgeInformation
|
|
8
|
+
import com.octopuscommunity.sdk.domain.model.Image
|
|
9
|
+
import kotlinx.coroutines.CoroutineScope
|
|
10
|
+
import kotlinx.coroutines.Dispatchers
|
|
11
|
+
import kotlinx.coroutines.launch
|
|
12
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
13
|
+
import kotlinx.coroutines.CancellationException
|
|
14
|
+
import java.util.UUID
|
|
15
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
16
|
+
import kotlin.coroutines.resume
|
|
17
|
+
import kotlin.coroutines.resumeWithException
|
|
18
|
+
|
|
19
|
+
class OctopusSSOAuthenticator(private val eventEmitter: OctopusEventEmitter) {
|
|
20
|
+
|
|
21
|
+
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
22
|
+
private val pendingTokenRequests =
|
|
23
|
+
ConcurrentHashMap<String, kotlin.coroutines.Continuation<String>>()
|
|
24
|
+
|
|
25
|
+
fun connectUser(params: ReadableMap, promise: Promise) {
|
|
26
|
+
coroutineScope.launch {
|
|
27
|
+
try {
|
|
28
|
+
val clientUser = parseClientUser(params)
|
|
29
|
+
|
|
30
|
+
OctopusSDK.connectUser(
|
|
31
|
+
user = clientUser,
|
|
32
|
+
tokenProvider = { requestTokenFromRN() }
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
promise.resolve(null)
|
|
36
|
+
} catch (e: Exception) {
|
|
37
|
+
promise.reject("CONNECT_USER_ERROR", "Failed to connect user: ${e.message}", e)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fun disconnectUser(promise: Promise) {
|
|
43
|
+
coroutineScope.launch {
|
|
44
|
+
try {
|
|
45
|
+
OctopusSDK.disconnectUser()
|
|
46
|
+
|
|
47
|
+
// Cancel all pending token requests
|
|
48
|
+
for ((_, continuation) in pendingTokenRequests) {
|
|
49
|
+
continuation.resumeWithException(CancellationException("User disconnected"))
|
|
50
|
+
}
|
|
51
|
+
pendingTokenRequests.clear()
|
|
52
|
+
|
|
53
|
+
promise.resolve(null)
|
|
54
|
+
} catch (e: Exception) {
|
|
55
|
+
promise.reject("DISCONNECT_USER_ERROR", "Failed to disconnect user: ${e.message}", e)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun completeTokenRequest(requestId: String, token: String) {
|
|
61
|
+
val continuation = pendingTokenRequests.remove(requestId)
|
|
62
|
+
continuation?.resume(token)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun cancelTokenRequest(requestId: String) {
|
|
66
|
+
val continuation = pendingTokenRequests.remove(requestId)
|
|
67
|
+
continuation?.resumeWithException(CancellationException("Token request cancelled"))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private suspend fun requestTokenFromRN(): String {
|
|
71
|
+
val requestId = UUID.randomUUID().toString()
|
|
72
|
+
|
|
73
|
+
return suspendCancellableCoroutine { continuation ->
|
|
74
|
+
pendingTokenRequests[requestId] = continuation
|
|
75
|
+
|
|
76
|
+
continuation.invokeOnCancellation {
|
|
77
|
+
pendingTokenRequests.remove(requestId)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
eventEmitter.emitUserTokenRequest(requestId)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private fun parseClientUser(params: ReadableMap): ClientUser {
|
|
85
|
+
val userId = params.getString("userId")
|
|
86
|
+
?: throw IllegalArgumentException("Missing user id")
|
|
87
|
+
|
|
88
|
+
val profile = parseUserProfile(params.getMap("profile"))
|
|
89
|
+
|
|
90
|
+
return ClientUser(
|
|
91
|
+
userId = userId,
|
|
92
|
+
profile = profile
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun parseUserProfile(profileParams: ReadableMap?): ClientUser.Profile {
|
|
97
|
+
if (profileParams == null) {
|
|
98
|
+
return ClientUser.Profile()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
val ageInformation = if (profileParams.hasKey("legalAgeReached")) {
|
|
102
|
+
if (profileParams.getBoolean("legalAgeReached")) {
|
|
103
|
+
AgeInformation.LegalAgeReached
|
|
104
|
+
} else {
|
|
105
|
+
AgeInformation.Underage
|
|
106
|
+
}
|
|
107
|
+
} else null
|
|
108
|
+
|
|
109
|
+
val profilePicture = profileParams.getString("profilePicture")?.let { pictureUrl ->
|
|
110
|
+
if (pictureUrl.startsWith("http://") || pictureUrl.startsWith("https://")) {
|
|
111
|
+
Image.Remote(pictureUrl)
|
|
112
|
+
} else {
|
|
113
|
+
Image.Local(pictureUrl)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ClientUser.Profile(
|
|
118
|
+
nickname = profileParams.getString("username"),
|
|
119
|
+
bio = profileParams.getString("biography"),
|
|
120
|
+
ageInformation = ageInformation,
|
|
121
|
+
avatar = profilePicture
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package com.octopuscommunity.octopusreactnativesdk
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.IntentFilter
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import androidx.activity.ComponentActivity
|
|
10
|
+
import androidx.activity.compose.setContent
|
|
11
|
+
import androidx.compose.foundation.layout.Box
|
|
12
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
13
|
+
import androidx.compose.runtime.Composable
|
|
14
|
+
import androidx.compose.ui.Modifier
|
|
15
|
+
import androidx.navigation.compose.NavHost
|
|
16
|
+
import androidx.navigation.compose.rememberNavController
|
|
17
|
+
import com.octopuscommunity.sdk.ui.OctopusDestination
|
|
18
|
+
import com.octopuscommunity.sdk.ui.octopusComposables
|
|
19
|
+
|
|
20
|
+
class OctopusUIActivity : ComponentActivity() {
|
|
21
|
+
private val closeUIReceiver = object : BroadcastReceiver() {
|
|
22
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
23
|
+
finish()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
28
|
+
super.onCreate(savedInstanceState)
|
|
29
|
+
registerCloseUIReceiver()
|
|
30
|
+
setContent {
|
|
31
|
+
OctopusUI()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun onDestroy() {
|
|
36
|
+
super.onDestroy()
|
|
37
|
+
unregisterReceiver(closeUIReceiver)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private fun registerCloseUIReceiver() {
|
|
41
|
+
val intentFilter = IntentFilter(OctopusUIController.CLOSE_UI_ACTION)
|
|
42
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
43
|
+
registerReceiver(closeUIReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
|
|
44
|
+
} else {
|
|
45
|
+
@Suppress("UnspecifiedRegisterReceiverFlag")
|
|
46
|
+
registerReceiver(closeUIReceiver, intentFilter)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Composable
|
|
52
|
+
private fun OctopusUI() {
|
|
53
|
+
Box(modifier = Modifier.fillMaxSize()) {
|
|
54
|
+
val navController = rememberNavController()
|
|
55
|
+
|
|
56
|
+
NavHost(
|
|
57
|
+
navController = navController,
|
|
58
|
+
startDestination = OctopusDestination.Home
|
|
59
|
+
) {
|
|
60
|
+
octopusComposables(
|
|
61
|
+
navController = navController,
|
|
62
|
+
onNavigateToLogin = {
|
|
63
|
+
OctopusEventEmitter.instance?.emitLoginRequired()
|
|
64
|
+
},
|
|
65
|
+
onNavigateToProfileEdit = { fieldToEdit ->
|
|
66
|
+
OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.octopuscommunity.octopusreactnativesdk
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
|
|
7
|
+
class OctopusUIController(private val reactContext: ReactApplicationContext) {
|
|
8
|
+
|
|
9
|
+
fun openUI(promise: Promise) {
|
|
10
|
+
try {
|
|
11
|
+
val intent = Intent(reactContext, OctopusUIActivity::class.java)
|
|
12
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
13
|
+
reactContext.startActivity(intent)
|
|
14
|
+
promise.resolve(null)
|
|
15
|
+
} catch (e: Exception) {
|
|
16
|
+
promise.reject("OPEN_UI_ERROR", "Failed to open Octopus UI", e)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fun closeUI(promise: Promise) {
|
|
21
|
+
try {
|
|
22
|
+
val intent = Intent(CLOSE_UI_ACTION)
|
|
23
|
+
intent.setPackage(reactContext.packageName)
|
|
24
|
+
reactContext.sendBroadcast(intent)
|
|
25
|
+
promise.resolve(null)
|
|
26
|
+
} catch (e: Exception) {
|
|
27
|
+
promise.reject("CLOSE_UI_ERROR", "Failed to close Octopus UI", e)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
companion object {
|
|
32
|
+
const val CLOSE_UI_ACTION = "com.octopuscommunity.octopusreactnativesdk.CLOSE_UI"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.octopuscommunity.octopusreactnativesdk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray
|
|
4
|
+
import com.octopuscommunity.sdk.domain.model.ProfileField
|
|
5
|
+
|
|
6
|
+
object ProfileFieldMapper {
|
|
7
|
+
|
|
8
|
+
fun fromReactNativeArray(appManagedFields: ReadableArray?): Set<ProfileField> {
|
|
9
|
+
if (appManagedFields == null) {
|
|
10
|
+
return emptySet()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
val profileFields = mutableSetOf<ProfileField>()
|
|
14
|
+
for (i in 0 until appManagedFields.size()) {
|
|
15
|
+
fromReactNativeString(appManagedFields.getString(i))?.let { profileField ->
|
|
16
|
+
profileFields.add(profileField)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return profileFields
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fun fromReactNativeString(fieldName: String?): ProfileField? {
|
|
23
|
+
return when (fieldName) {
|
|
24
|
+
"username" -> ProfileField.NICKNAME
|
|
25
|
+
"biography" -> ProfileField.BIO
|
|
26
|
+
"profilePicture" -> ProfileField.AVATAR
|
|
27
|
+
else -> null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun toReactNativeString(profileField: ProfileField?): String? {
|
|
32
|
+
return when (profileField) {
|
|
33
|
+
ProfileField.NICKNAME -> "username"
|
|
34
|
+
ProfileField.BIO -> "biography"
|
|
35
|
+
ProfileField.AVATAR -> "profilePicture"
|
|
36
|
+
else -> null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import Octopus
|
|
3
|
+
|
|
4
|
+
class OctopusEventManager {
|
|
5
|
+
private weak var eventEmitter: RCTEventEmitter?
|
|
6
|
+
private var hasListeners = false
|
|
7
|
+
|
|
8
|
+
init(eventEmitter: RCTEventEmitter) {
|
|
9
|
+
self.eventEmitter = eventEmitter
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func emitLoginRequired() {
|
|
13
|
+
if hasListeners {
|
|
14
|
+
eventEmitter?.sendEvent(withName: "loginRequired", body: nil)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func emitEditUser(profileField: ConnectionMode.SSOConfiguration.ProfileField?) {
|
|
19
|
+
if hasListeners {
|
|
20
|
+
let fieldToEdit = ProfileFieldMapper.toReactNativeString(profileField)
|
|
21
|
+
let eventBody = ["fieldToEdit": fieldToEdit]
|
|
22
|
+
eventEmitter?.sendEvent(withName: "editUser", body: eventBody)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func emitUserTokenRequest(requestId: String) {
|
|
27
|
+
if hasListeners {
|
|
28
|
+
let eventBody = ["requestId": requestId]
|
|
29
|
+
eventEmitter?.sendEvent(withName: "userTokenRequest", body: eventBody)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func startObserving() {
|
|
34
|
+
hasListeners = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func stopObserving() {
|
|
38
|
+
hasListeners = false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func supportedEvents() -> [String] {
|
|
42
|
+
return ["loginRequired", "editUser", "userTokenRequest"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(OctopusReactNativeSdk, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(initialize:(NSDictionary *)options
|
|
7
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
|
|
10
|
+
RCT_EXTERN_METHOD(openUI:(RCTPromiseResolveBlock)resolve
|
|
11
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
12
|
+
|
|
13
|
+
RCT_EXTERN_METHOD(closeUI:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
|
|
16
|
+
RCT_EXTERN_METHOD(connectUser:(NSDictionary *)params
|
|
17
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
18
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
19
|
+
|
|
20
|
+
RCT_EXTERN_METHOD(completeUserTokenRequest:(NSString *)requestId
|
|
21
|
+
withToken:(NSString *)token
|
|
22
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
23
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
24
|
+
|
|
25
|
+
RCT_EXTERN_METHOD(cancelUserTokenRequest:(NSString *)requestId
|
|
26
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
27
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
28
|
+
|
|
29
|
+
RCT_EXTERN_METHOD(disconnectUser:(RCTPromiseResolveBlock)resolve
|
|
30
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
31
|
+
|
|
32
|
+
+ (BOOL)requiresMainQueueSetup
|
|
33
|
+
{
|
|
34
|
+
return NO;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import Octopus
|
|
2
|
+
import OctopusUI
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
@objc(OctopusReactNativeSdk)
|
|
7
|
+
class OctopusReactNativeSdk: RCTEventEmitter {
|
|
8
|
+
|
|
9
|
+
// MARK: - Properties
|
|
10
|
+
|
|
11
|
+
private var octopusSDK: OctopusSDK?
|
|
12
|
+
private lazy var uiManager = OctopusUIManager()
|
|
13
|
+
private lazy var eventManager = OctopusEventManager(eventEmitter: self)
|
|
14
|
+
private let sdkInitializer = OctopusSDKInitializer()
|
|
15
|
+
private var ssoAuthenticator: OctopusSSOAuthenticator?
|
|
16
|
+
|
|
17
|
+
// MARK: - Initialization
|
|
18
|
+
|
|
19
|
+
@objc(initialize:withResolver:withRejecter:)
|
|
20
|
+
func initialize(options: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
21
|
+
do {
|
|
22
|
+
self.octopusSDK = try sdkInitializer.initialize(options: options, eventManager: eventManager)
|
|
23
|
+
self.ssoAuthenticator = OctopusSSOAuthenticator(octopusSDK: self.octopusSDK!, eventManager: eventManager)
|
|
24
|
+
resolve(nil)
|
|
25
|
+
} catch {
|
|
26
|
+
reject("INITIALIZE_ERROR", "Failed to initialize Octopus SDK: \(error.localizedDescription)", error)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: - User authentication
|
|
31
|
+
|
|
32
|
+
@objc(connectUser:withResolver:withRejecter:)
|
|
33
|
+
func connectUser(params: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
34
|
+
guard let authenticator = ssoAuthenticator else {
|
|
35
|
+
reject("CONNECT_USER_ERROR", "SDK not initialized. Call initialize() first.", nil)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Task {
|
|
40
|
+
do {
|
|
41
|
+
try await authenticator.connectUser(params: params)
|
|
42
|
+
resolve(nil)
|
|
43
|
+
} catch {
|
|
44
|
+
reject("CONNECT_USER_ERROR", "Failed to connect user: \(error.localizedDescription)", error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@objc(completeUserTokenRequest:withToken:withResolver:withRejecter:)
|
|
51
|
+
func completeUserTokenRequest(requestId: String, token: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
52
|
+
guard let authenticator = ssoAuthenticator else {
|
|
53
|
+
reject("PROVIDE_TOKEN_ERROR", "SDK not initialized", nil)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
authenticator.completeTokenRequest(requestId: requestId, token: token)
|
|
58
|
+
resolve(nil)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@objc(cancelUserTokenRequest:withResolver:withRejecter:)
|
|
62
|
+
func cancelUserTokenRequest(requestId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
63
|
+
guard let authenticator = ssoAuthenticator else {
|
|
64
|
+
reject("PROVIDE_TOKEN_ERROR", "SDK not initialized", nil)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
authenticator.cancelTokenRequest(requestId: requestId)
|
|
69
|
+
resolve(nil)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@objc(disconnectUser:withRejecter:)
|
|
73
|
+
func disconnectUser(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
74
|
+
guard let authenticator = ssoAuthenticator else {
|
|
75
|
+
reject("DISCONNECT_USER_ERROR", "SDK not initialized. Call initialize() first.", nil)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
do {
|
|
80
|
+
try authenticator.disconnectUser()
|
|
81
|
+
resolve(nil)
|
|
82
|
+
} catch {
|
|
83
|
+
reject("DISCONNECT_USER_ERROR", "Failed to disconnect user: \(error.localizedDescription)", error)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// MARK: - UI management
|
|
89
|
+
|
|
90
|
+
@objc(openUI:withRejecter:)
|
|
91
|
+
func openUI(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
92
|
+
guard let octopus = octopusSDK else {
|
|
93
|
+
reject("OPEN_UI_ERROR", "SDK not initialized. Call initialize() first.", nil)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
DispatchQueue.main.async {
|
|
98
|
+
do {
|
|
99
|
+
try self.uiManager.openUI(octopus: octopus)
|
|
100
|
+
resolve(nil)
|
|
101
|
+
} catch {
|
|
102
|
+
reject("OPEN_UI_ERROR", error.localizedDescription, error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@objc(closeUI:withRejecter:)
|
|
108
|
+
func closeUI(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
109
|
+
DispatchQueue.main.async {
|
|
110
|
+
do {
|
|
111
|
+
try self.uiManager.closeUI()
|
|
112
|
+
resolve(nil)
|
|
113
|
+
} catch {
|
|
114
|
+
reject("CLOSE_UI_ERROR", error.localizedDescription, error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: - Lifecycle management
|
|
120
|
+
|
|
121
|
+
deinit {
|
|
122
|
+
cleanup()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func cleanup() {
|
|
126
|
+
uiManager.cleanup()
|
|
127
|
+
octopusSDK = nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@objc override func invalidate() {
|
|
131
|
+
cleanup()
|
|
132
|
+
super.invalidate()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// MARK: - RCTEventEmitter overrides
|
|
136
|
+
|
|
137
|
+
@objc override func supportedEvents() -> [String]! {
|
|
138
|
+
return eventManager.supportedEvents()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@objc override func startObserving() {
|
|
142
|
+
eventManager.startObserving()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@objc override func stopObserving() {
|
|
146
|
+
eventManager.stopObserving()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import Octopus
|
|
2
|
+
|
|
3
|
+
class OctopusSDKInitializer {
|
|
4
|
+
func initialize(options: [String: Any], eventManager: OctopusEventManager) throws -> OctopusSDK {
|
|
5
|
+
guard let apiKey = options["apiKey"] as? String else {
|
|
6
|
+
throw InitializationError.missingAPIKey
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
guard let connectionModeMap = options["connectionMode"] as? [String: Any] else {
|
|
10
|
+
throw InitializationError.missingConnectionMode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let connectionMode = try parseConnectionMode(from: connectionModeMap, eventManager: eventManager)
|
|
14
|
+
return try OctopusSDK(apiKey: apiKey, connectionMode: connectionMode)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private func parseConnectionMode(from connectionModeMap: [String: Any], eventManager: OctopusEventManager) throws -> ConnectionMode {
|
|
18
|
+
let connectionModeType = connectionModeMap["type"] as? String
|
|
19
|
+
|
|
20
|
+
switch connectionModeType {
|
|
21
|
+
case "sso":
|
|
22
|
+
return try createSSOConnectionMode(from: connectionModeMap, eventManager: eventManager)
|
|
23
|
+
case "octopus":
|
|
24
|
+
return .octopus(deepLink: nil)
|
|
25
|
+
default:
|
|
26
|
+
throw InitializationError.invalidConnectionModeType
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private func createSSOConnectionMode(from connectionModeMap: [String: Any], eventManager: OctopusEventManager) throws -> ConnectionMode {
|
|
31
|
+
let appManagedFields = ProfileFieldMapper.fromReactNativeArray(connectionModeMap["appManagedFields"] as? [String])
|
|
32
|
+
|
|
33
|
+
return .sso(
|
|
34
|
+
.init(
|
|
35
|
+
appManagedFields: appManagedFields,
|
|
36
|
+
loginRequired: {
|
|
37
|
+
eventManager.emitLoginRequired()
|
|
38
|
+
},
|
|
39
|
+
modifyUser: { profileField in
|
|
40
|
+
eventManager.emitEditUser(profileField: profileField)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
enum InitializationError: Error, LocalizedError {
|
|
48
|
+
case missingAPIKey
|
|
49
|
+
case missingConnectionMode
|
|
50
|
+
case invalidConnectionModeType
|
|
51
|
+
|
|
52
|
+
var errorDescription: String? {
|
|
53
|
+
switch self {
|
|
54
|
+
case .missingAPIKey:
|
|
55
|
+
return "API key is required"
|
|
56
|
+
case .missingConnectionMode:
|
|
57
|
+
return "Connection mode is required"
|
|
58
|
+
case .invalidConnectionModeType:
|
|
59
|
+
return "Invalid connection mode type"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|