otpless-headless-rn 2.2.0 → 3.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/README.md CHANGED
@@ -44,6 +44,24 @@ useEffect(() => {
44
44
  }, []);
45
45
  ```
46
46
 
47
+ ### SSL pinning (optional)
48
+
49
+ SSL pinning is **off by default**; the two-argument `initialize` call above behaves exactly as before. To opt in, pass a third `options` argument:
50
+
51
+ ```javascript
52
+ headlessModule.initialize("YOUR_APPID", null, { sslPinning: 'enabled' });
53
+ ```
54
+
55
+ ```ts
56
+ type OtplessSslPinning = 'enabled' | 'disabled';
57
+ interface OtplessInitOptions { sslPinning?: OtplessSslPinning }
58
+ initialize(appId: string, loginUri?: string | null, options?: OtplessInitOptions): void
59
+ ```
60
+
61
+ When enabled, the native SDK pins the TLS certificate of `sigma.otpless.app` on both Android and iOS. If the pin does not match (for example behind an intercepting proxy such as Charles or Proxyman), the SDK **fails closed**: no request leaves the device and you receive `FAILED` with `statusCode` `5004` (see [Response Handling](#response-handling)). If your organisation runs a corporate proxy or a network allowlist, make sure `sigma.otpless.app` is reachable directly with its original certificate.
62
+
63
+ Requires `io.github.otpless-tech:otpless-headless-sdk` >= 2.0.1 on Android and `OtplessBM/Core` >= 3.0.1 on iOS (both pulled in automatically by this package).
64
+
47
65
  # Initiate Authentication
48
66
 
49
67
  ## Phone Auth
@@ -86,8 +104,19 @@ const onHeadlessResult = (result: any) => {
86
104
  break;
87
105
  }
88
106
  case "FAILED": {
107
+ if (result.statusCode == 5003) {
108
+ // SDK initialization failed (e.g. invalid appId or network unavailable)
89
109
  console.log("SDK initialization failed");
90
- // Handle SDK initialization failure
110
+ } else if (result.statusCode == 5004) {
111
+ // SSL pin validation failed. Only emitted when `initialize` was called
112
+ // with `{ sslPinning: 'enabled' }`. The SDK sent nothing to the server;
113
+ // the connection to sigma.otpless.app is being intercepted or its
114
+ // certificate does not match the expected pin.
115
+ // result.response = { errorCode: "5004", errorMessage: "SSL pin validation failed" }
116
+ console.log("SSL pin validation failed");
117
+ } else {
118
+ console.log("SDK failed", result.statusCode);
119
+ }
91
120
  break;
92
121
  }
93
122
  case "INITIATE": {
@@ -8,7 +8,7 @@ buildscript {
8
8
  }
9
9
 
10
10
  dependencies {
11
- classpath "com.android.tools.build:gradle:8.3.0"
11
+ classpath "com.android.tools.build:gradle:8.10.0"
12
12
  // noinspection DifferentKotlinGradleVersion
13
13
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
14
  }
@@ -37,7 +37,7 @@ def getExtOrIntegerDefault(name) {
37
37
  }
38
38
 
39
39
  android {
40
- compileSdk 35
40
+ compileSdk 36
41
41
  namespace "com.otplessheadlessrn"
42
42
  defaultConfig {
43
43
  minSdkVersion 21
@@ -74,7 +74,7 @@ dependencies {
74
74
  //noinspection GradleDynamicVersion
75
75
  implementation "com.facebook.react:react-native:+"
76
76
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
- implementation ("io.github.otpless-tech:otpless-headless-sdk:0.8.1")
77
+ implementation ("io.github.otpless-tech:otpless-headless-sdk:2.0.1")
78
78
  }
79
79
 
80
80
  if (isNewArchitectureEnabled()) {
@@ -1,5 +1,5 @@
1
- OtplessHeadlessRN_kotlinVersion=1.9.25
1
+ OtplessHeadlessRN_kotlinVersion=2.0.21
2
2
  OtplessHeadlessRN_minSdkVersion=24
3
3
  OtplessHeadlessRN_targetSdkVersion=35
4
- OtplessHeadlessRN_compileSdkVersion=35
4
+ OtplessHeadlessRN_compileSdkVersion=36
5
5
  OtplessHeadlessRN_ndkversion=21.4.7075529
@@ -2,30 +2,29 @@ package com.otplessheadlessrn
2
2
 
3
3
  import android.app.Activity
4
4
  import android.content.Intent
5
- import androidx.appcompat.app.AppCompatActivity
5
+ import android.util.Log
6
6
  import androidx.fragment.app.FragmentActivity
7
- import androidx.lifecycle.lifecycleScope
8
7
  import com.facebook.react.bridge.ActivityEventListener
9
8
  import com.facebook.react.bridge.Promise
10
9
  import com.facebook.react.bridge.ReactApplicationContext
11
10
  import com.facebook.react.bridge.ReactContextBaseJavaModule
12
11
  import com.facebook.react.bridge.ReactMethod
13
12
  import com.facebook.react.bridge.ReadableMap
14
- import com.facebook.react.bridge.ReadableType
15
13
  import com.facebook.react.modules.core.DeviceEventManagerModule
16
14
  import com.otpless.longclaw.tc.OTScopeRequest
17
- import com.otpless.v2.android.sdk.dto.AuthEvent
18
- import com.otpless.v2.android.sdk.dto.OtplessChannelType
19
- import com.otpless.v2.android.sdk.dto.ProviderType
20
- import com.otpless.v2.android.sdk.dto.OtplessRequest
21
15
  import com.otpless.v2.android.sdk.dto.OtplessResponse
22
- import com.otpless.v2.android.sdk.dto.ResponseTypes
16
+ import com.otpless.v2.android.sdk.dto.OtplessSslKind
23
17
  import com.otpless.v2.android.sdk.main.OtplessSDK
18
+ import com.otpless.v2.android.sdk.utils.BuildPlatform
24
19
  import com.otpless.v2.android.sdk.utils.OtplessUtils
20
+ import kotlinx.coroutines.CoroutineExceptionHandler
25
21
  import kotlinx.coroutines.CoroutineScope
26
22
  import kotlinx.coroutines.Dispatchers
27
23
  import kotlinx.coroutines.Job
24
+ import kotlinx.coroutines.SupervisorJob
28
25
  import kotlinx.coroutines.launch
26
+ import kotlinx.coroutines.sync.Mutex
27
+ import kotlinx.coroutines.sync.withLock
29
28
  import org.json.JSONException
30
29
  import org.json.JSONObject
31
30
 
@@ -33,6 +32,10 @@ class OtplessHeadlessRNModule(private val reactContext: ReactApplicationContext)
33
32
  ReactContextBaseJavaModule(reactContext), ActivityEventListener {
34
33
 
35
34
  private var otplessJob: Job? = null
35
+ private val lifecycleMutex = Mutex()
36
+ private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { context, throwable ->
37
+ Log.d("OTPLESS", "Error in coroutine", throwable)
38
+ })
36
39
 
37
40
  init {
38
41
  reactContext.addActivityEventListener(this)
@@ -71,24 +74,28 @@ class OtplessHeadlessRNModule(private val reactContext: ReactApplicationContext)
71
74
  }
72
75
 
73
76
  @ReactMethod
74
- fun initialize(appId: String, loginUri: String? = null) {
75
- if (currentActivity == null) return
76
- (currentActivity as? AppCompatActivity)?.lifecycleScope?.let { scope ->
77
- scope.launch(Dispatchers.IO) {
77
+ fun initialize(appId: String, loginUri: String?, sslPinning: String?, buildPlatform: String?) {
78
+ val activity = currentActivity ?: return
79
+ val sslKind: OtplessSslKind =
80
+ if (sslPinning == "enabled") OtplessSslKind.SslEnabled else OtplessSslKind.SslDisabled
81
+ // Wrapper attribution, computed in JS (which owns the single source of
82
+ // truth for the package version) and arriving as
83
+ // "react-native-android-<packageVersion>". This bridge is a dumb forwarder;
84
+ // the literal fallback covers an older JS layer that does not send the
85
+ // argument, so the token can never be blank.
86
+ val buildPlatformToken = buildPlatform?.trim()?.takeIf { it.isNotEmpty() } ?: "react-native-android"
87
+ ioScope.launch {
88
+ lifecycleMutex.withLock {
89
+ // Tells the native SDK this session came through the React Native
90
+ // wrapper, so device telemetry reports
91
+ // platform = "otpless-headless-sdk(react-native-android-<version>)"
92
+ // instead of the default "otpless-headless-sdk(android)". Must be set
93
+ // before initialize, which is what emits the event.
94
+ OtplessSDK.buildPlatform = BuildPlatform(buildPlatformToken)
78
95
  OtplessSDK.initialize(
79
- appId = appId,
80
- activity = currentActivity!!,
81
- loginUri = loginUri,
82
- callback = this@OtplessHeadlessRNModule::sendHeadlessEventCallback
83
- )
84
- }
85
- } ?: run {
86
- CoroutineScope(Dispatchers.IO).launch {
87
- OtplessSDK.initialize(
88
- appId = appId,
89
- activity = currentActivity!!,
90
- loginUri = loginUri,
91
- callback = this@OtplessHeadlessRNModule::sendHeadlessEventCallback
96
+ appId = appId, activity = activity,
97
+ loginUri = loginUri, callback = this@OtplessHeadlessRNModule::sendHeadlessEventCallback,
98
+ sslKind = sslKind
92
99
  )
93
100
  }
94
101
  }
@@ -96,11 +103,11 @@ class OtplessHeadlessRNModule(private val reactContext: ReactApplicationContext)
96
103
 
97
104
  @ReactMethod
98
105
  fun initTrueCaller(requestMap: ReadableMap, promise: Promise) {
99
- if (currentActivity == null) return
106
+ val activity = currentActivity ?: return
100
107
  val request = parseTrueCallerRequest(requestMap)
101
108
  val scopes = parseTrueCallerScope(requestMap)
102
- val result = OtplessSDK.initTrueCaller(currentActivity!!, request) {
103
- OTScopeRequest.ActivityRequest(currentActivity as FragmentActivity, scopes)
109
+ val result = OtplessSDK.initTrueCaller(activity, request) {
110
+ OTScopeRequest.ActivityRequest(activity as FragmentActivity, scopes)
104
111
  }
105
112
  debugLog("init truecaller result: $result")
106
113
  promise.resolve(result)
@@ -108,100 +115,32 @@ class OtplessHeadlessRNModule(private val reactContext: ReactApplicationContext)
108
115
 
109
116
  @ReactMethod
110
117
  fun userAuthEvent(event: String, fallback: Boolean, providerType: String, providerInfo: ReadableMap?) {
111
- // Defensive enum parsing unknown values log and return rather than crashing the bridge.
112
- val authEvent = runCatching { AuthEvent.valueOf(event) }.getOrElse {
118
+ val authEvent = parseAuthEvent(event) ?: run {
113
119
  debugLog("userAuthEvent: unknown AuthEvent '$event', ignoring call")
114
120
  return
115
121
  }
116
- val provider = runCatching { ProviderType.valueOf(providerType) }.getOrElse {
122
+ val provider = parseProviderType(providerType) ?: run {
117
123
  debugLog("userAuthEvent: unknown ProviderType '$providerType', ignoring call")
118
124
  return
119
125
  }
120
- // Defensive providerInfo parsing — JS passes `any`, so coerce each value to String:
121
- // primitives via toString(), Maps/Arrays via JSON serialisation, Null skipped.
122
- val infoMap = mutableMapOf<String, String>()
123
- if (providerInfo != null) {
124
- try {
125
- val iterator = providerInfo.keySetIterator()
126
- while (iterator.hasNextKey()) {
127
- val key = iterator.nextKey() ?: continue
128
- val value: String? = when (providerInfo.getType(key)) {
129
- ReadableType.String -> providerInfo.getString(key)
130
- ReadableType.Number -> providerInfo.getDouble(key).toString()
131
- ReadableType.Boolean -> providerInfo.getBoolean(key).toString()
132
- ReadableType.Map -> convertMapToJson(providerInfo.getMap(key))?.toString()
133
- ReadableType.Array -> convertArrayToJson(providerInfo.getArray(key))?.toString()
134
- else -> null // Null — skip
135
- }
136
- if (value != null) infoMap[key] = value
137
- }
138
- } catch (_: Exception) {
139
- // swallow any unexpected bridge parsing errors
140
- }
141
- }
126
+ val infoMap = parseProviderInfo(providerInfo)
142
127
  debugLog("pushing the user auth event\nauthEvent: $authEvent, providerType: $providerType")
143
128
  OtplessSDK.userAuthEvent(authEvent, fallback, provider, infoMap)
144
129
  }
145
130
 
146
131
  @ReactMethod
147
132
  fun start(data: ReadableMap) {
148
- val otplessRequest = OtplessRequest()
149
- val phone = data.getString("phone") ?: ""
150
- var isOtpPresent = false
151
-
152
- // phone number authentication
153
- if (phone.isNotEmpty()) {
154
- val countryCode = data.getString("countryCode") ?: ""
155
- otplessRequest.setPhoneNumber(number = phone, countryCode = countryCode)
156
- data.getString("otp")?.let {
157
- otplessRequest.setOtp(it)
158
- isOtpPresent = true
159
- }
160
- } else {
161
- // email authentication
162
- val email = data.getString("email") ?: ""
163
- if (email.isNotEmpty()) {
164
- otplessRequest.setEmail(email)
165
- data.getString("otp")?.let {
166
- otplessRequest.setOtp(it)
167
- isOtpPresent = true
168
- }
169
- } else {
170
- // oauth case
171
- otplessRequest.setChannelType(
172
- OtplessChannelType.fromString(
173
- data.getString("channelType") ?: ""
174
- )
175
- )
176
- }
177
- }
178
- data.getString("expiry")?.takeIf { it.isNotBlank() }?.let {
179
- otplessRequest.setExpiry(it)
180
- }
181
-
182
- data.getString("otpLength")?.takeIf { it.isNotBlank() }?.let {
183
- otplessRequest.setOtpLength(it)
184
- }
133
+ val otplessRequest = parseOtplessRequest(data)
134
+ val isOtpVerification = !data.getString("otp").isNullOrEmpty()
185
135
 
186
- data.getString("deliveryChannel")?.takeIf { it.isNotBlank() }?.let { deliveryChannel ->
187
- otplessRequest.setDeliveryChannel(deliveryChannel.uppercase())
188
- }
189
-
190
- data.getString("tid")?.takeIf { it.isNotBlank() }?.let { templateId ->
191
- otplessRequest.setTemplateId(templateId)
192
- }
193
-
194
- otplessJob?.cancel()
195
- currentActivity?.let {
196
- if (isOtpPresent) (it as AppCompatActivity).lifecycleScope.launch(Dispatchers.IO) {
136
+ if (isOtpVerification) {
137
+ // OTP submit — slot into the current auth flow; don't cancel, don't track
138
+ ioScope.launch {
197
139
  OtplessSDK.start(request = otplessRequest, this@OtplessHeadlessRNModule::sendHeadlessEventCallback)
198
- } else {
199
- otplessJob = (it as AppCompatActivity).lifecycleScope.launch {
200
- OtplessSDK.start(request = otplessRequest, this@OtplessHeadlessRNModule::sendHeadlessEventCallback)
201
- }
202
140
  }
203
- } ?: run {
204
- CoroutineScope(Dispatchers.IO).launch {
141
+ } else {
142
+ otplessJob?.cancel()
143
+ otplessJob = ioScope.launch {
205
144
  OtplessSDK.start(request = otplessRequest, this@OtplessHeadlessRNModule::sendHeadlessEventCallback)
206
145
  }
207
146
  }
@@ -215,7 +154,11 @@ class OtplessHeadlessRNModule(private val reactContext: ReactApplicationContext)
215
154
  @ReactMethod
216
155
  fun cleanup() {
217
156
  otplessJob?.cancel()
218
- OtplessSDK.cleanup()
157
+ ioScope.launch {
158
+ lifecycleMutex.withLock {
159
+ OtplessSDK.cleanup()
160
+ }
161
+ }
219
162
  }
220
163
 
221
164
  @ReactMethod
@@ -224,47 +167,82 @@ class OtplessHeadlessRNModule(private val reactContext: ReactApplicationContext)
224
167
  OtplessSDK.devLogging = devLogging
225
168
  }
226
169
 
227
- companion object {
228
- const val NAME = "OtplessHeadlessRN"
170
+ @ReactMethod
171
+ fun setMfaEnabled(enabled: Boolean) {
172
+ OtplessSDK.isMfaEnabled = enabled
173
+ }
174
+
175
+ @ReactMethod
176
+ fun setSimBindingEnabled(enabled: Boolean) {
177
+ OtplessSDK.isSimBindingEnabled = enabled
178
+ }
179
+
180
+ @ReactMethod
181
+ fun checkSimBindingStatus(promise: Promise) {
182
+ ioScope.launch {
183
+ try {
184
+ val bound = OtplessSDK.checkSimBindingStatus(reactContext.applicationContext)
185
+ promise.resolve(bound)
186
+ } catch (_: Throwable) {
187
+ promise.resolve(false)
188
+ }
189
+ }
190
+ }
191
+
192
+ @ReactMethod
193
+ fun clearSimBinding(promise: Promise) {
194
+ ioScope.launch {
195
+ try {
196
+ OtplessSDK.clearSimBinding(reactContext.applicationContext)
197
+ promise.resolve(null)
198
+ } catch (_: Throwable) {
199
+ promise.resolve(null)
200
+ }
201
+ }
202
+ }
203
+
204
+ @ReactMethod
205
+ fun startInBackground(data: ReadableMap) {
206
+ val otplessRequest = parseOtplessRequest(data)
207
+ otplessJob?.cancel()
208
+ otplessJob = ioScope.launch(Dispatchers.IO) {
209
+ OtplessSDK.startInBackground(otplessRequest, this@OtplessHeadlessRNModule::sendHeadlessEventCallback)
210
+ }
211
+ }
212
+
213
+ @ReactMethod
214
+ fun startBackgroundAuth(config: ReadableMap, promise: Promise) {
215
+ val activity = currentActivity
216
+ if (activity == null) {
217
+ promise.resolve(false)
218
+ return
219
+ }
220
+ val authConfig = parseOtplessAuthConfig(config)
221
+ ioScope.launch {
222
+ val result = OtplessSDK.start(authConfig)
223
+ promise.resolve(result)
224
+ }
229
225
  }
230
226
 
231
227
  override fun onActivityResult(
232
- activity: Activity?,
233
- requestCode: Int,
234
- resultCode: Int,
235
- data: Intent?
228
+ activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?
236
229
  ) {
237
230
  OtplessSDK.onActivityResult(requestCode, resultCode, data)
238
231
  }
239
232
 
240
233
  override fun onNewIntent(intent: Intent?) {
241
234
  intent ?: return
242
- (currentActivity as? AppCompatActivity)?.let { ac ->
243
- ac.lifecycleScope.launch(Dispatchers.IO) {
244
- OtplessSDK.onNewIntent(intent)
245
- }
246
- }
235
+ ioScope.launch { OtplessSDK.onNewIntent(intent) }
247
236
  }
248
237
 
249
238
  @ReactMethod
250
239
  fun commitResponse(data: ReadableMap?) {
251
- val dataMap = data?.toHashMap() ?: return
252
- val jsonResponse = JSONObject(dataMap as Map<*, *>)
253
- val otplessResponse = OtplessResponse(
254
- responseType = getResponseType(
255
- responseTypeString = jsonResponse.optString(
256
- "responseType",
257
- ""
258
- )
259
- ),
260
- jsonResponse.optJSONObject("response"),
261
- jsonResponse.optInt("statusCode", 0),
262
- )
240
+ val otplessResponse = parseOtplessResponse(data) ?: return
263
241
  OtplessSDK.commit(otplessResponse)
264
242
  }
265
243
 
266
- private fun getResponseType(responseTypeString: String): ResponseTypes {
267
- return ResponseTypes.valueOf(responseTypeString)
244
+ companion object {
245
+ const val NAME = "OtplessHeadlessRN"
268
246
  }
269
247
  }
270
248
 
@@ -17,6 +17,14 @@ import com.otpless.longclaw.tc.OTLoginPrefixText
17
17
  import com.otpless.longclaw.tc.OTScope
18
18
  import com.otpless.longclaw.tc.OTVerifyOption
19
19
  import com.otpless.longclaw.tc.OtplessTruecallerRequest
20
+ import com.otpless.v2.android.sdk.dto.AuthEvent
21
+ import com.otpless.v2.android.sdk.dto.DeviceFingerprintMode
22
+ import com.otpless.v2.android.sdk.dto.OtplessChannelType
23
+ import com.otpless.v2.android.sdk.dto.OtplessRequest
24
+ import com.otpless.v2.android.sdk.dto.OtplessResponse
25
+ import com.otpless.v2.android.sdk.dto.ProviderType
26
+ import com.otpless.v2.android.sdk.dto.ResponseTypes
27
+ import com.otpless.v2.android.sdk.view.models.OtplessAuthConfig
20
28
  import org.json.JSONArray
21
29
  import org.json.JSONException
22
30
  import org.json.JSONObject
@@ -131,6 +139,94 @@ internal fun convertJsonToArray(jsonArray: JSONArray): WritableArray {
131
139
  return array
132
140
  }
133
141
 
142
+ internal fun parseOtplessRequest(data: ReadableMap): OtplessRequest {
143
+ val otplessRequest = OtplessRequest()
144
+ data.getString("phone")?.takeIf { it.isNotBlank() }?.let { phone ->
145
+ val countryCode = data.getString("countryCode") ?: ""
146
+ otplessRequest.setPhoneNumber(number = phone, countryCode = countryCode)
147
+ }
148
+ data.getString("email")?.takeIf { it.isNotBlank() }?.let { otplessRequest.setEmail(it) }
149
+ data.getString("channelType")?.takeIf { it.isNotBlank() }?.let {
150
+ otplessRequest.setChannelType(OtplessChannelType.fromString(it))
151
+ }
152
+ data.getString("otp")?.takeIf { it.isNotBlank() }?.let { otplessRequest.setOtp(it) }
153
+ data.getString("requestId")?.takeIf { it.isNotBlank() }?.let { otplessRequest.requestId = it }
154
+ data.getString("expiry")?.takeIf { it.isNotBlank() }?.let { otplessRequest.setExpiry(it) }
155
+ data.getString("otpLength")?.takeIf { it.isNotBlank() }?.let { otplessRequest.setOtpLength(it) }
156
+ data.getString("deliveryChannel")?.takeIf { it.isNotBlank() }?.let {
157
+ otplessRequest.setDeliveryChannel(it.uppercase())
158
+ }
159
+ data.getString("tid")?.takeIf { it.isNotBlank() }?.let { otplessRequest.setTemplateId(it) }
160
+ data.getString("deviceFingerprintMode")?.takeIf { it.isNotBlank() }?.let { mode ->
161
+ runCatching { DeviceFingerprintMode.valueOf(mode.uppercase()) }.getOrNull()?.let {
162
+ otplessRequest.deviceFingerprintMode = it
163
+ }
164
+ }
165
+ return otplessRequest
166
+ }
167
+
168
+ internal fun parseOtplessAuthConfig(config: ReadableMap): OtplessAuthConfig {
169
+ val isForeground = if (config.hasKey("isForeground")) config.getBoolean("isForeground") else false
170
+ val otp = config.getString("otp") ?: ""
171
+ val tid = config.getString("tid")
172
+ val fingerprintMode = config.getString("deviceFingerprintMode")
173
+ ?.takeIf { it.isNotBlank() }
174
+ ?.let { runCatching { DeviceFingerprintMode.valueOf(it.uppercase()) }.getOrNull() }
175
+ ?: DeviceFingerprintMode.NONE
176
+ return OtplessAuthConfig(
177
+ isForeground = isForeground,
178
+ otp = otp,
179
+ tid = tid,
180
+ deviceFingerprintMode = fingerprintMode
181
+ )
182
+ }
183
+
184
+ internal fun parseOtplessResponse(data: ReadableMap?): OtplessResponse? {
185
+ val dataMap = data?.toHashMap() ?: return null
186
+ val jsonResponse = JSONObject(dataMap as Map<*, *>)
187
+ val responseTypeString = jsonResponse.optString("responseType", "")
188
+ val responseType = runCatching { ResponseTypes.valueOf(responseTypeString) }.getOrElse {
189
+ debugLog("commitResponse: unknown ResponseType '$responseTypeString', ignoring call")
190
+ return null
191
+ }
192
+ return OtplessResponse(
193
+ responseType = responseType,
194
+ jsonResponse.optJSONObject("response"),
195
+ jsonResponse.optInt("statusCode", 0),
196
+ )
197
+ }
198
+
199
+ internal fun parseAuthEvent(event: String): AuthEvent? =
200
+ runCatching { AuthEvent.valueOf(event) }.getOrNull()
201
+
202
+ internal fun parseProviderType(providerType: String): ProviderType? =
203
+ runCatching { ProviderType.valueOf(providerType) }.getOrNull()
204
+
205
+ // Defensive providerInfo parsing — JS passes `any`, so coerce each value to String:
206
+ // primitives via toString(), Maps/Arrays via JSON serialisation, Null skipped.
207
+ internal fun parseProviderInfo(providerInfo: ReadableMap?): Map<String, String> {
208
+ val infoMap = mutableMapOf<String, String>()
209
+ if (providerInfo == null) return infoMap
210
+ try {
211
+ val iterator = providerInfo.keySetIterator()
212
+ while (iterator.hasNextKey()) {
213
+ val key = iterator.nextKey() ?: continue
214
+ val value: String? = when (providerInfo.getType(key)) {
215
+ ReadableType.String -> providerInfo.getString(key)
216
+ ReadableType.Number -> providerInfo.getDouble(key).toString()
217
+ ReadableType.Boolean -> providerInfo.getBoolean(key).toString()
218
+ ReadableType.Map -> convertMapToJson(providerInfo.getMap(key))?.toString()
219
+ ReadableType.Array -> convertArrayToJson(providerInfo.getArray(key))?.toString()
220
+ else -> null
221
+ }
222
+ if (value != null) infoMap[key] = value
223
+ }
224
+ } catch (_: Exception) {
225
+ // swallow any unexpected bridge parsing errors
226
+ }
227
+ return infoMap
228
+ }
229
+
134
230
  internal fun parseTrueCallerRequest(requestMap: ReadableMap): OtplessTruecallerRequest {
135
231
  val trueCallerRequestMap = requestMap.getMap("trueCallerRequest") ?: return OtplessTruecallerRequest()
136
232
  // parsing footer type
@@ -5,6 +5,8 @@
5
5
 
6
6
  RCT_EXTERN_METHOD(initialize:(NSString *)appId
7
7
  loginUri: (nullable NSString *) loginUri
8
+ sslPinning: (nullable NSString *) sslPinning
9
+ buildPlatform: (nullable NSString *) buildPlatform
8
10
  )
9
11
 
10
12
  RCT_EXTERN_METHOD(start:(NSDictionary *)request)
@@ -15,15 +17,22 @@ RCT_EXTERN_METHOD(cleanup)
15
17
 
16
18
  RCT_EXTERN_METHOD(decimateAll)
17
19
 
18
- RCT_EXTERN_METHOD(setOneTapDataCallback)
19
-
20
- RCT_EXTERN_METHOD(performOneTap: (NSDictionary *)request)
21
-
22
20
  RCT_EXTERN_METHOD(authorizeViaPasskey: (NSDictionary *)request)
23
21
 
24
22
  RCT_EXTERN_METHOD(setDevLogging:(BOOL)enable)
25
23
 
24
+ RCT_EXTERN_METHOD(userAuthEvent:(NSString *)event
25
+ fallback:(BOOL)fallback
26
+ providerType:(NSString *)providerType
27
+ providerInfo:(nullable NSDictionary *)providerInfo)
28
+
26
29
  RCT_EXTERN_METHOD(isSdkReady: (RCTPromiseResolveBlock*)resolve reject: (RCTPromiseRejectBlock*)reject)
27
30
 
31
+ RCT_EXTERN_METHOD(setMfaEnabled:(BOOL)enabled)
32
+
33
+ RCT_EXTERN_METHOD(startBackgroundAuth:(NSDictionary *)config
34
+ resolver:(RCTPromiseResolveBlock)resolve
35
+ rejecter:(RCTPromiseRejectBlock)reject)
36
+
28
37
  @end
29
38