@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.
- package/android/build.gradle +22 -1
- package/android/src/main/assets/hyperswitch.bundle +590 -590
- package/android/src/main/java/com/hyperswitchsdkreactnative/modules/NativeLog.kt +107 -0
- package/android/src/main/java/com/hyperswitchsdkreactnative/modules/ReactNativeHyperswitchModule.kt +318 -117
- package/android/src/main/java/io/hyperswitch/paymentsession/ExitHeadlessCallBackManager.kt +4 -2
- package/android/src/main/java/io/hyperswitch/paymentsession/PaymentSessionHandlerImpl.kt +20 -4
- package/android/src/main/java/io/hyperswitch/paymentsession/PaymentSessionReactLauncher.kt +104 -9
- package/android/src/main/java/io/hyperswitch/paymentsheet/PaymentResult.kt +7 -1
- package/android/src/main/java/io/hyperswitch/react/HyperModule.kt +13 -0
- package/android/src/main/java/io/hyperswitch/view/PaymentWidgetView.kt +27 -4
- package/android/src/rn79/assets/hyperswitch.bundle +590 -590
- package/android/src/rn79/java/io/hyperswitch/react/HyperFragment.kt +20 -0
- package/android/src/rn81/java/io/hyperswitch/react/HyperFragment.kt +20 -0
- package/ios/Modules/ReactNative/NativeHyperswitchModuleImpl.swift +3 -1
- package/ios/hyperswitchSDK/Core/ApplePay/ApplePayHandler.swift +49 -38
- package/ios/hyperswitchSDK/Core/HyperPaymentSheet/PaymentSheetView+UIKit.swift +1 -5
- package/ios/hyperswitchSDK/Core/NativeModule/HyperModuleImpl.swift +49 -34
- package/ios/hyperswitchSDK/Core/NativeModule/RNHeadlessManager.swift +2 -12
- package/ios/hyperswitchSDK/Core/Resources/hyperswitch-rn82plus.bundle +590 -590
- package/ios/hyperswitchSDK/Core/Resources/hyperswitch.bundle +590 -590
- package/ios/hyperswitchSDK/Shared/Environment.swift +49 -3
- package/ios/hyperswitchSDK/Shared/HyperswitchEnvironment.swift +1 -0
- package/ios/hyperswitchSDK/Shared/NativeLogger/LogBuilder.swift +15 -0
- package/ios/hyperswitchSDK/Shared/NativeLogger/LogManager.swift +10 -2
- package/ios/hyperswitchSDK/Shared/NativeLogger/LogPayload.swift +1 -0
- package/ios/hyperswitchSDK/Shared/NativeLogger/NativeLog.swift +101 -0
- package/ios/hyperswitchSDK/Shared/PaymentSession.swift +8 -1
- package/lib/commonjs/index.bundle.js +2 -2
- package/lib/commonjs/index.bundle.js.map +1 -1
- package/lib/module/index.bundle.js +2 -2
- package/lib/module/index.bundle.js.map +1 -1
- package/lib/typescript/src/context/HyperElements.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/NativeResponseMapper.d.ts +2 -1
- package/lib/typescript/src/native/NativeResponseMapper.d.ts.map +1 -1
- package/lib/typescript/src/session/SavedPaymentMethods.d.ts.map +1 -1
- package/lib/typescript/src/session/presentPaymentSheet.d.ts +0 -12
- package/lib/typescript/src/session/presentPaymentSheet.d.ts.map +1 -1
- package/lib/typescript/src/views/PaymentElement.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/context/HyperElements.tsx +7 -6
- package/src/index.ts +9 -1
- package/src/native/NativeResponseMapper.ts +20 -4
- package/src/session/SavedPaymentMethods.ts +10 -2
- package/src/session/presentPaymentSheet.ts +7 -1
- package/src/views/PaymentElement.tsx +17 -8
|
@@ -20,10 +20,16 @@ import com.facebook.react.jstasks.HeadlessJsTaskContext
|
|
|
20
20
|
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
|
|
21
21
|
import com.facebook.react.uimanager.PixelUtil
|
|
22
22
|
import com.hyperswitchsdkreactnative.BuildConfig
|
|
23
|
+
import com.proyecto26.inappbrowser.ChromeTabsDismissedEvent
|
|
24
|
+
import com.proyecto26.inappbrowser.ChromeTabsManagerActivity
|
|
23
25
|
import io.hyperswitch.react.HyperActivity
|
|
24
26
|
import io.hyperswitch.react.HyperEventEmitter
|
|
25
27
|
import io.hyperswitch.react.HyperFragment
|
|
26
28
|
import io.hyperswitch.react.ReactNativeController
|
|
29
|
+
import io.hyperswitch.redirect.RedirectEvent
|
|
30
|
+
import java.lang.ref.WeakReference
|
|
31
|
+
import org.greenrobot.eventbus.EventBus
|
|
32
|
+
import org.greenrobot.eventbus.Subscribe
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
* React Native backed engine for presenting the payment sheet and running headless tasks.
|
|
@@ -32,9 +38,19 @@ import io.hyperswitch.react.ReactNativeController
|
|
|
32
38
|
* to pass all configuration data as plain [Bundle]s or [Map]s.
|
|
33
39
|
*/
|
|
34
40
|
class PaymentSessionReactLauncher(
|
|
35
|
-
private val
|
|
41
|
+
private val activityProvider: () -> Activity?,
|
|
36
42
|
) {
|
|
37
43
|
|
|
44
|
+
constructor(activity: Activity) : this({ activity })
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The host Activity, read on every use rather than captured once: a host Activity that is
|
|
48
|
+
* recreated (configuration change, recreate()) would otherwise leave the sheet committed to the
|
|
49
|
+
* destroyed Activity, where it is silently dropped.
|
|
50
|
+
*/
|
|
51
|
+
private val activity: Activity
|
|
52
|
+
get() = checkNotNull(activityProvider()) { "No current Activity" }
|
|
53
|
+
|
|
38
54
|
private var reactHost: ReactHost? = null
|
|
39
55
|
private var reactNativeHost: ReactNativeHost? = null
|
|
40
56
|
private var reactContext: ReactContext? = null
|
|
@@ -48,6 +64,7 @@ class PaymentSessionReactLauncher(
|
|
|
48
64
|
if (!ReactNativeController.getIsInitialized()) {
|
|
49
65
|
ReactNativeController.initialize(activity.application)
|
|
50
66
|
}
|
|
67
|
+
RedirectBridge.install(activity)
|
|
51
68
|
reactNativeHost = ReactNativeController.getReactNativeHost()
|
|
52
69
|
reactHost = ReactNativeController.getReactHost()
|
|
53
70
|
|
|
@@ -75,7 +92,8 @@ class PaymentSessionReactLauncher(
|
|
|
75
92
|
* Recreates the React context (if needed) and starts a headless JS task with the supplied
|
|
76
93
|
* [bundle]. The caller is responsible for assembling the bundle (e.g. via [LaunchOptions]).
|
|
77
94
|
*/
|
|
78
|
-
fun recreateReactContext(bundle: Bundle) {
|
|
95
|
+
fun recreateReactContext(bundle: Bundle): Boolean {
|
|
96
|
+
val activity = activityProvider() ?: return false
|
|
79
97
|
activity.runOnUiThread {
|
|
80
98
|
var context = reactContext
|
|
81
99
|
if (context == null) {
|
|
@@ -136,6 +154,7 @@ class PaymentSessionReactLauncher(
|
|
|
136
154
|
startHeadlessTask(context, bundle)
|
|
137
155
|
}
|
|
138
156
|
}
|
|
157
|
+
return true
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
private fun startHeadlessTask(reactContext: ReactContext, bundle: Bundle) {
|
|
@@ -145,6 +164,7 @@ class PaymentSessionReactLauncher(
|
|
|
145
164
|
|
|
146
165
|
val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext)
|
|
147
166
|
UiThreadUtil.runOnUiThread {
|
|
167
|
+
resumeHost()
|
|
148
168
|
headlessTaskId?.let {
|
|
149
169
|
headlessJsTaskContext.finishTask(it)
|
|
150
170
|
}
|
|
@@ -152,19 +172,63 @@ class PaymentSessionReactLauncher(
|
|
|
152
172
|
}
|
|
153
173
|
}
|
|
154
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Headless flows (wallet buttons, saved methods) have no ReactFragment/ReactActivity to resume
|
|
177
|
+
* the SDK host, and React Native runs JS timers (so every `fetch`) and sets `currentActivity`
|
|
178
|
+
* only for a resumed host. The headless task itself ends immediately, so resume it here.
|
|
179
|
+
*/
|
|
180
|
+
private fun resumeHost() {
|
|
181
|
+
val activity = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed } ?: return
|
|
182
|
+
RedirectBridge.install(activity)
|
|
183
|
+
val backHandler = activity as? DefaultHardwareBackBtnHandler
|
|
184
|
+
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
185
|
+
ReactNativeController.getReactHost().onHostResume(activity, backHandler)
|
|
186
|
+
} else {
|
|
187
|
+
val manager = ReactNativeController.getReactNativeHost().reactInstanceManager
|
|
188
|
+
if (backHandler != null) manager.onHostResume(activity, backHandler) else manager.onHostResume(activity)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resumes the SDK host with the current Activity. Headless flows started from a session created
|
|
194
|
+
* before the host Activity was recreated would otherwise launch from the destroyed one.
|
|
195
|
+
*/
|
|
196
|
+
fun resumeHostForCurrentActivity() {
|
|
197
|
+
UiThreadUtil.runOnUiThread { resumeHost() }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Like [resumeHostForCurrentActivity], then runs [action] after the resume, on the UI thread. */
|
|
201
|
+
fun runWithHostResumed(action: () -> Unit) {
|
|
202
|
+
UiThreadUtil.runOnUiThread {
|
|
203
|
+
resumeHost()
|
|
204
|
+
action()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Whether a payment sheet is currently attached and visible on [activity]. Closed sheets are
|
|
210
|
+
* hidden, not removed, so they do not count.
|
|
211
|
+
*/
|
|
212
|
+
fun isSheetVisible(activity: Activity): Boolean {
|
|
213
|
+
val fragmentActivity = activity as? FragmentActivity ?: return false
|
|
214
|
+
return fragmentActivity.supportFragmentManager.fragments.any {
|
|
215
|
+
it.tag == "paymentSheet" && it.isAdded && !it.isHidden
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
155
219
|
/**
|
|
156
220
|
* Presents the payment sheet using a fully prepared launch [bundle].
|
|
157
221
|
*
|
|
158
222
|
* The bundle is expected to contain a `props` bundle with `configuration`, `sdkParams`, etc.
|
|
159
223
|
*/
|
|
160
|
-
fun presentSheet(bundle: Bundle): Boolean {
|
|
224
|
+
fun presentSheet(bundle: Bundle, host: Activity = activity): Boolean {
|
|
161
225
|
applyFonts(bundle)
|
|
162
|
-
return presentSheetInternal(bottomInsetToDIPFromPixel(bundle))
|
|
226
|
+
return presentSheetInternal(bottomInsetToDIPFromPixel(bundle), host)
|
|
163
227
|
}
|
|
164
228
|
|
|
165
|
-
private fun presentSheetInternal(bundle: Bundle): Boolean {
|
|
166
|
-
if (
|
|
167
|
-
val fragmentActivity =
|
|
229
|
+
private fun presentSheetInternal(bundle: Bundle, host: Activity): Boolean {
|
|
230
|
+
if (host is DefaultHardwareBackBtnHandler && host is FragmentActivity) {
|
|
231
|
+
val fragmentActivity = host as FragmentActivity
|
|
168
232
|
val fragmentManager = fragmentActivity.supportFragmentManager
|
|
169
233
|
try {
|
|
170
234
|
fragmentManager.findFragmentByTag("paymentSheet")?.let { existingFragment ->
|
|
@@ -192,9 +256,9 @@ class PaymentSessionReactLauncher(
|
|
|
192
256
|
|
|
193
257
|
return true
|
|
194
258
|
} else {
|
|
195
|
-
|
|
259
|
+
host.startActivity(
|
|
196
260
|
Intent(
|
|
197
|
-
|
|
261
|
+
host.applicationContext,
|
|
198
262
|
HyperActivity::class.java
|
|
199
263
|
).apply {
|
|
200
264
|
putExtra("flow", 1)
|
|
@@ -253,4 +317,35 @@ class PaymentSessionReactLauncher(
|
|
|
253
317
|
}
|
|
254
318
|
return bundle
|
|
255
319
|
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The in-app browser returns through RedirectActivity, which posts a [RedirectEvent]. Only
|
|
323
|
+
* HyperFragment and HyperActivity turn that into the browser's own dismiss event, and the
|
|
324
|
+
* wallet-button and saved-methods flows have neither on screen, so the tab stayed open and the
|
|
325
|
+
* confirm never resumed. Registered once per process; a duplicate post while a sheet is up is
|
|
326
|
+
* ignored, because the browser drops it when no open call is pending.
|
|
327
|
+
*/
|
|
328
|
+
private object RedirectBridge {
|
|
329
|
+
/* Weak: this object outlives the session, and the dismiss intent must be started from the
|
|
330
|
+
Activity, as HyperFragment does, so it lands in the task holding the browser tab. */
|
|
331
|
+
private var activityRef: WeakReference<Activity>? = null
|
|
332
|
+
|
|
333
|
+
fun install(activity: Activity) {
|
|
334
|
+
activityRef = WeakReference(activity)
|
|
335
|
+
val eventBus = EventBus.getDefault()
|
|
336
|
+
if (!eventBus.isRegistered(this)) {
|
|
337
|
+
eventBus.register(this)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@Subscribe
|
|
342
|
+
fun onEvent(event: RedirectEvent) {
|
|
343
|
+
Log.d("HyperswitchRedirect", "RedirectBridge got redirect: ${event.message}")
|
|
344
|
+
EventBus.getDefault().post(
|
|
345
|
+
ChromeTabsDismissedEvent(event.message, event.resultType, event.isError)
|
|
346
|
+
)
|
|
347
|
+
val activity = activityRef?.get() ?: return
|
|
348
|
+
activity.startActivity(ChromeTabsManagerActivity.createDismissIntent(activity))
|
|
349
|
+
}
|
|
350
|
+
}
|
|
256
351
|
}
|
|
@@ -42,9 +42,15 @@ sealed class PaymentResult : Parcelable {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
is Failed -> {
|
|
45
|
+
val text = throwable.message?.takeIf { it.isNotEmpty() }
|
|
46
|
+
val code = throwable.cause?.message?.takeIf { it.isNotEmpty() }
|
|
45
47
|
JSONObject()
|
|
46
48
|
.put("status", "failed")
|
|
47
|
-
|
|
49
|
+
// `error` is kept for existing consumers; `message`/`code` match StandardResult
|
|
50
|
+
// and what the JS layer reads.
|
|
51
|
+
.put("error", text)
|
|
52
|
+
.put("message", text)
|
|
53
|
+
.put("code", code)
|
|
48
54
|
.put("type", throwable::class.java.name)
|
|
49
55
|
.toString()
|
|
50
56
|
}
|
|
@@ -23,6 +23,7 @@ import com.facebook.react.uimanager.UIManagerModule
|
|
|
23
23
|
import com.facebook.react.uimanager.common.UIManagerType
|
|
24
24
|
import com.hyperswitchsdkreactnative.BuildConfig
|
|
25
25
|
import com.hyperswitchsdkreactnative.NativeHyperModuleSpec
|
|
26
|
+
import com.hyperswitchsdkreactnative.modules.NativeLog
|
|
26
27
|
import io.hyperswitch.payments.GooglePayCallbackManager
|
|
27
28
|
import io.hyperswitch.paymentsession.LaunchOptions
|
|
28
29
|
import io.hyperswitch.paymentsession.PaymentSheetCallbackManager
|
|
@@ -67,6 +68,7 @@ class HyperModule internal constructor(private val rct: ReactApplicationContext)
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
init {
|
|
71
|
+
NativeLog.attach(rct)
|
|
70
72
|
activeInstance = this
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -143,11 +145,17 @@ class HyperModule internal constructor(private val rct: ReactApplicationContext)
|
|
|
143
145
|
// Method to launch Google Pay payment
|
|
144
146
|
@ReactMethod
|
|
145
147
|
override fun launchGPay(googlePayRequest: String, callBack: Callback) {
|
|
148
|
+
val activityName = currentActivity?.javaClass?.simpleName
|
|
149
|
+
NativeLog.debug("wallet_launch_called", mapOf("wallet" to "google_pay", "activity" to activityName))
|
|
150
|
+
if (currentActivity == null) {
|
|
151
|
+
NativeLog.warning("wallet_launch_without_activity", mapOf("wallet" to "google_pay"))
|
|
152
|
+
}
|
|
146
153
|
currentActivity?.let {
|
|
147
154
|
GooglePayCallbackManager.setCallback(
|
|
148
155
|
it,
|
|
149
156
|
googlePayRequest,
|
|
150
157
|
fun(data: Map<String, Any?>) {
|
|
158
|
+
NativeLog.debug("wallet_result_delivered", mapOf("wallet" to "google_pay", "status" to data["status"]?.toString()))
|
|
151
159
|
callBack.invoke(
|
|
152
160
|
Arguments.fromBundle(
|
|
153
161
|
LaunchOptions(
|
|
@@ -162,6 +170,7 @@ class HyperModule internal constructor(private val rct: ReactApplicationContext)
|
|
|
162
170
|
reactApplicationContext,
|
|
163
171
|
googlePayRequest,
|
|
164
172
|
fun(data: Map<String, Any?>) {
|
|
173
|
+
NativeLog.debug("wallet_result_delivered", mapOf("wallet" to "google_pay", "status" to data["status"]?.toString()))
|
|
165
174
|
callBack.invoke(
|
|
166
175
|
Arguments.fromBundle(
|
|
167
176
|
LaunchOptions(
|
|
@@ -199,6 +208,10 @@ class HyperModule internal constructor(private val rct: ReactApplicationContext)
|
|
|
199
208
|
@ReactMethod
|
|
200
209
|
override fun exitPaymentsheet(rootTag: Double, paymentResult: String, reset: Boolean) {
|
|
201
210
|
val isFragment = PaymentSheetCallbackManager.executeCallback(paymentResult)
|
|
211
|
+
NativeLog.debug(
|
|
212
|
+
"exit_payment_sheet_resolved",
|
|
213
|
+
mapOf("isFragment" to isFragment, "activityPresent" to (currentActivity != null))
|
|
214
|
+
)
|
|
202
215
|
(currentActivity as? FragmentActivity)?.let {
|
|
203
216
|
if (isFragment) it.supportFragmentManager.findFragmentByTag("paymentSheet")
|
|
204
217
|
?.let { fragment ->
|
|
@@ -20,6 +20,7 @@ import io.hyperswitch.paymentsession.LaunchOptions
|
|
|
20
20
|
import io.hyperswitch.react.HyperFragment
|
|
21
21
|
import io.hyperswitch.react.HyperFragmentManager
|
|
22
22
|
import io.hyperswitch.react.ReactNativeController
|
|
23
|
+
import io.hyperswitch.utils.StandardResult
|
|
23
24
|
import kotlin.collections.orEmpty
|
|
24
25
|
|
|
25
26
|
import kotlin.math.abs
|
|
@@ -186,13 +187,23 @@ open class PaymentWidgetView : FrameLayout {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
fun confirmPayment(callback: (String) -> Unit) {
|
|
189
|
-
this.fragment
|
|
190
|
+
val fragment = this.fragment
|
|
191
|
+
if (fragment != null) {
|
|
192
|
+
fragment.confirmPayment(callback)
|
|
193
|
+
} else {
|
|
194
|
+
/* Without this a detached fragment dropped the callback and the confirm promise hung. */
|
|
195
|
+
callback(fragmentNotAttachedFailure())
|
|
196
|
+
}
|
|
190
197
|
}
|
|
191
198
|
|
|
192
|
-
|
|
193
199
|
fun updatePaymentIntentInit(callback: () -> Unit) {
|
|
194
200
|
if (isEligibleForUpdateIntent()) {
|
|
195
|
-
this.fragment
|
|
201
|
+
val fragment = this.fragment
|
|
202
|
+
if (fragment != null) {
|
|
203
|
+
fragment.updatePaymentIntentInit(callback)
|
|
204
|
+
} else {
|
|
205
|
+
callback()
|
|
206
|
+
}
|
|
196
207
|
} else {
|
|
197
208
|
callback()
|
|
198
209
|
}
|
|
@@ -245,9 +256,21 @@ open class PaymentWidgetView : FrameLayout {
|
|
|
245
256
|
billing: String?,
|
|
246
257
|
callback: (String) -> Unit
|
|
247
258
|
) {
|
|
248
|
-
this.fragment
|
|
259
|
+
val fragment = this.fragment
|
|
260
|
+
if (fragment != null) {
|
|
261
|
+
fragment.confirmCvcPayment(sdkAuthorization, paymentToken, billing, callback)
|
|
262
|
+
} else {
|
|
263
|
+
callback(fragmentNotAttachedFailure())
|
|
264
|
+
}
|
|
249
265
|
}
|
|
250
266
|
|
|
267
|
+
/* Mirrors StandardResult.Failed so the JS layer maps it to a failed result. */
|
|
268
|
+
private fun fragmentNotAttachedFailure() = StandardResult.Failed(
|
|
269
|
+
code = "FRAGMENT_NOT_ATTACHED",
|
|
270
|
+
message = "The payment widget's fragment is not attached",
|
|
271
|
+
error = Throwable("FRAGMENT_NOT_ATTACHED")
|
|
272
|
+
).toJSONString()
|
|
273
|
+
|
|
251
274
|
fun setSdkAuthorization(sdkAuthorization: String) {
|
|
252
275
|
this.sdkAuthorization = sdkAuthorization
|
|
253
276
|
if (isAttachedToWindow && !isSdkAuthorizationEmpty()) {
|