@juspay-tech/react-native-hyperswitch 1.3.4 → 1.3.6

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 (46) hide show
  1. package/android/build.gradle +22 -1
  2. package/android/src/main/assets/hyperswitch.bundle +590 -590
  3. package/android/src/main/java/com/hyperswitchsdkreactnative/modules/NativeLog.kt +107 -0
  4. package/android/src/main/java/com/hyperswitchsdkreactnative/modules/ReactNativeHyperswitchModule.kt +318 -117
  5. package/android/src/main/java/io/hyperswitch/paymentsession/ExitHeadlessCallBackManager.kt +4 -2
  6. package/android/src/main/java/io/hyperswitch/paymentsession/PaymentSessionHandlerImpl.kt +20 -4
  7. package/android/src/main/java/io/hyperswitch/paymentsession/PaymentSessionReactLauncher.kt +104 -9
  8. package/android/src/main/java/io/hyperswitch/paymentsheet/PaymentResult.kt +7 -1
  9. package/android/src/main/java/io/hyperswitch/react/HyperModule.kt +13 -0
  10. package/android/src/main/java/io/hyperswitch/view/PaymentWidgetView.kt +27 -4
  11. package/android/src/rn79/assets/hyperswitch.bundle +590 -590
  12. package/android/src/rn79/java/io/hyperswitch/react/HyperFragment.kt +20 -0
  13. package/android/src/rn81/java/io/hyperswitch/react/HyperFragment.kt +20 -0
  14. package/ios/Modules/ReactNative/NativeHyperswitchModuleImpl.swift +3 -1
  15. package/ios/hyperswitchSDK/Core/ApplePay/ApplePayHandler.swift +49 -38
  16. package/ios/hyperswitchSDK/Core/HyperPaymentSheet/PaymentSheetView+UIKit.swift +1 -5
  17. package/ios/hyperswitchSDK/Core/NativeModule/HyperModuleImpl.swift +49 -34
  18. package/ios/hyperswitchSDK/Core/NativeModule/RNHeadlessManager.swift +2 -12
  19. package/ios/hyperswitchSDK/Core/Resources/hyperswitch-rn82plus.bundle +590 -590
  20. package/ios/hyperswitchSDK/Core/Resources/hyperswitch.bundle +590 -590
  21. package/ios/hyperswitchSDK/Shared/Environment.swift +49 -3
  22. package/ios/hyperswitchSDK/Shared/HyperswitchEnvironment.swift +1 -0
  23. package/ios/hyperswitchSDK/Shared/NativeLogger/LogBuilder.swift +15 -0
  24. package/ios/hyperswitchSDK/Shared/NativeLogger/LogManager.swift +10 -2
  25. package/ios/hyperswitchSDK/Shared/NativeLogger/LogPayload.swift +1 -0
  26. package/ios/hyperswitchSDK/Shared/NativeLogger/NativeLog.swift +101 -0
  27. package/ios/hyperswitchSDK/Shared/PaymentSession.swift +8 -1
  28. package/lib/commonjs/index.bundle.js +2 -2
  29. package/lib/commonjs/index.bundle.js.map +1 -1
  30. package/lib/module/index.bundle.js +2 -2
  31. package/lib/module/index.bundle.js.map +1 -1
  32. package/lib/typescript/src/context/HyperElements.d.ts.map +1 -1
  33. package/lib/typescript/src/index.d.ts.map +1 -1
  34. package/lib/typescript/src/native/NativeResponseMapper.d.ts +2 -1
  35. package/lib/typescript/src/native/NativeResponseMapper.d.ts.map +1 -1
  36. package/lib/typescript/src/session/SavedPaymentMethods.d.ts.map +1 -1
  37. package/lib/typescript/src/session/presentPaymentSheet.d.ts +0 -12
  38. package/lib/typescript/src/session/presentPaymentSheet.d.ts.map +1 -1
  39. package/lib/typescript/src/views/PaymentElement.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/context/HyperElements.tsx +7 -6
  42. package/src/index.ts +9 -1
  43. package/src/native/NativeResponseMapper.ts +20 -4
  44. package/src/session/SavedPaymentMethods.ts +10 -2
  45. package/src/session/presentPaymentSheet.ts +7 -1
  46. package/src/views/PaymentElement.tsx +17 -8
@@ -1,5 +1,8 @@
1
1
  package com.hyperswitchsdkreactnative.modules
2
2
 
3
+ import android.app.Activity
4
+ import android.content.pm.ActivityInfo
5
+ import android.content.res.Configuration
3
6
  import com.facebook.react.bridge.Promise
4
7
  import com.facebook.react.bridge.ReactApplicationContext
5
8
  import com.facebook.react.bridge.ReadableMap
@@ -18,6 +21,7 @@ import com.google.android.gms.common.GoogleApiAvailability
18
21
  import com.google.android.gms.wallet.IsReadyToPayRequest
19
22
  import com.google.android.gms.wallet.Wallet
20
23
  import com.google.android.gms.wallet.WalletConstants
24
+ import io.hyperswitch.logs.HyperLogManager
21
25
  import io.hyperswitch.paymentsession.GetPaymentSessionCallBackManager
22
26
  import io.hyperswitch.paymentsession.GetWalletSessionCallBackManager
23
27
  import io.hyperswitch.paymentsession.WalletSessionHandler
@@ -32,6 +36,7 @@ import io.hyperswitch.utils.StandardResult
32
36
  import io.hyperswitch.view.PaymentWidgetView
33
37
  import org.json.JSONArray
34
38
  import org.json.JSONObject
39
+ import java.lang.ref.WeakReference
35
40
  import java.util.UUID
36
41
  import kotlin.String
37
42
  import kotlin.collections.orEmpty
@@ -40,11 +45,22 @@ import kotlin.collections.orEmpty
40
45
  class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
41
46
  NativeHyperswitchModuleSpec(reactContext) {
42
47
 
48
+ init {
49
+ NativeLog.attach(reactContext)
50
+ }
51
+
43
52
  private var paymentSessionReactLauncher: PaymentSessionReactLauncher? = null
44
- private var launchOptions: LaunchOptions? = null
45
53
  private var handler: PaymentSessionHandler? = null
46
54
  private var walletHandler: WalletSessionHandler? = null
47
55
  private var walletSdkAuthorization: String? = null
56
+ /*
57
+ * The Activity seen by the last SDK call and a copy of its configuration, kept so the log can
58
+ * say why and how it changed when a later call finds a different one.
59
+ */
60
+ private var lastActivity: WeakReference<Activity>? = null
61
+ private var lastConfiguration: Configuration? = null
62
+ /* Set only once initialise has completed. */
63
+ private var initialised = false
48
64
 
49
65
  private val uiManagerType = if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
50
66
  UIManagerType.FABRIC
@@ -56,6 +72,19 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
56
72
  return NAME
57
73
  }
58
74
 
75
+ /**
76
+ * Accepts the same spellings as the iOS module ("PROD"/"PRODUCTION", "SANDBOX", "INTEG",
77
+ * case-insensitive). Anything else yields null (SDK picks the environment from the
78
+ * publishable key) instead of throwing from `valueOf`.
79
+ */
80
+ private fun parseEnvironment(value: String?): HyperswitchEnvironment? =
81
+ when (value?.trim()?.uppercase()) {
82
+ "PROD", "PRODUCTION" -> HyperswitchEnvironment.PROD
83
+ "SANDBOX" -> HyperswitchEnvironment.SANDBOX
84
+ "INTEG" -> HyperswitchEnvironment.INTEG
85
+ else -> HyperswitchEnvironment.PROD
86
+ }
87
+
59
88
  override fun initialise(
60
89
  publishableKey: String?,
61
90
  platformPublishableKey: String?,
@@ -64,6 +93,7 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
64
93
  customEndpoints: ReadableMap?,
65
94
  promise: Promise?
66
95
  ) {
96
+ initialised = false
67
97
  val activity = reactApplicationContext.currentActivity
68
98
  if (publishableKey.isNullOrBlank()) {
69
99
  promise?.reject("INITIALIZATION_ERROR", "publishableKey is required")
@@ -74,38 +104,156 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
74
104
  return
75
105
  }
76
106
 
77
- activity.let {
78
- paymentSessionReactLauncher = PaymentSessionReactLauncher(activity)
79
- paymentSessionReactLauncher?.initializeReactNativeInstance()
80
- }
81
- val overrideEndpoints: OverrideEndpoints? = customEndpoints?.getMap("overrideEndpoints")?.let {
82
- val overrideEndpointsMap = customEndpoints.getMap("overrideEndpoints")
83
- OverrideEndpoints(
84
- customBackendEndpoint = overrideEndpointsMap?.getString("customBackendEndpoint"),
85
- customLoggingEndpoint = overrideEndpointsMap?.getString("customLoggingEndpoint"),
86
- customAssetEndpoint = overrideEndpointsMap?.getString("customAssetEndpoint"),
87
- customSDKConfigEndpoint = overrideEndpointsMap?.getString("customSDKConfigEndpoint"),
88
- customConfirmEndpoint = overrideEndpointsMap?.getString("customConfirmEndpoint"),
89
- customAirborneEndpoint = overrideEndpointsMap?.getString("customAirborneEndpoint"),
107
+ try {
108
+ val launcher = paymentSessionReactLauncher
109
+ ?: PaymentSessionReactLauncher { reactApplicationContext.currentActivity }
110
+ .also { paymentSessionReactLauncher = it }
111
+ launcher.initializeReactNativeInstance()
112
+ val overrideEndpoints: OverrideEndpoints? = customEndpoints?.getMap("overrideEndpoints")?.let {
113
+ val overrideEndpointsMap = customEndpoints.getMap("overrideEndpoints")
114
+ OverrideEndpoints(
115
+ customBackendEndpoint = overrideEndpointsMap?.getString("customBackendEndpoint"),
116
+ customLoggingEndpoint = overrideEndpointsMap?.getString("customLoggingEndpoint"),
117
+ customAssetEndpoint = overrideEndpointsMap?.getString("customAssetEndpoint"),
118
+ customSDKConfigEndpoint = overrideEndpointsMap?.getString("customSDKConfigEndpoint"),
119
+ customConfirmEndpoint = overrideEndpointsMap?.getString("customConfirmEndpoint"),
120
+ customAirborneEndpoint = overrideEndpointsMap?.getString("customAirborneEndpoint"),
121
+ )
122
+ }
123
+ val customConfig = CustomEndpointConfiguration(
124
+ overrideEndpoints = overrideEndpoints,
125
+ commonEndpoint = customEndpoints?.getString("commonEndpoint")
126
+
127
+ )
128
+
129
+ hyperswitchConfig = HyperswitchConfiguration(
130
+ publishableKey = publishableKey,
131
+ profileId = profileId,
132
+ environment = parseEnvironment(environment),
133
+ customConfig = customConfig
134
+ )
135
+ // Without this, logs added through HyperLogManager are kept in memory and never sent.
136
+ val loggingEndpoint =
137
+ getLoggingEndpoint(publishableKey, overrideEndpoints, customConfig.commonEndpoint)
138
+ NativeLog.enabled = loggingEndpoint != null
139
+ loggingEndpoint?.let {
140
+ HyperLogManager.initialise(publishableKey, it)
141
+ HyperLogManager.setSdkVersion(BuildConfig.VERSION_NAME)
142
+ HyperLogManager.setAppId(reactApplicationContext.packageName)
143
+ }
144
+ lastActivity = WeakReference(activity)
145
+ lastConfiguration = Configuration(activity.resources.configuration)
146
+ NativeLog.debug(
147
+ "sdk_initialised",
148
+ mapOf(
149
+ "environment" to NativeLog.environmentName(hyperswitchConfig?.environment?.name),
150
+ "activity" to activity.javaClass.simpleName
151
+ )
90
152
  )
153
+ initialised = true
154
+ val handle = UUID.randomUUID().toString()
155
+ promise?.resolve(handle)
156
+ } catch (e: Exception) {
157
+ /* initializeReactNativeInstance throws on a broken RN setup; without this the
158
+ initialise promise stayed pending and loadHyper never settled. */
159
+ promise?.reject("INITIALIZATION_ERROR", "Failed to initialise the SDK: ${e.message}")
91
160
  }
92
- val customConfig = CustomEndpointConfiguration(
93
- overrideEndpoints = overrideEndpoints,
94
- commonEndpoint = customEndpoints?.getString("commonEndpoint")
161
+ }
95
162
 
96
- )
163
+ /**
164
+ * The live Activity to present on or start a headless flow from. When there is none, or the SDK
165
+ * was never initialised, resolves [promise] with a failure and returns null, so a call never
166
+ * hangs waiting on a sheet that cannot be shown.
167
+ */
168
+ private fun requireReady(trigger: String, promise: Promise?): Activity? {
169
+ if (!requireInitialised(trigger, promise)) return null
170
+ val activity = reactApplicationContext.currentActivity
171
+ if (
172
+ activity == null ||
173
+ activity.isFinishing ||
174
+ activity.isDestroyed ||
175
+ activity.isChangingConfigurations
176
+ ) {
177
+ NativeLog.error("activity_unavailable", mapOf("trigger" to trigger))
178
+ promise?.resolve(failed("ACTIVITY_UNAVAILABLE", "No active Activity"))
179
+ return null
180
+ }
181
+ noteActivityChange(trigger, activity)
182
+ return activity
183
+ }
97
184
 
98
- hyperswitchConfig = HyperswitchConfiguration(
99
- publishableKey = publishableKey,
100
- profileId = profileId,
101
- environment = environment?.let { HyperswitchEnvironment.valueOf(it) },
102
- customConfig = customConfig
185
+ private fun requireInitialised(trigger: String, promise: Promise?): Boolean {
186
+ if (initialised && paymentSessionReactLauncher != null) return true
187
+ NativeLog.error("sdk_not_initialised", mapOf("trigger" to trigger))
188
+ promise?.resolve(failed("NOT_INITIALISED", "SDK is not initialised"))
189
+ return false
190
+ }
191
+
192
+ private fun failed(code: String, message: String): String =
193
+ StandardResult.Failed(code = code, message = message, error = Throwable(message)).toJSONString()
194
+
195
+ private fun launchOptionsFor(activity: Activity) =
196
+ LaunchOptions(activity, BuildConfig.VERSION_NAME, hyperswitchConfig)
197
+
198
+ /** Logs when the host Activity differs from the one the previous SDK call saw. */
199
+ private fun noteActivityChange(trigger: String, current: Activity) {
200
+ val previous = lastActivity?.get()
201
+ if (previous === current) return
202
+ NativeLog.warning(
203
+ "activity_changed",
204
+ mapOf(
205
+ "trigger" to trigger,
206
+ "reason" to replacementReason(previous),
207
+ "configChanged" to configurationChanges(lastConfiguration, current.resources.configuration),
208
+ "activity" to current.javaClass.simpleName
209
+ )
103
210
  )
104
- activity.let {
105
- launchOptions = LaunchOptions(activity, BuildConfig.VERSION_NAME, hyperswitchConfig)
211
+ lastActivity = WeakReference(current)
212
+ lastConfiguration = Configuration(current.resources.configuration)
213
+ }
214
+
215
+ /** Why the previously bound Activity is no longer the current one. */
216
+ private fun replacementReason(previous: Activity?): String = when {
217
+ previous == null -> "unknown, previous Activity already collected"
218
+ previous.isChangingConfigurations -> "configuration change or recreate()"
219
+ previous.isFinishing -> "previous Activity finished"
220
+ previous.isDestroyed -> "previous Activity destroyed by the system"
221
+ else -> "another Activity is in front"
222
+ }
223
+
224
+ /** Names of the configuration fields that differ, e.g. "fontScale,locale"; "none" for recreate(). */
225
+ private fun configurationChanges(previous: Configuration?, current: Configuration): String {
226
+ previous ?: return "unknown"
227
+ val diff = previous.diff(current)
228
+ return CONFIG_NAMES.filter { (bit, _) -> diff and bit != 0 }.joinToString(",") { it.second }
229
+ .ifEmpty { if (diff == 0) "none" else "0x" + Integer.toHexString(diff) }
230
+ }
231
+
232
+ /**
233
+ * Mirrors the JS logger (GlobalHooks.getLoggingUrl): endpoints come from overrideEndpoints when
234
+ * given, otherwise from commonEndpoint. A custom backend without a logging endpoint, or an empty
235
+ * logging endpoint, turns logging off (null). With neither, the default host is used, picked
236
+ * like GlobalVars.checkEnv: sandbox for pk_snd_ keys, production otherwise.
237
+ */
238
+ private fun getLoggingEndpoint(
239
+ publishableKey: String,
240
+ overrideEndpoints: OverrideEndpoints?,
241
+ commonEndpoint: String?
242
+ ): String? {
243
+ val backend: String?
244
+ val logs: String?
245
+ if (overrideEndpoints != null) {
246
+ backend = overrideEndpoints.customBackendEndpoint
247
+ logs = overrideEndpoints.customLoggingEndpoint
248
+ } else {
249
+ backend = commonEndpoint?.let { "$it$BACKEND_PATH" }
250
+ logs = commonEndpoint?.let { "$it$LOGS_PATH" }
251
+ }
252
+ return when {
253
+ logs != null -> logs.ifEmpty { null }
254
+ backend != null -> null
255
+ else -> (if (publishableKey.startsWith("pk_snd_")) SANDBOX_HOST else PROD_HOST) + LOGS_PATH
106
256
  }
107
- val handle = UUID.randomUUID().toString()
108
- promise?.resolve(handle)
109
257
  }
110
258
 
111
259
  override fun presentPaymentSheet(
@@ -113,25 +261,37 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
113
261
  promise: Promise?
114
262
  ) {
115
263
  try {
264
+ NativeLog.setSdkAuthorization(params?.getMap("paymentSessionConfig")?.getString("sdkAuthorization"))
265
+ val activity = requireReady("presentPaymentSheet", promise) ?: return
266
+ NativeLog.debug("present_payment_sheet_called", mapOf("activity" to activity.javaClass.simpleName))
116
267
  val props = mutableMapOf<String, Any?>().apply {
117
268
  putAll(params?.toHashMap().orEmpty())
118
269
  put("type", "payment")
119
270
  }
120
- val bundle = launchOptions?.getBundleWithHyperParams(props)
121
- bundle?.let {
122
- val isFragment = paymentSessionReactLauncher?.presentSheet(bundle)
123
- val resultCallback: (String) -> Unit = { it ->
124
- promise?.resolve(it)
271
+ val bundle = launchOptionsFor(activity).getBundleWithHyperParams(props)
272
+ if (PaymentSheetCallbackManager.getCallback() != null) {
273
+ if (paymentSessionReactLauncher?.isSheetVisible(activity) == true) {
274
+ promise?.resolve(
275
+ JSONObject().apply {
276
+ put("status", "cancelled")
277
+ put("code", "sheet_already_presented")
278
+ put("message", "A payment sheet is already presented.")
279
+ }.toString()
280
+ )
281
+ return
125
282
  }
126
- PaymentSheetCallbackManager.setCallback(resultCallback, isFragment == true)
283
+ /* The previous call's sheet is gone without reporting back; settle it before replacing. */
284
+ PaymentSheetCallbackManager.executeCallback(
285
+ failed("SHEET_REPLACED", "Replaced by a newer presentPaymentSheet call")
286
+ )
127
287
  }
128
- } catch (e: Exception) {
129
- val map = mutableMapOf<String, Any>().apply {
130
- put("code", "failed")
131
- put("message", "failed to open")
132
- put("reason", e.message.toString())
288
+ val isFragment = paymentSessionReactLauncher?.presentSheet(bundle, activity)
289
+ val resultCallback: (String) -> Unit = { it ->
290
+ promise?.resolve(it)
133
291
  }
134
- promise?.resolve(map)
292
+ PaymentSheetCallbackManager.setCallback(resultCallback, isFragment == true)
293
+ } catch (e: Exception) {
294
+ promise?.resolve(failed("PRESENT_FAILED", "failed to open: ${e.message}"))
135
295
  }
136
296
  }
137
297
 
@@ -139,17 +299,17 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
139
299
  params: ReadableMap?,
140
300
  promise: Promise
141
301
  ) {
302
+ NativeLog.setSdkAuthorization(params?.getMap("paymentSessionConfig")?.getString("sdkAuthorization"))
303
+ val activity = requireReady("getCustomerSavedPaymentMethods", promise) ?: return
142
304
  val props = mutableMapOf<String, Any?>().apply {
143
305
  putAll(params?.toHashMap().orEmpty())
144
306
  put("type", "payment")
145
307
  }
146
308
 
147
- val map: Map<String, Any?> = mapOf(
148
- "props" to props
149
- )
150
- val bundle = launchOptions?.toBundle(map)
151
- bundle?.let {
152
- val savedPaymentMethodCallback: (PaymentSessionHandler) -> Unit = { it ->
309
+ // sdkParams carries appId, which the headless confirm needs to send a return_url.
310
+ val bundle = launchOptionsFor(activity).getBundleWithHyperParams(props)
311
+ run {
312
+ val savedPaymentMethodCallback: (PaymentSessionHandler) -> Unit = {
153
313
  handler = it
154
314
  promise.resolve(
155
315
  JSONObject().apply {
@@ -162,75 +322,89 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
162
322
  params?.getMap("paymentSessionConfig")?.getString("sdkAuthorization"),
163
323
  savedPaymentMethodCallback
164
324
  )
165
- paymentSessionReactLauncher?.recreateReactContext(it)
325
+ if (paymentSessionReactLauncher?.recreateReactContext(bundle) != true) {
326
+ promise.resolve(failed("ACTIVITY_UNAVAILABLE", "No active Activity"))
327
+ }
166
328
  }
167
329
 
168
330
  }
169
331
 
170
332
  override fun getCustomerLastUsedPaymentMethodData(promise: Promise) {
171
- handler?.let {
172
- it.getCustomerLastUsedPaymentMethodData().fold(
173
- onSuccess = { data ->
174
- promise.resolve(ConversionUtils.convertMapToJson(data.toMap()).toString())
175
- },
176
- onFailure = { error ->
177
- val pmError = error as? PMError
178
- promise.resolve(
179
- StandardResult.Failed(
180
- code = pmError?.code ?: "UNKNOWN",
181
- message = pmError?.message ?: error.message ?: "Unknown error",
182
- error = Throwable(pmError?.message ?: error.message ?: "Unknown error")
183
- ).toJSONString()
184
- )
185
- }
186
- )
333
+ val handler = handler
334
+ if (handler == null) {
335
+ /* A null handler previously fell through ?.let without settling the promise. */
336
+ promise.resolve(failed("NO_HANDLER", "Saved payment methods are not initialized"))
337
+ return
187
338
  }
339
+ handler.getCustomerLastUsedPaymentMethodData().fold(
340
+ onSuccess = { data ->
341
+ promise.resolve(ConversionUtils.convertMapToJson(data.toMap()).toString())
342
+ },
343
+ onFailure = { error ->
344
+ val pmError = error as? PMError
345
+ promise.resolve(
346
+ StandardResult.Failed(
347
+ code = pmError?.code ?: "UNKNOWN",
348
+ message = pmError?.message ?: error.message ?: "Unknown error",
349
+ error = Throwable(pmError?.message ?: error.message ?: "Unknown error")
350
+ ).toJSONString()
351
+ )
352
+ }
353
+ )
188
354
  }
189
355
 
190
356
  override fun getCustomerDefaultSavedPaymentMethodData(promise: Promise) {
191
- handler?.let {
192
- it.getCustomerDefaultSavedPaymentMethodData().fold(
193
- onSuccess = { data ->
194
- promise.resolve(ConversionUtils.convertMapToJson(data.toMap()).toString())
195
- },
196
- onFailure = { error ->
197
- val pmError = error as? PMError
198
- promise.resolve(
199
- StandardResult.Failed(
200
- code = pmError?.code ?: "UNKNOWN",
201
- message = pmError?.message ?: error.message ?: "Unknown error",
202
- error = Throwable(pmError?.message ?: error.message ?: "Unknown error")
203
- ).toJSONString()
204
- )
205
- }
206
- )
357
+ val handler = handler
358
+ if (handler == null) {
359
+ /* A null handler previously fell through ?.let without settling the promise. */
360
+ promise.resolve(failed("NO_HANDLER", "Saved payment methods are not initialized"))
361
+ return
207
362
  }
363
+ handler.getCustomerDefaultSavedPaymentMethodData().fold(
364
+ onSuccess = { data ->
365
+ promise.resolve(ConversionUtils.convertMapToJson(data.toMap()).toString())
366
+ },
367
+ onFailure = { error ->
368
+ val pmError = error as? PMError
369
+ promise.resolve(
370
+ StandardResult.Failed(
371
+ code = pmError?.code ?: "UNKNOWN",
372
+ message = pmError?.message ?: error.message ?: "Unknown error",
373
+ error = Throwable(pmError?.message ?: error.message ?: "Unknown error")
374
+ ).toJSONString()
375
+ )
376
+ }
377
+ )
208
378
  }
209
379
 
210
380
  override fun getCustomerSavedPaymentMethodData(promise: Promise) {
211
- handler?.let {
212
- it.getCustomerSavedPaymentMethodData().fold(
213
- onSuccess = { data ->
214
- val jsonArray = JSONArray()
215
- data.forEach { item ->
216
- jsonArray.put(
217
- ConversionUtils.convertMapToJson(item.toMap())
218
- )
219
- }
220
- promise.resolve(jsonArray.toString())
221
- },
222
- onFailure = { error ->
223
- val pmError = error as? PMError
224
- promise.resolve(
225
- StandardResult.Failed(
226
- code = pmError?.code ?: "UNKNOWN",
227
- message = pmError?.message ?: error.message ?: "Unknown error",
228
- error = Throwable(pmError?.message ?: error.message ?: "Unknown error")
229
- ).toJSONString()
381
+ val handler = handler
382
+ if (handler == null) {
383
+ /* A null handler previously fell through ?.let without settling the promise. */
384
+ promise.resolve(failed("NO_HANDLER", "Saved payment methods are not initialized"))
385
+ return
386
+ }
387
+ handler.getCustomerSavedPaymentMethodData().fold(
388
+ onSuccess = { data ->
389
+ val jsonArray = JSONArray()
390
+ data.forEach { item ->
391
+ jsonArray.put(
392
+ ConversionUtils.convertMapToJson(item.toMap())
230
393
  )
231
394
  }
232
- )
233
- }
395
+ promise.resolve(jsonArray.toString())
396
+ },
397
+ onFailure = { error ->
398
+ val pmError = error as? PMError
399
+ promise.resolve(
400
+ StandardResult.Failed(
401
+ code = pmError?.code ?: "UNKNOWN",
402
+ message = pmError?.message ?: error.message ?: "Unknown error",
403
+ error = Throwable(pmError?.message ?: error.message ?: "Unknown error")
404
+ ).toJSONString()
405
+ )
406
+ }
407
+ )
234
408
  }
235
409
 
236
410
  override fun confirmWithCustomerLastUsedPaymentMethod(reactTag: Double, promise: Promise?) {
@@ -241,6 +415,7 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
241
415
  )
242
416
  return
243
417
  }
418
+ paymentSessionReactLauncher?.resumeHostForCurrentActivity()
244
419
 
245
420
  val reactTag = reactTag.toInt()
246
421
 
@@ -282,6 +457,7 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
282
457
  )
283
458
  return
284
459
  }
460
+ paymentSessionReactLauncher?.resumeHostForCurrentActivity()
285
461
 
286
462
  val reactTag = reactTag.toInt()
287
463
 
@@ -340,6 +516,7 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
340
516
  )
341
517
  return
342
518
  } else {
519
+ paymentSessionReactLauncher?.resumeHostForCurrentActivity()
343
520
  handler?.confirmWithCustomerPaymentToken(token, null) { result ->
344
521
  promise?.resolve(result.toJSONString())
345
522
  }
@@ -382,6 +559,7 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
382
559
  override fun getWalletSession(params: ReadableMap?, promise: Promise) {
383
560
  val sdkAuthorization =
384
561
  params?.getMap("paymentSessionConfig")?.getString("sdkAuthorization")
562
+ NativeLog.setSdkAuthorization(sdkAuthorization)
385
563
 
386
564
  if (sdkAuthorization.isNullOrEmpty()) {
387
565
  promise.resolve(
@@ -394,8 +572,12 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
394
572
  return
395
573
  }
396
574
 
397
- // Reuse only while the intent is unchanged. A new sdkAuthorization means
398
- // updateIntent replaced the intent, so the handler is rebuilt against it.
575
+ if (!requireInitialised("getWalletSession", promise)) return
576
+
577
+ /*
578
+ * Reuse only while the intent is unchanged. A new sdkAuthorization means
579
+ * updateIntent replaced the intent, so the handler is rebuilt against it.
580
+ */
399
581
  if (walletHandler != null && walletSdkAuthorization == sdkAuthorization) {
400
582
  promise.resolve(
401
583
  JSONObject().apply {
@@ -406,6 +588,7 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
406
588
  return
407
589
  }
408
590
 
591
+ val activity = requireReady("getWalletSession", promise) ?: return
409
592
  walletHandler = null
410
593
  walletSdkAuthorization = sdkAuthorization
411
594
 
@@ -414,19 +597,8 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
414
597
  put("type", "walletWidget")
415
598
  }
416
599
 
417
- val map: Map<String, Any?> = mapOf("props" to props)
418
- val bundle = launchOptions?.toBundle(map)
419
-
420
- if (bundle == null) {
421
- promise.resolve(
422
- StandardResult.Failed(
423
- code = "NOT_INITIALISED",
424
- message = "SDK is not initialised",
425
- error = Throwable("SDK is not initialised")
426
- ).toJSONString()
427
- )
428
- return
429
- }
600
+ // sdkParams carries appId, which the headless confirm needs to send a return_url.
601
+ val bundle = launchOptionsFor(activity).getBundleWithHyperParams(props)
430
602
 
431
603
  var resolved = false
432
604
  val walletSessionCallback: (WalletSessionHandler) -> Unit = {
@@ -443,7 +615,10 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
443
615
  }
444
616
 
445
617
  GetWalletSessionCallBackManager.setCallback(sdkAuthorization, walletSessionCallback)
446
- paymentSessionReactLauncher?.recreateReactContext(bundle)
618
+ if (paymentSessionReactLauncher?.recreateReactContext(bundle) != true) {
619
+ resolved = true
620
+ promise.resolve(failed("ACTIVITY_UNAVAILABLE", "No active Activity"))
621
+ }
447
622
  }
448
623
 
449
624
  override fun isWalletEligible(wallet: String, promise: Promise) {
@@ -467,8 +642,12 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
467
642
  )
468
643
  return
469
644
  }
470
- current.launchWallet(wallet) { result ->
471
- promise.resolve(result.toJSONString())
645
+ requireReady("launchWallet", promise) ?: return
646
+ val launcher = paymentSessionReactLauncher ?: return
647
+ launcher.runWithHostResumed {
648
+ current.launchWallet(wallet) { result ->
649
+ promise.resolve(result.toJSONString())
650
+ }
472
651
  }
473
652
  }
474
653
 
@@ -530,5 +709,27 @@ class ReactNativeHyperswitchModule(reactContext: ReactApplicationContext) :
530
709
  private const val IS_READY_TO_PAY_REQUEST =
531
710
  """{"apiVersion":2,"apiVersionMinor":0,"allowedPaymentMethods":[{"type":"CARD","parameters":{"allowedAuthMethods":["PAN_ONLY","CRYPTOGRAM_3DS"],"allowedCardNetworks":["AMEX","DISCOVER","JCB","MASTERCARD","VISA"]}}]}"""
532
711
  private var hyperswitchConfig: HyperswitchConfiguration? = null
712
+ private const val PROD_HOST = "https://live.hyperswitch.io"
713
+ private const val SANDBOX_HOST = "https://app.hyperswitch.io"
714
+ private const val BACKEND_PATH = "/api"
715
+ private val CONFIG_NAMES = listOf(
716
+ ActivityInfo.CONFIG_FONT_SCALE to "fontScale",
717
+ ActivityInfo.CONFIG_LOCALE to "locale",
718
+ ActivityInfo.CONFIG_LAYOUT_DIRECTION to "layoutDirection",
719
+ ActivityInfo.CONFIG_UI_MODE to "uiMode",
720
+ ActivityInfo.CONFIG_DENSITY to "density",
721
+ ActivityInfo.CONFIG_ORIENTATION to "orientation",
722
+ ActivityInfo.CONFIG_SCREEN_SIZE to "screenSize",
723
+ ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE to "smallestScreenSize",
724
+ ActivityInfo.CONFIG_SCREEN_LAYOUT to "screenLayout",
725
+ ActivityInfo.CONFIG_KEYBOARD to "keyboard",
726
+ ActivityInfo.CONFIG_KEYBOARD_HIDDEN to "keyboardHidden",
727
+ ActivityInfo.CONFIG_NAVIGATION to "navigation",
728
+ ActivityInfo.CONFIG_TOUCHSCREEN to "touchscreen",
729
+ ActivityInfo.CONFIG_MCC to "mcc",
730
+ ActivityInfo.CONFIG_MNC to "mnc",
731
+ ActivityInfo.CONFIG_COLOR_MODE to "colorMode",
732
+ )
733
+ private const val LOGS_PATH = "/api/logs/sdk"
533
734
  }
534
735
  }
@@ -36,8 +36,10 @@ object ExitHeadlessCallBackManager {
36
36
  return when (val status = message.getString("status")) {
37
37
  "cancelled" -> PaymentResult.Canceled(status)
38
38
  "failed", "requires_payment_method" -> {
39
- val throwable = Throwable(message.getString("message"))
40
- throwable.initCause(Throwable(message.getString("code")))
39
+ // optString: the core doesn't always send both keys, and getString would throw.
40
+ val throwable = Throwable(message.optString("message", ""))
41
+ message.optString("code", "").takeIf { it.isNotEmpty() }
42
+ ?.let { throwable.initCause(Throwable(it)) }
41
43
  PaymentResult.Failed(throwable)
42
44
  }
43
45
 
@@ -40,17 +40,33 @@ internal class PaymentSessionHandlerImpl(
40
40
  override fun confirmWithCustomerDefaultPaymentMethod(
41
41
  cvc: String?, resultHandler: (PaymentResult) -> Unit
42
42
  ) {
43
- defaultMethodData.getString("payment_token")
44
- ?.let { confirmWithCustomerPaymentToken(it, cvc, resultHandler) }
43
+ val paymentToken = defaultMethodData.getString("payment_token")
44
+ if (paymentToken == null) {
45
+ resultHandler(paymentTokenMissingFailure("default"))
46
+ return
47
+ }
48
+ confirmWithCustomerPaymentToken(paymentToken, cvc, resultHandler)
45
49
  }
46
50
 
47
51
  override fun confirmWithCustomerLastUsedPaymentMethod(
48
52
  cvc: String?, resultHandler: (PaymentResult) -> Unit
49
53
  ) {
50
- lastUsedMethodData.getString("payment_token")
51
- ?.let { confirmWithCustomerPaymentToken(it, cvc, resultHandler) }
54
+ val paymentToken = lastUsedMethodData.getString("payment_token")
55
+ if (paymentToken == null) {
56
+ resultHandler(paymentTokenMissingFailure("last used"))
57
+ return
58
+ }
59
+ confirmWithCustomerPaymentToken(paymentToken, cvc, resultHandler)
52
60
  }
53
61
 
62
+ /* A missing token previously fell through ?.let without invoking resultHandler,
63
+ leaving the merchant's confirm promise pending forever. */
64
+ private fun paymentTokenMissingFailure(which: String) = PaymentResult.Failed(
65
+ Throwable("payment_token missing on the $which saved payment method").apply {
66
+ initCause(Throwable("PAYMENT_TOKEN_MISSING"))
67
+ }
68
+ )
69
+
54
70
  override fun updateSdkAuthorization(sdkAuthorization: String){
55
71
  this.sdkAuthorization = sdkAuthorization
56
72
  }