diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f33284a..3ee5d9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.2.0 * Improved dynamic viewport of card component. +* Added support of iDEAL to the Instant Component. * Added the missing loading bottom sheet for the advanced flow google pay component. * Updated iOS SDK to v5.14.0. * Updated Android SDK to v5.8.0. Gradle v8 is now mandatory. diff --git a/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/InstantComponentManager.kt b/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/InstantComponentManager.kt index 48955c36..4318d2c2 100644 --- a/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/InstantComponentManager.kt +++ b/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/InstantComponentManager.kt @@ -9,13 +9,19 @@ import com.adyen.checkout.action.core.internal.ActionHandlingComponent import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.flutter.components.instant.advanced.IdealComponentAdvancedCallback import com.adyen.checkout.flutter.components.instant.advanced.InstantComponentAdvancedCallback +import com.adyen.checkout.flutter.components.instant.session.IdealComponentSessionCallback import com.adyen.checkout.flutter.components.instant.session.InstantComponentSessionCallback import com.adyen.checkout.flutter.components.view.ComponentLoadingBottomSheet import com.adyen.checkout.flutter.session.SessionHolder import com.adyen.checkout.flutter.utils.ConfigurationMapper.mapToCheckoutConfiguration import com.adyen.checkout.flutter.utils.Constants +import com.adyen.checkout.flutter.utils.Constants.Companion.UNKNOWN_PAYMENT_METHOD_TYPE_ERROR_MESSAGE +import com.adyen.checkout.ideal.IdealComponent import com.adyen.checkout.instant.InstantPaymentComponent import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionSetupResponse @@ -29,6 +35,7 @@ class InstantComponentManager( private val assignCurrentComponent: (ActionHandlingComponent?) -> Unit, ) { private var instantPaymentComponent: InstantPaymentComponent? = null + private var idealPaymentComponent: IdealComponent? = null private var componentId: String? = null fun start( @@ -37,31 +44,17 @@ class InstantComponentManager( componentId: String ) { try { + if (ComponentLoadingBottomSheet.isVisible(activity.supportFragmentManager)) { + hideLoadingBottomSheet() + } + val paymentMethod = PaymentMethod.SERIALIZER.deserialize(JSONObject(encodedPaymentMethod)) val configuration = instantPaymentConfigurationDTO.mapToCheckoutConfiguration() - val instantPaymentComponent = - when (componentId) { - Constants.INSTANT_ADVANCED_COMPONENT_KEY -> - createInstantAdvancedComponent( - configuration, - paymentMethod, - componentId - ) - - Constants.INSTANT_SESSION_COMPONENT_KEY -> - createInstantSessionComponent( - configuration, - paymentMethod, - componentId - ) - - else -> throw IllegalStateException("Instant component not available for payment flow.") - } - - this.instantPaymentComponent = instantPaymentComponent - this.componentId = componentId - assignCurrentComponent(instantPaymentComponent) - ComponentLoadingBottomSheet.show(activity.supportFragmentManager, instantPaymentComponent) + when (paymentMethod.type) { + null, PaymentMethodTypes.UNKNOWN -> throw CheckoutException(UNKNOWN_PAYMENT_METHOD_TYPE_ERROR_MESSAGE) + PaymentMethodTypes.IDEAL -> startIdealPaymentComponent(componentId, configuration, paymentMethod) + else -> startInstantPaymentComponent(componentId, configuration, paymentMethod) + } } catch (exception: Exception) { val model = ComponentCommunicationModel( @@ -77,9 +70,72 @@ class InstantComponentManager( } } + private fun startInstantPaymentComponent( + componentId: String, + configuration: CheckoutConfiguration, + paymentMethod: PaymentMethod + ) { + val instantPaymentComponent = + when (componentId) { + Constants.INSTANT_ADVANCED_COMPONENT_KEY -> + createInstantAdvancedComponent( + configuration, + paymentMethod, + componentId + ) + + Constants.INSTANT_SESSION_COMPONENT_KEY -> + createInstantSessionComponent( + configuration, + paymentMethod, + componentId + ) + + else -> throw IllegalStateException("Instant component not available for payment flow.") + } + + this.instantPaymentComponent = instantPaymentComponent + this.componentId = componentId + this.idealPaymentComponent = null + assignCurrentComponent(instantPaymentComponent) + ComponentLoadingBottomSheet.show(activity.supportFragmentManager, instantPaymentComponent) + } + + private fun startIdealPaymentComponent( + componentId: String, + configuration: CheckoutConfiguration, + paymentMethod: PaymentMethod + ) { + val idealPaymentComponent = + when (componentId) { + Constants.INSTANT_ADVANCED_COMPONENT_KEY -> + createIdealAdvancedComponent( + configuration, + paymentMethod, + componentId + ) + + Constants.INSTANT_SESSION_COMPONENT_KEY -> + createIdealSessionComponent( + configuration, + paymentMethod, + componentId + ) + + else -> throw IllegalStateException("Ideal component not available for payment flow.") + } + + this.idealPaymentComponent = idealPaymentComponent + this.componentId = componentId + this.instantPaymentComponent = null + assignCurrentComponent(idealPaymentComponent) + ComponentLoadingBottomSheet.show(activity.supportFragmentManager, idealPaymentComponent) + } + fun onDispose(componentId: String) { if (componentId == this.componentId) { instantPaymentComponent = null + idealPaymentComponent = null } } @@ -132,7 +188,58 @@ class InstantComponentManager( ) } - private fun handleAction(action: Action) = instantPaymentComponent?.handleAction(action, activity) + // We delete the ideal component integration when the instant component supports ideal. + private fun createIdealAdvancedComponent( + configuration: CheckoutConfiguration, + paymentMethod: PaymentMethod, + componentId: String, + ): IdealComponent { + return IdealComponent.PROVIDER.get( + activity = activity, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration, + callback = + IdealComponentAdvancedCallback( + componentFlutterInterface, + componentId, + ::hideLoadingBottomSheet + ), + key = UUID.randomUUID().toString() + ) + } + + private fun createIdealSessionComponent( + configuration: CheckoutConfiguration, + paymentMethod: PaymentMethod, + componentId: String, + ): IdealComponent { + val sessionSetupResponse = SessionSetupResponse.SERIALIZER.deserialize(sessionHolder.sessionSetupResponse) + val order = sessionHolder.orderResponse?.let { Order.SERIALIZER.deserialize(it) } + val checkoutSession = + CheckoutSession( + sessionSetupResponse = sessionSetupResponse, + order = order, + environment = configuration.environment, + clientKey = configuration.clientKey + ) + return IdealComponent.PROVIDER.get( + activity = activity, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration, + componentCallback = + IdealComponentSessionCallback( + componentFlutterInterface, + componentId, + ::handleAction, + ::hideLoadingBottomSheet + ), + key = UUID.randomUUID().toString() + ) + } + + private fun handleAction(action: Action) = + instantPaymentComponent?.handleAction(action, activity) ?: idealPaymentComponent?.handleAction(action, activity) private fun hideLoadingBottomSheet() = ComponentLoadingBottomSheet.hide(activity.supportFragmentManager) } diff --git a/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/advanced/IdealComponentAdvancedCallback.kt b/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/advanced/IdealComponentAdvancedCallback.kt new file mode 100644 index 00000000..fa7cb9c2 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/advanced/IdealComponentAdvancedCallback.kt @@ -0,0 +1,37 @@ +package com.adyen.checkout.flutter.components.instant.advanced + +import ComponentCommunicationModel +import ComponentFlutterInterface +import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.PaymentComponentData +import com.adyen.checkout.flutter.components.base.ComponentAdvancedCallback +import com.adyen.checkout.flutter.utils.Constants +import com.adyen.checkout.ideal.IdealComponentState +import org.json.JSONObject + +class IdealComponentAdvancedCallback( + private val componentFlutterApi: ComponentFlutterInterface, + private val componentId: String, + private val hideLoadingBottomSheet: () -> Unit, +) : ComponentAdvancedCallback(componentFlutterApi, componentId) { + override fun onSubmit(state: IdealComponentState) { + val data = PaymentComponentData.SERIALIZER.serialize(state.data) + val submitData = + JSONObject().apply { + put(Constants.ADVANCED_PAYMENT_DATA_KEY, data) + put(Constants.ADVANCED_EXTRA_DATA_KEY, null) + } + val model = + ComponentCommunicationModel( + ComponentCommunicationType.ONSUBMIT, + componentId = componentId, + data = submitData.toString(), + ) + componentFlutterApi.onComponentCommunication(model) {} + } + + override fun onError(componentError: ComponentError) { + hideLoadingBottomSheet() + super.onError(componentError) + } +} diff --git a/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/session/IdealComponentSessionCallback.kt b/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/session/IdealComponentSessionCallback.kt new file mode 100644 index 00000000..d3b0c013 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/checkout/flutter/components/instant/session/IdealComponentSessionCallback.kt @@ -0,0 +1,25 @@ +package com.adyen.checkout.flutter.components.instant.session + +import ComponentFlutterInterface +import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.flutter.components.base.ComponentSessionCallback +import com.adyen.checkout.ideal.IdealComponentState +import com.adyen.checkout.sessions.core.SessionPaymentResult + +class IdealComponentSessionCallback( + private val componentFlutterApi: ComponentFlutterInterface, + private val componentId: String, + private val onActionCallback: (Action) -> Unit, + private val hideLoadingBottomSheet: () -> Unit, +) : ComponentSessionCallback(componentFlutterApi, componentId, onActionCallback) { + override fun onError(componentError: ComponentError) { + hideLoadingBottomSheet() + super.onError(componentError) + } + + override fun onFinished(result: SessionPaymentResult) { + hideLoadingBottomSheet() + super.onFinished(result) + } +} diff --git a/android/src/main/kotlin/com/adyen/checkout/flutter/components/view/ComponentLoadingBottomSheet.kt b/android/src/main/kotlin/com/adyen/checkout/flutter/components/view/ComponentLoadingBottomSheet.kt index eb21c9ba..5f86b11c 100644 --- a/android/src/main/kotlin/com/adyen/checkout/flutter/components/view/ComponentLoadingBottomSheet.kt +++ b/android/src/main/kotlin/com/adyen/checkout/flutter/components/view/ComponentLoadingBottomSheet.kt @@ -55,5 +55,7 @@ internal class ComponentLoadingBottomSheet : BottomSheetDialogFragment() wher (it as? BottomSheetDialogFragment)?.dismiss() } } + + fun isVisible(fragmentManager: FragmentManager): Boolean = fragmentManager.findFragmentByTag(TAG) != null } } diff --git a/android/src/main/kotlin/com/adyen/checkout/flutter/utils/Constants.kt b/android/src/main/kotlin/com/adyen/checkout/flutter/utils/Constants.kt index 7f201ab8..07ef4ceb 100644 --- a/android/src/main/kotlin/com/adyen/checkout/flutter/utils/Constants.kt +++ b/android/src/main/kotlin/com/adyen/checkout/flutter/utils/Constants.kt @@ -4,6 +4,7 @@ class Constants { companion object { const val WRONG_FLUTTER_ACTIVITY_USAGE_ERROR_MESSAGE = "FlutterFragmentActivity not used. Your activity needs to inherit from FlutterFragmentActivity." + const val UNKNOWN_PAYMENT_METHOD_TYPE_ERROR_MESSAGE = "Unknown payment method type." const val GOOGLE_PAY_SESSION_COMPONENT_KEY = "GOOGLE_PAY_SESSION_COMPONENT" const val GOOGLE_PAY_ADVANCED_COMPONENT_KEY = "GOOGLE_PAY_ADVANCED_COMPONENT" diff --git a/example/lib/screens/component/instant/instant_advanced_component_screen.dart b/example/lib/screens/component/instant/instant_advanced_component_screen.dart index 10008591..d7d4dd09 100644 --- a/example/lib/screens/component/instant/instant_advanced_component_screen.dart +++ b/example/lib/screens/component/instant/instant_advanced_component_screen.dart @@ -53,6 +53,8 @@ class InstantAdvancedComponentScreen extends StatelessWidget { _extractPaymentMethod(snapshot.data!, "paypal"); final klarnaPaymentMethodResponse = _extractPaymentMethod(snapshot.data!, "klarna"); + final idealPaymentMethodResponse = + _extractPaymentMethod(snapshot.data!, "ideal"); return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -93,7 +95,23 @@ class InstantAdvancedComponentScreen extends StatelessWidget { } }); }, - child: const Text("Klarna")) + child: const Text("Klarna")), + TextButton( + onPressed: () { + AdyenCheckout.advanced + .startInstantComponent( + configuration: instantComponentConfiguration, + paymentMethod: idealPaymentMethodResponse, + checkout: advancedCheckout, + ) + .then((paymentResult) { + if (context.mounted) { + DialogBuilder.showPaymentResultDialog( + paymentResult, context); + } + }); + }, + child: const Text("iDEAL")) ], ); } else { @@ -107,7 +125,7 @@ class InstantAdvancedComponentScreen extends StatelessWidget { Map paymentMethods, String key) { return paymentMethods["paymentMethods"].firstWhere( (paymentMethod) => paymentMethod["type"] == key, - orElse: () => throw Exception("$key payment method not provided"), + orElse: () => {}, ); } } diff --git a/example/lib/screens/component/instant/instant_session_component_screen.dart b/example/lib/screens/component/instant/instant_session_component_screen.dart index 86bf59ab..158d76c4 100644 --- a/example/lib/screens/component/instant/instant_session_component_screen.dart +++ b/example/lib/screens/component/instant/instant_session_component_screen.dart @@ -48,6 +48,8 @@ class InstantSessionComponentScreen extends StatelessWidget { _extractPaymentMethod(sessionCheckout.paymentMethods, "paypal"); final klarnaPaymentMethodResponse = _extractPaymentMethod(sessionCheckout.paymentMethods, "klarna"); + final idealPaymentMethodResponse = + _extractPaymentMethod(sessionCheckout.paymentMethods, "ideal"); return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -88,7 +90,23 @@ class InstantSessionComponentScreen extends StatelessWidget { } }); }, - child: const Text("Klarna")) + child: const Text("Klarna")), + TextButton( + onPressed: () { + AdyenCheckout.session + .startInstantComponent( + configuration: instantComponentConfiguration, + paymentMethod: idealPaymentMethodResponse, + checkout: sessionCheckout, + ) + .then((paymentResult) { + if (context.mounted) { + DialogBuilder.showPaymentResultDialog( + paymentResult, context); + } + }); + }, + child: const Text("iDEAL")) ], ); } else { @@ -102,7 +120,7 @@ class InstantSessionComponentScreen extends StatelessWidget { Map paymentMethods, String key) { return paymentMethods["paymentMethods"].firstWhere( (paymentMethod) => paymentMethod["type"] == key, - orElse: () => throw Exception("$key payment method not provided"), + orElse: () => {}, ); } }