diff --git a/android/build.gradle b/android/build.gradle index 18058d70..0abf5b58 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -49,7 +49,7 @@ android { } dependencies { - implementation 'com.adyen.checkout:drop-in:5.0.0' + implementation 'com.adyen.checkout:drop-in:5.0.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.6.0' diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/AdyenCheckoutPlugin.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/AdyenCheckoutPlugin.kt index be778728..1970ca4f 100644 --- a/android/src/main/kotlin/com/adyen/adyen_checkout/AdyenCheckoutPlugin.kt +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/AdyenCheckoutPlugin.kt @@ -2,6 +2,8 @@ package com.adyen.adyen_checkout import CheckoutFlutterApi import CheckoutPlatformInterface +import ComponentFlutterInterface +import ComponentPlatformInterface import PaymentResultDTO import PaymentResultEnum import PaymentResultModelDTO @@ -10,6 +12,9 @@ import PlatformCommunicationType import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import com.adyen.adyen_checkout.components.ComponentPlatformApi +import com.adyen.adyen_checkout.components.card.advancedFlow.CardAdvancedFlowComponentFactory +import com.adyen.adyen_checkout.components.card.session.CardSessionFlowComponentFactory import com.adyen.adyen_checkout.utils.ConfigurationMapper.mapToOrderResponseModel import com.adyen.adyen_checkout.utils.Constants.Companion.WRONG_FLUTTER_ACTIVITY_USAGE_ERROR_MESSAGE import com.adyen.checkout.dropin.DropIn @@ -18,6 +23,7 @@ import com.adyen.checkout.dropin.DropInResult import com.adyen.checkout.dropin.SessionDropInCallback import com.adyen.checkout.dropin.SessionDropInResult import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference @@ -26,18 +32,27 @@ import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference class AdyenCheckoutPlugin : FlutterPlugin, ActivityAware { private var checkoutPlatformApi: CheckoutPlatformApi? = null private var checkoutFlutterApi: CheckoutFlutterApi? = null + private var componentPlatformApi: ComponentPlatformApi? = null + private var componentFlutterApi: ComponentFlutterInterface? = null private var lifecycleReference: HiddenLifecycleReference? = null private var lifecycleObserver: LifecycleEventObserver? = null + private var flutterPluginBinding: FlutterPluginBinding? = null - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + override fun onAttachedToEngine(flutterPluginBinding: FlutterPluginBinding) { + this.flutterPluginBinding = flutterPluginBinding checkoutFlutterApi = CheckoutFlutterApi(flutterPluginBinding.binaryMessenger) checkoutPlatformApi = CheckoutPlatformApi(checkoutFlutterApi) + componentFlutterApi = ComponentFlutterInterface(flutterPluginBinding.binaryMessenger) + componentPlatformApi = ComponentPlatformApi() CheckoutPlatformInterface.setUp(flutterPluginBinding.binaryMessenger, checkoutPlatformApi) + ComponentPlatformInterface.setUp(flutterPluginBinding.binaryMessenger, componentPlatformApi) } - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { CheckoutPlatformInterface.setUp(binding.binaryMessenger, null) + ComponentPlatformInterface.setUp(binding.binaryMessenger, null) checkoutFlutterApi = null + componentFlutterApi = null } override fun onAttachedToActivity(binding: ActivityPluginBinding) = setupActivity(binding) @@ -62,6 +77,16 @@ class AdyenCheckoutPlugin : FlutterPlugin, ActivityAware { lifecycleObserver?.let { lifecycleReference?.lifecycle?.addObserver(it) } + + componentFlutterApi?.let { + flutterPluginBinding?.platformViewRegistry?.registerViewFactory( + "cardComponentAdvancedFlow", CardAdvancedFlowComponentFactory(fragmentActivity, it) + ) + + flutterPluginBinding?.platformViewRegistry?.registerViewFactory( + "cardComponentSessionFlow", CardSessionFlowComponentFactory(fragmentActivity, it) + ) + } } private fun lifecycleEventObserver(fragmentActivity: FragmentActivity): LifecycleEventObserver { @@ -101,6 +126,7 @@ class AdyenCheckoutPlugin : FlutterPlugin, ActivityAware { PaymentResultModelDTO( sessionId, sessionData, + sessionResult, resultCode, order?.mapToOrderResponseModel() ) diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/PlatformApi.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/PlatformApi.kt index c044ea4c..5eded797 100644 --- a/android/src/main/kotlin/com/adyen/adyen_checkout/PlatformApi.kt +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/PlatformApi.kt @@ -139,6 +139,20 @@ enum class PlatformCommunicationType(val raw: Int) { } } +enum class ComponentCommunicationType(val raw: Int) { + ONSUBMIT(0), + ADDITIONALDETAILS(1), + RESULT(2), + ERROR(3), + RESIZE(4); + + companion object { + fun ofRaw(raw: Int): ComponentCommunicationType? { + return values().firstOrNull { it.raw == raw } + } + } +} + enum class PaymentFlowResultType(val raw: Int) { FINISHED(0), ACTION(1), @@ -235,7 +249,7 @@ data class DropInConfigurationDTO ( val countryCode: String, val amount: AmountDTO, val shopperLocale: String, - val cardsConfigurationDTO: CardsConfigurationDTO? = null, + val cardConfigurationDTO: CardConfigurationDTO? = null, val applePayConfigurationDTO: ApplePayConfigurationDTO? = null, val googlePayConfigurationDTO: GooglePayConfigurationDTO? = null, val cashAppPayConfigurationDTO: CashAppPayConfigurationDTO? = null, @@ -253,8 +267,8 @@ data class DropInConfigurationDTO ( val countryCode = list[2] as String val amount = AmountDTO.fromList(list[3] as List) val shopperLocale = list[4] as String - val cardsConfigurationDTO: CardsConfigurationDTO? = (list[5] as List?)?.let { - CardsConfigurationDTO.fromList(it) + val cardConfigurationDTO: CardConfigurationDTO? = (list[5] as List?)?.let { + CardConfigurationDTO.fromList(it) } val applePayConfigurationDTO: ApplePayConfigurationDTO? = (list[6] as List?)?.let { ApplePayConfigurationDTO.fromList(it) @@ -271,7 +285,7 @@ data class DropInConfigurationDTO ( val showPreselectedStoredPaymentMethod = list[10] as Boolean val skipListWhenSinglePaymentMethod = list[11] as Boolean val isRemoveStoredPaymentMethodEnabled = list[12] as Boolean - return DropInConfigurationDTO(environment, clientKey, countryCode, amount, shopperLocale, cardsConfigurationDTO, applePayConfigurationDTO, googlePayConfigurationDTO, cashAppPayConfigurationDTO, analyticsOptionsDTO, showPreselectedStoredPaymentMethod, skipListWhenSinglePaymentMethod, isRemoveStoredPaymentMethodEnabled) + return DropInConfigurationDTO(environment, clientKey, countryCode, amount, shopperLocale, cardConfigurationDTO, applePayConfigurationDTO, googlePayConfigurationDTO, cashAppPayConfigurationDTO, analyticsOptionsDTO, showPreselectedStoredPaymentMethod, skipListWhenSinglePaymentMethod, isRemoveStoredPaymentMethodEnabled) } } fun toList(): List { @@ -281,7 +295,7 @@ data class DropInConfigurationDTO ( countryCode, amount.toList(), shopperLocale, - cardsConfigurationDTO?.toList(), + cardConfigurationDTO?.toList(), applePayConfigurationDTO?.toList(), googlePayConfigurationDTO?.toList(), cashAppPayConfigurationDTO?.toList(), @@ -294,7 +308,7 @@ data class DropInConfigurationDTO ( } /** Generated class from Pigeon that represents data sent in messages. */ -data class CardsConfigurationDTO ( +data class CardConfigurationDTO ( val holderNameRequired: Boolean, val addressMode: AddressMode, val showStorePaymentField: Boolean, @@ -307,7 +321,7 @@ data class CardsConfigurationDTO ( ) { companion object { @Suppress("UNCHECKED_CAST") - fun fromList(list: List): CardsConfigurationDTO { + fun fromList(list: List): CardConfigurationDTO { val holderNameRequired = list[0] as Boolean val addressMode = AddressMode.ofRaw(list[1] as Int)!! val showStorePaymentField = list[2] as Boolean @@ -316,7 +330,7 @@ data class CardsConfigurationDTO ( val kcpFieldVisibility = FieldVisibility.ofRaw(list[5] as Int)!! val socialSecurityNumberFieldVisibility = FieldVisibility.ofRaw(list[6] as Int)!! val supportedCardTypes = list[7] as List - return CardsConfigurationDTO(holderNameRequired, addressMode, showStorePaymentField, showCvcForStoredCard, showCvc, kcpFieldVisibility, socialSecurityNumberFieldVisibility, supportedCardTypes) + return CardConfigurationDTO(holderNameRequired, addressMode, showStorePaymentField, showCvcForStoredCard, showCvc, kcpFieldVisibility, socialSecurityNumberFieldVisibility, supportedCardTypes) } } fun toList(): List { @@ -459,6 +473,7 @@ data class PaymentResultDTO ( data class PaymentResultModelDTO ( val sessionId: String? = null, val sessionData: String? = null, + val sessionResult: String? = null, val resultCode: String? = null, val order: OrderResponseDTO? = null @@ -468,17 +483,19 @@ data class PaymentResultModelDTO ( fun fromList(list: List): PaymentResultModelDTO { val sessionId = list[0] as String? val sessionData = list[1] as String? - val resultCode = list[2] as String? - val order: OrderResponseDTO? = (list[3] as List?)?.let { + val sessionResult = list[2] as String? + val resultCode = list[3] as String? + val order: OrderResponseDTO? = (list[4] as List?)?.let { OrderResponseDTO.fromList(it) } - return PaymentResultModelDTO(sessionId, sessionData, resultCode, order) + return PaymentResultModelDTO(sessionId, sessionData, sessionResult, resultCode, order) } } fun toList(): List { return listOf( sessionId, sessionData, + sessionResult, resultCode, order?.toList(), ) @@ -544,6 +561,33 @@ data class PlatformCommunicationModel ( } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class ComponentCommunicationModel ( + val type: ComponentCommunicationType, + val data: Any? = null, + val paymentResult: PaymentResultModelDTO? = null + +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): ComponentCommunicationModel { + val type = ComponentCommunicationType.ofRaw(list[0] as Int)!! + val data = list[1] + val paymentResult: PaymentResultModelDTO? = (list[2] as List?)?.let { + PaymentResultModelDTO.fromList(it) + } + return ComponentCommunicationModel(type, data, paymentResult) + } + } + fun toList(): List { + return listOf( + type.raw, + data, + paymentResult?.toList(), + ) + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class PaymentFlowOutcomeDTO ( val paymentFlowResultType: PaymentFlowResultType, @@ -621,6 +665,40 @@ data class DeletedStoredPaymentMethodResultDTO ( } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class CardComponentConfigurationDTO ( + val environment: Environment, + val clientKey: String, + val countryCode: String, + val amount: AmountDTO, + val shopperLocale: String? = null, + val cardConfiguration: CardConfigurationDTO + +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): CardComponentConfigurationDTO { + val environment = Environment.ofRaw(list[0] as Int)!! + val clientKey = list[1] as String + val countryCode = list[2] as String + val amount = AmountDTO.fromList(list[3] as List) + val shopperLocale = list[4] as String? + val cardConfiguration = CardConfigurationDTO.fromList(list[5] as List) + return CardComponentConfigurationDTO(environment, clientKey, countryCode, amount, shopperLocale, cardConfiguration) + } + } + fun toList(): List { + return listOf( + environment.raw, + clientKey, + countryCode, + amount.toList(), + shopperLocale, + cardConfiguration.toList(), + ) + } +} + @Suppress("UNCHECKED_CAST") private object CheckoutPlatformInterfaceCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { @@ -642,7 +720,7 @@ private object CheckoutPlatformInterfaceCodec : StandardMessageCodec() { } 131.toByte() -> { return (readValue(buffer) as? List)?.let { - CardsConfigurationDTO.fromList(it) + CardConfigurationDTO.fromList(it) } } 132.toByte() -> { @@ -697,7 +775,7 @@ private object CheckoutPlatformInterfaceCodec : StandardMessageCodec() { stream.write(130) writeValue(stream, value.toList()) } - is CardsConfigurationDTO -> { + is CardConfigurationDTO -> { stream.write(131) writeValue(stream, value.toList()) } @@ -1023,3 +1101,233 @@ class CheckoutFlutterApi(private val binaryMessenger: BinaryMessenger) { } } } +@Suppress("UNCHECKED_CAST") +private object ComponentPlatformInterfaceCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 128.toByte() -> { + return (readValue(buffer) as? List)?.let { + ErrorDTO.fromList(it) + } + } + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + PaymentFlowOutcomeDTO.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is ErrorDTO -> { + stream.write(128) + writeValue(stream, value.toList()) + } + is PaymentFlowOutcomeDTO -> { + stream.write(129) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface ComponentPlatformInterface { + fun updateViewHeight(viewId: Long) + fun onPaymentsResult(paymentsResult: PaymentFlowOutcomeDTO) + fun onPaymentsDetailsResult(paymentsDetailsResult: PaymentFlowOutcomeDTO) + + companion object { + /** The codec used by ComponentPlatformInterface. */ + val codec: MessageCodec by lazy { + ComponentPlatformInterfaceCodec + } + /** Sets up an instance of `ComponentPlatformInterface` to handle messages through the `binaryMessenger`. */ + @Suppress("UNCHECKED_CAST") + fun setUp(binaryMessenger: BinaryMessenger, api: ComponentPlatformInterface?) { + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.updateViewHeight", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val viewIdArg = args[0].let { if (it is Int) it.toLong() else it as Long } + var wrapped: List + try { + api.updateViewHeight(viewIdArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.onPaymentsResult", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val paymentsResultArg = args[0] as PaymentFlowOutcomeDTO + var wrapped: List + try { + api.onPaymentsResult(paymentsResultArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.onPaymentsDetailsResult", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val paymentsDetailsResultArg = args[0] as PaymentFlowOutcomeDTO + var wrapped: List + try { + api.onPaymentsDetailsResult(paymentsDetailsResultArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +@Suppress("UNCHECKED_CAST") +private object ComponentFlutterInterfaceCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 128.toByte() -> { + return (readValue(buffer) as? List)?.let { + AmountDTO.fromList(it) + } + } + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + AmountDTO.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + CardComponentConfigurationDTO.fromList(it) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + CardConfigurationDTO.fromList(it) + } + } + 132.toByte() -> { + return (readValue(buffer) as? List)?.let { + ComponentCommunicationModel.fromList(it) + } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { + OrderResponseDTO.fromList(it) + } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + PaymentResultModelDTO.fromList(it) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + SessionDTO.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is AmountDTO -> { + stream.write(128) + writeValue(stream, value.toList()) + } + is AmountDTO -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is CardComponentConfigurationDTO -> { + stream.write(130) + writeValue(stream, value.toList()) + } + is CardConfigurationDTO -> { + stream.write(131) + writeValue(stream, value.toList()) + } + is ComponentCommunicationModel -> { + stream.write(132) + writeValue(stream, value.toList()) + } + is OrderResponseDTO -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is PaymentResultModelDTO -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is SessionDTO -> { + stream.write(135) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +@Suppress("UNCHECKED_CAST") +class ComponentFlutterInterface(private val binaryMessenger: BinaryMessenger) { + companion object { + /** The codec used by ComponentFlutterInterface. */ + val codec: MessageCodec by lazy { + ComponentFlutterInterfaceCodec + } + } + fun _generateCodecForDTOs(cardComponentConfigurationDTOArg: CardComponentConfigurationDTO, sessionDTOArg: SessionDTO, callback: (Result) -> Unit) { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface._generateCodecForDTOs", codec) + channel.send(listOf(cardComponentConfigurationDTOArg, sessionDTOArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))); + } else { + callback(Result.success(Unit)); + } + } else { + callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", ""))); + } + } + } + fun onComponentCommunication(componentCommunicationModelArg: ComponentCommunicationModel, callback: (Result) -> Unit) { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface.onComponentCommunication", codec) + channel.send(listOf(componentCommunicationModelArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))); + } else { + callback(Result.success(Unit)); + } + } else { + callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", ""))); + } + } + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentMessenger.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentMessenger.kt new file mode 100644 index 00000000..3c7afef2 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentMessenger.kt @@ -0,0 +1,48 @@ +package com.adyen.adyen_checkout.components + +import ErrorDTO +import PaymentResultModelDTO +import androidx.lifecycle.LiveData +import com.adyen.adyen_checkout.utils.Event +import org.json.JSONObject + + +class ComponentHeightMessenger : LiveData>() { + companion object { + private val componentHeightMessenger = ComponentHeightMessenger() + fun instance() = componentHeightMessenger + fun sendResult(value: Long) { + componentHeightMessenger.postValue(Event(value)) + } + } +} + +class ComponentActionMessenger : LiveData>() { + companion object { + private val componentActionMessenger = ComponentActionMessenger() + fun instance() = componentActionMessenger + fun sendResult(value: JSONObject) { + componentActionMessenger.postValue(Event(value)) + } + } +} + +class ComponentResultMessenger : LiveData>() { + companion object { + private val componentResultMessenger = ComponentResultMessenger() + fun instance() = componentResultMessenger + fun sendResult(value: PaymentResultModelDTO) { + componentResultMessenger.postValue(Event(value)) + } + } +} + +class ComponentErrorMessenger : LiveData>() { + companion object { + private val componentErrorMessenger = ComponentErrorMessenger() + fun instance() = componentErrorMessenger + fun sendResult(value: ErrorDTO) { + componentErrorMessenger.postValue(Event(value)) + } + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentPlatformApi.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentPlatformApi.kt new file mode 100644 index 00000000..a66e9522 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentPlatformApi.kt @@ -0,0 +1,50 @@ +package com.adyen.adyen_checkout.components + +import ComponentPlatformInterface +import ErrorDTO +import PaymentFlowOutcomeDTO +import PaymentFlowResultType +import PaymentResultModelDTO +import org.json.JSONObject + +class ComponentPlatformApi : ComponentPlatformInterface { + + override fun updateViewHeight(viewId: Long) { + ComponentHeightMessenger.sendResult(viewId); + } + + override fun onPaymentsResult(paymentsResult: PaymentFlowOutcomeDTO) { + handlePaymentFlowOutcome(paymentsResult) + } + + override fun onPaymentsDetailsResult(paymentsDetailsResult: PaymentFlowOutcomeDTO) { + handlePaymentFlowOutcome(paymentsDetailsResult) + } + + private fun handlePaymentFlowOutcome(paymentFlowOutcomeDTO: PaymentFlowOutcomeDTO) { + when (paymentFlowOutcomeDTO.paymentFlowResultType) { + PaymentFlowResultType.FINISHED -> onFinished(paymentFlowOutcomeDTO.result) + PaymentFlowResultType.ACTION -> onAction(paymentFlowOutcomeDTO.actionResponse) + PaymentFlowResultType.ERROR -> onError(paymentFlowOutcomeDTO.error) + } + } + + private fun onFinished(resultCode: String?) { + val paymentResult = PaymentResultModelDTO(resultCode = resultCode) + ComponentResultMessenger.sendResult(paymentResult) + } + + private fun onAction(actionResponse: Map?) { + actionResponse?.let { + val jsonActionResponse = JSONObject(it) + ComponentActionMessenger.sendResult(jsonActionResponse) + } + } + + private fun onError(error: ErrorDTO?) { + error?.let { + ComponentErrorMessenger.sendResult(it) + } + } + +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentWrapperView.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentWrapperView.kt new file mode 100644 index 00000000..e1074cd9 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/ComponentWrapperView.kt @@ -0,0 +1,78 @@ +package com.adyen.adyen_checkout.components + +import ComponentCommunicationModel +import ComponentCommunicationType +import ComponentFlutterInterface +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import androidx.activity.ComponentActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.lifecycle.lifecycleScope +import com.adyen.adyen_checkout.R +import com.adyen.checkout.components.core.internal.Component +import com.adyen.checkout.ui.core.AdyenComponentView +import com.adyen.checkout.ui.core.internal.ui.ViewableComponent +import com.google.android.material.button.MaterialButton +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.math.round + +class ComponentWrapperView +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + constructor(activity: ComponentActivity, componentFlutterApi: ComponentFlutterInterface) : this(context = activity) { + this.activity = activity + this.componentFlutterApi = componentFlutterApi + } + + private lateinit var activity: ComponentActivity + private lateinit var componentFlutterApi: ComponentFlutterInterface + private val screenDensity = context.resources.displayMetrics.density + + init { + inflate(context, R.layout.component_wrapper_view, this) + } + + fun addComponent(cardComponent: T) where T : ViewableComponent, T : Component { + with(findViewById(R.id.adyen_component_view)) { + attach(cardComponent, activity) + addComponentHeightObserver() + } + } + + private fun addComponentHeightObserver() { + ComponentHeightMessenger.instance().removeObservers(activity) + ComponentHeightMessenger.instance().observe(activity) { + activity.lifecycleScope.launch { + //We need to wait for animation to finish e.g. when scheme icons disappear + delay(300) + updateComponentViewHeight() + } + } + } + + private fun updateComponentViewHeight() { + val cardViewHeight = findViewById(R.id.frameLayout_componentContainer)?.getChildAt(0)?.height + if (cardViewHeight == null) { + activity.lifecycleScope.launch() { + //View not rendered therefore we try again after delay. + //This is a workaround because there is currently no notifier from the native view. + delay(100) + updateComponentViewHeight() + } + return + } + + val standardMargin = resources.getDimension(R.dimen.standard_margin) + val buttonHeight = (findViewById(R.id.payButton)?.height ?: 0).plus((standardMargin)) + val componentHeight = ((cardViewHeight + buttonHeight) / screenDensity).toDouble() + val roundedHeight = round(componentHeight * 100) / 100 + componentFlutterApi.onComponentCommunication( + ComponentCommunicationModel(type = ComponentCommunicationType.RESIZE, data = roundedHeight) + ) {} + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/BaseCardComponent.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/BaseCardComponent.kt new file mode 100644 index 00000000..7c9a9e88 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/BaseCardComponent.kt @@ -0,0 +1,119 @@ +package com.adyen.adyen_checkout.components.card + +import CardComponentConfigurationDTO +import ComponentFlutterInterface +import android.content.Context +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.activity.ComponentActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.util.Consumer +import androidx.core.view.children +import androidx.core.view.doOnNextLayout +import androidx.core.view.updateLayoutParams +import com.adyen.adyen_checkout.R +import com.adyen.adyen_checkout.components.ComponentActionMessenger +import com.adyen.adyen_checkout.components.ComponentErrorMessenger +import com.adyen.adyen_checkout.components.ComponentHeightMessenger +import com.adyen.adyen_checkout.components.ComponentResultMessenger +import com.adyen.adyen_checkout.components.ComponentWrapperView +import com.adyen.adyen_checkout.utils.ConfigurationMapper.toNativeModel +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.redirect.RedirectComponent +import com.adyen.checkout.ui.core.AdyenComponentView +import io.flutter.plugin.platform.PlatformView + +abstract class BaseCardComponent( + private val activity: ComponentActivity, + private val componentFlutterApi: ComponentFlutterInterface, + context: Context, + id: Int, + creationParams: Map<*, *>? +) : PlatformView { + private val configuration = creationParams?.get(CARD_COMPONENT_CONFIGURATION_KEY) as? CardComponentConfigurationDTO + ?: throw Exception("Card configuration not found") + private val environment = configuration.environment.toNativeModel() + private val componentWrapperView = ComponentWrapperView(activity, componentFlutterApi) + private val intentListener = Consumer { handleIntent(it) } + val cardConfiguration = configuration.cardConfiguration.toNativeModel( + "${configuration.shopperLocale}", + context, + environment, + configuration.clientKey + ) + + lateinit var cardComponent: CardComponent + + init { + activity.addOnNewIntentListener(intentListener); + } + + override fun getView(): View = componentWrapperView + + override fun onFlutterViewAttached(flutterView: View) { + super.onFlutterViewAttached(flutterView) + flutterView.doOnNextLayout { + adjustCardComponentLayout(it) + } + } + + override fun dispose() { + activity.removeOnNewIntentListener(intentListener) + ComponentHeightMessenger.instance().removeObservers(activity) + ComponentActionMessenger.instance().removeObservers(activity) + ComponentResultMessenger.instance().removeObservers(activity) + ComponentErrorMessenger.instance().removeObservers(activity) + } + + fun addComponent(cardComponent: CardComponent) { + componentWrapperView.addComponent(cardComponent) + } + + private fun handleIntent(intent: Intent) { + if (intent.data?.toString().orEmpty().startsWith(RedirectComponent.REDIRECT_RESULT_SCHEME)) { + cardComponent.handleIntent(intent) + } + } + + private fun adjustCardComponentLayout(flutterView: View) { + val linearLayoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + //Adyen component view + val adyenComponentView = flutterView.findViewById(R.id.adyen_component_view) + adyenComponentView.layoutParams = ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + + //Component container + val componentContainer = flutterView.findViewById(R.id.frameLayout_componentContainer) + componentContainer.layoutParams = linearLayoutParams + + //Button container + val buttonContainer = flutterView.findViewById(R.id.frameLayout_buttonContainer) + buttonContainer.layoutParams = linearLayoutParams + + //Pay button + val button = buttonContainer.children.firstOrNull() + val buttonParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + button?.layoutParams = buttonParams + + //Card + val card = componentContainer.children.firstOrNull() as ViewGroup? + card?.updateLayoutParams { + height = LinearLayout.LayoutParams.WRAP_CONTENT + } + } + + companion object { + const val CARD_COMPONENT_CONFIGURATION_KEY = "cardComponentConfiguration" + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowCallback.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowCallback.kt new file mode 100644 index 00000000..3a7475d8 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowCallback.kt @@ -0,0 +1,44 @@ +package com.adyen.adyen_checkout.components.card.advancedFlow + +import ComponentCommunicationModel +import ComponentCommunicationType +import ComponentFlutterInterface +import com.adyen.adyen_checkout.components.ComponentHeightMessenger +import com.adyen.checkout.card.CardComponentState +import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.ComponentCallback +import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.PaymentComponentData + +class CardAdvancedFlowCallback(private val componentFlutterApi: ComponentFlutterInterface) : + ComponentCallback { + override fun onSubmit(state: CardComponentState) { + val paymentComponentJson = PaymentComponentData.SERIALIZER.serialize(state.data) + val model = ComponentCommunicationModel( + ComponentCommunicationType.ONSUBMIT, + data = paymentComponentJson.toString(), + ) + componentFlutterApi.onComponentCommunication(model) {} + } + + override fun onAdditionalDetails(actionComponentData: ActionComponentData) { + val actionComponentJson = ActionComponentData.SERIALIZER.serialize(actionComponentData) + val model = ComponentCommunicationModel( + ComponentCommunicationType.ADDITIONALDETAILS, + data = actionComponentJson.toString(), + ) + componentFlutterApi.onComponentCommunication(model) {} + } + + override fun onError(componentError: ComponentError) { + val model = ComponentCommunicationModel( + ComponentCommunicationType.ERROR, + data = componentError.exception.toString(), + ) + componentFlutterApi.onComponentCommunication(model) {} + } + + override fun onStateChanged(state: CardComponentState) { + ComponentHeightMessenger.sendResult(1) + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowComponent.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowComponent.kt new file mode 100644 index 00000000..788c9a81 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowComponent.kt @@ -0,0 +1,114 @@ +package com.adyen.adyen_checkout.components.card.advancedFlow + +import ComponentCommunicationModel +import ComponentCommunicationType +import ComponentFlutterInterface +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.core.view.doOnNextLayout +import androidx.fragment.app.FragmentActivity +import com.adyen.adyen_checkout.R +import com.adyen.adyen_checkout.components.ComponentActionMessenger +import com.adyen.adyen_checkout.components.ComponentErrorMessenger +import com.adyen.adyen_checkout.components.ComponentResultMessenger +import com.adyen.adyen_checkout.components.card.BaseCardComponent +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.components.core.PaymentMethodsApiResponse +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.ui.core.AdyenComponentView +import org.json.JSONObject +import java.util.UUID + +internal class CardAdvancedFlowComponent( + private val activity: FragmentActivity, + private val componentFlutterApi: ComponentFlutterInterface, + context: Context, + id: Int, + creationParams: Map<*, *>? +) : BaseCardComponent(activity, componentFlutterApi, context, id, creationParams) { + private val paymentMethods = creationParams?.get(PAYMENT_METHODS_KEY) as? String ?: throw Exception("Payment methods not found") + private val paymentMethodsApiResponse = PaymentMethodsApiResponse.SERIALIZER.deserialize(JSONObject(paymentMethods)) + private val paymentMethod = paymentMethodsApiResponse.paymentMethods?.first { it.type == SCHEME_KEY } + ?: throw Exception("Card payment method not provided") //TODO: Define custom exception + + init { + cardComponent = CardComponent.PROVIDER.get( + activity = activity, + paymentMethod = paymentMethod, + configuration = cardConfiguration, + callback = CardAdvancedFlowCallback(componentFlutterApi), + key = UUID.randomUUID().toString() + ) + addComponent(cardComponent) + addActionListener() + addResultListener() + addErrorListener() + } + + override fun onFlutterViewAttached(flutterView: View) { + super.onFlutterViewAttached(flutterView) + flutterView.doOnNextLayout { + adjustCardComponentLayout(it) + } + } + + private fun adjustCardComponentLayout(flutterView: View) { + val adyenComponentView = flutterView.findViewById(R.id.adyen_component_view) + adyenComponentView.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + + private fun addActionListener() { + ComponentActionMessenger.instance().removeObservers(activity) + ComponentActionMessenger.instance().observe(activity) { message -> + if (message.hasBeenHandled()) { + return@observe + } + + val action = message.contentIfNotHandled?.let { Action.SERIALIZER.deserialize(it) } + action?.let { + cardComponent.handleAction(action = it, activity = activity) + } + } + } + + private fun addResultListener() { + ComponentResultMessenger.instance().removeObservers(activity) + ComponentResultMessenger.instance().observe(activity) { message -> + if (message.hasBeenHandled()) { + return@observe + } + + val model = ComponentCommunicationModel( + ComponentCommunicationType.RESULT, + paymentResult = message.contentIfNotHandled, + ) + componentFlutterApi.onComponentCommunication(model) {} + } + } + + private fun addErrorListener() { + ComponentErrorMessenger.instance().removeObservers(activity) + ComponentErrorMessenger.instance().observe(activity) { message -> + if (message.hasBeenHandled()) { + return@observe + } + + + val model = ComponentCommunicationModel( + ComponentCommunicationType.ERROR, + data = message.contentIfNotHandled?.errorMessage, + ) + componentFlutterApi.onComponentCommunication(model) {} + } + } + + companion object { + const val PAYMENT_METHODS_KEY = "paymentMethods" + const val SCHEME_KEY = "scheme" + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowComponentFactory.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowComponentFactory.kt new file mode 100644 index 00000000..efb4b669 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/advancedFlow/CardAdvancedFlowComponentFactory.kt @@ -0,0 +1,17 @@ +package com.adyen.adyen_checkout.components.card.advancedFlow + +import ComponentFlutterInterface +import android.content.Context +import androidx.fragment.app.FragmentActivity +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class CardAdvancedFlowComponentFactory( + private val activity: FragmentActivity, + private val componentFlutterApi: ComponentFlutterInterface, +) : PlatformViewFactory(ComponentFlutterInterface.codec) { + override fun create(context: Context, viewId: Int, args: Any?): PlatformView { + val creationParams = args as Map<*, *>? + return CardAdvancedFlowComponent(activity, componentFlutterApi, context, viewId, creationParams) + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowCallback.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowCallback.kt new file mode 100644 index 00000000..1829c241 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowCallback.kt @@ -0,0 +1,50 @@ +package com.adyen.adyen_checkout.components.card.session + +import ComponentCommunicationModel +import ComponentCommunicationType +import ComponentFlutterInterface +import PaymentResultModelDTO +import com.adyen.adyen_checkout.components.ComponentHeightMessenger +import com.adyen.adyen_checkout.utils.ConfigurationMapper.mapToOrderResponseModel +import com.adyen.checkout.card.CardComponentState +import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.sessions.core.SessionComponentCallback +import com.adyen.checkout.sessions.core.SessionPaymentResult + +class CardSessionFlowCallback( + private val componentFlutterApi: ComponentFlutterInterface, + private val onActionCallback: (Action) -> Unit +) : + SessionComponentCallback { + override fun onFinished(result: SessionPaymentResult) { + val paymentResult = PaymentResultModelDTO( + result.sessionId, + result.sessionData, + result.sessionResult, + result.resultCode, + result.order?.mapToOrderResponseModel() + ) + val model = ComponentCommunicationModel( + ComponentCommunicationType.RESULT, + paymentResult = paymentResult + ) + componentFlutterApi.onComponentCommunication(model) {} + } + + override fun onAction(action: Action) { + onActionCallback(action) + } + + override fun onError(componentError: ComponentError) { + val model = ComponentCommunicationModel( + ComponentCommunicationType.ERROR, + data = componentError.exception.toString(), + ) + componentFlutterApi.onComponentCommunication(model) {} + } + + override fun onStateChanged(state: CardComponentState) { + ComponentHeightMessenger.sendResult(1) + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowComponent.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowComponent.kt new file mode 100644 index 00000000..0a37374d --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowComponent.kt @@ -0,0 +1,93 @@ +package com.adyen.adyen_checkout.components.card.session + +import ComponentCommunicationModel +import ComponentCommunicationType +import ComponentFlutterInterface +import SessionDTO +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.doOnNextLayout +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import com.adyen.adyen_checkout.R +import com.adyen.adyen_checkout.components.card.BaseCardComponent +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.sessions.core.CheckoutSessionProvider +import com.adyen.checkout.sessions.core.CheckoutSessionResult +import com.adyen.checkout.sessions.core.SessionModel +import com.adyen.checkout.ui.core.AdyenComponentView +import kotlinx.coroutines.launch +import java.util.UUID + +class CardSessionFlowComponent( + private val activity: FragmentActivity, + private val componentFlutterApi: ComponentFlutterInterface, + context: Context, + id: Int, + creationParams: Map<*, *>? +) : BaseCardComponent(activity, componentFlutterApi, context, id, creationParams) { + private val session = creationParams?.get(SESSION_KEY) as SessionDTO + private val sessionModel = SessionModel(id = session.id, sessionData = session.sessionData) + + init { + activity.lifecycleScope.launch { + when (val sessionResult = CheckoutSessionProvider.createSession(sessionModel, cardConfiguration)) { + is CheckoutSessionResult.Error -> { + sessionResult.exception.message?.let { sendErrorToFlutterLayer(it) } + return@launch + } + + is CheckoutSessionResult.Success -> { + val paymentMethod = sessionResult.checkoutSession.getPaymentMethod(PaymentMethodTypes.SCHEME) + if (paymentMethod == null) { + sendErrorToFlutterLayer("Session does not contain SCHEME payment method.") + return@launch + } + + cardComponent = CardComponent.PROVIDER.get( + activity = activity, + checkoutSession = sessionResult.checkoutSession, + paymentMethod = paymentMethod, + configuration = cardConfiguration, + componentCallback = CardSessionFlowCallback(componentFlutterApi) { action -> onAction(action) }, + key = UUID.randomUUID().toString() + ) + addComponent(cardComponent) + } + } + } + } + + override fun onFlutterViewAttached(flutterView: View) { + super.onFlutterViewAttached(flutterView) + flutterView.doOnNextLayout { + adjustCardComponentLayout(it) + } + } + + private fun adjustCardComponentLayout(flutterView: View) { + val adyenComponentView = flutterView.findViewById(R.id.adyen_component_view) + adyenComponentView.layoutParams = ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + + private fun onAction(action: Action) = cardComponent.handleAction(action, activity) + + private fun sendErrorToFlutterLayer(errorMessage: String) { + val model = ComponentCommunicationModel( + ComponentCommunicationType.ERROR, + data = errorMessage, + ) + componentFlutterApi.onComponentCommunication(model) {} + } + + companion object { + const val SESSION_KEY = "session" + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowComponentFactory.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowComponentFactory.kt new file mode 100644 index 00000000..dd5196b2 --- /dev/null +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/components/card/session/CardSessionFlowComponentFactory.kt @@ -0,0 +1,17 @@ +package com.adyen.adyen_checkout.components.card.session + +import ComponentFlutterInterface +import android.content.Context +import androidx.fragment.app.FragmentActivity +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class CardSessionFlowComponentFactory( + private val activity: FragmentActivity, + private val componentFlutterApi: ComponentFlutterInterface, +) : PlatformViewFactory(ComponentFlutterInterface.codec) { + override fun create(context: Context, viewId: Int, args: Any?): PlatformView { + val creationParams = args as Map<*, *>? + return CardSessionFlowComponent(activity, componentFlutterApi, context, viewId, creationParams) + } +} diff --git a/android/src/main/kotlin/com/adyen/adyen_checkout/utils/ConfigurationMapper.kt b/android/src/main/kotlin/com/adyen/adyen_checkout/utils/ConfigurationMapper.kt index 99e269fb..138cd1e6 100644 --- a/android/src/main/kotlin/com/adyen/adyen_checkout/utils/ConfigurationMapper.kt +++ b/android/src/main/kotlin/com/adyen/adyen_checkout/utils/ConfigurationMapper.kt @@ -2,7 +2,7 @@ package com.adyen.adyen_checkout.utils import AddressMode import AmountDTO -import CardsConfigurationDTO +import CardConfigurationDTO import CashAppPayConfigurationDTO import CashAppPayEnvironment import DropInConfigurationDTO @@ -46,10 +46,10 @@ object ConfigurationMapper { } fun DropInConfigurationDTO.mapToDropInConfiguration(context: Context): DropInConfiguration { - val environment = environment.mapToEnvironment() + val environment = environment.toNativeModel() val amount = amount.mapToAmount() - val shopperLocale = Locale.forLanguageTag(shopperLocale) - val dropInConfiguration = DropInConfiguration.Builder(shopperLocale, environment, clientKey) + val locale = Locale.forLanguageTag(shopperLocale) + val dropInConfiguration = DropInConfiguration.Builder(locale, environment, clientKey) isRemoveStoredPaymentMethodEnabled.let { dropInConfiguration.setEnableRemovingStoredPaymentMethods(it) @@ -63,20 +63,20 @@ object ConfigurationMapper { dropInConfiguration.setSkipListWhenSinglePaymentMethod(it) } - if (cardsConfigurationDTO != null) { - val cardConfiguration = buildCardConfiguration(context, environment, cardsConfigurationDTO) + if (cardConfigurationDTO != null) { + val cardConfiguration = cardConfigurationDTO.toNativeModel(shopperLocale, context, environment, clientKey) dropInConfiguration.addCardConfiguration(cardConfiguration) } if (googlePayConfigurationDTO != null) { val googlePayConfiguration = - buildGooglePayConfiguration(shopperLocale, environment, googlePayConfigurationDTO) + buildGooglePayConfiguration(locale, environment, googlePayConfigurationDTO) dropInConfiguration.addGooglePayConfiguration(googlePayConfiguration) } if (cashAppPayConfigurationDTO != null) { val cashAppPayConfiguration = - buildCashAppPayConfiguration(shopperLocale, environment, cashAppPayConfigurationDTO) + buildCashAppPayConfiguration(locale, environment, cashAppPayConfigurationDTO) dropInConfiguration.addCashAppPayConfiguration(cashAppPayConfiguration) } @@ -84,28 +84,29 @@ object ConfigurationMapper { return dropInConfiguration.build() } - private fun DropInConfigurationDTO.buildCardConfiguration( + fun CardConfigurationDTO.toNativeModel( + shopperLocale: String, context: Context, environment: com.adyen.checkout.core.Environment, - cardsConfigurationDTO: CardsConfigurationDTO + clientKey: String, ): CardConfiguration { + val locale = Locale.forLanguageTag(shopperLocale) + return CardConfiguration.Builder( - context = context, + shopperLocale = locale, environment = environment, clientKey = clientKey ) - .setAddressConfiguration( - cardsConfigurationDTO.addressMode.mapToAddressConfiguration() - ) - .setShowStorePaymentField(cardsConfigurationDTO.showStorePaymentField) - .setHideCvcStoredCard(!cardsConfigurationDTO.showCvcForStoredCard) - .setHideCvc(!cardsConfigurationDTO.showCvc) - .setKcpAuthVisibility(determineKcpAuthVisibility(cardsConfigurationDTO.kcpFieldVisibility)) + .setAddressConfiguration(addressMode.mapToAddressConfiguration()) + .setShowStorePaymentField(showStorePaymentField) + .setHideCvcStoredCard(!showCvcForStoredCard) + .setHideCvc(!showCvc) + .setKcpAuthVisibility(determineKcpAuthVisibility(kcpFieldVisibility)) .setSocialSecurityNumberVisibility( - determineSocialSecurityNumberVisibility(cardsConfigurationDTO.socialSecurityNumberFieldVisibility) + determineSocialSecurityNumberVisibility(socialSecurityNumberFieldVisibility) ) - .setSupportedCardTypes(*mapToSupportedCardTypes(cardsConfigurationDTO.supportedCardTypes)) - .setHolderNameRequired(cardsConfigurationDTO.holderNameRequired) + .setSupportedCardTypes(*mapToSupportedCardTypes(supportedCardTypes)) + .setHolderNameRequired(holderNameRequired) .build() } @@ -168,7 +169,7 @@ object ConfigurationMapper { return mappedCardTypes.filterNotNull().toTypedArray() } - private fun Environment.mapToEnvironment(): SDKEnvironment { + fun Environment.toNativeModel(): SDKEnvironment { return when (this) { Environment.TEST -> SDKEnvironment.TEST Environment.EUROPE -> SDKEnvironment.EUROPE diff --git a/android/src/main/res/layout/component_wrapper_view.xml b/android/src/main/res/layout/component_wrapper_view.xml new file mode 100644 index 00000000..547928a4 --- /dev/null +++ b/android/src/main/res/layout/component_wrapper_view.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 1bbd4b62..b03bc8d4 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -18,13 +18,14 @@ to determine the Window background behind the Flutter UI. --> + android:resource="@style/AppTheme" /> + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml index cb1ef880..a6fe4558 100644 --- a/example/android/app/src/main/res/values/styles.xml +++ b/example/android/app/src/main/res/values/styles.xml @@ -15,4 +15,8 @@ + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 889b925e..085a711c 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,6 +1,6 @@ -import UIKit import Adyen import Flutter +import UIKit @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -11,8 +11,8 @@ import Flutter GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - - override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + + override func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { RedirectComponent.applicationDidOpen(from: url) return true } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index f240fd71..b6db5dcf 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -14,6 +14,11 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleLocalizations + + en + ar + CFBundleName adyen_checkout_example CFBundlePackageType diff --git a/example/ios/Runner/ar.lproj/LaunchScreen.strings b/example/ios/Runner/ar.lproj/LaunchScreen.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/example/ios/Runner/ar.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/example/ios/Runner/ar.lproj/Main.strings b/example/ios/Runner/ar.lproj/Main.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/example/ios/Runner/ar.lproj/Main.strings @@ -0,0 +1 @@ + diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift index fa5b9a62..ecfc8153 100644 --- a/example/ios/RunnerTests/RunnerTests.swift +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -9,18 +9,16 @@ import XCTest // See https://developer.apple.com/documentation/xctest for more information about using XCTest. class RunnerTests: XCTestCase { + func testGetPlatformVersion() { + let plugin = AdyenCheckoutPlugin() - func testGetPlatformVersion() { - let plugin = AdyenCheckoutPlugin() + let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) - let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) - - let resultExpectation = expectation(description: "result block must be called.") - plugin.handle(call) { result in - XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) - resultExpectation.fulfill() + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) + resultExpectation.fulfill() + } + waitForExpectations(timeout: 1) } - waitForExpectations(timeout: 1) - } - } diff --git a/example/lib/config.dart b/example/lib/config.dart index 90461794..54677827 100644 --- a/example/lib/config.dart +++ b/example/lib/config.dart @@ -17,12 +17,12 @@ class Config { //Environment constants static const String merchantAccount = "TestMerchantCheckout"; static const String merchantName = "Test Merchant"; - static const String countryCode = "NL"; - static const String shopperLocale = "nl_NL"; + static const String countryCode = "BE"; + static const String shopperLocale = "be_BE"; static const String shopperReference = "Test reference"; static const Environment environment = Environment.test; static const String baseUrl = "checkout-test.adyen.com"; - static const String apiVersion = "v70"; + static const String apiVersion = "v71"; static const String iOSReturnUrl = "flutter-ui-host://payments"; //Example data diff --git a/example/lib/main.dart b/example/lib/main.dart index 4cb4a8a8..dd70db8d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,13 +2,24 @@ import 'dart:async'; import 'package:adyen_checkout/adyen_checkout.dart'; import 'package:adyen_checkout_example/config.dart'; +import 'package:adyen_checkout_example/navigation/card_component_screen.dart'; +import 'package:adyen_checkout_example/network/models/session_response_network_model.dart'; import 'package:adyen_checkout_example/network/service.dart'; -import 'package:adyen_checkout_example/repositories/adyen_sessions_repository.dart'; +import 'package:adyen_checkout_example/repositories/adyen_card_component_repository.dart'; +import 'package:adyen_checkout_example/repositories/adyen_drop_in_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; void main() { - runApp(const MaterialApp(home: MyApp())); + runApp(const MaterialApp(localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], supportedLocales: [ + Locale('en'), // English + Locale('ar'), // Arabic + ], home: MyApp())); } class MyApp extends StatefulWidget { @@ -21,7 +32,9 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { String _platformVersion = 'Unknown'; final _adyenCheckout = AdyenCheckout(); - late AdyenSessionsRepository _adyenSessionRepository; + final _service = Service(); + late AdyenDropInRepository _adyenDropInRepository; + late AdyenCardComponentRepository _adyenCardComponentRepository; @override void initState() { @@ -32,9 +45,14 @@ class _MyAppState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initPlatformState() async { - _adyenSessionRepository = AdyenSessionsRepository( + _adyenDropInRepository = AdyenDropInRepository( adyenCheckout: _adyenCheckout, - service: Service(), + service: _service, + ); + + _adyenCardComponentRepository = AdyenCardComponentRepository( + adyenCheckout: _adyenCheckout, + service: _service, ); String platformVersion; @@ -78,7 +96,60 @@ class _MyAppState extends State { final result = await startDropInAdvancedFlow(); _dialogBuilder(context, result); }, - child: const Text("DropIn advanced flow")) + child: const Text("DropIn advanced flow")), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CardComponentScreen( + repository: _adyenCardComponentRepository, + )), + ); + }, + child: const Text("Card component scroll view")), + TextButton( + onPressed: () async { + await _adyenCardComponentRepository + .createSession() + .then((sessionResponse) => showModalBottomSheet( + context: context, + isDismissible: false, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + builder: (BuildContext context) { + return SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + Container(height: 8), + Container( + width: 48, + height: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.grey), + ), + Container(height: 8), + Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context) + .viewInsets + .bottom), + child: _buildSessionCardWidget( + context, + sessionResponse, + ), + ), + ], + )), + ); + })); + }, + child: const Text("Card component session sheet")), ], ), ), @@ -86,12 +157,13 @@ class _MyAppState extends State { } Future startDropInSessions() async { - final Session session = await _adyenSessionRepository.createSession( - Config.amount, - Config.environment, + final SessionResponseNetworkModel sessionResponse = + await _adyenDropInRepository.createSession(); + final Session session = Session( + id: sessionResponse.id, + sessionData: sessionResponse.sessionData, ); - - final CardsConfiguration cardsConfiguration = CardsConfiguration( + const CardConfiguration cardsConfiguration = CardConfiguration( showStorePaymentField: true, ); @@ -100,13 +172,13 @@ class _MyAppState extends State { showPreselectedStoredPaymentMethod: true, isRemoveStoredPaymentMethodEnabled: true, deleteStoredPaymentMethodCallback: - _adyenSessionRepository.deleteStoredPaymentMethod, + _adyenDropInRepository.deleteStoredPaymentMethod, ); final CashAppPayConfiguration cashAppPayConfiguration = await _createCashAppPayConfiguration(); - final ApplePayConfiguration applePayConfiguration = ApplePayConfiguration( + const ApplePayConfiguration applePayConfiguration = ApplePayConfiguration( merchantId: Config.merchantAccount, merchantName: Config.merchantName, ); @@ -117,14 +189,14 @@ class _MyAppState extends State { countryCode: Config.countryCode, amount: Config.amount, shopperLocale: Config.shopperLocale, - cardsConfiguration: cardsConfiguration, + cardConfiguration: cardsConfiguration, storedPaymentMethodConfiguration: storedPaymentMethodConfiguration, cashAppPayConfiguration: cashAppPayConfiguration, applePayConfiguration: applePayConfiguration, ); return await _adyenCheckout.startPayment( - paymentFlow: DropInSession( + paymentFlow: DropInSessionFlow( dropInConfiguration: dropInConfiguration, session: session, ), @@ -133,18 +205,18 @@ class _MyAppState extends State { Future startDropInAdvancedFlow() async { final String paymentMethodsResponse = - await _adyenSessionRepository.fetchPaymentMethods(); + await _adyenDropInRepository.fetchPaymentMethods(); - final CardsConfiguration cardsConfiguration = CardsConfiguration( - showStorePaymentField: true, + const CardConfiguration cardsConfiguration = CardConfiguration( + showStorePaymentField: true, ); - final ApplePayConfiguration applePayConfiguration = ApplePayConfiguration( + const ApplePayConfiguration applePayConfiguration = ApplePayConfiguration( merchantId: Config.merchantAccount, merchantName: Config.merchantName, ); - final GooglePayConfiguration googlePayConfiguration = + const GooglePayConfiguration googlePayConfiguration = GooglePayConfiguration( googlePayEnvironment: GooglePayEnvironment.test, shippingAddressRequired: true, @@ -159,7 +231,7 @@ class _MyAppState extends State { showPreselectedStoredPaymentMethod: false, isRemoveStoredPaymentMethodEnabled: true, deleteStoredPaymentMethodCallback: - _adyenSessionRepository.deleteStoredPaymentMethod, + _adyenDropInRepository.deleteStoredPaymentMethod, ); final DropInConfiguration dropInConfiguration = DropInConfiguration( @@ -168,7 +240,7 @@ class _MyAppState extends State { countryCode: Config.countryCode, shopperLocale: Config.shopperLocale, amount: Config.amount, - cardsConfiguration: cardsConfiguration, + cardConfiguration: cardsConfiguration, applePayConfiguration: applePayConfiguration, googlePayConfiguration: googlePayConfiguration, cashAppPayConfiguration: cashAppPayConfiguration, @@ -179,8 +251,8 @@ class _MyAppState extends State { paymentFlow: DropInAdvancedFlow( dropInConfiguration: dropInConfiguration, paymentMethodsResponse: paymentMethodsResponse, - postPayments: _adyenSessionRepository.postPayments, - postPaymentsDetails: _adyenSessionRepository.postPaymentsDetails, + postPayments: _adyenDropInRepository.postPayments, + postPaymentsDetails: _adyenDropInRepository.postPaymentsDetails, ), ); } @@ -189,7 +261,37 @@ class _MyAppState extends State { Future _createCashAppPayConfiguration() async { return CashAppPayConfiguration( CashAppPayEnvironment.sandbox, - await _adyenSessionRepository.determineExampleReturnUrl(), + await _adyenDropInRepository.determineBaseReturnUrl(), + ); + } + + Widget _buildSessionCardWidget( + BuildContext context, + SessionResponseNetworkModel sessionResponse, + ) { + final cardComponentConfiguration = CardComponentConfiguration( + environment: Config.environment, + clientKey: Config.clientKey, + countryCode: Config.countryCode, + amount: Config.amount, + shopperLocale: Config.shopperLocale, + cardConfiguration: const CardConfiguration(), + ); + + final session = Session( + id: sessionResponse.id, + sessionData: sessionResponse.sessionData, + ); + + return AdyenCardComponentWidget( + componentPaymentFlow: CardComponentSessionFlow( + cardComponentConfiguration: cardComponentConfiguration, + session: session, + ), + onPaymentResult: (event) async { + Navigator.pop(context); + _dialogBuilder(context, event); + }, ); } diff --git a/example/lib/navigation/card_component_screen.dart b/example/lib/navigation/card_component_screen.dart new file mode 100644 index 00000000..6b189298 --- /dev/null +++ b/example/lib/navigation/card_component_screen.dart @@ -0,0 +1,112 @@ +import 'package:adyen_checkout/adyen_checkout.dart'; +import 'package:adyen_checkout_example/repositories/adyen_card_component_repository.dart'; +import 'package:flutter/material.dart'; + +import '../config.dart'; + +class CardComponentScreen extends StatelessWidget { + const CardComponentScreen({ + required this.repository, + super.key, + }); + + final AdyenCardComponentRepository repository; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar(title: const Text('Adyen card component')), + body: SafeArea( + child: FutureBuilder( + future: repository.fetchPaymentMethods(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.data == null) { + return const SizedBox.shrink(); + } else { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + children: [ + _buildCardWidget( + snapshot.data!, + context, + ), + Container(height: 200, color: Colors.yellow), + Container(height: 200, color: Colors.blue), + Container(height: 200, color: Colors.green), + Container(height: 200, color: Colors.purple), + ], + ), + ); + } + }, + ), + )); + } + + Widget _buildCardWidget( + String paymentMethods, + BuildContext context, + ) { + final cardComponentConfiguration = CardComponentConfiguration( + environment: Config.environment, + clientKey: Config.clientKey, + countryCode: Config.countryCode, + amount: Config.amount, + shopperLocale: Config.shopperLocale, + cardConfiguration: const CardConfiguration(), + ); + + return AdyenCardComponentWidget( + componentPaymentFlow: CardComponentAdvancedFlow( + cardComponentConfiguration: cardComponentConfiguration, + paymentMethods: paymentMethods, + onPayments: repository.postPayments, + onPaymentsDetails: repository.postPaymentsDetails, + ), + onPaymentResult: (event) async { + Navigator.pop(context); + _dialogBuilder(context, event); + }, + ); + } + + _dialogBuilder(BuildContext context, PaymentResult paymentResult) { + String title = ""; + String message = ""; + switch (paymentResult) { + case PaymentAdvancedFlowFinished(): + title = "Finished"; + message = "Result code: ${paymentResult.resultCode}"; + case PaymentSessionFinished(): + title = "Finished"; + message = "Result code: ${paymentResult.resultCode}"; + case PaymentError(): + title = "Error occurred"; + message = "${paymentResult.reason}"; + default: + } + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + child: const Text('Close'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/example/lib/network/models/session_request_network_model.dart b/example/lib/network/models/session_request_network_model.dart index 232e31fb..c25c5647 100644 --- a/example/lib/network/models/session_request_network_model.dart +++ b/example/lib/network/models/session_request_network_model.dart @@ -47,6 +47,30 @@ class SessionRequestNetworkModel { String toRawJson() => json.encode(toJson()); Map toJson() { + // ignore: unused_local_variable + Map installmentOptions = json.decode("""{ + "visa": { + "plans": [ + "regular" + ], + "values": [ + 2, + 4 + ] + }, + "mc": { + "values": [ + 2, + 3, + 5 + ], + "plans": [ + "regular", + "revolving" + ] + } + }"""); + final Map data = {}; data['merchantAccount'] = merchantAccount; data['amount'] = amount.toJson(); @@ -66,6 +90,7 @@ class SessionRequestNetworkModel { data['deliveryAddress'] = deliveryAddress?.toJson(); data['lineItems'] = lineItems?.map((lineItem) => lineItem.toJson()).toList(); + // data['installmentOptions'] = installmentOptions; return data; } } diff --git a/example/lib/network/models/session_response_network_model.dart b/example/lib/network/models/session_response_network_model.dart index 2b8a4f69..0188ec5c 100644 --- a/example/lib/network/models/session_response_network_model.dart +++ b/example/lib/network/models/session_response_network_model.dart @@ -1,49 +1,13 @@ import 'dart:convert'; -import 'package:adyen_checkout_example/network/models/amount_network_model.dart'; -import 'package:adyen_checkout_example/network/models/billing_address.dart'; -import 'package:adyen_checkout_example/network/models/delivery_address.dart'; -import 'package:adyen_checkout_example/network/models/line_item.dart'; - class SessionResponseNetworkModel { - final AmountNetworkModel amount; - final String countryCode; - final DateTime expiresAt; final String id; - final String merchantAccount; - final String reference; - final String returnUrl; + final String sessionData; - final String? shopperLocale; - final String? shopperReference; - final String? storePaymentMethodMode; - final String? recurringProcessingModel; - final String? telephoneNumber; - final String? dateOfBirth; - final String? socialSecurityNumber; - final DeliveryAddress? deliveryAddress; - final BillingAddress? billingAddress; - final List? lineItems; SessionResponseNetworkModel({ - required this.amount, - required this.countryCode, - required this.expiresAt, required this.id, - required this.merchantAccount, - required this.reference, - required this.returnUrl, required this.sessionData, - this.shopperLocale, - this.shopperReference, - this.storePaymentMethodMode, - this.recurringProcessingModel, - this.telephoneNumber, - this.dateOfBirth, - this.socialSecurityNumber, - this.deliveryAddress, - this.billingAddress, - this.lineItems, }); factory SessionResponseNetworkModel.fromRawJson(String str) => @@ -51,28 +15,7 @@ class SessionResponseNetworkModel { factory SessionResponseNetworkModel.fromJson(Map json) => SessionResponseNetworkModel( - amount: AmountNetworkModel.fromJson(json["amount"]), - countryCode: json["countryCode"], - expiresAt: DateTime.parse(json["expiresAt"]), id: json["id"], - merchantAccount: json["merchantAccount"], - reference: json["reference"], - returnUrl: json["returnUrl"], sessionData: json["sessionData"], - shopperLocale: json["shopperLocale"], - shopperReference: json["shopperReference"], - storePaymentMethodMode: json["storePaymentMethodMode"], - recurringProcessingModel: json["recurringProcessingModel"], - telephoneNumber: json["telephoneNumber"], - dateOfBirth: json["dateOfBirth"], - socialSecurityNumber: json["socialSecurityNumber"], - deliveryAddress: json["deliveryAddress"] != null - ? DeliveryAddress.fromJson(json["deliveryAddress"]) - : null, - billingAddress: json["billingAddress"] != null - ? BillingAddress.fromJson(json["billingAddress"]) - : null, - lineItems: List.from( - json["lineItems"].map((model) => LineItem.fromJson(model))), ); } diff --git a/example/lib/repositories/adyen_base_repository.dart b/example/lib/repositories/adyen_base_repository.dart new file mode 100644 index 00000000..228c0c65 --- /dev/null +++ b/example/lib/repositories/adyen_base_repository.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:adyen_checkout/adyen_checkout.dart'; +import 'package:adyen_checkout_example/config.dart'; +import 'package:adyen_checkout_example/network/service.dart'; +import 'package:adyen_checkout_example/repositories/payment_flow_outcome_handler.dart'; + +class AdyenBaseRepository { + AdyenBaseRepository({ + required this.adyenCheckout, + required this.service, + }); + + final AdyenCheckout adyenCheckout; + final Service service; + final PaymentFlowOutcomeHandler paymentFlowOutcomeHandler = + PaymentFlowOutcomeHandler(); + + Future determineBaseReturnUrl() async { + if (Platform.isAndroid) { + return await adyenCheckout.getReturnUrl(); + } else if (Platform.isIOS) { + return Config.iOSReturnUrl; + } else { + throw Exception("Unsupported platform"); + } + } + + String determineChannel() { + if (Platform.isAndroid) { + return "Android"; + } + + if (Platform.isIOS) { + return "iOS"; + } + + throw Exception("Unsupported platform"); + } +} diff --git a/example/lib/repositories/adyen_card_component_repository.dart b/example/lib/repositories/adyen_card_component_repository.dart new file mode 100644 index 00000000..913472f1 --- /dev/null +++ b/example/lib/repositories/adyen_card_component_repository.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:adyen_checkout/adyen_checkout.dart'; +import 'package:adyen_checkout_example/config.dart'; +import 'package:adyen_checkout_example/network/models/amount_network_model.dart'; +import 'package:adyen_checkout_example/network/models/payment_methods_request_network_model.dart'; +import 'package:adyen_checkout_example/network/models/payment_request_network_model.dart'; +import 'package:adyen_checkout_example/network/models/session_request_network_model.dart'; +import 'package:adyen_checkout_example/network/models/session_response_network_model.dart'; +import 'package:adyen_checkout_example/repositories/adyen_base_repository.dart'; + +class AdyenCardComponentRepository extends AdyenBaseRepository { + AdyenCardComponentRepository({ + required super.adyenCheckout, + required super.service, + }); + + Future createSession() async { + String returnUrl = await determineBaseReturnUrl(); + returnUrl += "/card"; + SessionRequestNetworkModel sessionRequestNetworkModel = + SessionRequestNetworkModel( + merchantAccount: Config.merchantAccount, + amount: AmountNetworkModel( + currency: Config.amount.currency, + value: Config.amount.value, + ), + returnUrl: returnUrl, + reference: + "flutter-session-test_${DateTime.now().millisecondsSinceEpoch}", + countryCode: Config.countryCode, + shopperLocale: Config.shopperLocale, + shopperReference: Config.shopperReference, + storePaymentMethodMode: + StorePaymentMethodMode.enabled.storePaymentMethodModeString, + recurringProcessingModel: + RecurringProcessingModel.cardOnFile.recurringModelString, + shopperInteraction: + ShopperInteractionModel.ecommerce.shopperInteractionModelString, + channel: determineChannel(), + ); + + return await service.createSession( + sessionRequestNetworkModel, + Config.environment, + ); + } + + Future fetchPaymentMethods() async { + return await service.fetchPaymentMethods(PaymentMethodsRequestNetworkModel( + merchantAccount: Config.merchantAccount, + countryCode: Config.countryCode, + channel: determineChannel(), + shopperReference: Config.shopperReference, + )); + } + + Future postPayments(String paymentComponentJson) async { + String returnUrl = await determineBaseReturnUrl(); + returnUrl += "/card"; + PaymentsRequestData paymentsRequestData = PaymentsRequestData( + merchantAccount: Config.merchantAccount, + shopperReference: Config.shopperReference, + reference: "flutter-test_${DateTime.now().millisecondsSinceEpoch}", + returnUrl: returnUrl, + amount: AmountNetworkModel( + value: Config.amount.value, + currency: Config.amount.currency, + ), + countryCode: Config.countryCode, + channel: determineChannel(), + additionalData: AdditionalData(allow3DS2: true, executeThreeD: true), + threeDS2RequestData: ThreeDS2RequestDataRequest(), + threeDSAuthenticationOnly: false, + recurringProcessingModel: RecurringProcessingModel.cardOnFile, + shopperInteraction: + ShopperInteractionModel.ecommerce.shopperInteractionModelString, + ); + + Map mergedJson = {}; + mergedJson.addAll(jsonDecode(paymentComponentJson)); + mergedJson.addAll(paymentsRequestData.toJson()); + final response = await service.postPayments(mergedJson); + return paymentFlowOutcomeHandler.handleResponse(response); + } + + Future postPaymentsDetails( + String additionalDetails) async { + final response = + await service.postPaymentsDetails(jsonDecode(additionalDetails)); + return paymentFlowOutcomeHandler.handleResponse(response); + } + + Future deleteStoredPaymentMethod(String storedPaymentMethodId) async { + return await service.deleteStoredPaymentMethod( + storedPaymentMethodId: storedPaymentMethodId, + merchantAccount: Config.merchantAccount, + shopperReference: Config.shopperReference, + ); + } +} diff --git a/example/lib/repositories/adyen_sessions_repository.dart b/example/lib/repositories/adyen_drop_in_repository.dart similarity index 68% rename from example/lib/repositories/adyen_sessions_repository.dart rename to example/lib/repositories/adyen_drop_in_repository.dart index 0bdd1ab1..6411d5b8 100644 --- a/example/lib/repositories/adyen_sessions_repository.dart +++ b/example/lib/repositories/adyen_drop_in_repository.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:io'; import 'package:adyen_checkout/adyen_checkout.dart'; import 'package:adyen_checkout_example/config.dart'; @@ -11,30 +10,24 @@ import 'package:adyen_checkout_example/network/models/payment_methods_request_ne import 'package:adyen_checkout_example/network/models/payment_request_network_model.dart'; import 'package:adyen_checkout_example/network/models/session_request_network_model.dart'; import 'package:adyen_checkout_example/network/models/session_response_network_model.dart'; -import 'package:adyen_checkout_example/network/service.dart'; -import 'package:adyen_checkout_example/repositories/payment_flow_outcome_handler.dart'; +import 'package:adyen_checkout_example/repositories/adyen_base_repository.dart'; -class AdyenSessionsRepository { - AdyenSessionsRepository( - {required AdyenCheckout adyenCheckout, required Service service}) - : _service = service, - _adyenCheckout = adyenCheckout; - - final AdyenCheckout _adyenCheckout; - final Service _service; - final PaymentFlowOutcomeHandler _paymentFlowOutcomeHandler = - PaymentFlowOutcomeHandler(); +class AdyenDropInRepository extends AdyenBaseRepository { + AdyenDropInRepository({ + required super.adyenCheckout, + required super.service, + }); //A session should not being created from the mobile application. //Please provide a CheckoutSession object from your own backend. - Future createSession(Amount amount, Environment environment) async { - String returnUrl = await determineExampleReturnUrl(); + Future createSession() async { + String returnUrl = await determineBaseReturnUrl(); SessionRequestNetworkModel sessionRequestNetworkModel = SessionRequestNetworkModel( merchantAccount: Config.merchantAccount, amount: AmountNetworkModel( - currency: amount.currency, - value: amount.value, + currency: Config.amount.currency, + value: Config.amount.value, ), returnUrl: returnUrl, reference: @@ -42,13 +35,13 @@ class AdyenSessionsRepository { countryCode: Config.countryCode, shopperLocale: Config.shopperLocale, shopperReference: Config.shopperReference, - storePaymentMethodMode: StorePaymentMethodMode - .askForConsent.storePaymentMethodModeString, + storePaymentMethodMode: + StorePaymentMethodMode.enabled.storePaymentMethodModeString, recurringProcessingModel: RecurringProcessingModel.cardOnFile.recurringModelString, shopperInteraction: ShopperInteractionModel.ecommerce.shopperInteractionModelString, - channel: _determineChannel(), + channel: determineChannel(), telephoneNumber: "+8613012345678", dateOfBirth: "1996-09-04", socialSecurityNumber: "0108", @@ -80,26 +73,23 @@ class AdyenSessionsRepository { ), ]); - SessionResponseNetworkModel sessionResponseNetworkModel = - await _service.createSession(sessionRequestNetworkModel, environment); - - return Session( - id: sessionResponseNetworkModel.id, - sessionData: sessionResponseNetworkModel.sessionData, + return await service.createSession( + sessionRequestNetworkModel, + Config.environment, ); } Future fetchPaymentMethods() async { - return await _service.fetchPaymentMethods(PaymentMethodsRequestNetworkModel( + return await service.fetchPaymentMethods(PaymentMethodsRequestNetworkModel( merchantAccount: Config.merchantAccount, countryCode: Config.countryCode, - channel: _determineChannel(), + channel: determineChannel(), shopperReference: Config.shopperReference, )); } Future postPayments(String paymentComponentJson) async { - String returnUrl = await determineExampleReturnUrl(); + String returnUrl = await determineBaseReturnUrl(); PaymentsRequestData paymentsRequestData = PaymentsRequestData( merchantAccount: Config.merchantAccount, shopperReference: Config.shopperReference, @@ -110,7 +100,7 @@ class AdyenSessionsRepository { currency: Config.amount.currency, ), countryCode: Config.countryCode, - channel: _determineChannel(), + channel: determineChannel(), additionalData: AdditionalData(allow3DS2: true, executeThreeD: true), threeDS2RequestData: ThreeDS2RequestDataRequest(), threeDSAuthenticationOnly: false, @@ -135,44 +125,22 @@ class AdyenSessionsRepository { Map mergedJson = {}; mergedJson.addAll(jsonDecode(paymentComponentJson)); mergedJson.addAll(paymentsRequestData.toJson()); - final response = await _service.postPayments(mergedJson); - return _paymentFlowOutcomeHandler.handleResponse(response); + final response = await service.postPayments(mergedJson); + return paymentFlowOutcomeHandler.handleResponse(response); } Future postPaymentsDetails( String additionalDetails) async { final response = - await _service.postPaymentsDetails(jsonDecode(additionalDetails)); - return _paymentFlowOutcomeHandler.handleResponse(response); - } - - Future determineExampleReturnUrl() async { - if (Platform.isAndroid) { - return await _adyenCheckout.getReturnUrl(); - } else if (Platform.isIOS) { - return Config.iOSReturnUrl; - } else { - throw Exception("Unsupported platform"); - } + await service.postPaymentsDetails(jsonDecode(additionalDetails)); + return paymentFlowOutcomeHandler.handleResponse(response); } Future deleteStoredPaymentMethod(String storedPaymentMethodId) async { - return await _service.deleteStoredPaymentMethod( + return await service.deleteStoredPaymentMethod( storedPaymentMethodId: storedPaymentMethodId, merchantAccount: Config.merchantAccount, shopperReference: Config.shopperReference, ); } - - String _determineChannel() { - if (Platform.isAndroid) { - return "Android"; - } - - if (Platform.isIOS) { - return "iOS"; - } - - throw Exception("Unsupported platform"); - } } diff --git a/example/lib/repositories/payment_flow_outcome_handler.dart b/example/lib/repositories/payment_flow_outcome_handler.dart index 9380779f..085d17f0 100644 --- a/example/lib/repositories/payment_flow_outcome_handler.dart +++ b/example/lib/repositories/payment_flow_outcome_handler.dart @@ -27,6 +27,7 @@ class PaymentFlowOutcomeHandler { if (_isRefusedInPartialPaymentFlow(jsonResponse)) { return Error( + errorMessage: "Payment is refused", reason: "Refused", dismissDropIn: true, ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6284c3e6..cc45ac3e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,6 +13,9 @@ dependencies: adyen_checkout: path: ../ http: 1.1.0 + flutter_localizations: + sdk: flutter + intl: any dev_dependencies: integration_test: diff --git a/ios/Classes/AdyenCheckoutPlugin.swift b/ios/Classes/AdyenCheckoutPlugin.swift index e05deb94..02e44bb6 100644 --- a/ios/Classes/AdyenCheckoutPlugin.swift +++ b/ios/Classes/AdyenCheckoutPlugin.swift @@ -4,8 +4,17 @@ import UIKit public class AdyenCheckoutPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let messenger: FlutterBinaryMessenger = registrar.messenger() + + // DropIn let checkoutFlutterApi = CheckoutFlutterApi(binaryMessenger: messenger) let checkoutPlatformApi = CheckoutPlatformApi(checkoutFlutterApi: checkoutFlutterApi) CheckoutPlatformInterfaceSetup.setUp(binaryMessenger: messenger, api: checkoutPlatformApi) + + // Component + let componentFlutterApi = ComponentFlutterInterface(binaryMessenger: messenger) + let cardComponentAdvancedFlowFactory = CardAdvancedFlowComponentFactory(messenger: messenger, componentFlutterApi: componentFlutterApi) + registrar.register(cardComponentAdvancedFlowFactory, withId: "cardComponentAdvancedFlow") + let cardComponentSessionFlowFactory = CardSessionFlowComponentFactory(messenger: messenger, componentFlutterApi: componentFlutterApi) + registrar.register(cardComponentSessionFlowFactory, withId: "cardComponentSessionFlow") } } diff --git a/ios/Classes/CheckoutPlatformApi.swift b/ios/Classes/CheckoutPlatformApi.swift index 95a9000d..fc150247 100644 --- a/ios/Classes/CheckoutPlatformApi.swift +++ b/ios/Classes/CheckoutPlatformApi.swift @@ -39,7 +39,7 @@ class CheckoutPlatformApi: CheckoutPlatformInterface { self.viewController = viewController dropInSessionDelegate = DropInSessionsDelegate(viewController: viewController, checkoutFlutterApi: checkoutFlutterApi) dropInSessionPresentationDelegate = DropInSessionsPresentationDelegate() - let adyenContext = try createAdyenContext(dropInConfiguration: dropInConfigurationDTO) + let adyenContext = try dropInConfigurationDTO.createAdyenContext() let sessionConfiguration = AdyenSession.Configuration(sessionIdentifier: session.id, initialSessionData: session.sessionData, context: adyenContext) @@ -83,7 +83,7 @@ class CheckoutPlatformApi: CheckoutPlatformInterface { return } self.viewController = viewController - let adyenContext = try createAdyenContext(dropInConfiguration: dropInConfigurationDTO) + let adyenContext = try dropInConfigurationDTO.createAdyenContext() let paymentMethods = try jsonDecoder.decode(PaymentMethods.self, from: Data(paymentMethodsResponse.utf8)) let paymentMethodsWithoutGiftCards = removeGiftCardPaymentMethods(paymentMethods: paymentMethods) let configuration = try configurationMapper.createDropInConfiguration(dropInConfigurationDTO: dropInConfigurationDTO) @@ -153,32 +153,6 @@ class CheckoutPlatformApi: CheckoutPlatformInterface { return rootViewController } - private func createAdyenContext(dropInConfiguration: DropInConfigurationDTO) throws -> AdyenContext { - let environment = mapToEnvironment(environment: dropInConfiguration.environment) - let apiContext = try APIContext(environment: environment, clientKey: dropInConfiguration.clientKey) - let value = Int(dropInConfiguration.amount.value) - let currencyCode = dropInConfiguration.amount.currency - let amount = Adyen.Amount(value: value, currencyCode: currencyCode) - return AdyenContext(apiContext: apiContext, payment: Payment(amount: amount, countryCode: dropInConfiguration.countryCode)) - } - - private func mapToEnvironment(environment: Environment) -> Adyen.Environment { - switch environment { - case .test: - return Adyen.Environment.test - case .europe: - return .liveEurope - case .unitedStates: - return .liveUnitedStates - case .australia: - return .liveAustralia - case .india: - return .liveIndia - case .apse: - return .liveApse - } - } - private func handlePaymentFlowOutcome(paymentFlowOutcomeDTO: PaymentFlowOutcomeDTO) { switch paymentFlowOutcomeDTO.paymentFlowResultType { case .finished: @@ -193,7 +167,7 @@ class CheckoutPlatformApi: CheckoutPlatformInterface { private func onDropInResultFinished(paymentFlowOutcome: PaymentFlowOutcomeDTO) { let resultCode = ResultCode(rawValue: paymentFlowOutcome.result ?? "") let success = resultCode == .authorised || resultCode == .received || resultCode == .pending - finalizeAndDismiss(success: true, completion: { [weak self] in + finalizeAndDismiss(success: success, completion: { [weak self] in let paymentResult = PaymentResultDTO(type: PaymentResultEnum.finished, result: PaymentResultModelDTO(resultCode: resultCode?.rawValue)) self?.checkoutFlutterApi.onDropInAdvancedFlowPlatformCommunication(platformCommunicationModel: PlatformCommunicationModel(type: PlatformCommunicationType.result, paymentResult: paymentResult), completion: { _ in }) }) @@ -242,8 +216,8 @@ class CheckoutPlatformApi: CheckoutPlatformInterface { } } -extension CheckoutPlatformApi : DropInInteractorDelegate { - func finalizeAndDismiss(success: Bool, completion: @escaping (() -> Void)) { +extension CheckoutPlatformApi: DropInInteractorDelegate { + func finalizeAndDismiss(success: Bool, completion: @escaping (() -> Void)) { dropInComponent?.finalizeIfNeeded(with: success) { [weak self] in self?.viewController?.dismiss(animated: true, completion: { completion() diff --git a/ios/Classes/DropInInteractorDelegate.swift b/ios/Classes/DropInInteractorDelegate.swift index cf83df65..d0bf55d4 100644 --- a/ios/Classes/DropInInteractorDelegate.swift +++ b/ios/Classes/DropInInteractorDelegate.swift @@ -1,4 +1,3 @@ -public protocol DropInInteractorDelegate : AnyObject { - +public protocol DropInInteractorDelegate: AnyObject { func finalizeAndDismiss(success: Bool, completion: @escaping (() -> Void)) } diff --git a/ios/Classes/PlatformApi.swift b/ios/Classes/PlatformApi.swift index db6667c7..07ecc4cc 100644 --- a/ios/Classes/PlatformApi.swift +++ b/ios/Classes/PlatformApi.swift @@ -87,6 +87,14 @@ enum PlatformCommunicationType: Int { case deleteStoredPaymentMethod = 3 } +enum ComponentCommunicationType: Int { + case onSubmit = 0 + case additionalDetails = 1 + case result = 2 + case error = 3 + case resize = 4 +} + enum PaymentFlowResultType: Int { case finished = 0 case action = 1 @@ -171,7 +179,7 @@ struct DropInConfigurationDTO { var countryCode: String var amount: AmountDTO var shopperLocale: String - var cardsConfigurationDTO: CardsConfigurationDTO? = nil + var cardConfigurationDTO: CardConfigurationDTO? = nil var applePayConfigurationDTO: ApplePayConfigurationDTO? = nil var googlePayConfigurationDTO: GooglePayConfigurationDTO? = nil var cashAppPayConfigurationDTO: CashAppPayConfigurationDTO? = nil @@ -186,9 +194,9 @@ struct DropInConfigurationDTO { let countryCode = list[2] as! String let amount = AmountDTO.fromList(list[3] as! [Any?])! let shopperLocale = list[4] as! String - var cardsConfigurationDTO: CardsConfigurationDTO? = nil - if let cardsConfigurationDTOList: [Any?] = nilOrValue(list[5]) { - cardsConfigurationDTO = CardsConfigurationDTO.fromList(cardsConfigurationDTOList) + var cardConfigurationDTO: CardConfigurationDTO? = nil + if let cardConfigurationDTOList: [Any?] = nilOrValue(list[5]) { + cardConfigurationDTO = CardConfigurationDTO.fromList(cardConfigurationDTOList) } var applePayConfigurationDTO: ApplePayConfigurationDTO? = nil if let applePayConfigurationDTOList: [Any?] = nilOrValue(list[6]) { @@ -216,7 +224,7 @@ struct DropInConfigurationDTO { countryCode: countryCode, amount: amount, shopperLocale: shopperLocale, - cardsConfigurationDTO: cardsConfigurationDTO, + cardConfigurationDTO: cardConfigurationDTO, applePayConfigurationDTO: applePayConfigurationDTO, googlePayConfigurationDTO: googlePayConfigurationDTO, cashAppPayConfigurationDTO: cashAppPayConfigurationDTO, @@ -233,7 +241,7 @@ struct DropInConfigurationDTO { countryCode, amount.toList(), shopperLocale, - cardsConfigurationDTO?.toList(), + cardConfigurationDTO?.toList(), applePayConfigurationDTO?.toList(), googlePayConfigurationDTO?.toList(), cashAppPayConfigurationDTO?.toList(), @@ -246,7 +254,7 @@ struct DropInConfigurationDTO { } /// Generated class from Pigeon that represents data sent in messages. -struct CardsConfigurationDTO { +struct CardConfigurationDTO { var holderNameRequired: Bool var addressMode: AddressMode var showStorePaymentField: Bool @@ -256,7 +264,7 @@ struct CardsConfigurationDTO { var socialSecurityNumberFieldVisibility: FieldVisibility var supportedCardTypes: [String?] - static func fromList(_ list: [Any?]) -> CardsConfigurationDTO? { + static func fromList(_ list: [Any?]) -> CardConfigurationDTO? { let holderNameRequired = list[0] as! Bool let addressMode = AddressMode(rawValue: list[1] as! Int)! let showStorePaymentField = list[2] as! Bool @@ -266,7 +274,7 @@ struct CardsConfigurationDTO { let socialSecurityNumberFieldVisibility = FieldVisibility(rawValue: list[6] as! Int)! let supportedCardTypes = list[7] as! [String?] - return CardsConfigurationDTO( + return CardConfigurationDTO( holderNameRequired: holderNameRequired, addressMode: addressMode, showStorePaymentField: showStorePaymentField, @@ -430,21 +438,24 @@ struct PaymentResultDTO { struct PaymentResultModelDTO { var sessionId: String? = nil var sessionData: String? = nil + var sessionResult: String? = nil var resultCode: String? = nil var order: OrderResponseDTO? = nil static func fromList(_ list: [Any?]) -> PaymentResultModelDTO? { let sessionId: String? = nilOrValue(list[0]) let sessionData: String? = nilOrValue(list[1]) - let resultCode: String? = nilOrValue(list[2]) + let sessionResult: String? = nilOrValue(list[2]) + let resultCode: String? = nilOrValue(list[3]) var order: OrderResponseDTO? = nil - if let orderList: [Any?] = nilOrValue(list[3]) { + if let orderList: [Any?] = nilOrValue(list[4]) { order = OrderResponseDTO.fromList(orderList) } return PaymentResultModelDTO( sessionId: sessionId, sessionData: sessionData, + sessionResult: sessionResult, resultCode: resultCode, order: order ) @@ -453,6 +464,7 @@ struct PaymentResultModelDTO { return [ sessionId, sessionData, + sessionResult, resultCode, order?.toList(), ] @@ -524,6 +536,35 @@ struct PlatformCommunicationModel { } } +/// Generated class from Pigeon that represents data sent in messages. +struct ComponentCommunicationModel { + var type: ComponentCommunicationType + var data: Any? = nil + var paymentResult: PaymentResultModelDTO? = nil + + static func fromList(_ list: [Any?]) -> ComponentCommunicationModel? { + let type = ComponentCommunicationType(rawValue: list[0] as! Int)! + let data: Any? = list[1] + var paymentResult: PaymentResultModelDTO? = nil + if let paymentResultList: [Any?] = nilOrValue(list[2]) { + paymentResult = PaymentResultModelDTO.fromList(paymentResultList) + } + + return ComponentCommunicationModel( + type: type, + data: data, + paymentResult: paymentResult + ) + } + func toList() -> [Any?] { + return [ + type.rawValue, + data, + paymentResult?.toList(), + ] + } +} + /// Generated class from Pigeon that represents data sent in messages. struct PaymentFlowOutcomeDTO { var paymentFlowResultType: PaymentFlowResultType @@ -605,6 +646,44 @@ struct DeletedStoredPaymentMethodResultDTO { } } +/// Generated class from Pigeon that represents data sent in messages. +struct CardComponentConfigurationDTO { + var environment: Environment + var clientKey: String + var countryCode: String + var amount: AmountDTO + var shopperLocale: String? = nil + var cardConfiguration: CardConfigurationDTO + + static func fromList(_ list: [Any?]) -> CardComponentConfigurationDTO? { + let environment = Environment(rawValue: list[0] as! Int)! + let clientKey = list[1] as! String + let countryCode = list[2] as! String + let amount = AmountDTO.fromList(list[3] as! [Any?])! + let shopperLocale: String? = nilOrValue(list[4]) + let cardConfiguration = CardConfigurationDTO.fromList(list[5] as! [Any?])! + + return CardComponentConfigurationDTO( + environment: environment, + clientKey: clientKey, + countryCode: countryCode, + amount: amount, + shopperLocale: shopperLocale, + cardConfiguration: cardConfiguration + ) + } + func toList() -> [Any?] { + return [ + environment.rawValue, + clientKey, + countryCode, + amount.toList(), + shopperLocale, + cardConfiguration.toList(), + ] + } +} + private class CheckoutPlatformInterfaceCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { @@ -615,7 +694,7 @@ private class CheckoutPlatformInterfaceCodecReader: FlutterStandardReader { case 130: return ApplePayConfigurationDTO.fromList(self.readValue() as! [Any?]) case 131: - return CardsConfigurationDTO.fromList(self.readValue() as! [Any?]) + return CardConfigurationDTO.fromList(self.readValue() as! [Any?]) case 132: return CashAppPayConfigurationDTO.fromList(self.readValue() as! [Any?]) case 133: @@ -647,7 +726,7 @@ private class CheckoutPlatformInterfaceCodecWriter: FlutterStandardWriter { } else if let value = value as? ApplePayConfigurationDTO { super.writeByte(130) super.writeValue(value.toList()) - } else if let value = value as? CardsConfigurationDTO { + } else if let value = value as? CardConfigurationDTO { super.writeByte(131) super.writeValue(value.toList()) } else if let value = value as? CashAppPayConfigurationDTO { @@ -929,3 +1008,201 @@ class CheckoutFlutterApi: CheckoutFlutterApiProtocol { } } } +private class ComponentPlatformInterfaceCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return ErrorDTO.fromList(self.readValue() as! [Any?]) + case 129: + return PaymentFlowOutcomeDTO.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class ComponentPlatformInterfaceCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? ErrorDTO { + super.writeByte(128) + super.writeValue(value.toList()) + } else if let value = value as? PaymentFlowOutcomeDTO { + super.writeByte(129) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class ComponentPlatformInterfaceCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return ComponentPlatformInterfaceCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return ComponentPlatformInterfaceCodecWriter(data: data) + } +} + +class ComponentPlatformInterfaceCodec: FlutterStandardMessageCodec { + static let shared = ComponentPlatformInterfaceCodec(readerWriter: ComponentPlatformInterfaceCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol ComponentPlatformInterface { + func updateViewHeight(viewId: Int64) throws + func onPaymentsResult(paymentsResult: PaymentFlowOutcomeDTO) throws + func onPaymentsDetailsResult(paymentsDetailsResult: PaymentFlowOutcomeDTO) throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class ComponentPlatformInterfaceSetup { + /// The codec used by ComponentPlatformInterface. + static var codec: FlutterStandardMessageCodec { ComponentPlatformInterfaceCodec.shared } + /// Sets up an instance of `ComponentPlatformInterface` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ComponentPlatformInterface?) { + let updateViewHeightChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.updateViewHeight", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + updateViewHeightChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let viewIdArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) + do { + try api.updateViewHeight(viewId: viewIdArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + updateViewHeightChannel.setMessageHandler(nil) + } + let onPaymentsResultChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.onPaymentsResult", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + onPaymentsResultChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let paymentsResultArg = args[0] as! PaymentFlowOutcomeDTO + do { + try api.onPaymentsResult(paymentsResult: paymentsResultArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + onPaymentsResultChannel.setMessageHandler(nil) + } + let onPaymentsDetailsResultChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.onPaymentsDetailsResult", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + onPaymentsDetailsResultChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let paymentsDetailsResultArg = args[0] as! PaymentFlowOutcomeDTO + do { + try api.onPaymentsDetailsResult(paymentsDetailsResult: paymentsDetailsResultArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + onPaymentsDetailsResultChannel.setMessageHandler(nil) + } + } +} +private class ComponentFlutterInterfaceCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return AmountDTO.fromList(self.readValue() as! [Any?]) + case 129: + return AmountDTO.fromList(self.readValue() as! [Any?]) + case 130: + return CardComponentConfigurationDTO.fromList(self.readValue() as! [Any?]) + case 131: + return CardConfigurationDTO.fromList(self.readValue() as! [Any?]) + case 132: + return ComponentCommunicationModel.fromList(self.readValue() as! [Any?]) + case 133: + return OrderResponseDTO.fromList(self.readValue() as! [Any?]) + case 134: + return PaymentResultModelDTO.fromList(self.readValue() as! [Any?]) + case 135: + return SessionDTO.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class ComponentFlutterInterfaceCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? AmountDTO { + super.writeByte(128) + super.writeValue(value.toList()) + } else if let value = value as? AmountDTO { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? CardComponentConfigurationDTO { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? CardConfigurationDTO { + super.writeByte(131) + super.writeValue(value.toList()) + } else if let value = value as? ComponentCommunicationModel { + super.writeByte(132) + super.writeValue(value.toList()) + } else if let value = value as? OrderResponseDTO { + super.writeByte(133) + super.writeValue(value.toList()) + } else if let value = value as? PaymentResultModelDTO { + super.writeByte(134) + super.writeValue(value.toList()) + } else if let value = value as? SessionDTO { + super.writeByte(135) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class ComponentFlutterInterfaceCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return ComponentFlutterInterfaceCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return ComponentFlutterInterfaceCodecWriter(data: data) + } +} + +class ComponentFlutterInterfaceCodec: FlutterStandardMessageCodec { + static let shared = ComponentFlutterInterfaceCodec(readerWriter: ComponentFlutterInterfaceCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol ComponentFlutterInterfaceProtocol { + func _generateCodecForDTOs(cardComponentConfigurationDTO cardComponentConfigurationDTOArg: CardComponentConfigurationDTO, sessionDTO sessionDTOArg: SessionDTO, completion: @escaping (Result) -> Void) + func onComponentCommunication(componentCommunicationModel componentCommunicationModelArg: ComponentCommunicationModel, completion: @escaping (Result) -> Void) +} +class ComponentFlutterInterface: ComponentFlutterInterfaceProtocol { + private let binaryMessenger: FlutterBinaryMessenger + init(binaryMessenger: FlutterBinaryMessenger){ + self.binaryMessenger = binaryMessenger + } + var codec: FlutterStandardMessageCodec { + return ComponentFlutterInterfaceCodec.shared + } + func _generateCodecForDTOs(cardComponentConfigurationDTO cardComponentConfigurationDTOArg: CardComponentConfigurationDTO, sessionDTO sessionDTOArg: SessionDTO, completion: @escaping (Result) -> Void) { + let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface._generateCodecForDTOs", binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([cardComponentConfigurationDTOArg, sessionDTOArg] as [Any?]) { _ in + completion(.success(Void())) + } + } + func onComponentCommunication(componentCommunicationModel componentCommunicationModelArg: ComponentCommunicationModel, completion: @escaping (Result) -> Void) { + let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface.onComponentCommunication", binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([componentCommunicationModelArg] as [Any?]) { _ in + completion(.success(Void())) + } + } +} diff --git a/ios/Classes/components/ComponentFactory.swift b/ios/Classes/components/ComponentFactory.swift new file mode 100644 index 00000000..6af3145a --- /dev/null +++ b/ios/Classes/components/ComponentFactory.swift @@ -0,0 +1,25 @@ +import Flutter + +class ComponentFactory: NSObject, FlutterPlatformViewFactory { + let messenger: FlutterBinaryMessenger + let componentFlutterApi: ComponentFlutterInterface + + init(messenger: FlutterBinaryMessenger, componentFlutterApi: ComponentFlutterInterface) { + self.messenger = messenger + self.componentFlutterApi = componentFlutterApi + + super.init() + } + + func create( + withFrame _: CGRect, + viewIdentifier _: Int64, + arguments _: Any? + ) -> FlutterPlatformView { + fatalError("Subclasses need to implement the `create()` method.") + } + + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return componentFlutterApi.codec + } +} diff --git a/ios/Classes/components/ComponentPlatformApi.swift b/ios/Classes/components/ComponentPlatformApi.swift new file mode 100644 index 00000000..c587cb4a --- /dev/null +++ b/ios/Classes/components/ComponentPlatformApi.swift @@ -0,0 +1,31 @@ +class ComponentPlatformApi: ComponentPlatformInterface { + //TODO: Group callbacks in a weak delegate + var onUpdateViewHeightCallback: () -> Void = { } + var onActionCallback: ([String?: Any?]) -> Void = { _ in } + var onFinishCallback: (PaymentFlowOutcomeDTO) -> Void = { _ in } + var onErrorCallback: (ErrorDTO?) -> Void = { _ in } + + func updateViewHeight(viewId: Int64) { + onUpdateViewHeightCallback() + } + + func onPaymentsResult(paymentsResult: PaymentFlowOutcomeDTO) { + handlePaymentFlowOutcome(paymentFlowOutcomeDTO: paymentsResult) + } + + func onPaymentsDetailsResult(paymentsDetailsResult: PaymentFlowOutcomeDTO) { + handlePaymentFlowOutcome(paymentFlowOutcomeDTO: paymentsDetailsResult) + } + + private func handlePaymentFlowOutcome(paymentFlowOutcomeDTO: PaymentFlowOutcomeDTO) { + switch paymentFlowOutcomeDTO.paymentFlowResultType { + case .finished: + onFinishCallback(paymentFlowOutcomeDTO) + case .action: + guard let jsonActionResponse = paymentFlowOutcomeDTO.actionResponse else { return } + onActionCallback(jsonActionResponse) + case .error: + onErrorCallback(paymentFlowOutcomeDTO.error) + } + } +} diff --git a/ios/Classes/components/ComponentWrapperView.swift b/ios/Classes/components/ComponentWrapperView.swift new file mode 100644 index 00000000..8329d417 --- /dev/null +++ b/ios/Classes/components/ComponentWrapperView.swift @@ -0,0 +1,18 @@ +class ComponentWrapperView: UIStackView { + var resizeViewportCallback: () -> Void = {} + + init() { + super.init(frame: .zero) + } + + @available(*, unavailable) + required init(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + resizeViewportCallback() + } +} diff --git a/ios/Classes/components/card/BaseCardComponent.swift b/ios/Classes/components/card/BaseCardComponent.swift new file mode 100644 index 00000000..3b37eeaa --- /dev/null +++ b/ios/Classes/components/card/BaseCardComponent.swift @@ -0,0 +1,103 @@ +@_spi(AdyenInternal) +import Adyen +import AdyenNetworking +import Flutter + +class BaseCardComponent: NSObject, FlutterPlatformView, UIScrollViewDelegate { + let componentFlutterApi: ComponentFlutterInterface + let componentPlatformApi: ComponentPlatformApi + let componentWrapperView: ComponentWrapperView + let configurationMapper = ConfigurationMapper() + + var cardComponent: CardComponent? + var cardDelegate: PaymentComponentDelegate? + var contentOffset : CGPoint? + + init( + frame _: CGRect, + viewIdentifier _: Int64, + arguments _: NSDictionary, + binaryMessenger: FlutterBinaryMessenger, + componentFlutterApi: ComponentFlutterInterface + ) { + self.componentFlutterApi = componentFlutterApi + componentPlatformApi = ComponentPlatformApi() + componentWrapperView = .init() + ComponentPlatformInterfaceSetup.setUp(binaryMessenger: binaryMessenger, api: componentPlatformApi) + super.init() + + setupResizeViewportCallback() + } + + func view() -> UIView { + return componentWrapperView + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + scrollView.contentOffset = .zero + } + + func getViewController() -> UIViewController? { + var rootViewController = UIApplication.shared.adyen.mainKeyWindow?.rootViewController + while let presentedViewController = rootViewController?.presentedViewController { + rootViewController = presentedViewController + } + + return rootViewController + } + + func attachCardView(cardComponentView: UIView) { + componentWrapperView.addSubview(cardComponentView) + disableNativeScrollingAndBouncing(cardComponentView: cardComponentView) + adjustCardComponentLayout(cardComponentView: cardComponentView) + sendHeightUpdate() + } + + func sendErrorToFlutterLayer(errorMessage: String) { + let componentCommunicationModel = ComponentCommunicationModel(type: ComponentCommunicationType.error, + data: errorMessage) + componentFlutterApi.onComponentCommunication(componentCommunicationModel: componentCommunicationModel, completion: { _ in }) + } + + func finalizeAndDismiss(success: Bool, completion: @escaping (() -> Void)) { + cardComponent?.finalizeIfNeeded(with: success) { [weak self] in + self?.getViewController()?.dismiss(animated: true , completion: { + completion() + }) + } + } + + private func disableNativeScrollingAndBouncing(cardComponentView: UIView) { + let formView = cardComponentView.subviews[0].subviews[0] as? UIScrollView + formView?.delegate = self + formView?.bounces = false + formView?.isScrollEnabled = false + formView?.alwaysBounceVertical = false + formView?.contentInsetAdjustmentBehavior = .never + } + + private func adjustCardComponentLayout(cardComponentView: UIView) { + cardComponentView.translatesAutoresizingMaskIntoConstraints = false + let leadingConstraint = cardComponentView.leadingAnchor.constraint(equalTo: componentWrapperView.leadingAnchor) + let trailingConstraint = cardComponentView.trailingAnchor.constraint(equalTo: componentWrapperView.trailingAnchor) + let topConstraint = cardComponentView.topAnchor.constraint(equalTo: componentWrapperView.topAnchor) + let bottomConstraint = cardComponentView.bottomAnchor.constraint(equalTo: componentWrapperView.bottomAnchor) + NSLayoutConstraint.activate([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint]) + } + + private func setupResizeViewportCallback() { + componentWrapperView.resizeViewportCallback = { [weak self] in + self?.sendHeightUpdate() + } + + componentPlatformApi.onUpdateViewHeightCallback = { [weak self] in + self?.sendHeightUpdate() + } + } + + private func sendHeightUpdate() { + guard let viewHeight = self.cardComponent?.viewController.preferredContentSize.height else { return } + let roundedViewHeight = Double(round(100 * viewHeight / 100)) + self.componentFlutterApi.onComponentCommunication(componentCommunicationModel: ComponentCommunicationModel(type: ComponentCommunicationType.resize, data: roundedViewHeight), completion: { _ in }) + } +} diff --git a/ios/Classes/components/card/CardPresentationDelegate.swift b/ios/Classes/components/card/CardPresentationDelegate.swift new file mode 100644 index 00000000..f1d354fc --- /dev/null +++ b/ios/Classes/components/card/CardPresentationDelegate.swift @@ -0,0 +1,32 @@ +import Adyen + +class CardPresentationDelegate: PresentationDelegate { + let topViewController: UIViewController? + var cardComponent: PresentableComponent? + + init(topViewController: UIViewController?) { + self.topViewController = topViewController + } + + func present(component: PresentableComponent) { + let componentViewController = viewController(for: component) + cardComponent = component + topViewController?.present(componentViewController, animated: true, completion: nil) + } + + private func viewController(for component: PresentableComponent) -> UIViewController { + guard component.requiresModalPresentation else { + return component.viewController + } + + let navigation = UINavigationController(rootViewController: component.viewController) + component.viewController.navigationItem.leftBarButtonItem = .init(barButtonSystemItem: .cancel, + target: self, + action: #selector(cancelPressed)) + return navigation + } + + @objc private func cancelPressed() { + cardComponent?.cancelIfNeeded() + } +} diff --git a/ios/Classes/components/card/advancedFlow/CardAdvancedFlowComponent.swift b/ios/Classes/components/card/advancedFlow/CardAdvancedFlowComponent.swift new file mode 100644 index 00000000..d078bf0d --- /dev/null +++ b/ios/Classes/components/card/advancedFlow/CardAdvancedFlowComponent.swift @@ -0,0 +1,96 @@ +@_spi(AdyenInternal) +import Adyen +import AdyenNetworking +import Flutter + +class CardAdvancedFlowComponent: BaseCardComponent { + private let actionComponentDelegate: ActionComponentDelegate + private var presentationDelegate: PresentationDelegate? + private var actionComponent: AdyenActionComponent? + private let initialFrame: CGRect + + override init( + frame: CGRect, + viewIdentifier: Int64, + arguments: NSDictionary, + binaryMessenger: FlutterBinaryMessenger, + componentFlutterApi: ComponentFlutterInterface + ) { + actionComponentDelegate = CardAdvancedFlowActionComponentDelegate(componentFlutterApi: componentFlutterApi) + initialFrame = frame + super.init( + frame: frame, + viewIdentifier: viewIdentifier, + arguments: arguments, + binaryMessenger: binaryMessenger, + componentFlutterApi: componentFlutterApi + ) + + setupCardComponentView(arguments: arguments) + setupFinalizeComponentCallback() + } + + private func setupCardComponentView(arguments: NSDictionary) { + do { + let cardComponentView = try createCardComponentView(arguments: arguments) + attachCardView(cardComponentView: cardComponentView) + componentPlatformApi.onActionCallback = { [weak self] jsonActionResponse in + self?.onAction(actionResponse: jsonActionResponse) + } + } catch { + sendErrorToFlutterLayer(errorMessage: error.localizedDescription) + } + } + + private func createCardComponentView(arguments: NSDictionary) throws -> UIView { + guard let paymentMethodsResponse = arguments.value(forKey: "paymentMethods") as? String else { throw PlatformError(errorDescription: "Payment methods not found") } + guard let cardComponentConfiguration = arguments.value(forKey: "cardComponentConfiguration") as? CardComponentConfigurationDTO else { throw PlatformError(errorDescription: "Card configuration not found") } + let paymentMethods = try JSONDecoder().decode(PaymentMethods.self, from: Data(paymentMethodsResponse.utf8)) + cardComponent = try buildCardComponent(paymentMethods: paymentMethods, cardComponentConfiguration: cardComponentConfiguration) + cardDelegate = CardAdvancedFlowDelegate(componentFlutterApi: componentFlutterApi) + cardComponent?.delegate = cardDelegate + cardComponent?.viewController.view.frame = initialFrame + guard let cardView = cardComponent?.viewController.view else { throw PlatformError(errorDescription: "Failed to get card component view") } + return cardView + } + + private func buildCardComponent(paymentMethods: PaymentMethods, cardComponentConfiguration: CardComponentConfigurationDTO) throws -> CardComponent { + guard let paymentMethod = paymentMethods.paymentMethod(ofType: CardPaymentMethod.self) else { throw PlatformError(errorDescription: "Card payment method not provided") } + let adyenContext = try cardComponentConfiguration.createAdyenContext() + let cardConfiguration = cardComponentConfiguration.cardConfiguration.mapToCardComponentConfiguration() + let cardComponent = CardComponent(paymentMethod: paymentMethod, context: adyenContext, configuration: cardConfiguration) + presentationDelegate = CardPresentationDelegate(topViewController: getViewController()) + actionComponent = buildActionComponent(adyenContext: adyenContext) + return cardComponent + } + + private func buildActionComponent(adyenContext: AdyenContext) -> AdyenActionComponent { + let actionComponent = AdyenActionComponent(context: adyenContext) + actionComponent.delegate = actionComponentDelegate + actionComponent.presentationDelegate = presentationDelegate + return actionComponent + } + + private func onAction(actionResponse: [String?: Any?]) { + do { + let jsonData = try JSONSerialization.data(withJSONObject: actionResponse, options: []) + let action = try JSONDecoder().decode(Action.self, from: jsonData) + actionComponent?.handle(action) + } catch { + sendErrorToFlutterLayer(errorMessage: error.localizedDescription) + } + } + + private func setupFinalizeComponentCallback() { + componentPlatformApi.onFinishCallback = { [weak self] paymentFlowOutcome in + let resultCode = ResultCode(rawValue: paymentFlowOutcome.result ?? "") + let success = resultCode == .authorised || resultCode == .received || resultCode == .pending + self?.finalizeAndDismiss(success: success, completion: { [weak self] in + let componentCommunicationModel = ComponentCommunicationModel(type: ComponentCommunicationType.result, + paymentResult: PaymentResultModelDTO(resultCode: resultCode?.rawValue)) + self?.componentFlutterApi.onComponentCommunication(componentCommunicationModel: componentCommunicationModel, completion: { _ in }) + }) + } + } + +} diff --git a/ios/Classes/components/card/advancedFlow/CardAdvancedFlowComponentFactory.swift b/ios/Classes/components/card/advancedFlow/CardAdvancedFlowComponentFactory.swift new file mode 100644 index 00000000..ed5635aa --- /dev/null +++ b/ios/Classes/components/card/advancedFlow/CardAdvancedFlowComponentFactory.swift @@ -0,0 +1,17 @@ +import Flutter + +class CardAdvancedFlowComponentFactory: ComponentFactory { + override func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + return CardAdvancedFlowComponent( + frame: frame, + viewIdentifier: viewId, + arguments: args as? NSDictionary ?? [:], + binaryMessenger: super.messenger, + componentFlutterApi: super.componentFlutterApi + ) + } +} diff --git a/ios/Classes/components/card/advancedFlow/delegates/CardAdvancedFlowActionComponentDelegate.swift b/ios/Classes/components/card/advancedFlow/delegates/CardAdvancedFlowActionComponentDelegate.swift new file mode 100644 index 00000000..0c0dec65 --- /dev/null +++ b/ios/Classes/components/card/advancedFlow/delegates/CardAdvancedFlowActionComponentDelegate.swift @@ -0,0 +1,33 @@ +import Adyen + +class CardAdvancedFlowActionComponentDelegate: ActionComponentDelegate { + private let componentFlutterApi: ComponentFlutterInterface + + init(componentFlutterApi: ComponentFlutterInterface) { + self.componentFlutterApi = componentFlutterApi + } + + func didProvide(_ data: Adyen.ActionComponentData, from _: Adyen.ActionComponent) { + do { + let actionComponentData = ActionComponentDataModel(details: data.details.encodable, paymentData: data.paymentData) + let actionComponentDataJson = try JSONEncoder().encode(actionComponentData) + let actionComponentDataString = String(data: actionComponentDataJson, encoding: .utf8) + componentFlutterApi.onComponentCommunication(componentCommunicationModel: ComponentCommunicationModel(type: ComponentCommunicationType.additionalDetails, data: actionComponentDataString), completion: { _ in }) + } catch { + sendErrorToFlutterLayer(error: error) + } + } + + func didComplete(from _: Adyen.ActionComponent) { + //Only for voucher payment method + } + + func didFail(with error: Error, from _: Adyen.ActionComponent) { + sendErrorToFlutterLayer(error: error) + } + + private func sendErrorToFlutterLayer(error: Error) { + let componentCommunicationModel = ComponentCommunicationModel(type: ComponentCommunicationType.error, data: error.localizedDescription) + componentFlutterApi.onComponentCommunication(componentCommunicationModel: componentCommunicationModel, completion: { _ in }) + } +} diff --git a/ios/Classes/components/card/advancedFlow/delegates/CardAdvancedFlowDelegate.swift b/ios/Classes/components/card/advancedFlow/delegates/CardAdvancedFlowDelegate.swift new file mode 100644 index 00000000..1c6fadde --- /dev/null +++ b/ios/Classes/components/card/advancedFlow/delegates/CardAdvancedFlowDelegate.swift @@ -0,0 +1,29 @@ +import Adyen + +class CardAdvancedFlowDelegate: PaymentComponentDelegate { + private let componentFlutterApi: ComponentFlutterInterface + + init(componentFlutterApi: ComponentFlutterInterface) { + self.componentFlutterApi = componentFlutterApi + } + + func didSubmit(_ data: Adyen.PaymentComponentData, from _: Adyen.PaymentComponent) { + do { + let paymentComponentData = PaymentComponentDataResponse(amount: data.amount, paymentMethod: data.paymentMethod.encodable, storePaymentMethod: data.storePaymentMethod, order: data.order, amountToPay: data.order?.remainingAmount, installments: data.installments, shopperName: data.shopperName, emailAddress: data.emailAddress, telephoneNumber: data.telephoneNumber, browserInfo: data.browserInfo, checkoutAttemptId: data.checkoutAttemptId, billingAddress: data.billingAddress, deliveryAddress: data.deliveryAddress, socialSecurityNumber: data.socialSecurityNumber, delegatedAuthenticationData: data.delegatedAuthenticationData) + let paymentComponentJson = try JSONEncoder().encode(paymentComponentData) + let paymentComponentString = String(data: paymentComponentJson, encoding: .utf8) + componentFlutterApi.onComponentCommunication(componentCommunicationModel: ComponentCommunicationModel(type: ComponentCommunicationType.onSubmit, data: paymentComponentString), completion: { _ in }) + } catch { + sendErrorToFlutterLayer(error: error) + } + } + + func didFail(with error: Error, from _: Adyen.PaymentComponent) { + sendErrorToFlutterLayer(error: error) + } + + private func sendErrorToFlutterLayer(error: Error) { + let componentCommunicationModel = ComponentCommunicationModel(type: ComponentCommunicationType.error, data: error.localizedDescription) + componentFlutterApi.onComponentCommunication(componentCommunicationModel: componentCommunicationModel, completion: { _ in }) + } +} diff --git a/ios/Classes/components/card/session/CardSessionFlowComponent.swift b/ios/Classes/components/card/session/CardSessionFlowComponent.swift new file mode 100644 index 00000000..56130024 --- /dev/null +++ b/ios/Classes/components/card/session/CardSessionFlowComponent.swift @@ -0,0 +1,80 @@ +@_spi(AdyenInternal) +import Adyen +import AdyenNetworking +import Flutter + +class CardSessionFlowComponent: BaseCardComponent { + private let cardSessionFlowDelegate: CardSessionFlowDelegate + private var presentationDelegate: PresentationDelegate? + private var adyenSession: AdyenSession? + + override init( + frame: CGRect, + viewIdentifier: Int64, + arguments: NSDictionary, + binaryMessenger: FlutterBinaryMessenger, + componentFlutterApi: ComponentFlutterInterface + ) { + cardSessionFlowDelegate = CardSessionFlowDelegate(componentFlutterApi: componentFlutterApi) + super.init( + frame: frame, + viewIdentifier: viewIdentifier, + arguments: arguments, + binaryMessenger: binaryMessenger, + componentFlutterApi: componentFlutterApi + ) + + cardSessionFlowDelegate.finalizeAndDismiss = finalizeAndDismiss + setupCardComponentView(arguments: arguments) + } + + private func setupCardComponentView(arguments: NSDictionary) { + do { + guard let cardComponentConfiguration = arguments.value(forKey: "cardComponentConfiguration") as? CardComponentConfigurationDTO else { throw PlatformError(errorDescription: "Card configuration not found") } + guard let session = arguments.value(forKey: "session") as? SessionDTO else { throw PlatformError(errorDescription: "Session not found") } + let sessionConfiguration = try createSessionConfiguration(cardComponentConfiguration: cardComponentConfiguration, session: session) + presentationDelegate = CardPresentationDelegate(topViewController: getViewController()) + AdyenSession.initialize(with: sessionConfiguration, delegate: cardSessionFlowDelegate, presentationDelegate: presentationDelegate!) { [weak self] result in + switch result { + case let .success(session): + self?.adyenSession = session + self?.attachComponent(session: session, cardComponentConfiguration: cardComponentConfiguration) + case let .failure(error): + self?.sendErrorToFlutterLayer(errorMessage: error.localizedDescription) + } + } + } catch { + sendErrorToFlutterLayer(errorMessage: error.localizedDescription) + } + } + + private func createSessionConfiguration(cardComponentConfiguration: CardComponentConfigurationDTO, session: SessionDTO) throws -> AdyenSession.Configuration { + let adyenContext = try cardComponentConfiguration.createAdyenContext() + return AdyenSession.Configuration( + sessionIdentifier: session.id, + initialSessionData: session.sessionData, + context: adyenContext, + actionComponent: .init() + ) + } + + private func attachComponent(session: AdyenSession, cardComponentConfiguration: CardComponentConfigurationDTO) { + do { + cardComponent = try buildCardComponent(session: session, cardComponentConfiguration: cardComponentConfiguration) + guard let cardComponentView = cardComponent?.viewController.view else { throw PlatformError(errorDescription: "Failed to get card component view") } + attachCardView(cardComponentView: cardComponentView) + } catch { + sendErrorToFlutterLayer(errorMessage: error.localizedDescription) + } + } + + private func buildCardComponent(session: AdyenSession, cardComponentConfiguration: CardComponentConfigurationDTO) throws -> CardComponent { + let paymentMethods = session.sessionContext.paymentMethods + guard let cardPaymentMethod = paymentMethods.paymentMethod(ofType: CardPaymentMethod.self) else { throw PlatformError(errorDescription: "Cannot find card payment method") } + let cardComponent = try CardComponent(paymentMethod: cardPaymentMethod, + context: cardComponentConfiguration.createAdyenContext(), + configuration: cardComponentConfiguration.cardConfiguration.mapToCardComponentConfiguration()) + cardComponent.delegate = session + return cardComponent + } +} diff --git a/ios/Classes/components/card/session/CardSessionFlowComponentFactory.swift b/ios/Classes/components/card/session/CardSessionFlowComponentFactory.swift new file mode 100644 index 00000000..c4e14de7 --- /dev/null +++ b/ios/Classes/components/card/session/CardSessionFlowComponentFactory.swift @@ -0,0 +1,17 @@ +import Flutter + +class CardSessionFlowComponentFactory: ComponentFactory { + override func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + return CardSessionFlowComponent( + frame: frame, + viewIdentifier: viewId, + arguments: args as? NSDictionary ?? [:], + binaryMessenger: messenger, + componentFlutterApi: componentFlutterApi + ) + } +} diff --git a/ios/Classes/components/card/session/delegates/CardSessionFlowDelegate.swift b/ios/Classes/components/card/session/delegates/CardSessionFlowDelegate.swift new file mode 100644 index 00000000..d5897cb3 --- /dev/null +++ b/ios/Classes/components/card/session/delegates/CardSessionFlowDelegate.swift @@ -0,0 +1,30 @@ +import Adyen +import AdyenNetworking + +class CardSessionFlowDelegate: AdyenSessionDelegate { + private let componentFlutterApi: ComponentFlutterInterface + var finalizeAndDismiss: ((Bool, @escaping (() -> Void)) -> Void)? + + init(componentFlutterApi: ComponentFlutterInterface) { + self.componentFlutterApi = componentFlutterApi + } + + func didComplete(with result: Adyen.AdyenSessionResult, component : Adyen.Component, session: Adyen.AdyenSession) { + let resultCode = result.resultCode + let success = resultCode == .authorised || resultCode == .received || resultCode == .pending + finalizeAndDismiss?(success, { [weak self] in + let paymentResult = PaymentResultModelDTO(sessionId: session.sessionContext.identifier, sessionData: session.sessionContext.data, resultCode: result.resultCode.rawValue) + let componentCommunicationModel = ComponentCommunicationModel(type: ComponentCommunicationType.result, paymentResult: paymentResult) + self?.componentFlutterApi.onComponentCommunication(componentCommunicationModel: componentCommunicationModel, completion: { _ in }) + }) + } + + func didFail(with error: Error, from _: Component, session _: AdyenSession) { + let componentCommunicationModel = ComponentCommunicationModel(type: ComponentCommunicationType.error, data: error.localizedDescription) + componentFlutterApi.onComponentCommunication(componentCommunicationModel: componentCommunicationModel, completion: { _ in }) + } + + func didOpenExternalApplication(component _: ActionComponent, session _: AdyenSession) { + // TODO: Add implementation when we support external applications + } +} diff --git a/ios/Classes/dropInAdvancedFlow/DropInAdvancedFlowDelegate.swift b/ios/Classes/dropInAdvancedFlow/DropInAdvancedFlowDelegate.swift index 4c9665dd..72cfc21a 100644 --- a/ios/Classes/dropInAdvancedFlow/DropInAdvancedFlowDelegate.swift +++ b/ios/Classes/dropInAdvancedFlow/DropInAdvancedFlowDelegate.swift @@ -32,20 +32,20 @@ class DropInAdvancedFlowDelegate: DropInComponentDelegate { } func didComplete(from _: ActionComponent, in _: AnyDropInComponent) { - dropInInteractorDelegate?.finalizeAndDismiss(success:true) { [weak self] in + dropInInteractorDelegate?.finalizeAndDismiss(success: true) { [weak self] in let paymentResult = PaymentResultDTO(type: PaymentResultEnum.finished, result: PaymentResultModelDTO(resultCode: ResultCode.received.rawValue)) let platformCommunicationModel = PlatformCommunicationModel(type: PlatformCommunicationType.result, paymentResult: paymentResult) self?.checkoutFlutterApi.onDropInAdvancedFlowPlatformCommunication(platformCommunicationModel: platformCommunicationModel, completion: { _ in }) } } - func didFail(with error: Error, from _: PaymentComponent, in dropInComponent: AnyDropInComponent) { + func didFail(with error: Error, from _: PaymentComponent, in _: AnyDropInComponent) { dropInInteractorDelegate?.finalizeAndDismiss(success: false) { [weak self] in self?.sendErrorToFlutterLayer(error: error) } } - func didFail(with error: Error, from _: ActionComponent, in dropInComponent: AnyDropInComponent) { + func didFail(with error: Error, from _: ActionComponent, in _: AnyDropInComponent) { dropInInteractorDelegate?.finalizeAndDismiss(success: false) { [weak self] in self?.sendErrorToFlutterLayer(error: error) } diff --git a/ios/Classes/utils/ConfigurationMapper.swift b/ios/Classes/utils/ConfigurationMapper.swift index 27bf2c21..24e2e362 100644 --- a/ios/Classes/utils/ConfigurationMapper.swift +++ b/ios/Classes/utils/ConfigurationMapper.swift @@ -9,7 +9,7 @@ class ConfigurationMapper { dropInConfiguration.paymentMethodsList.allowDisablingStoredPaymentMethods = dropInConfigurationDTO.isRemoveStoredPaymentMethodEnabled - if let cardsConfigurationDTO = dropInConfigurationDTO.cardsConfigurationDTO { + if let cardsConfigurationDTO = dropInConfigurationDTO.cardConfigurationDTO { let koreanAuthenticationMode = cardsConfigurationDTO.kcpFieldVisibility.toCardFieldVisibility() let socialSecurityNumberMode = cardsConfigurationDTO.socialSecurityNumberFieldVisibility.toCardFieldVisibility() let storedCardConfiguration = createStoredCardConfiguration(showCvcForStoredCard: cardsConfigurationDTO.showCvcForStoredCard) @@ -41,6 +41,13 @@ class ConfigurationMapper { return dropInConfiguration } + func createAdyenContext(environment: Environment, clientKey: String, amount: AmountDTO, countryCode: String) throws -> AdyenContext { + let environment = environment.mapToEnvironment() + let apiContext = try APIContext(environment: environment, clientKey: clientKey) + let amount = amount.mapToAmount() + return AdyenContext(apiContext: apiContext, payment: Payment(amount: amount, countryCode: countryCode), analyticsConfiguration: AnalyticsConfiguration()) + } + private func createStoredCardConfiguration(showCvcForStoredCard: Bool) -> StoredCardConfiguration { var storedCardConfiguration = StoredCardConfiguration() storedCardConfiguration.showsSecurityCodeField = showCvcForStoredCard @@ -100,3 +107,101 @@ extension FieldVisibility { } } } + +extension DropInConfigurationDTO { + func createAdyenContext() throws -> AdyenContext { + let environment = environment.mapToEnvironment() + let apiContext = try APIContext(environment: environment, clientKey: clientKey) + let amount = amount.mapToAmount() + return AdyenContext(apiContext: apiContext, payment: Payment(amount: amount, countryCode: countryCode), analyticsConfiguration: AnalyticsConfiguration()) + } +} + +extension CardConfigurationDTO { + func mapToCardComponentConfiguration() -> CardComponent.Configuration { + var formComponentStyle = FormComponentStyle() + formComponentStyle.backgroundColor = UIColor.white + let koreanAuthenticationMode = kcpFieldVisibility.toCardFieldVisibility() + let socialSecurityNumberMode = socialSecurityNumberFieldVisibility.toCardFieldVisibility() + let storedCardConfiguration = createStoredCardConfiguration(showCvcForStoredCard: showCvcForStoredCard) + let allowedCardTypes = determineAllowedCardTypes(cardTypes: supportedCardTypes) + let billingAddressConfiguration = determineBillingAddressConfiguration(addressMode: addressMode) + let cardConfiguration = CardComponent.Configuration( + style: formComponentStyle, + showsHolderNameField: holderNameRequired, + showsStorePaymentMethodField: showStorePaymentField, + showsSecurityCodeField: showCvc, + koreanAuthenticationMode: koreanAuthenticationMode, + socialSecurityNumberMode: socialSecurityNumberMode, + storedCardConfiguration: storedCardConfiguration, + allowedCardTypes: allowedCardTypes, + billingAddress: billingAddressConfiguration + ) + + return cardConfiguration + } + + private func createStoredCardConfiguration(showCvcForStoredCard: Bool) -> StoredCardConfiguration { + var storedCardConfiguration = StoredCardConfiguration() + storedCardConfiguration.showsSecurityCodeField = showCvcForStoredCard + return storedCardConfiguration + } + + private func determineAllowedCardTypes(cardTypes: [String?]?) -> [CardType]? { + guard let mappedCardTypes = cardTypes, !mappedCardTypes.isEmpty else { + return nil + } + + return mappedCardTypes.compactMap { $0 }.map { CardType(rawValue: $0.lowercased()) } + } + + private func determineBillingAddressConfiguration(addressMode: AddressMode?) -> BillingAddressConfiguration { + var billingAddressConfiguration = BillingAddressConfiguration() + switch addressMode { + case .full: + billingAddressConfiguration.mode = CardComponent.AddressFormType.full + case .postalCode: + billingAddressConfiguration.mode = CardComponent.AddressFormType.postalCode + case .none?: + billingAddressConfiguration.mode = CardComponent.AddressFormType.none + default: + billingAddressConfiguration.mode = CardComponent.AddressFormType.none + } + + return billingAddressConfiguration + } +} + +extension CardComponentConfigurationDTO { + func createAdyenContext() throws -> AdyenContext { + let environment = environment.mapToEnvironment() + let apiContext = try APIContext(environment: environment, clientKey: clientKey) + let amount = amount.mapToAmount() + return AdyenContext(apiContext: apiContext, payment: Payment(amount: amount, countryCode: countryCode), analyticsConfiguration: AnalyticsConfiguration()) + } +} + +extension Environment { + func mapToEnvironment() -> Adyen.Environment { + switch self { + case .test: + return Adyen.Environment.test + case .europe: + return .liveEurope + case .unitedStates: + return .liveUnitedStates + case .australia: + return .liveAustralia + case .india: + return .liveIndia + case .apse: + return .liveApse + } + } +} + +extension AmountDTO { + func mapToAmount() -> Adyen.Amount { + return Adyen.Amount(value: Int(value), currencyCode: currency) + } +} diff --git a/ios/Classes/utils/PlatformError.swift b/ios/Classes/utils/PlatformError.swift index f13db52e..b08ba5bc 100644 --- a/ios/Classes/utils/PlatformError.swift +++ b/ios/Classes/utils/PlatformError.swift @@ -1,5 +1,3 @@ -import Foundation - public struct PlatformError: Error, LocalizedError { public var errorDescription: String? diff --git a/lib/adyen_checkout.dart b/lib/adyen_checkout.dart index de27ee24..c53f9c1a 100644 --- a/lib/adyen_checkout.dart +++ b/lib/adyen_checkout.dart @@ -1,4 +1,5 @@ export 'src/adyen_checkout.dart'; +export 'src/components/card/adyen_card_component_widget.dart'; export 'src/generated/platform_api.g.dart' show Environment, @@ -10,12 +11,14 @@ export 'src/generated/platform_api.g.dart' GooglePayEnvironment, CashAppPayEnvironment; export 'src/models/amount.dart'; +export 'src/models/card_component_configuration.dart'; +export 'src/models/component_payment_flow.dart'; export 'src/models/drop_in_configuration.dart'; +export 'src/models/drop_in_payment_flow.dart'; export 'src/models/oder_response.dart'; -export 'src/models/payment_flow.dart'; export 'src/models/payment_flow_outcome.dart'; export 'src/models/payment_method_configurations/apple_pay_configuration.dart'; -export 'src/models/payment_method_configurations/cards_configuration.dart'; +export 'src/models/payment_method_configurations/card_configuration.dart'; export 'src/models/payment_method_configurations/cash_app_pay_configuration.dart'; export 'src/models/payment_method_configurations/google_pay_configuration.dart'; export 'src/models/payment_result.dart'; diff --git a/lib/src/adyen_checkout.dart b/lib/src/adyen_checkout.dart index 04d69d80..c1523b10 100644 --- a/lib/src/adyen_checkout.dart +++ b/lib/src/adyen_checkout.dart @@ -7,6 +7,7 @@ import 'package:adyen_checkout/src/logging/adyen_logger.dart'; import 'package:adyen_checkout/src/platform/adyen_checkout_platform_interface.dart'; import 'package:adyen_checkout/src/platform/adyen_checkout_result_api.dart'; import 'package:adyen_checkout/src/utils/dto_mapper.dart'; +import 'package:adyen_checkout/src/utils/payment_flow_outcome_handler.dart'; import 'package:flutter/foundation.dart'; class AdyenCheckout implements AdyenCheckoutInterface { @@ -16,6 +17,8 @@ class AdyenCheckout implements AdyenCheckoutInterface { final AdyenCheckoutResultApi _resultApi = AdyenCheckoutResultApi(); final AdyenLogger _adyenLogger = AdyenLogger(); + final PaymentFlowOutcomeHandler _paymentFlowOutcomeHandler = + PaymentFlowOutcomeHandler(); @override Future getPlatformVersion() => @@ -26,9 +29,10 @@ class AdyenCheckout implements AdyenCheckoutInterface { AdyenCheckoutPlatformInterface.instance.getReturnUrl(); @override - Future startPayment({required PaymentFlow paymentFlow}) async { + Future startPayment( + {required DropInPaymentFlow paymentFlow}) async { switch (paymentFlow) { - case DropInSession(): + case DropInSessionFlow(): return await _startDropInSessionsPayment(paymentFlow); case DropInAdvancedFlow(): return await _startDropInAdvancedFlowPayment(paymentFlow); @@ -44,12 +48,12 @@ class AdyenCheckout implements AdyenCheckoutInterface { } Future _startDropInSessionsPayment( - DropInSession dropInSession) async { + DropInSessionFlow dropInSession) async { _adyenLogger.print("Start Drop-in session"); final dropInSessionCompleter = Completer(); AdyenCheckoutPlatformInterface.instance.startDropInSessionPayment( - session: dropInSession.session.toDTO(), - dropInConfiguration: dropInSession.dropInConfiguration.toDTO(), + dropInSession.dropInConfiguration.toDTO(), + dropInSession.session.toDTO(), ); _resultApi.dropInSessionPlatformCommunicationStream = @@ -98,8 +102,8 @@ class AdyenCheckout implements AdyenCheckoutInterface { final dropInAdvancedFlowCompleter = Completer(); AdyenCheckoutPlatformInterface.instance.startDropInAdvancedFlowPayment( - paymentMethodsResponse: dropInAdvancedFlow.paymentMethodsResponse, - dropInConfiguration: dropInAdvancedFlow.dropInConfiguration.toDTO(), + dropInAdvancedFlow.dropInConfiguration.toDTO(), + dropInAdvancedFlow.paymentMethodsResponse, ); _resultApi.dropInAdvancedFlowPlatformCommunicationStream = @@ -164,7 +168,7 @@ class AdyenCheckout implements AdyenCheckoutInterface { final PaymentFlowOutcome paymentFlowOutcome = await postPayments(event.data!); PaymentFlowOutcomeDTO paymentFlowOutcomeDTO = - _mapToPaymentOutcomeDTO(paymentFlowOutcome); + _paymentFlowOutcomeHandler.mapToPaymentOutcomeDTO(paymentFlowOutcome); AdyenCheckoutPlatformInterface.instance .onPaymentsResult(paymentFlowOutcomeDTO); } catch (error) { @@ -195,7 +199,7 @@ class AdyenCheckout implements AdyenCheckoutInterface { final PaymentFlowOutcome paymentFlowOutcome = await postPaymentsDetails(event.data!); PaymentFlowOutcomeDTO paymentFlowOutcomeDTO = - _mapToPaymentOutcomeDTO(paymentFlowOutcome); + _paymentFlowOutcomeHandler.mapToPaymentOutcomeDTO(paymentFlowOutcome); AdyenCheckoutPlatformInterface.instance .onPaymentsDetailsResult(paymentFlowOutcomeDTO); } catch (error) { @@ -213,28 +217,6 @@ class AdyenCheckout implements AdyenCheckoutInterface { } } - PaymentFlowOutcomeDTO _mapToPaymentOutcomeDTO( - PaymentFlowOutcome dropInOutcome) { - return switch (dropInOutcome) { - Finished() => PaymentFlowOutcomeDTO( - paymentFlowResultType: PaymentFlowResultType.finished, - result: dropInOutcome.resultCode, - ), - Action() => PaymentFlowOutcomeDTO( - paymentFlowResultType: PaymentFlowResultType.action, - actionResponse: dropInOutcome.actionResponse, - ), - Error() => PaymentFlowOutcomeDTO( - paymentFlowResultType: PaymentFlowResultType.error, - error: ErrorDTO( - errorMessage: dropInOutcome.errorMessage, - reason: dropInOutcome.reason, - dismissDropIn: dropInOutcome.dismissDropIn, - ), - ), - }; - } - void _setupResultApi() => CheckoutFlutterApi.setup(_resultApi); Future _onDeleteStoredPaymentMethodCallback( diff --git a/lib/src/adyen_checkout_interface.dart b/lib/src/adyen_checkout_interface.dart index d3d57c86..68d7bcac 100644 --- a/lib/src/adyen_checkout_interface.dart +++ b/lib/src/adyen_checkout_interface.dart @@ -5,7 +5,7 @@ abstract class AdyenCheckoutInterface { Future getReturnUrl(); - Future startPayment({required PaymentFlow paymentFlow}); + Future startPayment({required DropInPaymentFlow paymentFlow}); void enableLogging({required bool loggingEnabled}); } diff --git a/lib/src/components/card/adyen_card_component_widget.dart b/lib/src/components/card/adyen_card_component_widget.dart new file mode 100644 index 00000000..4a2ca5c6 --- /dev/null +++ b/lib/src/components/card/adyen_card_component_widget.dart @@ -0,0 +1,130 @@ +import 'package:adyen_checkout/adyen_checkout.dart'; +import 'package:adyen_checkout/src/components/card/card_advanced_flow_widget.dart'; +import 'package:adyen_checkout/src/components/card/card_session_flow_widget.dart'; +import 'package:adyen_checkout/src/utils/dto_mapper.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +class AdyenCardComponentWidget extends StatelessWidget { + final ComponentPaymentFlow componentPaymentFlow; + final Future Function(PaymentResult) onPaymentResult; + final Set>? gestureRecognizers; + + const AdyenCardComponentWidget({ + super.key, + required this.componentPaymentFlow, + required this.onPaymentResult, + this.gestureRecognizers, + }); + + @override + Widget build(BuildContext context) { + return switch (componentPaymentFlow) { + CardComponentSessionFlow() => _buildCardSessionFlowWidget(), + CardComponentAdvancedFlow() => _buildCardAdvancedFlowWidget() + }; + } + + CardSessionFlowWidget _buildCardSessionFlowWidget() { + final CardComponentSessionFlow cardComponentSessionFlow = + componentPaymentFlow as CardComponentSessionFlow; + final double initialHeight = _determineInitialHeight( + cardComponentSessionFlow.cardComponentConfiguration.cardConfiguration); + return CardSessionFlowWidget( + cardComponentConfiguration: + cardComponentSessionFlow.cardComponentConfiguration.toDTO(), + session: cardComponentSessionFlow.session.toDTO(), + onPaymentResult: onPaymentResult, + initialViewHeight: initialHeight, + ); + } + + CardAdvancedFlowWidget _buildCardAdvancedFlowWidget() { + final CardComponentAdvancedFlow cardComponentAdvancedFlow = + componentPaymentFlow as CardComponentAdvancedFlow; + final double initialHeight = _determineInitialHeight( + cardComponentAdvancedFlow.cardComponentConfiguration.cardConfiguration); + return CardAdvancedFlowWidget( + cardComponentConfiguration: + cardComponentAdvancedFlow.cardComponentConfiguration.toDTO(), + paymentMethods: cardComponentAdvancedFlow.paymentMethods, + onPayments: cardComponentAdvancedFlow.onPayments, + onPaymentsDetails: cardComponentAdvancedFlow.onPaymentsDetails, + onPaymentResult: onPaymentResult, + initialViewHeight: initialHeight, + gestureRecognizers: gestureRecognizers, + ); + } + + double _determineInitialHeight(CardConfiguration cardConfiguration) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return _determineInitialAndroidViewHeight(cardConfiguration); + case TargetPlatform.iOS: + return _determineInitialIosViewHeight(cardConfiguration); + default: + throw UnsupportedError('Unsupported platform view'); + } + } + + double _determineInitialAndroidViewHeight( + CardConfiguration cardConfiguration) { + double androidViewHeight = 294; + + if (cardConfiguration.holderNameRequired) { + androidViewHeight += 61; + } + + if (cardConfiguration.showStorePaymentField) { + androidViewHeight += 43; + } + + if (cardConfiguration.addressMode == AddressMode.full) { + androidViewHeight += 422; + } + + if (cardConfiguration.addressMode == AddressMode.postalCode) { + androidViewHeight += 61; + } + + if (cardConfiguration.socialSecurityNumberFieldVisibility == + FieldVisibility.show) { + androidViewHeight += 61; + } + + if (cardConfiguration.kcpFieldVisibility == + FieldVisibility.show) { + androidViewHeight += 164; + } + + return androidViewHeight; + } + + double _determineInitialIosViewHeight(CardConfiguration cardConfiguration) { + double iosViewHeight = 272; + + if (cardConfiguration.holderNameRequired) { + iosViewHeight += 63; + } + + if (cardConfiguration.showStorePaymentField) { + iosViewHeight += 55; + } + + if (cardConfiguration.addressMode != AddressMode.none) { + iosViewHeight += 63; + } + + if (cardConfiguration.socialSecurityNumberFieldVisibility == + FieldVisibility.show) { + iosViewHeight += 63; + } + + if (cardConfiguration.kcpFieldVisibility == FieldVisibility.show) { + iosViewHeight += 63; + } + + return iosViewHeight; + } +} diff --git a/lib/src/components/card/card_advanced_flow_widget.dart b/lib/src/components/card/card_advanced_flow_widget.dart new file mode 100644 index 00000000..ded12a38 --- /dev/null +++ b/lib/src/components/card/card_advanced_flow_widget.dart @@ -0,0 +1,169 @@ +import 'dart:async'; + +import 'package:adyen_checkout/adyen_checkout.dart'; +import 'package:adyen_checkout/src/components/card/card_component_container_widget.dart'; +import 'package:adyen_checkout/src/components/component_flutter_api.dart'; +import 'package:adyen_checkout/src/components/component_platform_api.dart'; +import 'package:adyen_checkout/src/components/platform/android_platform_view.dart'; +import 'package:adyen_checkout/src/components/platform/ios_platform_view.dart'; +import 'package:adyen_checkout/src/generated/platform_api.g.dart'; +import 'package:adyen_checkout/src/logging/adyen_logger.dart'; +import 'package:adyen_checkout/src/utils/constants.dart'; +import 'package:adyen_checkout/src/utils/payment_flow_outcome_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_transform/stream_transform.dart'; + +class CardAdvancedFlowWidget extends StatefulWidget { + CardAdvancedFlowWidget({ + super.key, + required this.cardComponentConfiguration, + required this.paymentMethods, + required this.onPayments, + required this.onPaymentsDetails, + required this.onPaymentResult, + required this.initialViewHeight, + this.gestureRecognizers, + PaymentFlowOutcomeHandler? paymentFlowOutcomeHandler, + AdyenLogger? adyenLogger, + }) : paymentFlowOutcomeHandler = + paymentFlowOutcomeHandler ?? PaymentFlowOutcomeHandler(), + adyenLogger = adyenLogger ?? AdyenLogger(); + + final CardComponentConfigurationDTO cardComponentConfiguration; + final String paymentMethods; + final Future Function(String) onPayments; + final Future Function(String) onPaymentsDetails; + final Future Function(PaymentResult) onPaymentResult; + final double initialViewHeight; + final PaymentFlowOutcomeHandler paymentFlowOutcomeHandler; + final Set>? gestureRecognizers; + final AdyenLogger adyenLogger; + + @override + State createState() => _CardAdvancedFlowWidgetState(); +} + +class _CardAdvancedFlowWidgetState extends State { + final MessageCodec _codec = ComponentFlutterInterface.codec; + final ComponentFlutterApi _resultApi = ComponentFlutterApi(); + final StreamController _resizeStream = StreamController.broadcast(); + final ComponentPlatformApi _componentPlatformApi = ComponentPlatformApi(); + final GlobalKey _cardWidgetKey = GlobalKey(); + late Widget _cardWidget; + + @override + void initState() { + super.initState(); + + ComponentFlutterInterface.setup(_resultApi); + _cardWidget = _buildCardWidget(); + _resultApi.componentCommunicationStream.stream + .asBroadcastStream() + .listen(_handleComponentCommunication); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: _resizeStream.stream + .debounce(const Duration(milliseconds: 100)) + .distinct(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + return AdyenCardComponentContainerWidget( + snapshot: snapshot, + cardWidgetKey: _cardWidgetKey, + initialViewHeight: widget.initialViewHeight, + cardWidget: _cardWidget, + ); + }); + } + + @override + void dispose() { + _resultApi.componentCommunicationStream.close(); + _resizeStream.close(); + super.dispose(); + } + + void _handleComponentCommunication(event) async { + switch (event.type) { + case ComponentCommunicationType.onSubmit: + _onSubmit(event); + case ComponentCommunicationType.additionalDetails: + _onAdditionalDetails(event); + case ComponentCommunicationType.error: + _onError(event); + case ComponentCommunicationType.resize: + _onResize(event); + case ComponentCommunicationType.result: + _onHandleResult(event); + } + } + + Future _onSubmit(ComponentCommunicationModel event) async { + final PaymentFlowOutcome paymentFlowOutcome = + await widget.onPayments(event.data as String); + final PaymentFlowOutcomeDTO paymentFlowOutcomeDTO = widget + .paymentFlowOutcomeHandler + .mapToPaymentOutcomeDTO(paymentFlowOutcome); + _componentPlatformApi.onPaymentsResult(paymentFlowOutcomeDTO); + } + + Future _onAdditionalDetails(ComponentCommunicationModel event) async { + final PaymentFlowOutcome paymentFlowOutcome = + await widget.onPaymentsDetails(event.data as String); + final PaymentFlowOutcomeDTO paymentFlowOutcomeDTO = widget + .paymentFlowOutcomeHandler + .mapToPaymentOutcomeDTO(paymentFlowOutcome); + _componentPlatformApi.onPaymentsDetailsResult(paymentFlowOutcomeDTO); + } + + void _onError(ComponentCommunicationModel event) { + String errorMessage = event.data as String; + widget.onPaymentResult(PaymentError(reason: errorMessage)); + } + + void _onResize(ComponentCommunicationModel event) => + _resizeStream.add(event.data as double); + + void _onHandleResult(ComponentCommunicationModel event) { + String resultCode = event.paymentResult?.resultCode ?? ""; + widget.adyenLogger.print("Card advanced flow result code: $resultCode"); + widget.onPaymentResult(PaymentAdvancedFlowFinished(resultCode: resultCode)); + } + + Widget _buildCardWidget() { + final Map creationParams = { + Constants.paymentMethodsKey: widget.paymentMethods, + Constants.cardComponentConfigurationKey: + widget.cardComponentConfiguration, + }; + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return AndroidPlatformView( + key: UniqueKey(), + viewType: Constants.cardComponentAdvancedFlowKey, + codec: _codec, + creationParams: creationParams, + gestureRecognizers: widget.gestureRecognizers, + onPlatformViewCreated: _componentPlatformApi.updateViewHeight, + ); + case TargetPlatform.iOS: + return IosPlatformView( + key: UniqueKey(), + viewType: Constants.cardComponentAdvancedFlowKey, + codec: _codec, + creationParams: creationParams, + gestureRecognizers: widget.gestureRecognizers, + onPlatformViewCreated: _componentPlatformApi.updateViewHeight, + cardWidgetKey: _cardWidgetKey, + ); + default: + throw UnsupportedError('Unsupported platform'); + } + } +} diff --git a/lib/src/components/card/card_component_container_widget.dart b/lib/src/components/card/card_component_container_widget.dart new file mode 100644 index 00000000..f1d91b3f --- /dev/null +++ b/lib/src/components/card/card_component_container_widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class AdyenCardComponentContainerWidget extends StatelessWidget { + const AdyenCardComponentContainerWidget({ + super.key, + required this.snapshot, + required this.cardWidgetKey, + required this.initialViewHeight, + required this.cardWidget, + }); + + final double initialViewHeight; + final AsyncSnapshot snapshot; + final Key cardWidgetKey; + final Widget cardWidget; + final double marginBottom = 16; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: snapshot.data != null ? 1 : 0, + child: SizedBox( + key: cardWidgetKey, + height: _determineHeight(snapshot), + child: cardWidget, + ), + ), + if (snapshot.data == null) + SizedBox( + height: initialViewHeight, + child: _buildLoadingWidget(), + ) + ], + ); + } + + Widget _buildLoadingWidget() { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return const Center(child: CircularProgressIndicator()); + default: + return const Center(child: SizedBox.shrink()); + } + } + + double _determineHeight(AsyncSnapshot snapshot) { + switch (snapshot.data) { + case null: + return initialViewHeight; + case > 0: + return snapshot.data + marginBottom; + default: + return initialViewHeight; + } + } +} diff --git a/lib/src/components/card/card_session_flow_widget.dart b/lib/src/components/card/card_session_flow_widget.dart new file mode 100644 index 00000000..8d1dd7ef --- /dev/null +++ b/lib/src/components/card/card_session_flow_widget.dart @@ -0,0 +1,137 @@ +import 'dart:async'; + +import 'package:adyen_checkout/adyen_checkout.dart'; +import 'package:adyen_checkout/src/components/card/card_component_container_widget.dart'; +import 'package:adyen_checkout/src/components/component_flutter_api.dart'; +import 'package:adyen_checkout/src/components/component_platform_api.dart'; +import 'package:adyen_checkout/src/components/platform/android_platform_view.dart'; +import 'package:adyen_checkout/src/components/platform/ios_platform_view.dart'; +import 'package:adyen_checkout/src/generated/platform_api.g.dart'; +import 'package:adyen_checkout/src/logging/adyen_logger.dart'; +import 'package:adyen_checkout/src/utils/constants.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_transform/stream_transform.dart'; + +class CardSessionFlowWidget extends StatefulWidget { + CardSessionFlowWidget({ + super.key, + required this.cardComponentConfiguration, + required this.session, + required this.onPaymentResult, + required this.initialViewHeight, + this.gestureRecognizers, + AdyenLogger? adyenLogger, + }) : adyenLogger = adyenLogger ?? AdyenLogger(); + + final CardComponentConfigurationDTO cardComponentConfiguration; + final SessionDTO session; + final Future Function(PaymentResult) onPaymentResult; + final double initialViewHeight; + final Set>? gestureRecognizers; + final AdyenLogger adyenLogger; + + @override + State createState() => _CardSessionFlowWidgetState(); +} + +class _CardSessionFlowWidgetState extends State { + final MessageCodec _codec = ComponentFlutterInterface.codec; + final ComponentFlutterApi _resultApi = ComponentFlutterApi(); + final ComponentPlatformApi _componentPlatformApi = ComponentPlatformApi(); + final StreamController _resizeStream = StreamController.broadcast(); + final GlobalKey _cardWidgetKey = GlobalKey(); + late Widget _cardWidget; + + @override + void initState() { + super.initState(); + + ComponentFlutterInterface.setup(_resultApi); + _cardWidget = _buildCardWidget(); + _resultApi.componentCommunicationStream.stream + .asBroadcastStream() + .listen(_handleComponentCommunication); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: _resizeStream.stream + .debounce(const Duration(milliseconds: 100)) + .distinct(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + return AdyenCardComponentContainerWidget( + snapshot: snapshot, + cardWidgetKey: _cardWidgetKey, + initialViewHeight: widget.initialViewHeight, + cardWidget: _cardWidget, + ); + }); + } + + @override + void dispose() { + _resultApi.componentCommunicationStream.close(); + _resizeStream.close(); + super.dispose(); + } + + void _handleComponentCommunication(event) async { + if (event.type case ComponentCommunicationType.result) { + _onResult(event); + } else if (event.type case ComponentCommunicationType.error) { + _onError(event); + } else if (event.type case ComponentCommunicationType.resize) { + _onResize(event); + } + } + + void _onResult(ComponentCommunicationModel event) { + String resultCode = event.paymentResult?.resultCode ?? ""; + widget.adyenLogger.print("Card session flow result code: $resultCode"); + widget.onPaymentResult(PaymentAdvancedFlowFinished(resultCode: resultCode)); + } + + void _onError(ComponentCommunicationModel event) { + String errorMessage = event.data as String; + widget.onPaymentResult(PaymentError(reason: errorMessage)); + } + + void _onResize(ComponentCommunicationModel event) => + _resizeStream.add(event.data as double); + + Widget _buildCardWidget() { + final Map creationParams = { + Constants.sessionKey: widget.session, + Constants.cardComponentConfigurationKey: + widget.cardComponentConfiguration, + }; + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return AndroidPlatformView( + key: UniqueKey(), + viewType: Constants.cardComponentSessionFlowKey, + codec: _codec, + creationParams: creationParams, + gestureRecognizers: widget.gestureRecognizers, + onPlatformViewCreated: _componentPlatformApi.updateViewHeight, + ); + case TargetPlatform.iOS: + return IosPlatformView( + key: UniqueKey(), + viewType: Constants.cardComponentSessionFlowKey, + codec: _codec, + creationParams: creationParams, + gestureRecognizers: widget.gestureRecognizers, + onPlatformViewCreated: _componentPlatformApi.updateViewHeight, + cardWidgetKey: _cardWidgetKey, + ); + default: + throw UnsupportedError('Unsupported platform'); + } + } +} diff --git a/lib/src/components/component_flutter_api.dart b/lib/src/components/component_flutter_api.dart new file mode 100644 index 00000000..d6f5e1a9 --- /dev/null +++ b/lib/src/components/component_flutter_api.dart @@ -0,0 +1,14 @@ +import 'dart:async'; + +import 'package:adyen_checkout/src/generated/platform_api.g.dart'; + +class ComponentFlutterApi implements ComponentFlutterInterface { + var componentCommunicationStream = + StreamController.broadcast(); + + @override + void onComponentCommunication( + ComponentCommunicationModel componentCommunicationModel) { + componentCommunicationStream.sink.add(componentCommunicationModel); + } +} diff --git a/lib/src/components/component_platform_api.dart b/lib/src/components/component_platform_api.dart new file mode 100644 index 00000000..bd2d39f5 --- /dev/null +++ b/lib/src/components/component_platform_api.dart @@ -0,0 +1,22 @@ +import 'package:adyen_checkout/src/generated/platform_api.g.dart'; + +class ComponentPlatformApi implements ComponentPlatformInterface { + final ComponentPlatformInterface _componentPlatformInterface = + ComponentPlatformInterface(); + + @override + Future updateViewHeight(int viewId) async => + _componentPlatformInterface.updateViewHeight(viewId); + + @override + Future onPaymentsResult( + PaymentFlowOutcomeDTO paymentFlowOutcomeDTO) async { + _componentPlatformInterface.onPaymentsResult(paymentFlowOutcomeDTO); + } + + @override + Future onPaymentsDetailsResult( + PaymentFlowOutcomeDTO paymentFlowOutcomeDTO) async => + _componentPlatformInterface + .onPaymentsDetailsResult(paymentFlowOutcomeDTO); +} diff --git a/lib/src/components/platform/android_platform_view.dart b/lib/src/components/platform/android_platform_view.dart new file mode 100644 index 00000000..d9c6094e --- /dev/null +++ b/lib/src/components/platform/android_platform_view.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class AndroidPlatformView extends StatelessWidget { + final String viewType; + final Map creationParams; + final MessageCodec codec; + final Set>? gestureRecognizers; + final Function(int) onPlatformViewCreated; + + const AndroidPlatformView({ + super.key, + required this.viewType, + required this.creationParams, + required this.codec, + required this.onPlatformViewCreated, + this.gestureRecognizers, + }); + + @override + Widget build(BuildContext context) { + return PlatformViewLink( + viewType: viewType, + surfaceFactory: (context, controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? {}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: Directionality.of(context), + creationParams: creationParams, + creationParamsCodec: codec, + onFocus: () { + params.onFocusChanged(true); + }, + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener(onPlatformViewCreated) + ..create(); + }, + ); + } +} diff --git a/lib/src/components/platform/ios_platform_view.dart b/lib/src/components/platform/ios_platform_view.dart new file mode 100644 index 00000000..46bbfc26 --- /dev/null +++ b/lib/src/components/platform/ios_platform_view.dart @@ -0,0 +1,71 @@ +import 'package:adyen_checkout/src/generated/platform_api.g.dart'; +import 'package:adyen_checkout/src/utils/constants.dart'; +import 'package:adyen_checkout/src/utils/toggle_area_gesture_recognizer.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class IosPlatformView extends StatelessWidget { + final String viewType; + final Map creationParams; + final MessageCodec codec; + final Set>? gestureRecognizers; + final Function(int) onPlatformViewCreated; + final GlobalKey cardWidgetKey; + + const IosPlatformView({ + super.key, + required this.viewType, + required this.creationParams, + required this.codec, + required this.onPlatformViewCreated, + required this.cardWidgetKey, + this.gestureRecognizers, + }); + + @override + UiKitView build(BuildContext context) { + return UiKitView( + viewType: viewType, + onPlatformViewCreated: onPlatformViewCreated, + layoutDirection: Directionality.of(context), + creationParams: creationParams, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + creationParamsCodec: codec, + gestureRecognizers: _createGestureRecognizers(context), + ); + } + + Set> _createGestureRecognizers( + BuildContext context) { + final groupedGestureRecognizers = >{}; + gestureRecognizers?.forEach((gestureRecognizer) { + groupedGestureRecognizers.add(gestureRecognizer); + }); + _addToggleAreaGestureRecognizerForIOSIfRequired( + groupedGestureRecognizers, + context, + ); + return groupedGestureRecognizers; + } + + void _addToggleAreaGestureRecognizerForIOSIfRequired( + Set> groupedGestureRecognizers, + BuildContext context, + ) { + final cardConfiguration = + creationParams[Constants.cardComponentConfigurationKey] + as CardComponentConfigurationDTO; + if (cardConfiguration.cardConfiguration.showStorePaymentField) { + groupedGestureRecognizers.addAll({ + Factory( + () => HorizontalDragGestureRecognizer()), + Factory(() => ToggleAreaGestureRecognizer( + cardWidgetKey: cardWidgetKey, + textDirection: Directionality.of(context))), + }); + } + } +} diff --git a/lib/src/generated/platform_api.g.dart b/lib/src/generated/platform_api.g.dart index 41e77853..f3a1cd1f 100644 --- a/lib/src/generated/platform_api.g.dart +++ b/lib/src/generated/platform_api.g.dart @@ -66,6 +66,14 @@ enum PlatformCommunicationType { deleteStoredPaymentMethod, } +enum ComponentCommunicationType { + onSubmit, + additionalDetails, + result, + error, + resize, +} + enum PaymentFlowResultType { finished, action, @@ -162,7 +170,7 @@ class DropInConfigurationDTO { required this.countryCode, required this.amount, required this.shopperLocale, - this.cardsConfigurationDTO, + this.cardConfigurationDTO, this.applePayConfigurationDTO, this.googlePayConfigurationDTO, this.cashAppPayConfigurationDTO, @@ -182,7 +190,7 @@ class DropInConfigurationDTO { String shopperLocale; - CardsConfigurationDTO? cardsConfigurationDTO; + CardConfigurationDTO? cardConfigurationDTO; ApplePayConfigurationDTO? applePayConfigurationDTO; @@ -205,7 +213,7 @@ class DropInConfigurationDTO { countryCode, amount.encode(), shopperLocale, - cardsConfigurationDTO?.encode(), + cardConfigurationDTO?.encode(), applePayConfigurationDTO?.encode(), googlePayConfigurationDTO?.encode(), cashAppPayConfigurationDTO?.encode(), @@ -224,8 +232,8 @@ class DropInConfigurationDTO { countryCode: result[2]! as String, amount: AmountDTO.decode(result[3]! as List), shopperLocale: result[4]! as String, - cardsConfigurationDTO: result[5] != null - ? CardsConfigurationDTO.decode(result[5]! as List) + cardConfigurationDTO: result[5] != null + ? CardConfigurationDTO.decode(result[5]! as List) : null, applePayConfigurationDTO: result[6] != null ? ApplePayConfigurationDTO.decode(result[6]! as List) @@ -246,8 +254,8 @@ class DropInConfigurationDTO { } } -class CardsConfigurationDTO { - CardsConfigurationDTO({ +class CardConfigurationDTO { + CardConfigurationDTO({ required this.holderNameRequired, required this.addressMode, required this.showStorePaymentField, @@ -287,9 +295,9 @@ class CardsConfigurationDTO { ]; } - static CardsConfigurationDTO decode(Object result) { + static CardConfigurationDTO decode(Object result) { result as List; - return CardsConfigurationDTO( + return CardConfigurationDTO( holderNameRequired: result[0]! as bool, addressMode: AddressMode.values[result[1]! as int], showStorePaymentField: result[2]! as bool, @@ -464,6 +472,7 @@ class PaymentResultModelDTO { PaymentResultModelDTO({ this.sessionId, this.sessionData, + this.sessionResult, this.resultCode, this.order, }); @@ -472,6 +481,8 @@ class PaymentResultModelDTO { String? sessionData; + String? sessionResult; + String? resultCode; OrderResponseDTO? order; @@ -480,6 +491,7 @@ class PaymentResultModelDTO { return [ sessionId, sessionData, + sessionResult, resultCode, order?.encode(), ]; @@ -490,9 +502,10 @@ class PaymentResultModelDTO { return PaymentResultModelDTO( sessionId: result[0] as String?, sessionData: result[1] as String?, - resultCode: result[2] as String?, - order: result[3] != null - ? OrderResponseDTO.decode(result[3]! as List) + sessionResult: result[2] as String?, + resultCode: result[3] as String?, + order: result[4] != null + ? OrderResponseDTO.decode(result[4]! as List) : null, ); } @@ -571,6 +584,39 @@ class PlatformCommunicationModel { } } +class ComponentCommunicationModel { + ComponentCommunicationModel({ + required this.type, + this.data, + this.paymentResult, + }); + + ComponentCommunicationType type; + + Object? data; + + PaymentResultModelDTO? paymentResult; + + Object encode() { + return [ + type.index, + data, + paymentResult?.encode(), + ]; + } + + static ComponentCommunicationModel decode(Object result) { + result as List; + return ComponentCommunicationModel( + type: ComponentCommunicationType.values[result[0]! as int], + data: result[1], + paymentResult: result[2] != null + ? PaymentResultModelDTO.decode(result[2]! as List) + : null, + ); + } +} + class PaymentFlowOutcomeDTO { PaymentFlowOutcomeDTO({ required this.paymentFlowResultType, @@ -666,6 +712,52 @@ class DeletedStoredPaymentMethodResultDTO { } } +class CardComponentConfigurationDTO { + CardComponentConfigurationDTO({ + required this.environment, + required this.clientKey, + required this.countryCode, + required this.amount, + this.shopperLocale, + required this.cardConfiguration, + }); + + Environment environment; + + String clientKey; + + String countryCode; + + AmountDTO amount; + + String? shopperLocale; + + CardConfigurationDTO cardConfiguration; + + Object encode() { + return [ + environment.index, + clientKey, + countryCode, + amount.encode(), + shopperLocale, + cardConfiguration.encode(), + ]; + } + + static CardComponentConfigurationDTO decode(Object result) { + result as List; + return CardComponentConfigurationDTO( + environment: Environment.values[result[0]! as int], + clientKey: result[1]! as String, + countryCode: result[2]! as String, + amount: AmountDTO.decode(result[3]! as List), + shopperLocale: result[4] as String?, + cardConfiguration: CardConfigurationDTO.decode(result[5]! as List), + ); + } +} + class _CheckoutPlatformInterfaceCodec extends StandardMessageCodec { const _CheckoutPlatformInterfaceCodec(); @override @@ -679,7 +771,7 @@ class _CheckoutPlatformInterfaceCodec extends StandardMessageCodec { } else if (value is ApplePayConfigurationDTO) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is CardsConfigurationDTO) { + } else if (value is CardConfigurationDTO) { buffer.putUint8(131); writeValue(buffer, value.encode()); } else if (value is CashAppPayConfigurationDTO) { @@ -718,7 +810,7 @@ class _CheckoutPlatformInterfaceCodec extends StandardMessageCodec { case 130: return ApplePayConfigurationDTO.decode(readValue(buffer)!); case 131: - return CardsConfigurationDTO.decode(readValue(buffer)!); + return CardConfigurationDTO.decode(readValue(buffer)!); case 132: return CashAppPayConfigurationDTO.decode(readValue(buffer)!); case 133: @@ -1061,3 +1153,230 @@ abstract class CheckoutFlutterApi { } } } + +class _ComponentPlatformInterfaceCodec extends StandardMessageCodec { + const _ComponentPlatformInterfaceCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ErrorDTO) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is PaymentFlowOutcomeDTO) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ErrorDTO.decode(readValue(buffer)!); + case 129: + return PaymentFlowOutcomeDTO.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ComponentPlatformInterface { + /// Constructor for [ComponentPlatformInterface]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ComponentPlatformInterface({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _ComponentPlatformInterfaceCodec(); + + Future updateViewHeight(int arg_viewId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.updateViewHeight', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_viewId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future onPaymentsResult(PaymentFlowOutcomeDTO arg_paymentsResult) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.onPaymentsResult', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_paymentsResult]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future onPaymentsDetailsResult(PaymentFlowOutcomeDTO arg_paymentsDetailsResult) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.adyen_checkout.ComponentPlatformInterface.onPaymentsDetailsResult', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_paymentsDetailsResult]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +class _ComponentFlutterInterfaceCodec extends StandardMessageCodec { + const _ComponentFlutterInterfaceCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is AmountDTO) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is AmountDTO) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is CardComponentConfigurationDTO) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is CardConfigurationDTO) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is ComponentCommunicationModel) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is OrderResponseDTO) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is PaymentResultModelDTO) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is SessionDTO) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return AmountDTO.decode(readValue(buffer)!); + case 129: + return AmountDTO.decode(readValue(buffer)!); + case 130: + return CardComponentConfigurationDTO.decode(readValue(buffer)!); + case 131: + return CardConfigurationDTO.decode(readValue(buffer)!); + case 132: + return ComponentCommunicationModel.decode(readValue(buffer)!); + case 133: + return OrderResponseDTO.decode(readValue(buffer)!); + case 134: + return PaymentResultModelDTO.decode(readValue(buffer)!); + case 135: + return SessionDTO.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class ComponentFlutterInterface { + static const MessageCodec codec = _ComponentFlutterInterfaceCodec(); + + void _generateCodecForDTOs(CardComponentConfigurationDTO cardComponentConfigurationDTO, SessionDTO sessionDTO); + + void onComponentCommunication(ComponentCommunicationModel componentCommunicationModel); + + static void setup(ComponentFlutterInterface? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface._generateCodecForDTOs', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface._generateCodecForDTOs was null.'); + final List args = (message as List?)!; + final CardComponentConfigurationDTO? arg_cardComponentConfigurationDTO = (args[0] as CardComponentConfigurationDTO?); + assert(arg_cardComponentConfigurationDTO != null, + 'Argument for dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface._generateCodecForDTOs was null, expected non-null CardComponentConfigurationDTO.'); + final SessionDTO? arg_sessionDTO = (args[1] as SessionDTO?); + assert(arg_sessionDTO != null, + 'Argument for dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface._generateCodecForDTOs was null, expected non-null SessionDTO.'); + try { + api._generateCodecForDTOs(arg_cardComponentConfigurationDTO!, arg_sessionDTO!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface.onComponentCommunication', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface.onComponentCommunication was null.'); + final List args = (message as List?)!; + final ComponentCommunicationModel? arg_componentCommunicationModel = (args[0] as ComponentCommunicationModel?); + assert(arg_componentCommunicationModel != null, + 'Argument for dev.flutter.pigeon.adyen_checkout.ComponentFlutterInterface.onComponentCommunication was null, expected non-null ComponentCommunicationModel.'); + try { + api.onComponentCommunication(arg_componentCommunicationModel!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/lib/src/logging/adyen_logger.dart b/lib/src/logging/adyen_logger.dart index 6a0593ea..499c2223 100644 --- a/lib/src/logging/adyen_logger.dart +++ b/lib/src/logging/adyen_logger.dart @@ -3,6 +3,13 @@ import 'package:flutter/foundation.dart'; class AdyenLogger { + static final AdyenLogger _instance = AdyenLogger._init(); + + factory AdyenLogger() { + return _instance; + } + + AdyenLogger._init(); bool _loggingEnabled = true; diff --git a/lib/src/models/base_configuration.dart b/lib/src/models/base_configuration.dart new file mode 100644 index 00000000..63a933b0 --- /dev/null +++ b/lib/src/models/base_configuration.dart @@ -0,0 +1,18 @@ +import 'package:adyen_checkout/src/generated/platform_api.g.dart'; +import 'package:adyen_checkout/src/models/amount.dart'; + +base class BaseConfiguration { + final Environment environment; + final String clientKey; + final String countryCode; + final Amount amount; + final String? shopperLocale; + + BaseConfiguration({ + required this.environment, + required this.clientKey, + required this.countryCode, + required this.amount, + required this.shopperLocale, + }); +} diff --git a/lib/src/models/card_component_configuration.dart b/lib/src/models/card_component_configuration.dart new file mode 100644 index 00000000..89dfcdd7 --- /dev/null +++ b/lib/src/models/card_component_configuration.dart @@ -0,0 +1,15 @@ +import 'package:adyen_checkout/src/models/base_configuration.dart'; +import 'package:adyen_checkout/src/models/payment_method_configurations/card_configuration.dart'; + +final class CardComponentConfiguration extends BaseConfiguration { + final CardConfiguration cardConfiguration; + + CardComponentConfiguration({ + required super.environment, + required super.clientKey, + required super.countryCode, + required super.amount, + required super.shopperLocale, + this.cardConfiguration = const CardConfiguration(), + }); +} diff --git a/lib/src/models/component_payment_flow.dart b/lib/src/models/component_payment_flow.dart new file mode 100644 index 00000000..b53aa510 --- /dev/null +++ b/lib/src/models/component_payment_flow.dart @@ -0,0 +1,27 @@ +import 'package:adyen_checkout/adyen_checkout.dart'; + +sealed class ComponentPaymentFlow {} + +class CardComponentSessionFlow extends ComponentPaymentFlow { + final CardComponentConfiguration cardComponentConfiguration; + final Session session; + + CardComponentSessionFlow({ + required this.cardComponentConfiguration, + required this.session, + }); +} + +class CardComponentAdvancedFlow extends ComponentPaymentFlow { + final CardComponentConfiguration cardComponentConfiguration; + final String paymentMethods; + final Future Function(String) onPayments; + final Future Function(String) onPaymentsDetails; + + CardComponentAdvancedFlow({ + required this.cardComponentConfiguration, + required this.paymentMethods, + required this.onPayments, + required this.onPaymentsDetails, + }); +} diff --git a/lib/src/models/drop_in_configuration.dart b/lib/src/models/drop_in_configuration.dart index 4e436055..1e805cc8 100644 --- a/lib/src/models/drop_in_configuration.dart +++ b/lib/src/models/drop_in_configuration.dart @@ -1,13 +1,9 @@ import 'package:adyen_checkout/adyen_checkout.dart'; import 'package:adyen_checkout/src/models/analytics_options.dart'; +import 'package:adyen_checkout/src/models/base_configuration.dart'; -class DropInConfiguration { - final Environment environment; - final String clientKey; - final String countryCode; - final Amount amount; - final String? shopperLocale; - final CardsConfiguration? cardsConfiguration; +final class DropInConfiguration extends BaseConfiguration { + final CardConfiguration? cardConfiguration; final ApplePayConfiguration? applePayConfiguration; final GooglePayConfiguration? googlePayConfiguration; final CashAppPayConfiguration? cashAppPayConfiguration; @@ -16,12 +12,12 @@ class DropInConfiguration { final bool skipListWhenSinglePaymentMethod; DropInConfiguration({ - required this.environment, - required this.clientKey, - required this.countryCode, - required this.amount, - this.shopperLocale, - this.cardsConfiguration, + required super.environment, + required super.clientKey, + required super.countryCode, + required super.amount, + super.shopperLocale, + this.cardConfiguration, this.applePayConfiguration, this.googlePayConfiguration, this.cashAppPayConfiguration, diff --git a/lib/src/models/payment_flow.dart b/lib/src/models/drop_in_payment_flow.dart similarity index 64% rename from lib/src/models/payment_flow.dart rename to lib/src/models/drop_in_payment_flow.dart index 9a45eace..fa63c05e 100644 --- a/lib/src/models/payment_flow.dart +++ b/lib/src/models/drop_in_payment_flow.dart @@ -1,20 +1,18 @@ -import 'package:adyen_checkout/src/models/drop_in_configuration.dart'; -import 'package:adyen_checkout/src/models/payment_flow_outcome.dart'; -import 'package:adyen_checkout/src/models/session.dart'; +import 'package:adyen_checkout/adyen_checkout.dart'; -sealed class PaymentFlow {} +sealed class DropInPaymentFlow {} -class DropInSession extends PaymentFlow { +class DropInSessionFlow extends DropInPaymentFlow { final DropInConfiguration dropInConfiguration; final Session session; - DropInSession({ + DropInSessionFlow({ required this.dropInConfiguration, required this.session, }); } -class DropInAdvancedFlow extends PaymentFlow { +class DropInAdvancedFlow extends DropInPaymentFlow { final DropInConfiguration dropInConfiguration; final String paymentMethodsResponse; Future Function(String paymentComponentJson) postPayments; @@ -28,3 +26,4 @@ class DropInAdvancedFlow extends PaymentFlow { required this.postPaymentsDetails, }); } + diff --git a/lib/src/models/payment_flow_outcome.dart b/lib/src/models/payment_flow_outcome.dart index 26711a7b..fe13c21b 100644 --- a/lib/src/models/payment_flow_outcome.dart +++ b/lib/src/models/payment_flow_outcome.dart @@ -18,8 +18,8 @@ class Error extends PaymentFlowOutcome { final bool dismissDropIn; Error({ - this.errorMessage, - this.reason, + required this.errorMessage, + this.reason = "", this.dismissDropIn = false, }); } diff --git a/lib/src/models/payment_method_configurations/apple_pay_configuration.dart b/lib/src/models/payment_method_configurations/apple_pay_configuration.dart index d8a4b19c..acd3734b 100644 --- a/lib/src/models/payment_method_configurations/apple_pay_configuration.dart +++ b/lib/src/models/payment_method_configurations/apple_pay_configuration.dart @@ -3,7 +3,7 @@ class ApplePayConfiguration { final String merchantName; final bool allowOnboarding; - ApplePayConfiguration({ + const ApplePayConfiguration({ required this.merchantId, required this.merchantName, this.allowOnboarding = false, diff --git a/lib/src/models/payment_method_configurations/cards_configuration.dart b/lib/src/models/payment_method_configurations/card_configuration.dart similarity index 88% rename from lib/src/models/payment_method_configurations/cards_configuration.dart rename to lib/src/models/payment_method_configurations/card_configuration.dart index fa1e8ed2..f7f9e6df 100644 --- a/lib/src/models/payment_method_configurations/cards_configuration.dart +++ b/lib/src/models/payment_method_configurations/card_configuration.dart @@ -1,6 +1,6 @@ import 'package:adyen_checkout/src/generated/platform_api.g.dart'; -class CardsConfiguration { +class CardConfiguration { final bool holderNameRequired; final AddressMode addressMode; final bool showStorePaymentField; @@ -10,11 +10,11 @@ class CardsConfiguration { final FieldVisibility socialSecurityNumberFieldVisibility; final List supportedCardTypes; - CardsConfiguration({ + const CardConfiguration({ this.holderNameRequired = false, this.addressMode = AddressMode.none, this.showStorePaymentField = false, - this.showCvcForStoredCard = false, + this.showCvcForStoredCard = true, this.showCvc = true, this.kcpFieldVisibility = FieldVisibility.hide, this.socialSecurityNumberFieldVisibility = FieldVisibility.hide, diff --git a/lib/src/models/payment_method_configurations/cash_app_pay_configuration.dart b/lib/src/models/payment_method_configurations/cash_app_pay_configuration.dart index 068b18b3..2f1908a1 100644 --- a/lib/src/models/payment_method_configurations/cash_app_pay_configuration.dart +++ b/lib/src/models/payment_method_configurations/cash_app_pay_configuration.dart @@ -4,7 +4,7 @@ class CashAppPayConfiguration { final CashAppPayEnvironment cashAppPayEnvironment; final String returnUrl; - CashAppPayConfiguration( + const CashAppPayConfiguration( this.cashAppPayEnvironment, this.returnUrl, ); diff --git a/lib/src/models/payment_method_configurations/google_pay_configuration.dart b/lib/src/models/payment_method_configurations/google_pay_configuration.dart index 1b1e11bc..1dabbe89 100644 --- a/lib/src/models/payment_method_configurations/google_pay_configuration.dart +++ b/lib/src/models/payment_method_configurations/google_pay_configuration.dart @@ -12,7 +12,7 @@ class GooglePayConfiguration { final bool shippingAddressRequired; final bool existingPaymentMethodRequired; - GooglePayConfiguration({ + const GooglePayConfiguration({ required this.googlePayEnvironment, this.merchantAccount, this.totalPriceStatus, diff --git a/lib/src/platform/adyen_checkout_api.dart b/lib/src/platform/adyen_checkout_api.dart index 9c29fa31..d932e87b 100644 --- a/lib/src/platform/adyen_checkout_api.dart +++ b/lib/src/platform/adyen_checkout_api.dart @@ -8,48 +8,48 @@ class AdyenCheckoutApi implements AdyenCheckoutPlatformInterface { Future getPlatformVersion() => checkoutApi.getPlatformVersion(); @override - void startDropInSessionPayment({ - required SessionDTO session, - required DropInConfigurationDTO dropInConfiguration, - }) => + Future getReturnUrl() => checkoutApi.getReturnUrl(); + + @override + Future startDropInSessionPayment( + DropInConfigurationDTO dropInConfigurationDTO, + SessionDTO session, + ) => checkoutApi.startDropInSessionPayment( - dropInConfiguration, + dropInConfigurationDTO, session, ); @override - void startDropInAdvancedFlowPayment({ - required String paymentMethodsResponse, - required DropInConfigurationDTO dropInConfiguration, - }) => + Future startDropInAdvancedFlowPayment( + DropInConfigurationDTO dropInConfiguration, + String paymentMethodsResponse, + ) => checkoutApi.startDropInAdvancedFlowPayment( dropInConfiguration, paymentMethodsResponse, ); @override - Future getReturnUrl() => checkoutApi.getReturnUrl(); - - @override - void onPaymentsResult(PaymentFlowOutcomeDTO paymentsResult) => + Future onPaymentsResult(PaymentFlowOutcomeDTO paymentsResult) => checkoutApi.onPaymentsResult(paymentsResult); @override - void onPaymentsDetailsResult(PaymentFlowOutcomeDTO paymentsDetailsResult) => + Future onPaymentsDetailsResult( + PaymentFlowOutcomeDTO paymentsDetailsResult) => checkoutApi.onPaymentsDetailsResult(paymentsDetailsResult); @override - void onDeleteStoredPaymentMethodResult( + Future onDeleteStoredPaymentMethodResult( DeletedStoredPaymentMethodResultDTO deleteStoredPaymentMethodResultDTO) => checkoutApi.onDeleteStoredPaymentMethodResult( deleteStoredPaymentMethodResultDTO); @override - void enableLogging(bool loggingEnabled) { - checkoutApi.enableLogging(loggingEnabled); - } + Future enableLogging(bool loggingEnabled) => + checkoutApi.enableLogging(loggingEnabled); @override - void cleanUpDropIn() => checkoutApi.cleanUpDropIn(); + Future cleanUpDropIn() => checkoutApi.cleanUpDropIn(); } diff --git a/lib/src/platform/adyen_checkout_platform_interface.dart b/lib/src/platform/adyen_checkout_platform_interface.dart index 77954f75..8c725f60 100644 --- a/lib/src/platform/adyen_checkout_platform_interface.dart +++ b/lib/src/platform/adyen_checkout_platform_interface.dart @@ -2,7 +2,8 @@ import 'package:adyen_checkout/src/generated/platform_api.g.dart'; import 'package:adyen_checkout/src/platform/adyen_checkout_api.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -abstract class AdyenCheckoutPlatformInterface extends PlatformInterface { +abstract class AdyenCheckoutPlatformInterface extends PlatformInterface + implements CheckoutPlatformInterface { AdyenCheckoutPlatformInterface() : super(token: _token); static final Object _token = Object(); @@ -14,29 +15,4 @@ abstract class AdyenCheckoutPlatformInterface extends PlatformInterface { PlatformInterface.verifyToken(instance, _token); _instance = instance; } - - Future getPlatformVersion(); - - Future getReturnUrl(); - - void startDropInSessionPayment({ - required SessionDTO session, - required DropInConfigurationDTO dropInConfiguration, - }); - - void startDropInAdvancedFlowPayment({ - required String paymentMethodsResponse, - required DropInConfigurationDTO dropInConfiguration, - }); - - void onPaymentsResult(PaymentFlowOutcomeDTO paymentsResult); - - void onPaymentsDetailsResult(PaymentFlowOutcomeDTO paymentsDetailsResult); - - void onDeleteStoredPaymentMethodResult( - DeletedStoredPaymentMethodResultDTO deleteStoredPaymentMethodResultDTO); - - void enableLogging(bool loggingEnabled); - - void cleanUpDropIn(); } diff --git a/lib/src/platform/adyen_checkout_result_api.dart b/lib/src/platform/adyen_checkout_result_api.dart index 35d72631..2ffc6184 100644 --- a/lib/src/platform/adyen_checkout_result_api.dart +++ b/lib/src/platform/adyen_checkout_result_api.dart @@ -8,9 +8,11 @@ class AdyenCheckoutResultApi implements CheckoutFlutterApi { var dropInAdvancedFlowPlatformCommunicationStream = StreamController(); + var componentCommunicationStream = + StreamController(); + @override - void onDropInSessionPlatformCommunication( - PlatformCommunicationModel data) => + void onDropInSessionPlatformCommunication(PlatformCommunicationModel data) => dropInSessionPlatformCommunicationStream.sink.add(data); @override diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart new file mode 100644 index 00000000..f5a7b900 --- /dev/null +++ b/lib/src/utils/constants.dart @@ -0,0 +1,10 @@ +class Constants { + //Components + static const String paymentMethodsKey = "paymentMethods"; + static const String cardComponentConfigurationKey = + "cardComponentConfiguration"; + static const String sessionKey = "session"; + static const String cardComponentSessionFlowKey = "cardComponentSessionFlow"; + static const String cardComponentAdvancedFlowKey = + "cardComponentAdvancedFlow"; +} diff --git a/lib/src/utils/dto_mapper.dart b/lib/src/utils/dto_mapper.dart index 0a0db92c..a17b42c4 100644 --- a/lib/src/utils/dto_mapper.dart +++ b/lib/src/utils/dto_mapper.dart @@ -18,7 +18,7 @@ extension DropInConfigurationMapper on DropInConfiguration { countryCode: countryCode.toUpperCase(), amount: amount.toDTO(), shopperLocale: shopperLocale ?? Platform.localeName, - cardsConfigurationDTO: cardsConfiguration?.toDTO(), + cardConfigurationDTO: cardConfiguration?.toDTO(), applePayConfigurationDTO: applePayConfiguration?.toDTO(), googlePayConfigurationDTO: googlePayConfiguration?.toDTO(), cashAppPayConfigurationDTO: cashAppPayConfiguration?.toDTO(), @@ -39,8 +39,8 @@ extension DropInConfigurationMapper on DropInConfiguration { true; } -extension CardsConfigurationMapper on CardsConfiguration { - CardsConfigurationDTO toDTO() => CardsConfigurationDTO( +extension CardsConfigurationMapper on CardConfiguration { + CardConfigurationDTO toDTO() => CardConfigurationDTO( holderNameRequired: holderNameRequired, addressMode: addressMode, showStorePaymentField: showStorePaymentField, @@ -105,3 +105,14 @@ extension OrderResponseMapper on OrderResponseDTO { orderData: orderData, ); } + +extension CardComponentConfigurationMapper on CardComponentConfiguration { + CardComponentConfigurationDTO toDTO() => CardComponentConfigurationDTO( + environment: environment, + clientKey: clientKey, + countryCode: countryCode, + amount: amount.toDTO(), + shopperLocale: shopperLocale, + cardConfiguration: cardConfiguration.toDTO(), + ); +} diff --git a/lib/src/utils/payment_flow_outcome_handler.dart b/lib/src/utils/payment_flow_outcome_handler.dart new file mode 100644 index 00000000..f0984bae --- /dev/null +++ b/lib/src/utils/payment_flow_outcome_handler.dart @@ -0,0 +1,26 @@ +import 'package:adyen_checkout/src/generated/platform_api.g.dart'; +import 'package:adyen_checkout/src/models/payment_flow_outcome.dart'; + +class PaymentFlowOutcomeHandler { + PaymentFlowOutcomeDTO mapToPaymentOutcomeDTO( + PaymentFlowOutcome paymentFlowOutcome) { + return switch (paymentFlowOutcome) { + Finished() => PaymentFlowOutcomeDTO( + paymentFlowResultType: PaymentFlowResultType.finished, + result: paymentFlowOutcome.resultCode, + ), + Action() => PaymentFlowOutcomeDTO( + paymentFlowResultType: PaymentFlowResultType.action, + actionResponse: paymentFlowOutcome.actionResponse, + ), + Error() => PaymentFlowOutcomeDTO( + paymentFlowResultType: PaymentFlowResultType.error, + error: ErrorDTO( + errorMessage: paymentFlowOutcome.errorMessage, + reason: paymentFlowOutcome.reason, + dismissDropIn: paymentFlowOutcome.dismissDropIn, + ), + ), + }; + } +} diff --git a/lib/src/utils/toggle_area_gesture_recognizer.dart b/lib/src/utils/toggle_area_gesture_recognizer.dart new file mode 100644 index 00000000..0daff3ef --- /dev/null +++ b/lib/src/utils/toggle_area_gesture_recognizer.dart @@ -0,0 +1,66 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +class ToggleAreaGestureRecognizer extends OneSequenceGestureRecognizer { + final GlobalKey cardWidgetKey; + final TextDirection textDirection; + + ToggleAreaGestureRecognizer({ + required this.cardWidgetKey, + required this.textDirection, + }); + + @override + void addPointer(PointerDownEvent event) => + startTrackingPointer(event.pointer); + + @override + void handleEvent(PointerEvent event) { + final renderBox = + cardWidgetKey.currentContext?.findRenderObject() as RenderBox; + + switch (event) { + case PointerDownEvent(): + if (_isPointerOverToggle(event, renderBox)) { + resolve(GestureDisposition.accepted); + stopTrackingPointer(event.pointer); + } + + case PointerMoveEvent(): + case PointerUpEvent(): + resolve(GestureDisposition.rejected); + stopTrackingPointer(event.pointer); + } + } + + @override + void didStopTrackingLastPointer(int pointer) {} + + @override + String get debugDescription => "ToggleAreaGestureRecognizer"; + + // ignore: unused_element + bool _isPointerWithinBottomHalfOfCardView( + PointerDownEvent event, + RenderBox renderBox, + ) { + final deltaFromTop = renderBox.localToGlobal(Offset.zero).dy; + final cardWidgetHalfHeight = renderBox.size.height / 2; + final tapWithinBottomHalfOfCardWidget = event.position.dy - deltaFromTop; + return tapWithinBottomHalfOfCardWidget > cardWidgetHalfHeight; + } + + bool _isPointerOverToggle( + PointerDownEvent event, + RenderBox renderBox, + ) { + const toggleWidth = 80; + final cardWidgetWidth = renderBox.size.width; + switch (textDirection) { + case TextDirection.ltr: + return event.localPosition.dx > (cardWidgetWidth - toggleWidth); + case TextDirection.rtl: + return event.localPosition.dx < toggleWidth; + } + } +} diff --git a/pigeons/platform_api.dart b/pigeons/platform_api.dart index d267de78..e03a7ebd 100644 --- a/pigeons/platform_api.dart +++ b/pigeons/platform_api.dart @@ -59,6 +59,14 @@ enum PlatformCommunicationType { deleteStoredPaymentMethod, } +enum ComponentCommunicationType { + onSubmit, + additionalDetails, + result, + error, + resize, +} + enum PaymentFlowResultType { finished, action, @@ -106,7 +114,7 @@ class DropInConfigurationDTO { final String countryCode; final AmountDTO amount; final String shopperLocale; - final CardsConfigurationDTO? cardsConfigurationDTO; + final CardConfigurationDTO? cardConfigurationDTO; final ApplePayConfigurationDTO? applePayConfigurationDTO; final GooglePayConfigurationDTO? googlePayConfigurationDTO; final CashAppPayConfigurationDTO? cashAppPayConfigurationDTO; @@ -121,7 +129,7 @@ class DropInConfigurationDTO { this.countryCode, this.amount, this.shopperLocale, - this.cardsConfigurationDTO, + this.cardConfigurationDTO, this.applePayConfigurationDTO, this.googlePayConfigurationDTO, this.cashAppPayConfigurationDTO, @@ -132,7 +140,7 @@ class DropInConfigurationDTO { ); } -class CardsConfigurationDTO { +class CardConfigurationDTO { final bool holderNameRequired; final AddressMode addressMode; final bool showStorePaymentField; @@ -142,7 +150,7 @@ class CardsConfigurationDTO { final FieldVisibility socialSecurityNumberFieldVisibility; final List supportedCardTypes; - CardsConfigurationDTO( + CardConfigurationDTO( this.holderNameRequired, this.addressMode, this.showStorePaymentField, @@ -217,12 +225,14 @@ class PaymentResultDTO { class PaymentResultModelDTO { final String? sessionId; final String? sessionData; + final String? sessionResult; final String? resultCode; final OrderResponseDTO? order; PaymentResultModelDTO( this.sessionId, this.sessionData, + this.sessionResult, this.resultCode, this.order, ); @@ -254,6 +264,18 @@ class PlatformCommunicationModel { }); } +class ComponentCommunicationModel { + final ComponentCommunicationType type; + final Object? data; + final PaymentResultModelDTO? paymentResult; + + ComponentCommunicationModel({ + required this.type, + this.data, + this.paymentResult, + }); +} + //Use PaymentFlowOutcome class when sealed classes are supported by pigeon class PaymentFlowOutcomeDTO { final PaymentFlowResultType paymentFlowResultType; @@ -291,6 +313,24 @@ class DeletedStoredPaymentMethodResultDTO { ); } +class CardComponentConfigurationDTO { + final Environment environment; + final String clientKey; + final String countryCode; + final AmountDTO amount; + final String? shopperLocale; + final CardConfigurationDTO cardConfiguration; + + CardComponentConfigurationDTO( + this.environment, + this.clientKey, + this.countryCode, + this.amount, + this.shopperLocale, + this.cardConfiguration, + ); +} + @HostApi() abstract class CheckoutPlatformInterface { @async @@ -329,3 +369,24 @@ abstract class CheckoutFlutterApi { void onDropInAdvancedFlowPlatformCommunication( PlatformCommunicationModel platformCommunicationModel); } + +@HostApi() +abstract class ComponentPlatformInterface { + void updateViewHeight(int viewId); + + void onPaymentsResult(PaymentFlowOutcomeDTO paymentsResult); + + void onPaymentsDetailsResult(PaymentFlowOutcomeDTO paymentsDetailsResult); +} + +@FlutterApi() +abstract class ComponentFlutterInterface { + // ignore: unused_element + void _generateCodecForDTOs( + CardComponentConfigurationDTO cardComponentConfigurationDTO, + SessionDTO sessionDTO, + ); + + void onComponentCommunication( + ComponentCommunicationModel componentCommunicationModel); +} diff --git a/pubspec.yaml b/pubspec.yaml index 2beffcbb..213060de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: flutter: sdk: flutter plugin_platform_interface: 2.1.6 + stream_transform: 2.1.0 dev_dependencies: flutter_test: diff --git a/test/adyen_checkout_test.dart b/test/adyen_checkout_test.dart index cb29d771..e8a3a25b 100644 --- a/test/adyen_checkout_test.dart +++ b/test/adyen_checkout_test.dart @@ -11,11 +11,8 @@ class MockAdyenCheckoutPlatform Future getPlatformVersion() => Future.value('42'); @override - void startDropInSessionPayment( - {required SessionDTO session, - required DropInConfigurationDTO dropInConfiguration}) { - return; - } + Future startDropInSessionPayment( + DropInConfigurationDTO dropInConfiguration, SessionDTO session) async {} @override Future getReturnUrl() { @@ -23,12 +20,10 @@ class MockAdyenCheckoutPlatform } @override - void startDropInAdvancedFlowPayment({ - required String paymentMethodsResponse, - required DropInConfigurationDTO dropInConfiguration, - }) { - return; - } + Future startDropInAdvancedFlowPayment( + DropInConfigurationDTO dropInConfiguration, + String paymentMethodsResponse, + ) async {} @override Future onPaymentsResult(PaymentFlowOutcomeDTO paymentsResult) => @@ -40,14 +35,15 @@ class MockAdyenCheckoutPlatform Future.value(null); @override - void onDeleteStoredPaymentMethodResult( - DeletedStoredPaymentMethodResultDTO deleteStoredPaymentMethodResultDTO) {} + Future onDeleteStoredPaymentMethodResult( + DeletedStoredPaymentMethodResultDTO + deleteStoredPaymentMethodResultDTO) async {} @override - void enableLogging(bool loggingEnabled) {} + Future enableLogging(bool loggingEnabled) async {} @override - void cleanUpDropIn() {} + Future cleanUpDropIn() async {} } void main() {