@yuno-payments/yuno-sdk-react-native 1.0.16 → 1.0.17-rc.10

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.
@@ -0,0 +1,48 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+ folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5
+
6
+ Pod::Spec.new do |s|
7
+ s.name = "YunoSdk"
8
+ s.version = package["version"]
9
+ s.summary = package["description"]
10
+ s.homepage = package["homepage"]
11
+ s.license = package["license"]
12
+ s.authors = package["author"]
13
+
14
+ s.platforms = { :ios => "14.0" }
15
+ s.source = { :git => "https://github.com/yuno-payments/yuno-sdk-react-native.git", :tag => "#{s.version}" }
16
+
17
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
18
+
19
+ # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
20
+ # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
21
+ if respond_to?(:install_modules_dependencies, true)
22
+ install_modules_dependencies(s)
23
+ else
24
+ s.dependency "React-Core"
25
+
26
+ # Don't install the dependencies when we run `pod install` in the old architecture.
27
+ if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
28
+ s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
29
+ s.pod_target_xcconfig = {
30
+ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
31
+ "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
32
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
33
+ }
34
+ s.dependency "React-Codegen"
35
+ s.dependency "RCT-Folly"
36
+ s.dependency "RCTRequired"
37
+ s.dependency "RCTTypeSafety"
38
+ s.dependency "ReactCommon/turbomodule/core"
39
+ end
40
+ end
41
+
42
+ # Yuno iOS SDK
43
+ s.dependency "YunoSDK", "2.7.1"
44
+
45
+ s.swift_version = '5.0'
46
+ s.static_framework = true
47
+ end
48
+
@@ -2,6 +2,7 @@ package com.yunosdkreactnative
2
2
 
3
3
  import android.app.Activity
4
4
  import android.content.Context
5
+ import android.util.Log
5
6
  import androidx.activity.ComponentActivity
6
7
  import com.facebook.react.bridge.*
7
8
  import com.facebook.react.modules.core.DeviceEventManagerModule
@@ -10,12 +11,26 @@ import com.yuno.sdk.YunoConfig
10
11
  import com.yuno.sdk.YunoLanguage
11
12
  import com.yuno.sdk.enrollment.*
12
13
  import com.yuno.sdk.payments.*
14
+ import com.yuno.sdk.ApiClientPayment
15
+ import com.yuno.sdk.ApiClientPayment.Companion.generateToken
16
+ import com.yuno.sdk.ApiClientPayment.Companion.getThreeDSecureChallenge
17
+ import com.yuno.sdk.ApiClientEnroll
18
+ import com.yuno.sdk.ApiClientEnroll.Companion.continueEnrollment
19
+ import com.yuno.sdk.payments.TokenCollectedData
20
+ import com.yuno.sdk.enrollment.EnrollmentCollectedData
21
+ import com.yuno.sdk.ThreeDSecureChallengeResponse
13
22
  import com.yuno.presentation.core.components.PaymentSelected
14
23
  import com.yuno.presentation.core.card.CardFormType
15
24
  import com.yuno.payments.features.payment.models.OneTimeTokenModel
16
25
  import com.google.gson.Gson
17
26
  import org.json.JSONObject
18
27
  import org.json.JSONArray
28
+ import kotlinx.coroutines.CoroutineScope
29
+ import kotlinx.coroutines.Dispatchers
30
+ import kotlinx.coroutines.flow.launchIn
31
+ import kotlinx.coroutines.flow.onEach
32
+ import kotlinx.coroutines.flow.catch
33
+ import androidx.lifecycle.asFlow
19
34
 
20
35
  /**
21
36
  * Yuno SDK React Native Module for Android
@@ -51,6 +66,14 @@ class YunoSdkModule(private val reactContext: ReactApplicationContext) :
51
66
  @Volatile
52
67
  private var lastOneTimeTokenInfo: OneTimeTokenModel? = null
53
68
 
69
+ // Store the last payment status to prevent stale status from previous flows
70
+ @Volatile
71
+ private var lastPaymentStatus: String? = null
72
+
73
+ // Flag to track if we're starting a fresh payment flow
74
+ @Volatile
75
+ private var isPaymentFlowCleared: Boolean = false
76
+
54
77
  // Gson instance for JSON serialization
55
78
  private val gson = Gson()
56
79
 
@@ -612,6 +635,24 @@ class YunoSdkModule(private val reactContext: ReactApplicationContext) :
612
635
  }
613
636
  }
614
637
 
638
+ /**
639
+ * Clears the last stored payment status.
640
+ * This should be called at the start of each new payment flow
641
+ * to prevent stale status from previous flows.
642
+ */
643
+ @ReactMethod
644
+ fun clearLastPaymentStatus(promise: Promise) {
645
+ try {
646
+ // Don't set to null - keep the last status to compare against stale events
647
+ // Just mark that we're starting a fresh flow
648
+ isPaymentFlowCleared = true
649
+ Log.d(TAG, "💫 Payment status cleared - fresh flow starting (last status was: $lastPaymentStatus)")
650
+ promise.resolve(true)
651
+ } catch (e: Exception) {
652
+ promise.reject("CLEAR_STATUS_ERROR", "Error clearing last payment status: ${e.message}", e)
653
+ }
654
+ }
655
+
615
656
  // Lifecycle Methods
616
657
  override fun onHostResume() {}
617
658
  override fun onHostPause() {}
@@ -651,6 +692,26 @@ class YunoSdkModule(private val reactContext: ReactApplicationContext) :
651
692
 
652
693
  internal fun sendPaymentStatusEvent(status: String?) {
653
694
  val convertedStatus = convertPaymentStatus(status)
695
+
696
+ // If we just cleared the status and this is the same status as before,
697
+ // it's a stale event from the native SDK - ignore it
698
+ if (isPaymentFlowCleared && convertedStatus == lastPaymentStatus) {
699
+ Log.d(TAG, "🚫 Ignoring stale payment status: $convertedStatus (from previous flow)")
700
+ isPaymentFlowCleared = false
701
+ lastPaymentStatus = null // Clear it now that we've ignored the stale event
702
+ return
703
+ }
704
+
705
+ // Reset the cleared flag after processing first real new event
706
+ if (isPaymentFlowCleared) {
707
+ Log.d(TAG, "✅ First new status after clear: $convertedStatus (previous was: $lastPaymentStatus)")
708
+ isPaymentFlowCleared = false
709
+ }
710
+
711
+ // Store the current status for future comparison
712
+ lastPaymentStatus = convertedStatus
713
+
714
+ Log.d(TAG, "✅ Emitting payment status: $convertedStatus")
654
715
  val params = Arguments.createMap().apply {
655
716
  putString("status", convertedStatus)
656
717
  }
@@ -774,4 +835,284 @@ class YunoSdkModule(private val reactContext: ReactApplicationContext) :
774
835
  }
775
836
  return array
776
837
  }
838
+
839
+ // ==================== HEADLESS PAYMENT FLOW ====================
840
+
841
+ /**
842
+ * Generate a one-time token (OTT) from collected payment data using the headless flow.
843
+ * This method mirrors the native SDK's generateToken() API.
844
+ *
845
+ * @param tokenCollectedData Map containing checkout_session, customer_session, and payment_method data
846
+ * @param checkoutSession The checkout session ID
847
+ * @param countryCode The country code for the payment
848
+ * @param promise Promise to resolve with token or error
849
+ */
850
+ @ReactMethod
851
+ fun generateToken(
852
+ tokenCollectedData: ReadableMap,
853
+ checkoutSession: String,
854
+ countryCode: String,
855
+ promise: Promise
856
+ ) {
857
+ try {
858
+ Log.d(TAG, "generateToken called with checkoutSession: $checkoutSession")
859
+
860
+ val activity = currentActivity
861
+ if (activity == null) {
862
+ promise.reject("ACTIVITY_UNAVAILABLE", "Current activity is null. Cannot generate token.")
863
+ return
864
+ }
865
+
866
+ // Convert ReadableMap to TokenCollectedData using Gson
867
+ val gson = Gson()
868
+ val jsonString = convertReadableMapToJson(tokenCollectedData)
869
+ Log.d(TAG, "JSON String: $jsonString")
870
+ val collectedData = gson.fromJson(jsonString, TokenCollectedData::class.java)
871
+ Log.d(TAG, "Parsed TokenCollectedData: checkoutSession=${collectedData.checkoutSession}, paymentMethod=${collectedData.paymentMethod}")
872
+
873
+ // Create API client
874
+ val apiClient = Yuno.apiClientPayment(
875
+ checkoutSession = checkoutSession,
876
+ countryCode = countryCode,
877
+ context = activity.applicationContext
878
+ )
879
+
880
+ // Generate token - pass activity for WebView context
881
+ apiClient.generateToken(collectedData, activity)
882
+ .asFlow()
883
+ .onEach { result ->
884
+ try {
885
+ when {
886
+ result.containsKey("token") && result["token"] != null -> {
887
+ val token = result["token"] as String
888
+ Log.d(TAG, "✅ Token generated successfully")
889
+
890
+ val response = Arguments.createMap().apply {
891
+ putString("token", token)
892
+ }
893
+ promise.resolve(response)
894
+ }
895
+ result.containsKey("error") && result["error"] != null -> {
896
+ val error = result["error"] as String
897
+ Log.e(TAG, "❌ Token generation failed: $error")
898
+ promise.reject("TOKEN_GENERATION_ERROR", error)
899
+ }
900
+ else -> {
901
+ Log.e(TAG, "❌ Unknown response from token generation")
902
+ promise.reject("TOKEN_GENERATION_ERROR", "Unknown error occurred")
903
+ }
904
+ }
905
+ } catch (e: Exception) {
906
+ Log.e(TAG, "❌ Error processing token generation result: ${e.message}")
907
+ promise.reject("TOKEN_GENERATION_ERROR", e.message)
908
+ }
909
+ }
910
+ .catch { e ->
911
+ Log.e(TAG, "❌ Flow error in token generation: ${e.message}")
912
+ promise.reject("TOKEN_GENERATION_ERROR", e.message ?: "Unknown error in token generation flow")
913
+ }
914
+ .launchIn(CoroutineScope(Dispatchers.Main))
915
+ } catch (e: Exception) {
916
+ Log.e(TAG, "❌ Error in generateToken: ${e.message}")
917
+ promise.reject("TOKEN_GENERATION_ERROR", e.message)
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Get the 3D Secure challenge URL for a checkout session.
923
+ * This method mirrors the native SDK's getThreeDSecureChallenge() API.
924
+ *
925
+ * @param checkoutSession The checkout session ID
926
+ * @param countryCode The country code for the payment
927
+ * @param promise Promise to resolve with URL or error
928
+ */
929
+ @ReactMethod
930
+ fun getThreeDSecureChallenge(
931
+ checkoutSession: String,
932
+ countryCode: String,
933
+ promise: Promise
934
+ ) {
935
+ try {
936
+ Log.d(TAG, "getThreeDSecureChallenge called with checkoutSession: $checkoutSession")
937
+
938
+ val activity = currentActivity
939
+ if (activity == null) {
940
+ promise.reject("ACTIVITY_UNAVAILABLE", "Current activity is null. Cannot get 3DS challenge.")
941
+ return
942
+ }
943
+
944
+ // Create API client
945
+ val apiClient = Yuno.apiClientPayment(
946
+ checkoutSession = checkoutSession,
947
+ countryCode = countryCode,
948
+ context = activity.applicationContext
949
+ )
950
+
951
+ // Get 3DS challenge - pass activity for WebView context
952
+ apiClient.getThreeDSecureChallenge(activity, checkoutSession)
953
+ .asFlow()
954
+ .onEach { result ->
955
+ try {
956
+ val response = Arguments.createMap().apply {
957
+ putString("type", result.type)
958
+ putString("data", result.data)
959
+ }
960
+
961
+ if (result.type == "URL") {
962
+ Log.d(TAG, "✅ 3DS Challenge URL retrieved successfully")
963
+ promise.resolve(response)
964
+ } else {
965
+ Log.e(TAG, "❌ 3DS Challenge failed: ${result.data}")
966
+ promise.reject("THREE_DS_ERROR", result.data)
967
+ }
968
+ } catch (e: Exception) {
969
+ Log.e(TAG, "❌ Error processing 3DS challenge result: ${e.message}")
970
+ promise.reject("THREE_DS_ERROR", e.message)
971
+ }
972
+ }
973
+ .catch { e ->
974
+ Log.e(TAG, "❌ Flow error in 3DS challenge: ${e.message}")
975
+ promise.reject("THREE_DS_ERROR", e.message ?: "Unknown error in 3DS challenge flow")
976
+ }
977
+ .launchIn(CoroutineScope(Dispatchers.Main))
978
+ } catch (e: Exception) {
979
+ Log.e(TAG, "❌ Error in getThreeDSecureChallenge: ${e.message}")
980
+ promise.reject("THREE_DS_ERROR", e.message)
981
+ }
982
+ }
983
+
984
+ // ==================== HEADLESS ENROLLMENT FLOW ====================
985
+
986
+ @ReactMethod
987
+ fun continueEnrollment(
988
+ enrollmentCollectedData: ReadableMap,
989
+ customerSession: String,
990
+ countryCode: String,
991
+ promise: Promise
992
+ ) {
993
+ try {
994
+ Log.d(TAG, "continueEnrollment called with customerSession: $customerSession")
995
+
996
+ val activity = currentActivity
997
+ if (activity == null) {
998
+ promise.reject("ACTIVITY_UNAVAILABLE", "Current activity is null. Cannot continue enrollment.")
999
+ return
1000
+ }
1001
+
1002
+ // Convert ReadableMap to JSON String
1003
+ val jsonString = convertReadableMapToJson(enrollmentCollectedData)
1004
+ Log.d(TAG, "📦 EnrollmentCollectedData JSON received: $jsonString")
1005
+
1006
+ // Convert JSON String to EnrollmentCollectedData using Gson
1007
+ val gson = Gson()
1008
+ val collectedData = gson.fromJson(jsonString, EnrollmentCollectedData::class.java)
1009
+
1010
+ // Create API client for enrollment
1011
+ val apiClient = Yuno.apiClientEnroll(
1012
+ customerSession = customerSession,
1013
+ countryCode = countryCode,
1014
+ context = activity
1015
+ )
1016
+
1017
+ // Continue enrollment
1018
+ apiClient.continueEnrollment(collectedData, activity)
1019
+ .asFlow()
1020
+ .onEach { result ->
1021
+ try {
1022
+ when {
1023
+ result.containsKey("vaulted_token") && result["vaulted_token"] != null -> {
1024
+ val vaultedToken = result["vaulted_token"] as String
1025
+ Log.d(TAG, "✅ Vaulted token created successfully")
1026
+
1027
+ val response = Arguments.createMap().apply {
1028
+ putString("vaultedToken", vaultedToken)
1029
+ }
1030
+ promise.resolve(response)
1031
+ }
1032
+ result.containsKey("error") && result["error"] != null -> {
1033
+ val error = result["error"] as String
1034
+ Log.e(TAG, "❌ Enrollment failed: $error")
1035
+ promise.reject("ENROLLMENT_ERROR", error)
1036
+ }
1037
+ else -> {
1038
+ Log.e(TAG, "❌ Unknown response from enrollment")
1039
+ promise.reject("ENROLLMENT_ERROR", "Unknown error occurred")
1040
+ }
1041
+ }
1042
+ } catch (e: Exception) {
1043
+ Log.e(TAG, "❌ Error processing enrollment result: ${e.message}")
1044
+ promise.reject("ENROLLMENT_ERROR", e.message)
1045
+ }
1046
+ }
1047
+ .catch { e ->
1048
+ Log.e(TAG, "❌ Flow error during enrollment: ${e.message}")
1049
+ promise.reject("ENROLLMENT_FLOW_ERROR", e.message)
1050
+ }
1051
+ .launchIn(CoroutineScope(Dispatchers.Main))
1052
+ } catch (e: Exception) {
1053
+ Log.e(TAG, "❌ Error in continueEnrollment: ${e.message}")
1054
+ promise.reject("ENROLLMENT_ERROR", e.message)
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * Helper function to convert ReadableMap to JSON string.
1060
+ */
1061
+ private fun convertReadableMapToJson(readableMap: ReadableMap): String {
1062
+ val json = JSONObject()
1063
+ val iterator = readableMap.keySetIterator()
1064
+
1065
+ while (iterator.hasNextKey()) {
1066
+ val key = iterator.nextKey()
1067
+ val value = when (readableMap.getType(key)) {
1068
+ ReadableType.Boolean -> readableMap.getBoolean(key)
1069
+ ReadableType.Number -> readableMap.getDouble(key)
1070
+ ReadableType.String -> readableMap.getString(key)
1071
+ ReadableType.Map -> convertReadableMapToJsonObject(readableMap.getMap(key)!!)
1072
+ ReadableType.Array -> convertReadableArrayToJsonArray(readableMap.getArray(key)!!)
1073
+ ReadableType.Null -> JSONObject.NULL
1074
+ }
1075
+ json.put(key, value)
1076
+ }
1077
+
1078
+ return json.toString()
1079
+ }
1080
+
1081
+ private fun convertReadableMapToJsonObject(readableMap: ReadableMap): JSONObject {
1082
+ val json = JSONObject()
1083
+ val iterator = readableMap.keySetIterator()
1084
+
1085
+ while (iterator.hasNextKey()) {
1086
+ val key = iterator.nextKey()
1087
+ val value = when (readableMap.getType(key)) {
1088
+ ReadableType.Boolean -> readableMap.getBoolean(key)
1089
+ ReadableType.Number -> readableMap.getDouble(key)
1090
+ ReadableType.String -> readableMap.getString(key)
1091
+ ReadableType.Map -> convertReadableMapToJsonObject(readableMap.getMap(key)!!)
1092
+ ReadableType.Array -> convertReadableArrayToJsonArray(readableMap.getArray(key)!!)
1093
+ ReadableType.Null -> JSONObject.NULL
1094
+ }
1095
+ json.put(key, value)
1096
+ }
1097
+
1098
+ return json
1099
+ }
1100
+
1101
+ private fun convertReadableArrayToJsonArray(readableArray: ReadableArray): JSONArray {
1102
+ val json = JSONArray()
1103
+
1104
+ for (i in 0 until readableArray.size()) {
1105
+ val value = when (readableArray.getType(i)) {
1106
+ ReadableType.Boolean -> readableArray.getBoolean(i)
1107
+ ReadableType.Number -> readableArray.getDouble(i)
1108
+ ReadableType.String -> readableArray.getString(i)
1109
+ ReadableType.Map -> convertReadableMapToJsonObject(readableArray.getMap(i))
1110
+ ReadableType.Array -> convertReadableArrayToJsonArray(readableArray.getArray(i))
1111
+ ReadableType.Null -> JSONObject.NULL
1112
+ }
1113
+ json.put(value)
1114
+ }
1115
+
1116
+ return json
1117
+ }
777
1118
  }
@@ -0,0 +1,10 @@
1
+ #import <React/RCTViewManager.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(YunoPaymentMethodsViewManager, RCTViewManager)
4
+
5
+ // Export props
6
+ RCT_EXPORT_VIEW_PROPERTY(checkoutSession, NSString)
7
+ RCT_EXPORT_VIEW_PROPERTY(countryCode, NSString)
8
+
9
+ @end
10
+