diff --git a/android/src/main/kotlin/com/example/airwallex_payment_flutter/AirwallexPaymentSdkModule.kt b/android/src/main/kotlin/com/example/airwallex_payment_flutter/AirwallexPaymentSdkModule.kt index e76fd4c..d4ae207 100644 --- a/android/src/main/kotlin/com/example/airwallex_payment_flutter/AirwallexPaymentSdkModule.kt +++ b/android/src/main/kotlin/com/example/airwallex_payment_flutter/AirwallexPaymentSdkModule.kt @@ -25,6 +25,7 @@ import com.example.airwallex_payment_flutter.util.AirwallexPaymentSessionParser import com.example.airwallex_payment_flutter.util.AirwallexRecurringSessionParser import com.example.airwallex_payment_flutter.util.AirwallexRecurringWithIntentSessionParser import com.example.airwallex_payment_flutter.util.AirwallexSupportedBrandsParser +import com.example.airwallex_payment_flutter.util.PaymentSheetConfigurationParser import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import org.json.JSONObject @@ -67,9 +68,12 @@ class AirwallexPaymentSdkModule { result: MethodChannel.Result ) { val session = parseSessionFromCall(call) + val configJson = call.arguments()?.optJSONObject("configuration") + val configuration = PaymentSheetConfigurationParser.parse(configJson) AirwallexStarter.presentEntirePaymentFlow( activity = activity, session = session, + configuration = configuration, paymentResultListener = object : Airwallex.PaymentResultListener { override fun onCompleted(status: AirwallexPaymentStatus) { AirwallexLogger.info("AirwallexPaymentSdkModule: presentEntirePaymentFlow, status = $status") diff --git a/android/src/main/kotlin/com/example/airwallex_payment_flutter/util/PaymentSheetConfigurationParser.kt b/android/src/main/kotlin/com/example/airwallex_payment_flutter/util/PaymentSheetConfigurationParser.kt new file mode 100644 index 0000000..1765437 --- /dev/null +++ b/android/src/main/kotlin/com/example/airwallex_payment_flutter/util/PaymentSheetConfigurationParser.kt @@ -0,0 +1,15 @@ +package com.example.airwallex_payment_flutter.util + +import com.airwallex.android.core.PaymentMethodsLayoutType +import com.airwallex.android.view.composables.PaymentElementConfiguration +import org.json.JSONObject + +object PaymentSheetConfigurationParser { + fun parse(json: JSONObject?): PaymentElementConfiguration.PaymentSheet { + val layout = when (json?.optString("layout")?.lowercase()) { + "accordion" -> PaymentMethodsLayoutType.ACCORDION + else -> PaymentMethodsLayoutType.TAB + } + return PaymentElementConfiguration.PaymentSheet(layout = layout) + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 54efbc8..1b1d0a1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:airwallex_payment_flutter/airwallex.dart'; import 'package:airwallex_payment_flutter/types/environment.dart'; import 'package:airwallex_payment_flutter/types/payment_result.dart'; import 'package:airwallex_payment_flutter/types/payment_session.dart'; +import 'package:airwallex_payment_flutter/types/payment_sheet_configuration.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:restart_app/restart_app.dart'; @@ -267,7 +268,17 @@ class MyHomePageState extends State { onPressed: () => _handleSubmit(() async => airwallex.presentEntirePaymentFlow( await _createSession(customerId: customerId))), - child: const Text('presentEntirePaymentFlow'), + child: const Text('presentEntirePaymentFlow (tab)'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () => _handleSubmit(() async => + airwallex.presentEntirePaymentFlow( + await _createSession(customerId: customerId), + configuration: const PaymentSheetConfiguration( + layout: PaymentLayout.accordion), + )), + child: const Text('presentEntirePaymentFlow (accordion)'), ), const SizedBox(height: 20), ElevatedButton( diff --git a/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexPaymentFlutterPlugin.swift b/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexPaymentFlutterPlugin.swift index efd01a8..4ba2289 100644 --- a/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexPaymentFlutterPlugin.swift +++ b/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexPaymentFlutterPlugin.swift @@ -24,7 +24,8 @@ public class AirwallexPaymentFlutterPlugin: NSObject, FlutterPlugin { case "presentEntirePaymentFlow": let arguments = call.arguments as! NSDictionary let session = arguments["session"] as! NSDictionary - sdk?.presentEntirePaymentFlow(clientSecret: session["clientSecret"] as! String, session: session, result: result) + let configuration = arguments["configuration"] as? NSDictionary + sdk?.presentEntirePaymentFlow(clientSecret: session["clientSecret"] as! String, session: session, configuration: configuration, result: result) case "presentCardPaymentFlow": let arguments = call.arguments as! NSDictionary let session = arguments["session"] as! NSDictionary diff --git a/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexSdk.swift b/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexSdk.swift index 46a2422..b707b1c 100644 --- a/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexSdk.swift +++ b/ios/airwallex_payment_flutter/Sources/airwallex_payment_flutter/AirwallexSdk.swift @@ -13,19 +13,20 @@ class AirwallexSdk: NSObject { AnalyticsLogger.shared().bindExtraCommonData(["framework": "flutter", "frameworkVersion": "0.1.14"]) } - func presentEntirePaymentFlow(clientSecret: String, session: NSDictionary, result: @escaping FlutterResult) { + func presentEntirePaymentFlow(clientSecret: String, session: NSDictionary, configuration: NSDictionary?, result: @escaping FlutterResult) { self.result = result - + AWXAPIClientConfiguration.shared().clientSecret = clientSecret - + let session = buildAirwallexSession(from: session) - + DispatchQueue.main.async { + let config = AWXUIContext.Configuration(from: configuration) AWXUIContext.launchPayment( from: self.getViewController(), session: session, paymentResultDelegate: self, - launchStyle: .present + configuration: config ) } } @@ -157,6 +158,18 @@ extension AirwallexSdk: AWXPaymentResultDelegate { } } +private extension AWXUIContext.Configuration { + convenience init(from params: NSDictionary?) { + self.init() + self.launchStyle = .present + if let layout = params?["layout"] as? String, layout.lowercased() == "accordion" { + self.layout = .accordion + } else { + self.layout = .tab + } + } +} + private extension AirwallexSDKMode { static func from(_ stringValue: String) -> Self { switch stringValue { diff --git a/lib/airwallex.dart b/lib/airwallex.dart index 9daf4ad..a439d2f 100644 --- a/lib/airwallex.dart +++ b/lib/airwallex.dart @@ -8,6 +8,7 @@ import '/types/card.dart'; import '/types/card_brand.dart'; import '/types/payment_result.dart'; import '/types/payment_session.dart'; +import '/types/payment_sheet_configuration.dart'; import 'airwallex_payment_flutter_platform_interface.dart'; @@ -20,9 +21,12 @@ class Airwallex { AirwallexPaymentFlutterPlatform.instance.initialize(environment, enableLogging, saveLogToLocal); } - Future presentEntirePaymentFlow(BaseSession session) { + Future presentEntirePaymentFlow( + BaseSession session, { + PaymentSheetConfiguration? configuration, + }) { return AirwallexPaymentFlutterPlatform.instance - .presentEntirePaymentFlow(session); + .presentEntirePaymentFlow(session, configuration: configuration); } Future presentCardPaymentFlow( diff --git a/lib/airwallex_payment_flutter_method_channel.dart b/lib/airwallex_payment_flutter_method_channel.dart index cc7deb3..04c602a 100644 --- a/lib/airwallex_payment_flutter_method_channel.dart +++ b/lib/airwallex_payment_flutter_method_channel.dart @@ -10,6 +10,7 @@ import 'types/card.dart'; import 'types/card_brand.dart'; import 'types/payment_result.dart'; import 'types/payment_session.dart'; +import 'types/payment_sheet_configuration.dart'; /// An implementation of [AirwallexPaymentFlutterPlatform] that uses method channels. class MethodChannelAirwallexPaymentFlutter @@ -30,9 +31,15 @@ class MethodChannelAirwallexPaymentFlutter } @override - Future presentEntirePaymentFlow(BaseSession session) async { + Future presentEntirePaymentFlow( + BaseSession session, { + PaymentSheetConfiguration? configuration, + }) async { final result = await methodChannel.invokeMethod( - 'presentEntirePaymentFlow', {'session': session.toJson()}); + 'presentEntirePaymentFlow', { + 'session': session.toJson(), + if (configuration != null) 'configuration': configuration.toJson(), + }); return parsePaymentResult(result); } diff --git a/lib/airwallex_payment_flutter_platform_interface.dart b/lib/airwallex_payment_flutter_platform_interface.dart index e999fba..60303cc 100644 --- a/lib/airwallex_payment_flutter_platform_interface.dart +++ b/lib/airwallex_payment_flutter_platform_interface.dart @@ -6,6 +6,7 @@ import '/types/card.dart'; import '/types/card_brand.dart'; import '/types/payment_result.dart'; import '/types/payment_session.dart'; +import '/types/payment_sheet_configuration.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'airwallex_payment_flutter_method_channel.dart'; @@ -36,7 +37,10 @@ abstract class AirwallexPaymentFlutterPlatform extends PlatformInterface { throw UnimplementedError('initialize() has not been implemented.'); } - Future presentEntirePaymentFlow(BaseSession session) { + Future presentEntirePaymentFlow( + BaseSession session, { + PaymentSheetConfiguration? configuration, + }) { throw UnimplementedError('presentEntirePaymentFlow() has not been implemented.'); } diff --git a/lib/types/payment_sheet_configuration.dart b/lib/types/payment_sheet_configuration.dart new file mode 100644 index 0000000..d4dd3db --- /dev/null +++ b/lib/types/payment_sheet_configuration.dart @@ -0,0 +1,9 @@ +enum PaymentLayout { tab, accordion } + +class PaymentSheetConfiguration { + final PaymentLayout layout; + + const PaymentSheetConfiguration({this.layout = PaymentLayout.tab}); + + Map toJson() => {'layout': layout.name}; +} diff --git a/test/airwallex_payment_flutter_test.dart b/test/airwallex_payment_flutter_test.dart index e44fad2..4e663cd 100644 --- a/test/airwallex_payment_flutter_test.dart +++ b/test/airwallex_payment_flutter_test.dart @@ -6,6 +6,7 @@ import 'package:airwallex_payment_flutter/types/environment.dart'; import 'package:airwallex_payment_flutter/types/payment_consent.dart'; import 'package:airwallex_payment_flutter/types/payment_result.dart'; import 'package:airwallex_payment_flutter/types/payment_session.dart'; +import 'package:airwallex_payment_flutter/types/payment_sheet_configuration.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -28,7 +29,10 @@ class MockAirwallexPaymentFlutterPlatform } @override - Future presentEntirePaymentFlow(BaseSession session) { + Future presentEntirePaymentFlow( + BaseSession session, { + PaymentSheetConfiguration? configuration, + }) { throw UnimplementedError(); } @@ -68,4 +72,75 @@ void main() { expect( initialPlatform, isInstanceOf()); }); + + group('PaymentSheetConfiguration.toJson', () { + test('default layout serializes to tab', () { + const config = PaymentSheetConfiguration(); + expect(config.toJson(), {'layout': 'tab'}); + }); + + test('explicit tab layout serializes to tab', () { + const config = PaymentSheetConfiguration(layout: PaymentLayout.tab); + expect(config.toJson(), {'layout': 'tab'}); + }); + + test('accordion layout serializes to accordion', () { + const config = PaymentSheetConfiguration(layout: PaymentLayout.accordion); + expect(config.toJson(), {'layout': 'accordion'}); + }); + }); + + group('MethodChannelAirwallexPaymentFlutter.presentEntirePaymentFlow', () { + TestWidgetsFlutterBinding.ensureInitialized(); + + final plugin = MethodChannelAirwallexPaymentFlutter(); + final channel = plugin.methodChannel; + Map? capturedArgs; + + OneOffSession buildSession() => OneOffSession( + clientSecret: 'secret', + currency: 'USD', + countryCode: 'US', + amount: 10.0, + paymentIntentId: 'intent_123', + ); + + setUp(() { + capturedArgs = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler(channel.name, (message) async { + final call = const JSONMethodCodec().decodeMethodCall(message); + capturedArgs = call.arguments as Map; + return const JSONMethodCodec() + .encodeSuccessEnvelope({'status': 'success'}); + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler(channel.name, null); + }); + + test('omits configuration key when no configuration is passed', () async { + final result = await plugin.presentEntirePaymentFlow(buildSession()); + + expect(result, isA()); + expect(capturedArgs, isNotNull); + expect(capturedArgs!.containsKey('session'), isTrue); + expect(capturedArgs!.containsKey('configuration'), isFalse); + }); + + test('includes configuration json when accordion configuration is passed', + () async { + final result = await plugin.presentEntirePaymentFlow( + buildSession(), + configuration: const PaymentSheetConfiguration( + layout: PaymentLayout.accordion), + ); + + expect(result, isA()); + expect(capturedArgs, isNotNull); + expect(capturedArgs!['configuration'], {'layout': 'accordion'}); + }); + }); }