Skip to content

Commit d7da0f8

Browse files
committed
feat: Enhance payment intent creation and widget functionality
- Updated payment intent creation in mock server to include detailed order information and customer metadata. - Modified UIScreen component to utilize a ref for the payment widget and handle checkout process with improved error handling. - Introduced HeadlessPaymentResult class to manage payment results in headless mode, replacing previous PaymentResult usage. - Updated HyperHeadlessModule and PaymentSessionHandler to utilize HeadlessPaymentResult for callbacks. - Enhanced HyperFragment to manage payment result notifications and confirm actions more effectively. - Improved PaymentWidgetView and PaymentWidgetViewManager to handle payment results and events with structured JSON responses. - Added error handling and logging improvements across various components. - Updated WidgetRegistry and related hooks to support new payment result structures.
1 parent a9df448 commit d7da0f8

14 files changed

Lines changed: 334 additions & 238 deletions

File tree

example/src/UIScreen.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useState, useEffect, useCallback, useRef } from 'react';
1+
import { useState, useEffect, useCallback, useRef, use } from 'react';
22
import { View, Text, TextInput, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native';
33
import {
44
PaymentWidget,
55
CvcWidget,
66
HyperElements,
77
initPaymentSession,
8+
useWidget,
89
type HyperElementsOptions,
910
type HyperInstance,
1011
type PaymentEvent,
@@ -38,7 +39,7 @@ export default function UIScreen({ hyperPromise }: UIScreenProps) {
3839
const [defaultMethod, setDefaultMethod] = useState<PaymentMethod | null>(null);
3940
const [loading, setLoading] = useState<boolean>(false);
4041
const [sessionInitializing, setSessionInitializing] = useState<boolean>(false);
41-
42+
const widgetRef = useRef<any>(null);
4243
// Guard against double-initialization from React strict mode / fast re-renders
4344
const sessionInitRef = useRef(false);
4445

@@ -71,7 +72,26 @@ export default function UIScreen({ hyperPromise }: UIScreenProps) {
7172
}, [createPaymentIntent]);
7273

7374
const checkout = async (): Promise<void> => {
74-
console.log('Checkout initiated');
75+
// if (!widget.isReady) {
76+
// setStatus('Error');
77+
// setMessage('Widget not ready yet');
78+
// return;
79+
// }
80+
81+
try {
82+
setLoading(true);
83+
setStatus('Starting checkout...');
84+
let result = await widgetRef.current.confirmPayment();
85+
console.log('Checkout result:', result);
86+
setStatus(getStatus(result?.status));
87+
setMessage(result?.message || result?.status);
88+
} catch (error) {
89+
console.error('Checkout failed:', error);
90+
setStatus('Checkout Error');
91+
setMessage(getErrorMessage(error));
92+
} finally {
93+
setLoading(false);
94+
}
7595
};
7696

7797
useEffect(() => {
@@ -281,6 +301,7 @@ export default function UIScreen({ hyperPromise }: UIScreenProps) {
281301
{message && <Text style={styles.messageText}>{message}</Text>}
282302
</View>
283303
<PaymentWidget
304+
ref={widgetRef}
284305
widgetId="payment-widget"
285306
onPaymentResult={(result: any) => {
286307
console.log('Payment Result from Widget:', result);
@@ -297,6 +318,7 @@ export default function UIScreen({ hyperPromise }: UIScreenProps) {
297318
...getCustomisationOptions('accordion'),
298319
}}
299320
/>
321+
<TouchableOpacity style={styles.button} onPress={checkout}><Text>Checkout</Text></TouchableOpacity>
300322

301323
<Text style={styles.title}>CVC Widget + Headless</Text>
302324

packages/@juspay-tech/react-native-hyperswitch/android/src/main/java/com/hyperswitchsdkreactnative/headless/ExitHeadlessCallBackManager.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ package com.hyperswitchsdkreactnative.headless
22

33
import org.json.JSONObject
44

5-
typealias ExitCallback = (PaymentResult) -> Unit
5+
typealias ExitCallback = (HeadlessPaymentResult) -> Unit
66

77
object ExitHeadlessCallBackManager {
88
private var callback: ExitCallback? = null
99

10-
fun setCallback(newCallback: (PaymentResult) -> Unit) {
10+
fun setCallback(newCallback: (HeadlessPaymentResult) -> Unit) {
1111
callback = newCallback
1212
}
1313

@@ -18,14 +18,14 @@ object ExitHeadlessCallBackManager {
1818
fun executeCallback(data: String) {
1919
val message = JSONObject(data)
2020
val result = when (val status = message.getString("status")) {
21-
"cancelled" -> PaymentResult.Canceled(status)
21+
"cancelled" -> HeadlessPaymentResult.Canceled(status)
2222
"failed", "requires_payment_method" -> {
2323
val throwable = Throwable(message.getString("message"))
2424
throwable.initCause(Throwable(message.getString("code")))
25-
PaymentResult.Failed(throwable)
25+
HeadlessPaymentResult.Failed(throwable)
2626
}
2727

28-
else -> PaymentResult.Completed(status ?: "default")
28+
else -> HeadlessPaymentResult.Completed(status ?: "default")
2929
}
3030
val cb = callback
3131
callback = null

packages/@juspay-tech/react-native-hyperswitch/android/src/main/java/com/hyperswitchsdkreactnative/headless/PaymentResult.kt renamed to packages/@juspay-tech/react-native-hyperswitch/android/src/main/java/com/hyperswitchsdkreactnative/headless/HeadlessPaymentResult.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import kotlinx.parcelize.Parcelize
88
/**
99
* Result to be passed to the callback of [PaymentLauncher]
1010
*/
11-
sealed class PaymentResult : Parcelable {
11+
sealed class HeadlessPaymentResult : Parcelable {
1212
@Parcelize
13-
class Completed(val data: String) : PaymentResult()
13+
class Completed(val data: String) : HeadlessPaymentResult()
1414

1515
@Parcelize
16-
class Failed(val throwable: Throwable) : PaymentResult()
16+
class Failed(val throwable: Throwable) : HeadlessPaymentResult()
1717

1818
@Parcelize
19-
class Canceled(val data: String) : PaymentResult()
19+
class Canceled(val data: String) : HeadlessPaymentResult()
2020

2121
@JvmSynthetic
2222
fun toBundle() = Bundle().apply {
@@ -27,7 +27,7 @@ sealed class PaymentResult : Parcelable {
2727
private const val EXTRA = "extra_args"
2828

2929
@JvmSynthetic
30-
fun fromIntent(intent: Intent?): PaymentResult {
30+
fun fromIntent(intent: Intent?): HeadlessPaymentResult {
3131
return intent?.getParcelableExtra(EXTRA)
3232
?: Failed(IllegalStateException("Failed to get PaymentResult from Intent"))
3333
}

packages/@juspay-tech/react-native-hyperswitch/android/src/main/java/com/hyperswitchsdkreactnative/headless/HyperHeadlessModule.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,21 @@ class HyperHeadlessModule internal constructor(private val rct: ReactApplication
4848
}
4949

5050
override fun confirmWithCustomerDefaultPaymentMethod(
51-
cvc: String?, resultHandler: (PaymentResult) -> Unit
51+
cvc: String?, resultHandler: (HeadlessPaymentResult) -> Unit
5252
) {
5353
getPaymentMethodData.getString("payment_token")
5454
?.let { confirmWithCustomerPaymentToken(it, cvc, resultHandler) }
5555
}
5656

5757
override fun confirmWithCustomerLastUsedPaymentMethod(
58-
cvc: String?, resultHandler: (PaymentResult) -> Unit
58+
cvc: String?, resultHandler: (HeadlessPaymentResult) -> Unit
5959
) {
6060
getPaymentMethodData2.getString("payment_token")
6161
?.let { confirmWithCustomerPaymentToken(it, cvc, resultHandler) }
6262
}
6363

6464
override fun confirmWithCustomerPaymentToken(
65-
paymentToken: String, cvc: String?, resultHandler: (PaymentResult) -> Unit
65+
paymentToken: String, cvc: String?, resultHandler: (HeadlessPaymentResult) -> Unit
6666
) {
6767
try {
6868
ExitHeadlessCallBackManager.setCallback(resultHandler)
@@ -73,7 +73,7 @@ class HyperHeadlessModule internal constructor(private val rct: ReactApplication
7373
} catch (ex: Exception) {
7474
val throwable = Throwable("Not Initialised")
7575
throwable.initCause(Throwable("Not Initialised"))
76-
resultHandler(PaymentResult.Failed(throwable))
76+
resultHandler(HeadlessPaymentResult.Failed(throwable))
7777
}
7878
}
7979
}

packages/@juspay-tech/react-native-hyperswitch/android/src/main/java/com/hyperswitchsdkreactnative/headless/PaymentSessionHandler.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ interface PaymentSessionHandler {
55
fun getCustomerLastUsedPaymentMethodData(): Result<PaymentMethod>
66
fun getCustomerSavedPaymentMethodData(): Result<List<PaymentMethod>>
77
fun confirmWithCustomerDefaultPaymentMethod(
8-
cvc: String? = null, resultHandler: (PaymentResult) -> Unit
8+
cvc: String? = null, resultHandler: (HeadlessPaymentResult) -> Unit
99
)
1010

1111
fun confirmWithCustomerLastUsedPaymentMethod(
12-
cvc: String? = null, resultHandler: (PaymentResult) -> Unit
12+
cvc: String? = null, resultHandler: (HeadlessPaymentResult) -> Unit
1313
)
1414

1515
fun confirmWithCustomerPaymentToken(
16-
paymentToken: String, cvc: String? = null, resultHandler: (PaymentResult) -> Unit
16+
paymentToken: String, cvc: String? = null, resultHandler: (HeadlessPaymentResult) -> Unit
1717
)
1818
}

packages/@juspay-tech/react-native-hyperswitch/android/src/main/java/com/hyperswitchsdkreactnative/modules/HyperswitchRNWrapperNativeModule.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.hyperswitchsdkreactnative.NativeHyperswitchSdkReactNativeSpec
1010
import com.hyperswitchsdkreactnative.headless.HeadlessFlowController
1111
import com.hyperswitchsdkreactnative.headless.PMError
1212
import com.hyperswitchsdkreactnative.headless.PaymentMethodType
13-
import com.hyperswitchsdkreactnative.headless.PaymentResult
13+
import com.hyperswitchsdkreactnative.headless.HeadlessPaymentResult
1414
import com.hyperswitchsdkreactnative.headless.PaymentSessionHandler
1515
import com.hyperswitchsdkreactnative.provider.HyperProvider
1616
import com.hyperswitchsdkreactnative.views.PaymentWidgetViewManager
@@ -321,20 +321,20 @@ class HyperswitchRNWrapperNativeModule(reactContext: ReactApplicationContext) :
321321
* Convert PaymentResult (Throwable-based Failed) to JSON string.
322322
* Old pattern: code in throwable.cause.message, message in throwable.message
323323
*/
324-
private fun paymentResultToString(result: PaymentResult): String {
324+
private fun paymentResultToString(result: HeadlessPaymentResult): String {
325325
val json = JSONObject()
326326
when (result) {
327-
is PaymentResult.Completed -> {
327+
is HeadlessPaymentResult.Completed -> {
328328
json.put("status", "success")
329329
json.put("message", "Payment confirmed successfully")
330330
json.put("data", result.data)
331331
}
332-
is PaymentResult.Failed -> {
332+
is HeadlessPaymentResult.Failed -> {
333333
json.put("status", "failed")
334334
json.put("code", result.throwable.cause?.message ?: "UNKNOWN_ERROR")
335335
json.put("message", result.throwable.message ?: "An error has occurred.")
336336
}
337-
is PaymentResult.Canceled -> {
337+
is HeadlessPaymentResult.Canceled -> {
338338
json.put("status", "cancelled")
339339
json.put("message", "Payment confirmation cancelled")
340340
json.put("data", result.data)

packages/@juspay-tech/react-native-hyperswitch/android/src/main/java/com/hyperswitchsdkreactnative/modules/HyperswitchSDKNativeModule.kt

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package com.hyperswitchsdkreactnative.modules
22

33
import android.util.Log
4+
import androidx.fragment.app.FragmentManager
45
import com.facebook.react.bridge.ReactApplicationContext
56
import com.facebook.react.bridge.Callback
67
import com.facebook.react.bridge.ReactContext
78
import com.facebook.react.bridge.WritableMap
89
import com.facebook.react.bridge.WritableNativeMap
910
import com.facebook.react.bridge.ReactMethod
1011
import com.facebook.react.bridge.ReadableMap
12+
import com.facebook.react.uimanager.IllegalViewOperationException
13+
import com.facebook.react.uimanager.UIManagerModule
1114
import org.json.JSONException
1215
import com.hyperswitchsdkreactnative.NativeHyperswitchSdkNativeSpec
1316
import com.hyperswitchsdkreactnative.modules.HyperswitchRNWrapperNativeModule.Companion.resetView
1417
import com.hyperswitchsdkreactnative.modules.HyperswitchRNWrapperNativeModule.Companion.resolvePromise
18+
import com.hyperswitchsdkreactnative.provider.CallbackType
1519
import com.hyperswitchsdkreactnative.provider.HyperFragment
1620
import io.hyperswitch.payments.GooglePayCallbackManager
1721

@@ -81,10 +85,32 @@ class HyperswitchSdkNativeModule(reactContext: ReactApplicationContext) :
8185
}
8286
}
8387

88+
private fun findFragmentWithRootTag(rootTag: Int, onFound: (HyperFragment?) -> Unit) {
89+
val uiManagerModule =
90+
reactApplicationContext.getNativeModule<UIManagerModule?>(UIManagerModule::class.java)
91+
92+
if (uiManagerModule == null) {
93+
onFound(null)
94+
return
95+
}
96+
97+
uiManagerModule.addUIBlock { nvhm ->
98+
try {
99+
val reactRootView = nvhm.resolveView(rootTag)
100+
onFound(FragmentManager.findFragment<HyperFragment>(reactRootView))
101+
} catch (e: IllegalViewOperationException) {
102+
onFound(null)
103+
} catch (e: IllegalStateException) {
104+
onFound(null)
105+
}
106+
}
107+
}
84108

85109
override fun notifyWidgetPaymentResult(rootTag: Int, result: String) {
86110
try {
87-
HyperFragment.resolveConfirmPayment(rootTag, result)
111+
findFragmentWithRootTag(rootTag, {
112+
it?.notifyResult(CallbackType.CONFIRM_ACTION, result)
113+
})
88114
} catch (_: Exception) {
89115
Log.i("HyperModule", "Error in notifyWidgetPaymentResult")
90116
}
@@ -113,7 +139,9 @@ class HyperswitchSdkNativeModule(reactContext: ReactApplicationContext) :
113139
result: String,
114140
reset: Boolean
115141
) {
116-
HyperFragment.onPaymentResultEvent(rootTag.toInt(), result)
142+
findFragmentWithRootTag(rootTag.toInt(), {
143+
it?.notifyResult(CallbackType.PAYMENT_RESULT, result)
144+
})
117145
}
118146

119147
override fun launchWidgetPaymentSheet(requestObj: String, callback: Callback) {
@@ -133,9 +161,12 @@ class HyperswitchSdkNativeModule(reactContext: ReactApplicationContext) :
133161
if (widgetId.isEmpty()) {
134162
HyperswitchRNWrapperNativeModule.emitPaymentSheetEvent(eventType, payload)
135163
} else {
164+
// findFragmentWithRootTag(rootTag.toInt(), {
165+
// it?.notifyResult(CallbackType.PAYMENT_RESULT, result)
166+
// })
136167
// WidgetCallbackManager.sendEvent(widgetId, eventType, payload)
137-
HyperFragment.onEvents(widgetId, eventType, payload)
138-
168+
// HyperFragment.onEvents(widgetId, eventType, payload)
169+
// HyperFragment.onEvents(widgetId, CallbackType.PAYMENT_RESULT,"", result)
139170
}
140171
} catch (e: Exception) {
141172
Log.e("HyperModule", "Error emitting payment event", e)

0 commit comments

Comments
 (0)