diff --git a/README.md b/README.md index 0c4638d..4b9cf55 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,15 @@ The native **iOS N-Genius SDK** does not provide specific statuses like **author | Transaction successful | `PAYMENT_SUCCESSFUL` | `200` | --- + ### 🚀 **Get Started with N-Genius Flutter SDK** 1. Install the plugin in your Flutter project. 2. Configure **Android settings** as mentioned above. 3. Call the **N-Genius SDK** to launch the payment flow. -#### **Example Implementation** +--- + +#### **Example Implementation (Normal Payment)** ```dart class NgeniusExample extends StatelessWidget { const NgeniusExample({super.key}); @@ -103,6 +106,85 @@ class NgeniusExample extends StatelessWidget { } } ``` +--- + +#### **Example Implementation (for using Saved Cards)** + +> **Note:** The key difference when using a saved card is in the **order creation step**. You must pass a `savedCard` object (`NGeniusSavedCardModel`) in your `createOrder` request body. If `recaptureCsc` is set to `true`, the user will be prompted to enter their CVV — you can skip this by passing the `cvv` directly to `launchSavedCardPayment`. + +```dart +class NgeniusSavedCardExample extends StatelessWidget { + const NgeniusSavedCardExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('N-Genius Saved Card Example'), + ), + body: Center( + child: ElevatedButton( + onPressed: () async { + // 1. Build your saved card model from your stored card data + final savedCard = NGeniusSavedCardModel( + maskedPan: "400555******0001", + expiry: "2025-12", + cardholderName: "John Doe", + scheme: "VISA", + cardToken: "your-card-token", + recaptureCsc: true, // true = user will be prompted for CVV + ); + + // 2. Call YOUR backend/API to create the order. + // This is not part of the plugin — you are responsible for + // implementing this API call following the N-Genius documentation: + // https://docs.ngenius-payments.com/reference/two-stage-payments-orders + // The savedCard object must be included in the request body. + final Map orderJsonObject = await yourApiService.createOrder( + amountValue: 10.50, + currency: "AED", + savedCard: savedCard, + ); + + // 3. Launch saved card payment + // Pass cvv to skip the CVV page, or omit it to let the user enter it + final ngeniusFlutterSdk = NgeniusFlutterSdk(); + NGeniusResponseModel ngeniusResponse = await ngeniusFlutterSdk.launchSavedCardPayment( + orderJsonObject: orderJsonObject, + //cvv: "123" //If you want your UI to handle CVV + ); + + if (context.mounted) { + if (ngeniusResponse.message == "PAYMENT_SUCCESSFUL") { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Transaction Successful"))); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Transaction Failed :: code :: ${ngeniusResponse.code} :: message :: ${ngeniusResponse.message}"))); + } + } + }, + child: Text("Launch Saved Card Payment")), + ), + ); + } +} +``` + +--- + +## 🔄 N-Genius SavedCard Model + +| Parameter | Example | DataType | +|----------------|----------------------------------------|------------| +| maskedPan | `400555******0001` | `String` | +| expiry | `"2025-12"` | `String` | +| cardholderName | `"John Doe"` | `String` | +| scheme | `"VISA"` | `String` | +| cardToken | `"your-card-token"` | `String` | +| recaptureCsc | `true` = user will be prompted for CVV | `boolean` | + +--- ### **Passing the Order JSON Object to `launchCardPayment` Method** @@ -110,11 +192,11 @@ To pass the `orderJsonObject` to the `launchCardPayment` method, you need to pro #### **Steps to Get the Order JSON Object** -1. **Get the Access Token** +1- **Get the Access Token** First, you need to obtain the access token by following the official N-Genius documentation: [Request an Access Token](https://docs.ngenius-payments.com/reference/request-an-access-token-direct) -2. **Get the Order Object** +2- **Get the Order Object** Once you have the access token, use it to call the API to get the order object. For more information, refer to the N-Genius documentation: [Two-Stage Payments Orders](https://docs.ngenius-payments.com/reference/two-stage-payments-orders) @@ -123,7 +205,23 @@ To pass the `orderJsonObject` to the `launchCardPayment` method, you need to pro After calling the APIs, you will receive the N-Genius order object. You can check the structure of the order object by referring to the official sample here: [Order Object in Full](https://docs.ngenius-payments.com/reference/the-order-object-in-full) -#### **Test Payment Using N-Genius Test Cards** +--- + +### **Getting the Saved-Card Token** + +1- Ensure that Tokenization is enabled for your merchant account. + +2- After a successful payment, retrieve the order details using the following endpoint: +`GET /transactions/outlets/{{outletId}}/orders/{{orderId}}` + +3- The saved card token is returned in the response at: +`_embedded.payment.savedCard.cardToken` + +You can use this token to perform future saved card payments without requiring the customer to re-enter their card details. + +--- + +### **Test Payment Using N-Genius Test Cards** You can test payment using test cards for N-Genius from the following link: [Sandbox Test Environment](https://docs.ngenius-payments.com/reference/sandbox-test-environment) diff --git a/android/build.gradle b/android/build.gradle index 0f3d8fe..175035f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,6 +52,7 @@ android { testImplementation("org.mockito:mockito-core:5.0.0") implementation "com.github.network-international.payment-sdk-android:payment-sdk-core:3.1.7" implementation "com.github.network-international.payment-sdk-android:payment-sdk:3.1.7" + implementation "com.google.code.gson:gson:2.10.1" } testOptions { diff --git a/android/src/main/kotlin/com/example/ngenius_flutter_sdk/NgeniusFlutterSdkPlugin.kt b/android/src/main/kotlin/com/example/ngenius_flutter_sdk/NgeniusFlutterSdkPlugin.kt index 3858e67..65c668b 100644 --- a/android/src/main/kotlin/com/example/ngenius_flutter_sdk/NgeniusFlutterSdkPlugin.kt +++ b/android/src/main/kotlin/com/example/ngenius_flutter_sdk/NgeniusFlutterSdkPlugin.kt @@ -14,8 +14,12 @@ import io.flutter.plugin.common.PluginRegistry.ActivityResultListener import payment.sdk.android.PaymentClient import payment.sdk.android.cardpayment.CardPaymentData import payment.sdk.android.cardpayment.CardPaymentRequest +import payment.sdk.android.core.Order import io.flutter.plugin.common.PluginRegistry import org.json.JSONObject +import java.util.HashMap +import com.google.gson.Gson + /** NgeniusFlutterSdkPlugin */ class NgeniusFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { @@ -35,52 +39,90 @@ class NgeniusFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, } override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "launchCardPayment") { - try { - val jsonData = call.argument>("orderJsonObject") - ?: throw IllegalArgumentException("orderJsonObject is required") + when (call.method) { + + // =========================== + // Standard Payment + // =========================== + "launchCardPayment" -> { + try { + val jsonData = call.argument>("orderJsonObject") + ?: throw IllegalArgumentException("orderJsonObject is required") + + val links = jsonData["_links"] as? HashMap + ?: throw Exception("_links not found in orderJsonObject") + + val authUrl = ((links["payment-authorization"] as? HashMap)?.get("href") as? String) + ?: throw Exception("Authorization URL not found in orderJsonObject") + + val paymentUrl = ((links["payment"] as? HashMap)?.get("href") as? String) + ?: throw Exception("Payment URL not found in orderJsonObject") + + val code = if (paymentUrl.contains("code=")) { + paymentUrl.substringAfter("code=").takeIf { it.isNotEmpty() } + ?: throw Exception("Code value is empty in orderJsonObject") + } else { + throw Exception("Code parameter not found in payment URL") + } + + pendingResult = result + activity?.let { currentActivity -> + paymentClient = PaymentClient(currentActivity, "DEMO_VAL") + paymentClient.launchCardPayment( + CardPaymentRequest.Builder() + .gatewayUrl(authUrl) + .code(code) + .build(), + CARD_PAYMENT_REQUEST_CODE + ) + } ?: result.error("NO_ACTIVITY", "No activity available", null) + } catch (e: Exception) { + result.error("LAUNCH_ERROR", e.message ?: "Unknown error occurred", null) + } + } - val links = (jsonData["_links"] as? HashMap) - ?: throw Exception("_links not found in orderJsonObject") + // =========================== + // Saved Card Payment + // =========================== + "launchSavedCardPayment" -> { + try { + val jsonData = call.argument>("orderJsonObject") + ?: throw IllegalArgumentException("orderJsonObject is required") - val authUrl = ((links["payment-authorization"] as? HashMap)?.get("href") as? String) - ?: throw Exception("Authorization URL not found in orderJsonObject") - val paymentUrl = ((links["payment"] as? HashMap)?.get("href") as? String) - ?: throw Exception("Payment URL not found in orderJsonObject") + // Convert to JSON string, then to Order object + val orderJson = JSONObject(jsonData as Map<*, *>).toString() + val order = Gson().fromJson(orderJson, Order::class.java) + val cvv = call.argument("cvv") // May be null or empty - val code = if (paymentUrl.contains("code=")) { - paymentUrl.substringAfter("code=").takeIf { it.isNotEmpty() } - ?: throw Exception("Code value is empty in orderJsonObject") - } else { - throw Exception("Code parameter not found in payment URL") - } + pendingResult = result + activity?.let { currentActivity -> + paymentClient = PaymentClient(currentActivity, "YOUR_SERVICE_ID") - pendingResult = result - activity?.let { currentActivity -> - paymentClient = PaymentClient(currentActivity, "DEMO_VAL") - paymentClient.launchCardPayment( - CardPaymentRequest.Builder() - .gatewayUrl(authUrl) - .code(code) - .build(), - CARD_PAYMENT_REQUEST_CODE - ) - } ?: result.error( - "NO_ACTIVITY", - "No activity available", - null - ) - } catch (e: Exception) { - result.error( - "LAUNCH_ERROR", - e.message ?: "Unknown error occurred", - null - ) + if (!cvv.isNullOrEmpty()) { + // Option 2: Launch with custom CVV, skipping the pay page + paymentClient.launchSavedCardPayment( + order = order, + cvv = cvv, + code = CARD_PAYMENT_REQUEST_CODE + ) + } else { + // Option 1: Launch without CVV. This will redirect to the pay page if 'recaptureCsc' was true in the order. + paymentClient.launchSavedCardPayment( + order = order, + code = CARD_PAYMENT_REQUEST_CODE + ) + } + } ?: result.error("NO_ACTIVITY", "No activity available", null) + + } catch (e: Exception) { + result.error("SAVED_CARD_ERROR", e.message ?: "Unknown error occurred", null) + } } - } else { - result.notImplemented() + + + else -> result.notImplemented() } } diff --git a/example/pubspec.lock b/example/pubspec.lock index 0336fc5..db396ba 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,18 +53,18 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -102,18 +102,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -134,10 +134,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -150,33 +150,33 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" ngenius_flutter_sdk: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.1" + version: "1.0.2" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" sky_engine: dependency: transitive description: flutter @@ -202,34 +202,34 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -242,18 +242,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" vector_math: dependency: transitive description: @@ -266,18 +266,18 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.0" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" sdks: - dart: ">=3.6.1 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/ios/Classes/NgeniusFlutterSdkPlugin.swift b/ios/Classes/NgeniusFlutterSdkPlugin.swift index bff8326..e2f6c96 100644 --- a/ios/Classes/NgeniusFlutterSdkPlugin.swift +++ b/ios/Classes/NgeniusFlutterSdkPlugin.swift @@ -5,137 +5,201 @@ import NISdk public class NgeniusFlutterSdkPlugin: NSObject, FlutterPlugin, CardPaymentDelegate { + private var methodChannel: FlutterMethodChannel! + private var resultCallback: FlutterResult? + + // MARK: - Error Enum + enum NGeniusFlutterError: String { + case invalidArguments = "INVALID_ARGUMENTS" + case noRootViewController = "NO_ROOT_VIEW_CONTROLLER" + case orderParsingFailed = "ORDER_RESPONSE_ERROR" + case applePayFailed = "APPLE_PAY_ERROR" + case authorizationFailed = "AUTHORIZATION_FAILED" + case paymentFailed = "PAYMENT_FAILED" + case unknown = "UNKNOWN_ERROR" + + func flutterError(message: String) -> FlutterError { + return FlutterError(code: self.rawValue, message: message, details: nil) + } + } + + // MARK: - Register + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "ngenius_flutter_sdk", binaryMessenger: registrar.messenger()) + let instance = NgeniusFlutterSdkPlugin() + instance.methodChannel = channel + NISdk.initialize() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + // MARK: - Handle Calls + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "launchCardPayment": + guard let args = call.arguments as? [String: Any] else { + result(NGeniusFlutterError.invalidArguments.flutterError(message: "Arguments are missing or invalid")) + return + } + launchCardPayment(args: args, result: result) + + case "launchSavedCardPayment": + guard let args = call.arguments as? [String: Any] else { + result(NGeniusFlutterError.invalidArguments.flutterError(message: "Arguments are missing or invalid")) + return + } + launchSavedCardPayment(args: args, result: result) + + default: + result(FlutterMethodNotImplemented) + } + } + + // MARK: - CARD PAYMENT + private func launchCardPayment(args: [String: Any], result: @escaping FlutterResult) { + self.resultCallback = result + + guard let orderResponseMap = args["orderJsonObject"] as? [String: Any], !orderResponseMap.isEmpty else { + result(NGeniusFlutterError.invalidArguments.flutterError(message: "Missing or invalid orderResponse")) + return + } + + guard let viewController = UIApplication.shared.keyWindow?.rootViewController else { + result(NGeniusFlutterError.noRootViewController.flutterError(message: "Root view controller is not available")) + return + } + + do { + let jsonData = try JSONSerialization.data(withJSONObject: orderResponseMap, options: []) + let orderResponse = try JSONDecoder().decode(OrderResponse.self, from: jsonData) + + NISdk.sharedInstance.showCardPaymentViewWith( + cardPaymentDelegate: self, + overParent: viewController, + for: orderResponse + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.removeButtonText(from: viewController.view) + } + + } catch let error { + result(NGeniusFlutterError.orderParsingFailed.flutterError( + message: "Failed to parse orderResponse: \(error.localizedDescription)")) + } + } + + // MARK: - SAVED CARD PAYMENT + private func launchSavedCardPayment(args: [String: Any], result: @escaping FlutterResult) { + self.resultCallback = result + + guard let orderResponseMap = args["orderJsonObject"] as? [String: Any], !orderResponseMap.isEmpty else { + result(NGeniusFlutterError.invalidArguments.flutterError(message: "Missing orderResponse")) + return + } + + guard let viewController = UIApplication.shared.keyWindow?.rootViewController else { + result(NGeniusFlutterError.noRootViewController.flutterError(message: "Root view controller is not available")) + return + } + + do { + let jsonData = try JSONSerialization.data(withJSONObject: orderResponseMap, options: []) + let orderResponse = try JSONDecoder().decode(OrderResponse.self, from: jsonData) + let sharedSDKInstance = NISdk.sharedInstance + + if let cvv = args["cvv"] as? String, !cvv.isEmpty { + sharedSDKInstance.launchSavedCardPayment( + cardPaymentDelegate: self, + overParent: viewController, + for: orderResponse, + with: cvv + ) + } else { + sharedSDKInstance.launchSavedCardPayment( + cardPaymentDelegate: self, + overParent: viewController, + for: orderResponse + ) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.removeButtonText(from: viewController.view) + } + + } catch let error { + result(NGeniusFlutterError.orderParsingFailed.flutterError(message: "Failed to parse orderResponse: \(error.localizedDescription)")) + } + } + + // MARK: - UI FIX + private func removeButtonText(from view: UIView) { + let allButtons = view.subviews.compactMap { $0 as? UIButton } + + for button in allButtons { + if let title = button.title(for: .normal), title.contains("0") { + button.setTitle("Pay", for: .normal) + button.setImage(nil, for: .normal) + } + } + } + + // MARK: - DELEGATES + public func paymentDidComplete(with status: PaymentStatus) { + var resultDict: [String: Any] + + switch status { + case .PaymentSuccess: + resultDict = [ + "code": 200, + "status": "success", + "reason": "PAYMENT_SUCCESSFUL", + "result": "success" + ] + case .PaymentFailed: + resultDict = [ + "code": 0, + "status": "failed", + "reason": "STATUS_PAYMENT_FAILED", + "result": "failed" + ] + case .PaymentCancelled: + resultDict = [ + "code": 401, + "status": "canceled", + "reason": "CANCELLED_BY_USER", + "result": "canceled" + ] + @unknown default: + resultDict = [ + "code": -1, + "status": "unknown", + "reason": "STATUS_GENERIC_ERROR", + "result": "unknown" + ] + } + + sendResult(resultDict) + } + + public func authorizationDidComplete(with status: AuthorizationStatus) { + if status == .AuthFailed { + let resultDict: [String: Any] = [ + "code": -1, + "status": "failed", + "reason": "STATUS_GENERIC_ERROR", + "result": "auth_failed" + ] + sendResult(resultDict) + } + } -private var methodChannel: FlutterMethodChannel! -private var resultCallback: FlutterResult? - - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "ngenius_flutter_sdk", binaryMessenger: registrar.messenger()) - let instance = NgeniusFlutterSdkPlugin() - instance.methodChannel = channel - NISdk.initialize() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "launchCardPayment": - guard let args = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Arguments are missing or invalid", details: nil)) - return - } - launchCardPayment(args: args, result: result) - default: - result(FlutterMethodNotImplemented) + // MARK: - HELPER + private func sendResult(_ dict: [String: Any]) { + if let jsonData = try? JSONSerialization.data(withJSONObject: dict), + let jsonString = String(data: jsonData, encoding: .utf8), + let resultCallback = self.resultCallback { + resultCallback(jsonString) + self.resultCallback = nil + } } - } - private func launchCardPayment(args: [String: Any], result: @escaping FlutterResult) { - - self.resultCallback = result - - guard let orderResponseMap = args["orderJsonObject"] as? [String: Any], !orderResponseMap.isEmpty else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing or invalid orderResponse", details: nil)) - return - } - - do { - let jsonData = try JSONSerialization.data(withJSONObject: orderResponseMap, options: []) - let orderResponse = try JSONDecoder().decode(OrderResponse.self, from: jsonData) - - guard let viewController = UIApplication.shared.keyWindow?.rootViewController else { - result(FlutterError(code: "NO_ROOT_VIEW_CONTROLLER", - message: "Root view controller is not available", - details: nil)) - return - } - - let sharedSDKInstance = NISdk.sharedInstance - sharedSDKInstance.showCardPaymentViewWith( - cardPaymentDelegate: self, - overParent: viewController, - for: orderResponse - ) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.removeButtonText(from: viewController.view) - } - - } catch let error { - result(FlutterError(code: "ORDER_RESPONSE_ERROR", - message: "Failed to parse orderResponse: \(error.localizedDescription)", - details: nil)) - } - } - private func removeButtonText(from view: UIView) { - let allButtons = view.subviews.compactMap { $0 as? UIButton } - - for button in allButtons { - if let title = button.title(for: .normal), title.contains("0") { - button.setTitle("Pay", for: .normal) - button.setImage(nil, for: .normal) - } - } - } - - public func paymentDidComplete(with status: PaymentStatus) { - var resultDict: [String: Any] - - switch status { - case .PaymentSuccess: - resultDict = [ - "code": 200, - "status": "success", - "reason": "PAYMENT_SUCCESSFUL", - "result": "success" - ] - case .PaymentFailed: - resultDict = [ - "code": 0, - "status": "failed", - "reason": "STATUS_PAYMENT_FAILED", - "result": "failed" - ] - case .PaymentCancelled: - resultDict = [ - "code": 401, - "status": "canceled", - "reason": "CANCELLED_BY_USER", - "result": "canceled" - ] - @unknown default: - resultDict = [ - "code": -1, - "status": "unknown", - "reason": "STATUS_GENERIC_ERROR", - "result": "unknown" - ] - } - - if let jsonData = try? JSONSerialization.data(withJSONObject: resultDict), - let jsonString = String(data: jsonData, encoding: .utf8) { - if let resultCallback = self.resultCallback { - resultCallback(jsonString) - self.resultCallback = nil - } - } - } - public func authorizationDidComplete(with status: AuthorizationStatus) { - if status == .AuthFailed { - let resultDict: [String: Any] = [ - "code": -1, - "status": "failed", - "reason": "STATUS_GENERIC_ERROR", - "result": "auth_failed" - ] - - if let jsonData = try? JSONSerialization.data(withJSONObject: resultDict), - let jsonString = String(data: jsonData, encoding: .utf8) { - if let resultCallback = self.resultCallback { - resultCallback(jsonString) - self.resultCallback = nil - } - } - } - } } \ No newline at end of file diff --git a/lib/ngenius_flutter_sdk.dart b/lib/ngenius_flutter_sdk.dart index 0c5b434..095e11d 100644 --- a/lib/ngenius_flutter_sdk.dart +++ b/lib/ngenius_flutter_sdk.dart @@ -8,4 +8,12 @@ class NgeniusFlutterSdk { return NgeniusFlutterSdkPlatform.instance .launchCardPayment(orderJsonObject: orderJsonObject); } + + Future launchSavedCardPayment({ + required Map orderJsonObject, + String? cvv, + }) { + return NgeniusFlutterSdkPlatform.instance + .launchSavedCardPayment(orderJsonObject: orderJsonObject, cvv: cvv); + } } diff --git a/lib/ngenius_flutter_sdk_method_channel.dart b/lib/ngenius_flutter_sdk_method_channel.dart index 2896a2b..4b234e2 100644 --- a/lib/ngenius_flutter_sdk_method_channel.dart +++ b/lib/ngenius_flutter_sdk_method_channel.dart @@ -20,4 +20,26 @@ class MethodChannelNgeniusFlutterSdk extends NgeniusFlutterSdkPlatform { return NGeniusResponseModel(message: err.toString()); } } + + @override + Future launchSavedCardPayment({ + required Map orderJsonObject, + String? cvv, + }) async { + try { + final Map args = { + "orderJsonObject": orderJsonObject, + }; + if (cvv != null) { + args["cvv"] = cvv; + } + dynamic response = await methodChannel.invokeMethod( + "launchSavedCardPayment", + args, + ); + return NGeniusResponseModel.fromJson(json: jsonDecode(response)); + } catch (err) { + return NGeniusResponseModel(message: err.toString()); + } + } } diff --git a/lib/ngenius_flutter_sdk_platform_interface.dart b/lib/ngenius_flutter_sdk_platform_interface.dart index 0d2806c..2f682ed 100644 --- a/lib/ngenius_flutter_sdk_platform_interface.dart +++ b/lib/ngenius_flutter_sdk_platform_interface.dart @@ -26,4 +26,11 @@ abstract class NgeniusFlutterSdkPlatform extends PlatformInterface { Future launchCardPayment({required Map orderJsonObject}) async { throw UnimplementedError("launchCardPayment() has not been implemented."); } + + Future launchSavedCardPayment({ + required Map orderJsonObject, + String? cvv, + }) async { + throw UnimplementedError("launchSavedCardPayment() has not been implemented."); + } } diff --git a/lib/ngenius_saved_card_model.dart b/lib/ngenius_saved_card_model.dart new file mode 100755 index 0000000..3382737 --- /dev/null +++ b/lib/ngenius_saved_card_model.dart @@ -0,0 +1,39 @@ +class NGeniusSavedCardModel { + final String maskedPan; + final String expiry; + final String cardholderName; + final String scheme; + final String cardToken; + final bool? recaptureCsc; + + NGeniusSavedCardModel({ + required this.maskedPan, + required this.expiry, + required this.cardholderName, + required this.scheme, + required this.cardToken, + this.recaptureCsc, + }); + + factory NGeniusSavedCardModel.fromJson(Map json) { + return NGeniusSavedCardModel( + maskedPan: json['maskedPan'] as String, + expiry: json['expiry'] as String, + cardholderName: json['cardholderName'] as String, + scheme: json['scheme'] as String, + cardToken: json['cardToken'] as String, + recaptureCsc: json['recaptureCsc'] != null ? json['recaptureCsc'] as bool : null, + ); + } + + Map toJson() { + return { + 'maskedPan': maskedPan, + 'expiry': expiry, + 'cardholderName': cardholderName, + 'scheme': scheme, + 'cardToken': cardToken, + 'recaptureCsc': recaptureCsc, + }; + } +} \ No newline at end of file