From 8b5c32e4666929afe786359b3e18d68f880bdd3f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 4 Jul 2024 17:36:17 +0300 Subject: [PATCH 01/29] Interactor, VM, states, activity --- .../ui/checkout/DynamicCheckoutActivity.kt | 81 ++++++++++++++++++- .../ui/checkout/DynamicCheckoutInteractor.kt | 69 ++++++++++++++++ .../DynamicCheckoutInteractorState.kt | 8 ++ .../sdk/ui/checkout/DynamicCheckoutScreen.kt | 23 +++++- .../ui/checkout/DynamicCheckoutViewModel.kt | 47 +++++++++++ .../checkout/DynamicCheckoutViewModelState.kt | 16 ++++ .../PODynamicCheckoutConfiguration.kt | 9 ++- 7 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt index 43583a829..949b6c3c2 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt @@ -1,31 +1,110 @@ package com.processout.sdk.ui.checkout import android.app.Activity +import android.content.Intent import android.graphics.Color import android.os.Build import android.os.Bundle import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.processout.sdk.core.POFailure.Code.Generic +import com.processout.sdk.core.POUnit +import com.processout.sdk.core.ProcessOutActivityResult +import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.toActivityResult import com.processout.sdk.ui.R import com.processout.sdk.ui.base.BaseTransparentPortraitActivity +import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_CONFIGURATION +import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_RESULT +import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure +import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Success +import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options import com.processout.sdk.ui.core.theme.ProcessOutTheme internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { + private var configuration: PODynamicCheckoutConfiguration? = null + + private val viewModel: DynamicCheckoutViewModel by viewModels { + DynamicCheckoutViewModel.Factory( + app = application, + invoiceId = configuration?.invoiceId ?: String(), + options = configuration?.options ?: Options() + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge( statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT), navigationBarStyle = SystemBarStyle.dark(Color.BLACK) ) + if (savedInstanceState == null) { + initConfiguration() + } setContent { ProcessOutTheme { - DynamicCheckoutScreen() + with(viewModel.completion.collectAsStateWithLifecycle()) { + LaunchedEffect(value) { handle(value) } + } + DynamicCheckoutScreen( + state = viewModel.state.collectAsStateWithLifecycle().value, + onEvent = remember { viewModel::onEvent }, + style = DynamicCheckoutScreen.style(custom = configuration?.style) + ) + } + } + } + + private fun initConfiguration() { + @Suppress("DEPRECATION") + configuration = intent.getParcelableExtra(EXTRA_CONFIGURATION) + configuration?.run { + if (invoiceId.isBlank()) { + dismiss( + ProcessOutResult.Failure( + code = Generic(), + message = "Invalid configuration." + ) + ) } } } + private fun handle(completion: DynamicCheckoutCompletion) = + when (completion) { + Success -> finishWithActivityResult( + resultCode = Activity.RESULT_OK, + result = ProcessOutActivityResult.Success(POUnit) + ) + is Failure -> finishWithActivityResult( + resultCode = Activity.RESULT_CANCELED, + result = completion.failure.toActivityResult() + ) + else -> {} + } + + private fun dismiss(failure: ProcessOutResult.Failure) { + // TODO: notify VM + finishWithActivityResult( + resultCode = Activity.RESULT_CANCELED, + result = failure.toActivityResult() + ) + } + + private fun finishWithActivityResult( + resultCode: Int, + result: ProcessOutActivityResult + ) { + setResult(resultCode, Intent().putExtra(EXTRA_RESULT, result)) + finish() + } + override fun finish() { super.finish() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt new file mode 100644 index 000000000..0bad96261 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt @@ -0,0 +1,69 @@ +package com.processout.sdk.ui.checkout + +import android.app.Application +import com.processout.sdk.api.service.POInvoicesService +import com.processout.sdk.core.POFailure.Code.Generic +import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.onFailure +import com.processout.sdk.core.onSuccess +import com.processout.sdk.ui.base.BaseInteractor +import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Awaiting +import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DynamicCheckoutInteractor( + private val app: Application, + private val invoiceId: String, + private val invoicesService: POInvoicesService +) : BaseInteractor() { + + private val _completion = MutableStateFlow(Awaiting) + val completion = _completion.asStateFlow() + + private val _state = MutableStateFlow(initState()) + val state = _state.asStateFlow() + + init { + fetchConfiguration() + } + + private fun initState() = DynamicCheckoutInteractorState( + loading = true, + paymentMethods = emptyList() + ) + + private fun fetchConfiguration() { + interactorScope.launch { + invoicesService.invoice(invoiceId) + .onSuccess { invoice -> + val paymentMethods = invoice.paymentMethods + if (paymentMethods.isNullOrEmpty()) { + _completion.update { + Failure( + ProcessOutResult.Failure( + code = Generic(), + message = "Missing configuration." + ) + ) + } + return@launch + } + _state.update { + it.copy( + loading = false, + paymentMethods = paymentMethods + ) + } + }.onFailure { failure -> + _completion.update { Failure(failure) } + } + } + } + + fun onEvent(event: DynamicCheckoutEvent) { + // TODO + } +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt new file mode 100644 index 000000000..3603dce56 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt @@ -0,0 +1,8 @@ +package com.processout.sdk.ui.checkout + +import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod + +internal data class DynamicCheckoutInteractorState( + val loading: Boolean, + val paymentMethods: List +) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt index 30917c342..afe6af7c7 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt @@ -5,15 +5,21 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import com.processout.sdk.ui.core.component.field.POField import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.shared.component.isImeVisibleAsState @Composable -internal fun DynamicCheckoutScreen() { +internal fun DynamicCheckoutScreen( + state: DynamicCheckoutViewModelState, + onEvent: (DynamicCheckoutEvent) -> Unit, + style: DynamicCheckoutScreen.Style = DynamicCheckoutScreen.style() +) { Column { Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars)) Scaffold( @@ -71,3 +77,18 @@ private fun Footer() { ) } } + +internal object DynamicCheckoutScreen { + + @Immutable + data class Style( + val field: POField.Style + ) + + @Composable + fun style(custom: PODynamicCheckoutConfiguration.Style? = null) = Style( + field = custom?.field?.let { + POField.custom(style = it) + } ?: POField.default + ) +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt new file mode 100644 index 000000000..0538f3fef --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -0,0 +1,47 @@ +package com.processout.sdk.ui.checkout + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.processout.sdk.api.ProcessOut +import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options +import com.processout.sdk.ui.shared.extension.map + +internal class DynamicCheckoutViewModel( + private val app: Application, + private val options: Options, + private val interactor: DynamicCheckoutInteractor +) : ViewModel() { + + class Factory( + private val app: Application, + private val invoiceId: String, + private val options: Options + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + DynamicCheckoutViewModel( + app = app, + options = options, + interactor = DynamicCheckoutInteractor( + app = app, + invoiceId = invoiceId, + invoicesService = ProcessOut.instance.invoices + ) + ) as T + } + + val completion = interactor.completion + + val state = interactor.state.map(viewModelScope, ::map) + + init { + addCloseable(interactor.interactorScope) + } + + fun onEvent(event: DynamicCheckoutEvent) = interactor.onEvent(event) + + private fun map(state: DynamicCheckoutInteractorState): DynamicCheckoutViewModelState = + DynamicCheckoutViewModelState.Starting +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt new file mode 100644 index 000000000..3687b905a --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt @@ -0,0 +1,16 @@ +package com.processout.sdk.ui.checkout + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface DynamicCheckoutViewModelState { + + @Immutable + data object Starting : DynamicCheckoutViewModelState + + @Immutable + data object Started : DynamicCheckoutViewModelState + + @Immutable + data object Success : DynamicCheckoutViewModelState +} diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt index 32b28ef0c..f945affce 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt @@ -9,9 +9,16 @@ import kotlinx.parcelize.Parcelize @ProcessOutInternalApi @Parcelize data class PODynamicCheckoutConfiguration( - val invoiceId: String + val invoiceId: String, + val options: Options = Options(), + val style: Style? = null ) : Parcelable { + @Parcelize + data class Options( + val title: String? = null + ) : Parcelable + @Parcelize data class Style( val field: POFieldStyle? = null From bb9a6c486d46adc0c7d71c628d13254fb6972e24 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 8 Jul 2024 15:04:31 +0300 Subject: [PATCH 02/29] ProcessOutInternalApi for 'paymentMethods' in POInvoice --- .../com/processout/sdk/api/model/response/InvoiceResponse.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt index 553bd82e3..db9d47c14 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt @@ -1,5 +1,6 @@ package com.processout.sdk.api.model.response +import com.processout.sdk.core.annotation.ProcessOutInternalApi import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -25,7 +26,7 @@ data class POInvoice( @Json(name = "return_url") val returnUrl: String?, @Json(name = "payment_methods") - internal val paymentMethods: List? + @ProcessOutInternalApi val paymentMethods: List? ) /** From bb561611dcf0589bda616ad00a68cb98497339d5 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 8 Jul 2024 15:13:42 +0300 Subject: [PATCH 03/29] DC APM config in invoice response: removed 'gatewayName', changed to 'gateway_configuration_id' --- .../com/processout/sdk/api/model/response/InvoiceResponse.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt index db9d47c14..6c0596a96 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt @@ -171,15 +171,12 @@ sealed class PODynamicCheckoutPaymentMethod { * Alternative payment configuration. * * @param[gatewayConfigurationId] Gateway configuration ID. - * @param[gatewayName] Gateway name. * @param[redirectUrl] Redirect URL. If it's _null_, then payment should go through the native flow. */ @JsonClass(generateAdapter = true) data class AlternativePaymentConfiguration( - @Json(name = "gateway_configuration_uid") + @Json(name = "gateway_configuration_id") val gatewayConfigurationId: String, - @Json(name = "gateway_name") - val gatewayName: String, @Json(name = "redirect_url") val redirectUrl: String? ) From 803903c227639ab0c3129c8f52845f6ab8e4b7da Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 8 Jul 2024 15:16:30 +0300 Subject: [PATCH 04/29] Replace AndroidViewModel() with ViewModel() in nAPM --- .../ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index 8e7afc428..e72f82f35 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -9,7 +9,6 @@ import android.view.inputmethod.EditorInfo import androidx.annotation.StringRes import androidx.core.os.postDelayed import androidx.core.text.isDigitsOnly -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -60,7 +59,7 @@ internal class NativeAlternativePaymentMethodViewModel( private val captureRetryStrategy: PORetryStrategy, val options: Options, val logAttributes: Map -) : AndroidViewModel(app) { +) : ViewModel() { class Factory( private val app: Application, From e2a3e48fc6bc0601536fb5459bcc37f54bc47aaf Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 8 Jul 2024 15:23:31 +0300 Subject: [PATCH 05/29] private constructor for all VMs --- .../sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt | 2 +- .../sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt | 2 +- .../sdk/ui/card/tokenization/CardTokenizationViewModel.kt | 2 +- .../com/processout/sdk/ui/card/update/CardUpdateViewModel.kt | 2 +- .../com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt | 2 +- .../processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index e72f82f35..82cac9787 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -50,7 +50,7 @@ import java.text.NumberFormat import java.util.Currency import java.util.concurrent.TimeUnit -internal class NativeAlternativePaymentMethodViewModel( +internal class NativeAlternativePaymentMethodViewModel private constructor( private val app: Application, private val gatewayConfigurationId: String, private val invoiceId: String, diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt index 558700888..948ef6a06 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt @@ -14,7 +14,7 @@ import com.processout.sdk.ui.web.customtab.CustomTabAuthorizationActivityContrac import com.processout.sdk.ui.web.customtab.CustomTabAuthorizationUiState.* import java.util.concurrent.TimeUnit -internal class CustomTabAuthorizationViewModel( +internal class CustomTabAuthorizationViewModel private constructor( private val savedState: SavedStateHandle, private val configuration: CustomTabConfiguration ) : ViewModel() { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index b205635f1..21f246a0e 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -30,7 +30,7 @@ import com.processout.sdk.ui.shared.state.FieldState import com.processout.sdk.ui.shared.transformation.CardExpirationVisualTransformation import com.processout.sdk.ui.shared.transformation.CardNumberVisualTransformation -internal class CardTokenizationViewModel( +internal class CardTokenizationViewModel private constructor( private val app: Application, private val configuration: POCardTokenizationConfiguration, private val interactor: CardTokenizationInteractor diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt index 3967a46e9..d337294d3 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/update/CardUpdateViewModel.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -internal class CardUpdateViewModel( +internal class CardUpdateViewModel private constructor( private val app: Application, private val cardId: String, private val options: POCardUpdateConfiguration.Options, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index 0538f3fef..6c54dd55f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -8,7 +8,7 @@ import com.processout.sdk.api.ProcessOut import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options import com.processout.sdk.ui.shared.extension.map -internal class DynamicCheckoutViewModel( +internal class DynamicCheckoutViewModel private constructor( private val app: Application, private val options: Options, private val interactor: DynamicCheckoutInteractor diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt index b6fcaa273..b96cc8509 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt @@ -34,7 +34,7 @@ import com.processout.sdk.ui.shared.transformation.PhoneNumberVisualTransformati import java.text.NumberFormat import java.util.Currency -internal class NativeAlternativePaymentViewModel( +internal class NativeAlternativePaymentViewModel private constructor( private val app: Application, private val options: Options, private val interactor: NativeAlternativePaymentInteractor From 09bf497a8171415aa2ad86f13d47d53ae1eba332 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 8 Jul 2024 15:44:52 +0300 Subject: [PATCH 06/29] Separate create() method in nAPM and CardTokenization VM Factories --- .../tokenization/CardTokenizationViewModel.kt | 23 +++++----- .../napm/NativeAlternativePaymentViewModel.kt | 45 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 21f246a0e..46a1ce47d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -41,19 +41,20 @@ internal class CardTokenizationViewModel private constructor( private val configuration: POCardTokenizationConfiguration ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - CardTokenizationViewModel( + override fun create(modelClass: Class): T = create() as T + + fun create() = CardTokenizationViewModel( + app = app, + configuration = configuration, + interactor = CardTokenizationInteractor( app = app, configuration = configuration, - interactor = CardTokenizationInteractor( - app = app, - configuration = configuration, - cardsRepository = ProcessOut.instance.cards, - cardSchemeProvider = CardSchemeProvider(), - addressSpecificationProvider = AddressSpecificationProvider(app), - eventDispatcher = PODefaultCardTokenizationEventDispatcher - ) - ) as T + cardsRepository = ProcessOut.instance.cards, + cardSchemeProvider = CardSchemeProvider(), + addressSpecificationProvider = AddressSpecificationProvider(app), + eventDispatcher = PODefaultCardTokenizationEventDispatcher + ) + ) } private data class KeyboardAction( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt index b96cc8509..d2b52176f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt @@ -47,30 +47,31 @@ internal class NativeAlternativePaymentViewModel private constructor( private val options: Options ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - NativeAlternativePaymentViewModel( + override fun create(modelClass: Class): T = create() as T + + fun create() = NativeAlternativePaymentViewModel( + app = app, + options = options, + interactor = NativeAlternativePaymentInteractor( app = app, - options = options, - interactor = NativeAlternativePaymentInteractor( - app = app, - invoiceId = invoiceId, - gatewayConfigurationId = gatewayConfigurationId, - options = options.validated(), - invoicesService = ProcessOut.instance.invoices, - captureRetryStrategy = Exponential( - maxRetries = Int.MAX_VALUE, - initialDelay = 150, - minDelay = 3 * 1000, - maxDelay = 90 * 1000, - factor = 1.45 - ), - eventDispatcher = PODefaultNativeAlternativePaymentMethodEventDispatcher, - logAttributes = mapOf( - POLogAttribute.INVOICE_ID to invoiceId, - POLogAttribute.GATEWAY_CONFIGURATION_ID to gatewayConfigurationId - ) + invoiceId = invoiceId, + gatewayConfigurationId = gatewayConfigurationId, + options = options.validated(), + invoicesService = ProcessOut.instance.invoices, + captureRetryStrategy = Exponential( + maxRetries = Int.MAX_VALUE, + initialDelay = 150, + minDelay = 3 * 1000, + maxDelay = 90 * 1000, + factor = 1.45 + ), + eventDispatcher = PODefaultNativeAlternativePaymentMethodEventDispatcher, + logAttributes = mapOf( + POLogAttribute.INVOICE_ID to invoiceId, + POLogAttribute.GATEWAY_CONFIGURATION_ID to gatewayConfigurationId ) - ) as T + ) + ) private fun Options.validated() = copy( paymentConfirmation = with(paymentConfirmation) { From b3b6653e4ccb9050ed40b1280439452df16be088 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 8 Jul 2024 18:51:21 +0300 Subject: [PATCH 07/29] revert --- .../tokenization/CardTokenizationViewModel.kt | 23 +++++----- .../napm/NativeAlternativePaymentViewModel.kt | 45 +++++++++---------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 46a1ce47d..21f246a0e 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -41,20 +41,19 @@ internal class CardTokenizationViewModel private constructor( private val configuration: POCardTokenizationConfiguration ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = create() as T - - fun create() = CardTokenizationViewModel( - app = app, - configuration = configuration, - interactor = CardTokenizationInteractor( + override fun create(modelClass: Class): T = + CardTokenizationViewModel( app = app, configuration = configuration, - cardsRepository = ProcessOut.instance.cards, - cardSchemeProvider = CardSchemeProvider(), - addressSpecificationProvider = AddressSpecificationProvider(app), - eventDispatcher = PODefaultCardTokenizationEventDispatcher - ) - ) + interactor = CardTokenizationInteractor( + app = app, + configuration = configuration, + cardsRepository = ProcessOut.instance.cards, + cardSchemeProvider = CardSchemeProvider(), + addressSpecificationProvider = AddressSpecificationProvider(app), + eventDispatcher = PODefaultCardTokenizationEventDispatcher + ) + ) as T } private data class KeyboardAction( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt index d2b52176f..b96cc8509 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt @@ -47,31 +47,30 @@ internal class NativeAlternativePaymentViewModel private constructor( private val options: Options ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = create() as T - - fun create() = NativeAlternativePaymentViewModel( - app = app, - options = options, - interactor = NativeAlternativePaymentInteractor( + override fun create(modelClass: Class): T = + NativeAlternativePaymentViewModel( app = app, - invoiceId = invoiceId, - gatewayConfigurationId = gatewayConfigurationId, - options = options.validated(), - invoicesService = ProcessOut.instance.invoices, - captureRetryStrategy = Exponential( - maxRetries = Int.MAX_VALUE, - initialDelay = 150, - minDelay = 3 * 1000, - maxDelay = 90 * 1000, - factor = 1.45 - ), - eventDispatcher = PODefaultNativeAlternativePaymentMethodEventDispatcher, - logAttributes = mapOf( - POLogAttribute.INVOICE_ID to invoiceId, - POLogAttribute.GATEWAY_CONFIGURATION_ID to gatewayConfigurationId + options = options, + interactor = NativeAlternativePaymentInteractor( + app = app, + invoiceId = invoiceId, + gatewayConfigurationId = gatewayConfigurationId, + options = options.validated(), + invoicesService = ProcessOut.instance.invoices, + captureRetryStrategy = Exponential( + maxRetries = Int.MAX_VALUE, + initialDelay = 150, + minDelay = 3 * 1000, + maxDelay = 90 * 1000, + factor = 1.45 + ), + eventDispatcher = PODefaultNativeAlternativePaymentMethodEventDispatcher, + logAttributes = mapOf( + POLogAttribute.INVOICE_ID to invoiceId, + POLogAttribute.GATEWAY_CONFIGURATION_ID to gatewayConfigurationId + ) ) - ) - ) + ) as T private fun Options.validated() = copy( paymentConfirmation = with(paymentConfirmation) { From 05a398fbe54f2a20d463e7669f81bb246b22d663 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 8 Jul 2024 19:39:23 +0300 Subject: [PATCH 08/29] start() function for Card Tokenization and nAPM flows --- .../sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt | 1 + .../sdk/ui/card/tokenization/CardTokenizationInteractor.kt | 2 +- .../sdk/ui/card/tokenization/CardTokenizationViewModel.kt | 4 ++++ .../sdk/ui/napm/NativeAlternativePaymentBottomSheet.kt | 2 ++ .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 2 +- .../sdk/ui/napm/NativeAlternativePaymentViewModel.kt | 4 ++++ 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt index 26a94d789..f31955cfe 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt @@ -47,6 +47,7 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment Date: Tue, 9 Jul 2024 11:48:15 +0300 Subject: [PATCH 09/29] Remove super.onCleared() from VMs --- .../sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt | 1 - .../sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index 82cac9787..916ef05a0 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -734,7 +734,6 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( } override fun onCleared() { - super.onCleared() handler.removeCallbacksAndMessages(null) } } diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt index 948ef6a06..3706d94cd 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/web/customtab/CustomTabAuthorizationViewModel.kt @@ -100,7 +100,6 @@ internal class CustomTabAuthorizationViewModel private constructor( } override fun onCleared() { - super.onCleared() timeoutHandler.removeCallbacksAndMessages(null) } } From 01e83ba3d7f9d9d2c851d65bea7eb0edab804ffb Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 9 Jul 2024 11:53:23 +0300 Subject: [PATCH 10/29] reset nAPM interactor --- .../tokenization/CardTokenizationViewModel.kt | 4 +- .../NativeAlternativePaymentInteractor.kt | 39 +++++++++++++++++-- .../napm/NativeAlternativePaymentViewModel.kt | 19 ++++----- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 22007a89a..7cfd435ea 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -69,9 +69,7 @@ internal class CardTokenizationViewModel private constructor( addCloseable(interactor.interactorScope) } - fun start() { - interactor.start() - } + fun start() = interactor.start() fun onEvent(event: CardTokenizationEvent) = interactor.onEvent(event) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index d6d86e762..d0c60905d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -29,6 +29,7 @@ import com.processout.sdk.core.POFailure.InvalidField import com.processout.sdk.core.POFailure.ValidationCode import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.fold +import com.processout.sdk.core.logger.POLogAttribute import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess @@ -50,17 +51,28 @@ import kotlinx.coroutines.flow.update internal class NativeAlternativePaymentInteractor( private val app: Application, - private val invoiceId: String, - private val gatewayConfigurationId: String, + private var invoiceId: String, + private var gatewayConfigurationId: String, private val options: Options, private val invoicesService: POInvoicesService, private val captureRetryStrategy: PORetryStrategy, private val eventDispatcher: PODefaultNativeAlternativePaymentMethodEventDispatcher, - private val logAttributes: Map + private var logAttributes: Map = logAttributes( + invoiceId = invoiceId, + gatewayConfigurationId = gatewayConfigurationId + ) ) : BaseInteractor() { - companion object { + private companion object { const val SUCCESS_DELAY_MS = 3000L + + fun logAttributes( + invoiceId: String, + gatewayConfigurationId: String + ): Map = mapOf( + POLogAttribute.INVOICE_ID to invoiceId, + POLogAttribute.GATEWAY_CONFIGURATION_ID to gatewayConfigurationId + ) } private val _completion = MutableStateFlow(Awaiting) @@ -84,6 +96,25 @@ internal class NativeAlternativePaymentInteractor( fetchTransactionDetails() } + fun reset( + invoiceId: String, + gatewayConfigurationId: String + ) { + onCleared() + interactorScope.coroutineContext.cancelChildren() + latestDefaultValuesRequest = null + captureStartTimestamp = 0L + capturePassedTimestamp = 0L + this.invoiceId = invoiceId + this.gatewayConfigurationId = gatewayConfigurationId + logAttributes = logAttributes( + invoiceId = invoiceId, + gatewayConfigurationId = gatewayConfigurationId + ) + _completion.update { Awaiting } + _state.update { Loading } + } + private fun fetchTransactionDetails() { interactorScope.launch { invoicesService.fetchNativeAlternativePaymentMethodTransactionDetails( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt index 70e0dd6b1..b465d0ae3 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt @@ -15,7 +15,6 @@ import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentM import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.* import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice -import com.processout.sdk.core.logger.POLogAttribute import com.processout.sdk.core.retry.PORetryStrategy.Exponential import com.processout.sdk.ui.core.state.POActionState import com.processout.sdk.ui.core.state.POActionState.Confirmation @@ -64,11 +63,7 @@ internal class NativeAlternativePaymentViewModel private constructor( maxDelay = 90 * 1000, factor = 1.45 ), - eventDispatcher = PODefaultNativeAlternativePaymentMethodEventDispatcher, - logAttributes = mapOf( - POLogAttribute.INVOICE_ID to invoiceId, - POLogAttribute.GATEWAY_CONFIGURATION_ID to gatewayConfigurationId - ) + eventDispatcher = PODefaultNativeAlternativePaymentMethodEventDispatcher ) ) as T @@ -100,9 +95,15 @@ internal class NativeAlternativePaymentViewModel private constructor( addCloseable(interactor.interactorScope) } - fun start() { - interactor.start() - } + fun start() = interactor.start() + + fun reset( + invoiceId: String, + gatewayConfigurationId: String + ) = interactor.reset( + invoiceId = invoiceId, + gatewayConfigurationId = gatewayConfigurationId + ) fun onEvent(event: NativeAlternativePaymentEvent) = interactor.onEvent(event) From ba5fa7dc1081c5aa10e740bfe9dcef1da8252bee Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 9 Jul 2024 12:35:10 +0300 Subject: [PATCH 11/29] reset CardTokenization VM and interactor --- .../card/tokenization/CardTokenizationInteractor.kt | 13 ++++++++++++- .../card/tokenization/CardTokenizationViewModel.kt | 7 ++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index ff24ec4ba..c9276e637 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -36,6 +36,7 @@ import com.processout.sdk.ui.shared.provider.address.AddressSpecification import com.processout.sdk.ui.shared.provider.address.AddressSpecification.AddressUnit import com.processout.sdk.ui.shared.provider.address.AddressSpecificationProvider import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -44,7 +45,7 @@ import java.util.Locale internal class CardTokenizationInteractor( private val app: Application, - private val configuration: POCardTokenizationConfiguration, + private var configuration: POCardTokenizationConfiguration, private val cardsRepository: POCardsRepository, private val cardSchemeProvider: CardSchemeProvider, private val addressSpecificationProvider: AddressSpecificationProvider, @@ -87,6 +88,16 @@ internal class CardTokenizationInteractor( } } + fun reset(configuration: POCardTokenizationConfiguration) { + interactorScope.coroutineContext.cancelChildren() + issuerInformationJob = null + latestPreferredSchemeRequest = null + latestShouldContinueRequest = null + this.configuration = configuration + _completion.update { Awaiting } + _state.update { initState() } + } + private fun initState() = CardTokenizationInteractorState( cardFields = cardFields(), addressFields = emptyList(), diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 7cfd435ea..18a9f1c7d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -32,7 +32,7 @@ import com.processout.sdk.ui.shared.transformation.CardNumberVisualTransformatio internal class CardTokenizationViewModel private constructor( private val app: Application, - private val configuration: POCardTokenizationConfiguration, + private var configuration: POCardTokenizationConfiguration, private val interactor: CardTokenizationInteractor ) : ViewModel() { @@ -71,6 +71,11 @@ internal class CardTokenizationViewModel private constructor( fun start() = interactor.start() + fun reset(configuration: POCardTokenizationConfiguration) { + this.configuration = configuration + interactor.reset(configuration) + } + fun onEvent(event: CardTokenizationEvent) = interactor.onEvent(event) private fun map(state: CardTokenizationInteractorState) = with(configuration) { From b99846fd2823c88804fcf09dc43d59918ce2be79 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 9 Jul 2024 12:51:55 +0300 Subject: [PATCH 12/29] Init VMs --- .../ui/checkout/DynamicCheckoutActivity.kt | 23 +++++++++++++++++-- .../ui/checkout/DynamicCheckoutViewModel.kt | 14 ++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt index 949b6c3c2..9c71cd1d5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt @@ -19,22 +19,41 @@ import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.toActivityResult import com.processout.sdk.ui.R import com.processout.sdk.ui.base.BaseTransparentPortraitActivity +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_CONFIGURATION import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_RESULT import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Success -import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options import com.processout.sdk.ui.core.theme.ProcessOutTheme +import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel +import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { private var configuration: PODynamicCheckoutConfiguration? = null private val viewModel: DynamicCheckoutViewModel by viewModels { + val cardTokenization: CardTokenizationViewModel by viewModels { + CardTokenizationViewModel.Factory( + app = application, + configuration = POCardTokenizationConfiguration() + ) + } + val nativeAlternativePayment: NativeAlternativePaymentViewModel by viewModels { + NativeAlternativePaymentViewModel.Factory( + app = application, + invoiceId = configuration?.invoiceId ?: String(), + gatewayConfigurationId = String(), + options = PONativeAlternativePaymentConfiguration.Options() + ) + } DynamicCheckoutViewModel.Factory( app = application, invoiceId = configuration?.invoiceId ?: String(), - options = configuration?.options ?: Options() + options = configuration?.options ?: PODynamicCheckoutConfiguration.Options(), + cardTokenization = cardTokenization, + nativeAlternativePayment = nativeAlternativePayment ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index 6c54dd55f..ee1aed425 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -5,19 +5,25 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.processout.sdk.api.ProcessOut +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options +import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel import com.processout.sdk.ui.shared.extension.map internal class DynamicCheckoutViewModel private constructor( private val app: Application, private val options: Options, - private val interactor: DynamicCheckoutInteractor + private val interactor: DynamicCheckoutInteractor, + private val cardTokenization: CardTokenizationViewModel, + private val nativeAlternativePayment: NativeAlternativePaymentViewModel ) : ViewModel() { class Factory( private val app: Application, private val invoiceId: String, - private val options: Options + private val options: Options, + private val cardTokenization: CardTokenizationViewModel, + private val nativeAlternativePayment: NativeAlternativePaymentViewModel ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = @@ -28,7 +34,9 @@ internal class DynamicCheckoutViewModel private constructor( app = app, invoiceId = invoiceId, invoicesService = ProcessOut.instance.invoices - ) + ), + cardTokenization = cardTokenization, + nativeAlternativePayment = nativeAlternativePayment ) as T } From ec3264f574bccadfcfb108e39dfb3d80a6c91125 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 9 Jul 2024 14:02:11 +0300 Subject: [PATCH 13/29] Combine state flows --- .../ui/checkout/DynamicCheckoutViewModel.kt | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index ee1aed425..b1e6d487a 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -6,9 +6,14 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.processout.sdk.api.ProcessOut import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel +import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.Started +import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.Starting import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel -import com.processout.sdk.ui.shared.extension.map +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn internal class DynamicCheckoutViewModel private constructor( private val app: Application, @@ -42,14 +47,22 @@ internal class DynamicCheckoutViewModel private constructor( val completion = interactor.completion - val state = interactor.state.map(viewModelScope, ::map) + val state: StateFlow = combine( + interactor.state, + cardTokenization.state, + nativeAlternativePayment.state + ) { interactorState, cardTokenizationState, nativeAlternativePaymentState -> + // TODO + Started + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = Starting + ) init { addCloseable(interactor.interactorScope) } fun onEvent(event: DynamicCheckoutEvent) = interactor.onEvent(event) - - private fun map(state: DynamicCheckoutInteractorState): DynamicCheckoutViewModelState = - DynamicCheckoutViewModelState.Starting } From 32e89b4f4fc9d5dc31f8c411b7b9a8b757fa2d40 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 9 Jul 2024 22:03:10 +0300 Subject: [PATCH 14/29] reset/start nAPM and Card Tokenization VMs and interactors with new configuration --- .../tokenization/CardTokenizationInteractor.kt | 9 +++++++-- .../tokenization/CardTokenizationViewModel.kt | 6 ++++-- .../napm/NativeAlternativePaymentInteractor.kt | 16 ++++++++++------ .../ui/napm/NativeAlternativePaymentViewModel.kt | 6 ++++-- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index c9276e637..f9fd1ab28 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -88,12 +88,17 @@ internal class CardTokenizationInteractor( } } - fun reset(configuration: POCardTokenizationConfiguration) { + fun start(configuration: POCardTokenizationConfiguration) { + this.configuration = configuration + _state.update { initState() } + start() + } + + fun reset() { interactorScope.coroutineContext.cancelChildren() issuerInformationJob = null latestPreferredSchemeRequest = null latestShouldContinueRequest = null - this.configuration = configuration _completion.update { Awaiting } _state.update { initState() } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 18a9f1c7d..704ab857c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -71,11 +71,13 @@ internal class CardTokenizationViewModel private constructor( fun start() = interactor.start() - fun reset(configuration: POCardTokenizationConfiguration) { + fun start(configuration: POCardTokenizationConfiguration) { this.configuration = configuration - interactor.reset(configuration) + interactor.start(configuration) } + fun reset() = interactor.reset() + fun onEvent(event: CardTokenizationEvent) = interactor.onEvent(event) private fun map(state: CardTokenizationInteractorState) = with(configuration) { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index d0c60905d..3dc3e1f36 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -96,21 +96,25 @@ internal class NativeAlternativePaymentInteractor( fetchTransactionDetails() } - fun reset( + fun start( invoiceId: String, gatewayConfigurationId: String ) { - onCleared() - interactorScope.coroutineContext.cancelChildren() - latestDefaultValuesRequest = null - captureStartTimestamp = 0L - capturePassedTimestamp = 0L this.invoiceId = invoiceId this.gatewayConfigurationId = gatewayConfigurationId logAttributes = logAttributes( invoiceId = invoiceId, gatewayConfigurationId = gatewayConfigurationId ) + start() + } + + fun reset() { + onCleared() + interactorScope.coroutineContext.cancelChildren() + latestDefaultValuesRequest = null + captureStartTimestamp = 0L + capturePassedTimestamp = 0L _completion.update { Awaiting } _state.update { Loading } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt index b465d0ae3..03adc7552 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt @@ -97,14 +97,16 @@ internal class NativeAlternativePaymentViewModel private constructor( fun start() = interactor.start() - fun reset( + fun start( invoiceId: String, gatewayConfigurationId: String - ) = interactor.reset( + ) = interactor.start( invoiceId = invoiceId, gatewayConfigurationId = gatewayConfigurationId ) + fun reset() = interactor.reset() + fun onEvent(event: NativeAlternativePaymentEvent) = interactor.onEvent(event) private fun map( From 62481df8361102c7c527f3d52c9bfe5f49a3149b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 10 Jul 2024 16:23:03 +0300 Subject: [PATCH 15/29] POColor --- .../sdk/api/model/response/InvoiceResponse.kt | 14 +------------- .../processout/sdk/api/model/response/POColor.kt | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/response/POColor.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt index 6c0596a96..92eec4beb 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt @@ -96,19 +96,7 @@ sealed class PODynamicCheckoutPaymentMethod { val name: String, val logo: POImageResource, @Json(name = "brand_color") - val brandColor: BrandColor - ) - - /** - * Brand color for light/dark themes. - * - * @param[light] Light color HEX. - * @param[dark] Dark color HEX. - */ - @JsonClass(generateAdapter = true) - data class BrandColor( - val light: String, - val dark: String + val brandColor: POColor ) /** diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POColor.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POColor.kt new file mode 100644 index 000000000..6e776ca43 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POColor.kt @@ -0,0 +1,15 @@ +package com.processout.sdk.api.model.response + +import com.squareup.moshi.JsonClass + +/** + * Color for light/dark themes. + * + * @param[light] Light color HEX. + * @param[dark] Dark color HEX. + */ +@JsonClass(generateAdapter = true) +data class POColor( + val light: String, + val dark: String +) From 291961aea9b78c9ea5ac507b30f2277c8430b27f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 11 Jul 2024 11:13:58 +0300 Subject: [PATCH 16/29] Interactor state and mapping, selection logic --- .../ui/checkout/DynamicCheckoutInteractor.kt | 52 +++++++++++++++++-- .../DynamicCheckoutInteractorState.kt | 38 ++++++++++++-- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt index 0bad96261..10f2b2c32 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt @@ -1,6 +1,8 @@ package com.processout.sdk.ui.checkout import android.app.Application +import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod +import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.Flow.express import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure.Code.Generic import com.processout.sdk.core.ProcessOutResult @@ -9,6 +11,9 @@ import com.processout.sdk.core.onSuccess import com.processout.sdk.ui.base.BaseInteractor import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Awaiting import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure +import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.PaymentMethodSelected +import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.PaymentMethod +import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.PaymentMethod.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -32,7 +37,8 @@ internal class DynamicCheckoutInteractor( private fun initState() = DynamicCheckoutInteractorState( loading = true, - paymentMethods = emptyList() + paymentMethods = emptyList(), + selectedPaymentMethodId = null ) private fun fetchConfiguration() { @@ -45,7 +51,7 @@ internal class DynamicCheckoutInteractor( Failure( ProcessOutResult.Failure( code = Generic(), - message = "Missing configuration." + message = "Missing remote configuration." ) ) } @@ -54,7 +60,7 @@ internal class DynamicCheckoutInteractor( _state.update { it.copy( loading = false, - paymentMethods = paymentMethods + paymentMethods = paymentMethods.map() ) } }.onFailure { failure -> @@ -63,7 +69,45 @@ internal class DynamicCheckoutInteractor( } } + private fun List.map(): List = + mapNotNull { + when (it) { + is PODynamicCheckoutPaymentMethod.Card -> Card( + configuration = it.configuration, + display = it.display + ) + is PODynamicCheckoutPaymentMethod.GooglePay -> GooglePay( + configuration = it.configuration + ) + is PODynamicCheckoutPaymentMethod.AlternativePayment -> { + val redirectUrl = it.configuration.redirectUrl + if (redirectUrl != null) { + AlternativePayment( + redirectUrl = redirectUrl, + display = it.display, + isExpress = it.flow == express + ) + } else { + NativeAlternativePayment( + gatewayConfigurationId = it.configuration.gatewayConfigurationId, + display = it.display + ) + } + } + else -> null + } + } + + fun paymentMethod(id: String): PaymentMethod? = + _state.value.paymentMethods.find { it.id == id } + fun onEvent(event: DynamicCheckoutEvent) { - // TODO + when (event) { + is PaymentMethodSelected -> + _state.update { + it.copy(selectedPaymentMethodId = event.id) + } + else -> {} // TODO + } } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt index 3603dce56..0966e902c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt @@ -1,8 +1,40 @@ package com.processout.sdk.ui.checkout -import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod +import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.* +import java.util.UUID internal data class DynamicCheckoutInteractorState( val loading: Boolean, - val paymentMethods: List -) + val paymentMethods: List, + val selectedPaymentMethodId: String? +) { + + sealed interface PaymentMethod { + + val id: String + + data class Card( + override val id: String = UUID.randomUUID().toString(), + val configuration: CardConfiguration, + val display: Display + ) : PaymentMethod + + data class GooglePay( + override val id: String = UUID.randomUUID().toString(), + val configuration: GooglePayConfiguration + ) : PaymentMethod + + data class AlternativePayment( + override val id: String = UUID.randomUUID().toString(), + val redirectUrl: String, + val display: Display, + val isExpress: Boolean + ) : PaymentMethod + + data class NativeAlternativePayment( + override val id: String = UUID.randomUUID().toString(), + val gatewayConfigurationId: String, + val display: Display + ) : PaymentMethod + } +} From f649c1ef260e43fe62e53671940556f0ccc8d4ca Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 11 Jul 2024 16:26:19 +0300 Subject: [PATCH 17/29] VM, state, regular payments, selection --- .../sdk/ui/checkout/DynamicCheckoutEvent.kt | 1 + .../sdk/ui/checkout/DynamicCheckoutScreen.kt | 34 ++++- .../ui/checkout/DynamicCheckoutViewModel.kt | 118 +++++++++++++++++- .../checkout/DynamicCheckoutViewModelState.kt | 65 +++++++++- 4 files changed, 208 insertions(+), 10 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt index 37e775b80..62592c129 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.core.ProcessOutResult internal sealed interface DynamicCheckoutEvent { + data class PaymentMethodSelected(val id: String) : DynamicCheckoutEvent data class FieldValueChanged(val id: String, val value: TextFieldValue) : DynamicCheckoutEvent data class FieldFocusChanged(val id: String, val isFocused: Boolean) : DynamicCheckoutEvent data class Action(val id: String) : DynamicCheckoutEvent diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt index afe6af7c7..488ab8a7b 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt @@ -10,7 +10,12 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.PaymentMethodSelected +import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.Started import com.processout.sdk.ui.core.component.field.POField +import com.processout.sdk.ui.core.component.field.radio.PORadioGroup +import com.processout.sdk.ui.core.state.POAvailableValue +import com.processout.sdk.ui.core.state.POImmutableList import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.shared.component.isImeVisibleAsState @@ -34,7 +39,13 @@ internal fun DynamicCheckoutScreen( .padding(scaffoldPadding) .verticalScroll(rememberScrollState()) ) { - Content() + when (state) { + is Started -> Content( + state = state, + onEvent = onEvent + ) + else -> {} + } } } } @@ -52,8 +63,27 @@ private fun Header() { } @Composable -private fun Content() { +private fun Content( + state: Started, + onEvent: (DynamicCheckoutEvent) -> Unit +) { // TODO + val selectedPaymentId = state.regularPayments.elements + .find { it.state.selected }?.id ?: String() + val regularPayments = state.regularPayments.elements + .map { regularPayment -> + POAvailableValue( + value = regularPayment.id, + text = regularPayment.state.name + ) + } + PORadioGroup( + value = selectedPaymentId, + onValueChange = { + onEvent(PaymentMethodSelected(id = it)) + }, + availableValues = POImmutableList(regularPayments) + ) } @Composable diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index b1e6d487a..261602bd6 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -5,11 +5,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.processout.sdk.api.ProcessOut +import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.Display import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel -import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.Started -import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.Starting +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration +import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.PaymentMethodSelected +import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.PaymentMethod.* +import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.* +import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.RegularPayment.Content import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options +import com.processout.sdk.ui.core.state.POActionState +import com.processout.sdk.ui.core.state.POImmutableList import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel +import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState +import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.Loading import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -17,6 +26,7 @@ import kotlinx.coroutines.flow.stateIn internal class DynamicCheckoutViewModel private constructor( private val app: Application, + private val invoiceId: String, private val options: Options, private val interactor: DynamicCheckoutInteractor, private val cardTokenization: CardTokenizationViewModel, @@ -34,6 +44,7 @@ internal class DynamicCheckoutViewModel private constructor( override fun create(modelClass: Class): T = DynamicCheckoutViewModel( app = app, + invoiceId = invoiceId, options = options, interactor = DynamicCheckoutInteractor( app = app, @@ -52,8 +63,7 @@ internal class DynamicCheckoutViewModel private constructor( cardTokenization.state, nativeAlternativePayment.state ) { interactorState, cardTokenizationState, nativeAlternativePaymentState -> - // TODO - Started + combine(interactorState, cardTokenizationState, nativeAlternativePaymentState) }.stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, @@ -64,5 +74,103 @@ internal class DynamicCheckoutViewModel private constructor( addCloseable(interactor.interactorScope) } - fun onEvent(event: DynamicCheckoutEvent) = interactor.onEvent(event) + fun onEvent(event: DynamicCheckoutEvent) { + when (event) { + is PaymentMethodSelected -> select(event) + else -> {} // TODO + } + } + + private fun select(event: PaymentMethodSelected) { + if (event.id != interactor.state.value.selectedPaymentMethodId) { + interactor.paymentMethod(event.id)?.let { paymentMethod -> + cardTokenization.reset() + nativeAlternativePayment.reset() + when (paymentMethod) { + is Card -> cardTokenization.start(POCardTokenizationConfiguration()) // TODO: apply config + is NativeAlternativePayment -> nativeAlternativePayment.start( + invoiceId = invoiceId, + gatewayConfigurationId = paymentMethod.gatewayConfigurationId + ) + else -> {} + } + interactor.onEvent(event) + } + } + } + + private fun combine( + interactorState: DynamicCheckoutInteractorState, + cardTokenizationState: CardTokenizationViewModelState, + nativeAlternativePaymentState: NativeAlternativePaymentViewModelState + ): DynamicCheckoutViewModelState = + if (interactorState.loading) { + Starting + } else { + Started( + expressPayments = POImmutableList(emptyList()), + regularPayments = regularPayments(interactorState, cardTokenizationState, nativeAlternativePaymentState) + ) + } + + private fun regularPayments( + interactorState: DynamicCheckoutInteractorState, + cardTokenizationState: CardTokenizationViewModelState, + nativeAlternativePaymentState: NativeAlternativePaymentViewModelState + ): POImmutableList = + interactorState.paymentMethods.mapNotNull { paymentMethod -> + val id = paymentMethod.id + val selected = id == interactorState.selectedPaymentMethodId + when (paymentMethod) { + is Card -> RegularPayment( + id = id, + state = regularPaymentState( + display = paymentMethod.display, + selected = selected + ), + content = Content.Card(cardTokenizationState), + action = null + ) + is AlternativePayment -> if (!paymentMethod.isExpress) + RegularPayment( + id = id, + state = regularPaymentState( + display = paymentMethod.display, + description = "This is redirect.", + selected = selected + ), + content = null, + action = POActionState( + id = id, + text = "Pay", + primary = true + ) + ) else null + is NativeAlternativePayment -> RegularPayment( + id = id, + state = regularPaymentState( + display = paymentMethod.display, + loading = nativeAlternativePaymentState is Loading, + selected = selected + ), + content = Content.NativeAlternativePayment(nativeAlternativePaymentState), + action = null + ) + else -> null + } + }.let { POImmutableList(it) } + + private fun regularPaymentState( + display: Display, + description: String? = null, + loading: Boolean = false, + selected: Boolean + ) = RegularPayment.State( + name = display.name, + logoResource = display.logo, + description = description, + loading = loading, + selectable = true, // TODO + selected = selected + ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt index 3687b905a..e0e4a690f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt @@ -1,16 +1,75 @@ package com.processout.sdk.ui.checkout import androidx.compose.runtime.Immutable +import com.processout.sdk.api.model.response.POColor +import com.processout.sdk.api.model.response.POImageResource +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState +import com.processout.sdk.ui.core.state.POActionState +import com.processout.sdk.ui.core.state.POImmutableList +import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState @Immutable internal sealed interface DynamicCheckoutViewModelState { - @Immutable + //region States + data object Starting : DynamicCheckoutViewModelState @Immutable - data object Started : DynamicCheckoutViewModelState + data class Started( + val expressPayments: POImmutableList, + val regularPayments: POImmutableList + ) : DynamicCheckoutViewModelState + + @Immutable + data class Success( + val message: String + ) : DynamicCheckoutViewModelState + + //endregion + + @Immutable + sealed interface ExpressPayment { + @Immutable + data class Express( + val id: String, + val name: String, + val logoResource: POImageResource, + val brandColor: POColor + ) : ExpressPayment + + @Immutable + data class GooglePay( + val id: String + ) : ExpressPayment + } @Immutable - data object Success : DynamicCheckoutViewModelState + data class RegularPayment( + val id: String, + val state: State, + val content: Content?, + val action: POActionState? + ) { + @Immutable + data class State( + val name: String, + val logoResource: POImageResource, + val description: String?, + val loading: Boolean, + val selectable: Boolean, + val selected: Boolean + ) + + @Immutable + sealed interface Content { + data class Card( + val state: CardTokenizationViewModelState + ) : Content + + data class NativeAlternativePayment( + val state: NativeAlternativePaymentViewModelState + ) : Content + } + } } From d15020d08ad880b5ecd4e780fb54049aac3d2d91 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 11 Jul 2024 16:47:55 +0300 Subject: [PATCH 18/29] Updated DynamicCheckoutViewModelState --- .../sdk/ui/checkout/DynamicCheckoutViewModelState.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt index e0e4a690f..d5c74d465 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt @@ -30,6 +30,11 @@ internal sealed interface DynamicCheckoutViewModelState { @Immutable sealed interface ExpressPayment { + @Immutable + data class GooglePay( + val id: String + ) : ExpressPayment + @Immutable data class Express( val id: String, @@ -37,11 +42,6 @@ internal sealed interface DynamicCheckoutViewModelState { val logoResource: POImageResource, val brandColor: POColor ) : ExpressPayment - - @Immutable - data class GooglePay( - val id: String - ) : ExpressPayment } @Immutable From 05a3ac93c89e40f976ffd155da06b6c9b08ef07e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 11 Jul 2024 17:07:58 +0300 Subject: [PATCH 19/29] ExpressPayment's --- .../ui/checkout/DynamicCheckoutViewModel.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index 261602bd6..ea107d0b7 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -108,11 +108,30 @@ internal class DynamicCheckoutViewModel private constructor( Starting } else { Started( - expressPayments = POImmutableList(emptyList()), + expressPayments = expressPayments(interactorState), regularPayments = regularPayments(interactorState, cardTokenizationState, nativeAlternativePaymentState) ) } + private fun expressPayments( + interactorState: DynamicCheckoutInteractorState + ): POImmutableList = + interactorState.paymentMethods.mapNotNull { paymentMethod -> + val id = paymentMethod.id + when (paymentMethod) { + is GooglePay -> ExpressPayment.GooglePay(id = id) + is AlternativePayment -> if (paymentMethod.isExpress) { + ExpressPayment.Express( + id = id, + name = paymentMethod.display.name, + logoResource = paymentMethod.display.logo, + brandColor = paymentMethod.display.brandColor + ) + } else null + else -> null + } + }.let { POImmutableList(it) } + private fun regularPayments( interactorState: DynamicCheckoutInteractorState, cardTokenizationState: CardTokenizationViewModelState, From a4a26d23bfc2346f6b82bab13df1960c32c87ab3 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 11 Jul 2024 17:35:28 +0300 Subject: [PATCH 20/29] KDoc --- .../com/processout/sdk/api/model/response/InvoiceResponse.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt index 92eec4beb..a7c270322 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/InvoiceResponse.kt @@ -124,7 +124,7 @@ sealed class PODynamicCheckoutPaymentMethod { * * @param[collectionMode] Billing address collection mode. * @param[restrictToCountryCodes] Set of ISO country codes that is supported for the billing address. - * When _null_, all countries are supported. + * When _null_, all countries are provided. */ @JsonClass(generateAdapter = true) data class BillingAddressConfiguration( From d689a8454b8fbadb0279531d90fa4865bdce2502 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 11 Jul 2024 17:51:30 +0300 Subject: [PATCH 21/29] apply CardConfiguration --- .../tokenization/CardTokenizationViewModel.kt | 5 +++- .../ui/checkout/DynamicCheckoutViewModel.kt | 24 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 704ab857c..0e169dcd9 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -32,7 +32,7 @@ import com.processout.sdk.ui.shared.transformation.CardNumberVisualTransformatio internal class CardTokenizationViewModel private constructor( private val app: Application, - private var configuration: POCardTokenizationConfiguration, + configuration: POCardTokenizationConfiguration, private val interactor: CardTokenizationInteractor ) : ViewModel() { @@ -61,6 +61,9 @@ internal class CardTokenizationViewModel private constructor( val actionId: String? ) + var configuration = configuration + private set + val completion = interactor.completion val state = interactor.state.map(viewModelScope, ::map) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index ea107d0b7..7c03fc1d7 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -5,10 +5,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.processout.sdk.api.ProcessOut +import com.processout.sdk.api.model.response.POBillingAddressCollectionMode +import com.processout.sdk.api.model.response.POBillingAddressCollectionMode.* +import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.CardConfiguration import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.Display import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.PaymentMethodSelected import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.PaymentMethod.* import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.* @@ -87,7 +91,9 @@ internal class DynamicCheckoutViewModel private constructor( cardTokenization.reset() nativeAlternativePayment.reset() when (paymentMethod) { - is Card -> cardTokenization.start(POCardTokenizationConfiguration()) // TODO: apply config + is Card -> cardTokenization.start( + configuration = cardTokenization.configuration.apply(paymentMethod.configuration) + ) is NativeAlternativePayment -> nativeAlternativePayment.start( invoiceId = invoiceId, gatewayConfigurationId = paymentMethod.gatewayConfigurationId @@ -99,6 +105,22 @@ internal class DynamicCheckoutViewModel private constructor( } } + private fun POCardTokenizationConfiguration.apply( + configuration: CardConfiguration + ) = copy( + isCardholderNameFieldVisible = configuration.requireCardholderName, + billingAddress = billingAddress.copy( + mode = configuration.billingAddress.collectionMode.map(), + countryCodes = configuration.billingAddress.restrictToCountryCodes + ) + ) + + private fun POBillingAddressCollectionMode.map() = when (this) { + full -> CollectionMode.Full + automatic -> CollectionMode.Automatic + never -> CollectionMode.Never + } + private fun combine( interactorState: DynamicCheckoutInteractorState, cardTokenizationState: CardTokenizationViewModelState, From c8dcec78067a3dab794aa97bc0b1a8ab95179b7e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 14:16:34 +0300 Subject: [PATCH 22/29] Events --- .../sdk/ui/checkout/DynamicCheckoutEvent.kt | 22 ++++++--- .../ui/checkout/DynamicCheckoutInteractor.kt | 7 +-- .../sdk/ui/checkout/DynamicCheckoutScreen.kt | 2 +- .../ui/checkout/DynamicCheckoutViewModel.kt | 47 ++++++++++++++++--- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt index 62592c129..6ab41c923 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt @@ -3,13 +3,23 @@ package com.processout.sdk.ui.checkout import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.core.ProcessOutResult +internal sealed interface DynamicCheckoutExtendedEvent : DynamicCheckoutEvent { + data class PaymentMethodSelected(val id: String) : DynamicCheckoutExtendedEvent + data class Dismiss(val failure: ProcessOutResult.Failure) : DynamicCheckoutExtendedEvent +} + internal sealed interface DynamicCheckoutEvent { - data class PaymentMethodSelected(val id: String) : DynamicCheckoutEvent - data class FieldValueChanged(val id: String, val value: TextFieldValue) : DynamicCheckoutEvent - data class FieldFocusChanged(val id: String, val isFocused: Boolean) : DynamicCheckoutEvent - data class Action(val id: String) : DynamicCheckoutEvent - data class ActionConfirmationRequested(val id: String) : DynamicCheckoutEvent - data class Dismiss(val failure: ProcessOutResult.Failure) : DynamicCheckoutEvent + data class FieldValueChanged( + val paymentMethodId: String, + val fieldId: String, + val value: TextFieldValue + ) : DynamicCheckoutEvent + + data class FieldFocusChanged( + val paymentMethodId: String, + val fieldId: String, + val isFocused: Boolean + ) : DynamicCheckoutEvent } internal sealed interface DynamicCheckoutCompletion { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt index 10f2b2c32..317fca434 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt @@ -11,7 +11,8 @@ import com.processout.sdk.core.onSuccess import com.processout.sdk.ui.base.BaseInteractor import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Awaiting import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure -import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.PaymentMethodSelected +import com.processout.sdk.ui.checkout.DynamicCheckoutExtendedEvent.Dismiss +import com.processout.sdk.ui.checkout.DynamicCheckoutExtendedEvent.PaymentMethodSelected import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.PaymentMethod import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.PaymentMethod.* import kotlinx.coroutines.flow.MutableStateFlow @@ -101,13 +102,13 @@ internal class DynamicCheckoutInteractor( fun paymentMethod(id: String): PaymentMethod? = _state.value.paymentMethods.find { it.id == id } - fun onEvent(event: DynamicCheckoutEvent) { + fun onEvent(event: DynamicCheckoutExtendedEvent) { when (event) { is PaymentMethodSelected -> _state.update { it.copy(selectedPaymentMethodId = event.id) } - else -> {} // TODO + is Dismiss -> {} // TODO } } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt index 488ab8a7b..b4257454d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.PaymentMethodSelected +import com.processout.sdk.ui.checkout.DynamicCheckoutExtendedEvent.PaymentMethodSelected import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.Started import com.processout.sdk.ui.core.component.field.POField import com.processout.sdk.ui.core.component.field.radio.PORadioGroup diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index 7c03fc1d7..c6bf33902 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -9,17 +9,21 @@ import com.processout.sdk.api.model.response.POBillingAddressCollectionMode import com.processout.sdk.api.model.response.POBillingAddressCollectionMode.* import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.CardConfiguration import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.Display +import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode -import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.PaymentMethodSelected +import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.FieldFocusChanged +import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.FieldValueChanged +import com.processout.sdk.ui.checkout.DynamicCheckoutExtendedEvent.PaymentMethodSelected import com.processout.sdk.ui.checkout.DynamicCheckoutInteractorState.PaymentMethod.* import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.* import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.RegularPayment.Content import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Options import com.processout.sdk.ui.core.state.POActionState import com.processout.sdk.ui.core.state.POImmutableList +import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.Loading @@ -80,19 +84,25 @@ internal class DynamicCheckoutViewModel private constructor( fun onEvent(event: DynamicCheckoutEvent) { when (event) { - is PaymentMethodSelected -> select(event) - else -> {} // TODO + is PaymentMethodSelected -> onPaymentMethodSelected(event) + is FieldValueChanged -> onFieldValueChanged(event) + is FieldFocusChanged -> onFieldFocusChanged(event) + else -> {} + } + if (event is DynamicCheckoutExtendedEvent) { + interactor.onEvent(event) } } - private fun select(event: PaymentMethodSelected) { + private fun onPaymentMethodSelected(event: PaymentMethodSelected) { if (event.id != interactor.state.value.selectedPaymentMethodId) { interactor.paymentMethod(event.id)?.let { paymentMethod -> cardTokenization.reset() nativeAlternativePayment.reset() when (paymentMethod) { is Card -> cardTokenization.start( - configuration = cardTokenization.configuration.apply(paymentMethod.configuration) + configuration = cardTokenization.configuration + .apply(paymentMethod.configuration) ) is NativeAlternativePayment -> nativeAlternativePayment.start( invoiceId = invoiceId, @@ -100,7 +110,6 @@ internal class DynamicCheckoutViewModel private constructor( ) else -> {} } - interactor.onEvent(event) } } } @@ -121,6 +130,32 @@ internal class DynamicCheckoutViewModel private constructor( never -> CollectionMode.Never } + private fun onFieldValueChanged(event: FieldValueChanged) { + val paymentMethod = interactor.paymentMethod(event.paymentMethodId) + when (paymentMethod) { + is Card -> cardTokenization.onEvent( + CardTokenizationEvent.FieldValueChanged(event.fieldId, event.value) + ) + is NativeAlternativePayment -> nativeAlternativePayment.onEvent( + NativeAlternativePaymentEvent.FieldValueChanged(event.fieldId, event.value) + ) + else -> {} + } + } + + private fun onFieldFocusChanged(event: FieldFocusChanged) { + val paymentMethod = interactor.paymentMethod(event.paymentMethodId) + when (paymentMethod) { + is Card -> cardTokenization.onEvent( + CardTokenizationEvent.FieldFocusChanged(event.fieldId, event.isFocused) + ) + is NativeAlternativePayment -> nativeAlternativePayment.onEvent( + NativeAlternativePaymentEvent.FieldFocusChanged(event.fieldId, event.isFocused) + ) + else -> {} + } + } + private fun combine( interactorState: DynamicCheckoutInteractorState, cardTokenizationState: CardTokenizationViewModelState, From 65a9b65f31c4aa31adbd897506b06d7450255169 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 16:21:11 +0300 Subject: [PATCH 23/29] Dismiss --- .../ui/checkout/DynamicCheckoutActivity.kt | 35 ++++++++++++------- .../ui/checkout/DynamicCheckoutInteractor.kt | 6 +++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt index 9c71cd1d5..a4b90dc98 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt @@ -6,12 +6,14 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import androidx.activity.SystemBarStyle +import androidx.activity.addCallback import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.processout.sdk.core.POFailure.Code.Cancelled import com.processout.sdk.core.POFailure.Code.Generic import com.processout.sdk.core.POUnit import com.processout.sdk.core.ProcessOutActivityResult @@ -25,6 +27,7 @@ import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion. import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_RESULT import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Success +import com.processout.sdk.ui.checkout.DynamicCheckoutExtendedEvent.Dismiss import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration @@ -66,6 +69,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { if (savedInstanceState == null) { initConfiguration() } + dispatchBackPressed() setContent { ProcessOutTheme { with(viewModel.completion.collectAsStateWithLifecycle()) { @@ -85,16 +89,31 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { configuration = intent.getParcelableExtra(EXTRA_CONFIGURATION) configuration?.run { if (invoiceId.isBlank()) { - dismiss( - ProcessOutResult.Failure( - code = Generic(), - message = "Invalid configuration." + viewModel.onEvent( + Dismiss( + ProcessOutResult.Failure( + code = Generic(), + message = "Invalid configuration." + ) ) ) } } } + private fun dispatchBackPressed() { + onBackPressedDispatcher.addCallback(this) { + viewModel.onEvent( + Dismiss( + ProcessOutResult.Failure( + code = Cancelled, + message = "Cancelled by the user with back press or gesture." + ) + ) + ) + } + } + private fun handle(completion: DynamicCheckoutCompletion) = when (completion) { Success -> finishWithActivityResult( @@ -108,14 +127,6 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { else -> {} } - private fun dismiss(failure: ProcessOutResult.Failure) { - // TODO: notify VM - finishWithActivityResult( - resultCode = Activity.RESULT_CANCELED, - result = failure.toActivityResult() - ) - } - private fun finishWithActivityResult( resultCode: Int, result: ProcessOutActivityResult diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt index 317fca434..59b6d0767 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt @@ -6,6 +6,7 @@ import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.Flow import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure.Code.Generic import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess import com.processout.sdk.ui.base.BaseInteractor @@ -108,7 +109,10 @@ internal class DynamicCheckoutInteractor( _state.update { it.copy(selectedPaymentMethodId = event.id) } - is Dismiss -> {} // TODO + is Dismiss -> { + POLogger.info("Dismissed: %s", event.failure) + _completion.update { Failure(event.failure) } + } } } } From 85c14e8a84b137a965fc1d9174b3da3e2098bfcb Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 16:39:35 +0300 Subject: [PATCH 24/29] combine completions --- .../sdk/ui/checkout/DynamicCheckoutViewModel.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index c6bf33902..20b36f756 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -14,6 +14,7 @@ import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode +import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Awaiting import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.FieldFocusChanged import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.FieldValueChanged import com.processout.sdk.ui.checkout.DynamicCheckoutExtendedEvent.PaymentMethodSelected @@ -64,7 +65,18 @@ internal class DynamicCheckoutViewModel private constructor( ) as T } - val completion = interactor.completion + val completion: StateFlow = combine( + interactor.completion, + cardTokenization.completion, + nativeAlternativePayment.completion + ) { interactorCompletion, cardTokenizationCompletion, nativeAlternativePaymentCompletion -> + // TODO: combine completions + interactorCompletion + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = Awaiting + ) val state: StateFlow = combine( interactor.state, From 83bf9a6ef44ac711d44a9f65aacaab81cab6f51e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 16:48:57 +0300 Subject: [PATCH 25/29] androidxTestCoreVersion = '1.6.1' --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0addd97e1..e01aaa5d8 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ ext { mockitoInlineVersion = '5.2.0' mockitoKotlinVersion = '5.2.1' robolectricVersion = '4.11.1' - androidxTestCoreVersion = '1.5.0' + androidxTestCoreVersion = '1.6.1' // Legacy volleyVersion = '1.2.1' From 85b1a5035cab07670f594eb8b4a7ca0373e8df91 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 16:58:35 +0300 Subject: [PATCH 26/29] Remove unused strings --- sdk/src/main/res/values-ar/strings.xml | 2 -- sdk/src/main/res/values-pl/strings.xml | 2 -- sdk/src/main/res/values-pt/strings.xml | 2 -- sdk/src/main/res/values/strings.xml | 2 -- 4 files changed, 8 deletions(-) diff --git a/sdk/src/main/res/values-ar/strings.xml b/sdk/src/main/res/values-ar/strings.xml index a1a3858db..7ecc4c835 100644 --- a/sdk/src/main/res/values-ar/strings.xml +++ b/sdk/src/main/res/values-ar/strings.xml @@ -6,9 +6,7 @@ ادفع سيتم إعادة توجيهك لإتمام هذه الدفعة. - طريقة الدفع هذه غير متاحة. لم نتمكن من إتمام دفعتك. يرجى التحقق من التفاصيل أو تجربة طريقة دفع أخرى. - طريقة الدفع المطلوبة غير متاحة، يرجى تجربة طريقة دفع أخرى. الدفع بواسطة %s diff --git a/sdk/src/main/res/values-pl/strings.xml b/sdk/src/main/res/values-pl/strings.xml index 567ec1c76..c69ad69b1 100644 --- a/sdk/src/main/res/values-pl/strings.xml +++ b/sdk/src/main/res/values-pl/strings.xml @@ -6,9 +6,7 @@ Zapłać Zostaniesz przekierowany, aby sfinalizować tę płatność. - Ta metoda płatności nie jest obsługiwana. Nie byliśmy w stanie sfinalizować Twojej płatności. Zweryfikuj swoje dane lub wybierz inną metody płatności. - Wybrana metoda płatności nie jest obsługiwana. Proszę wybierz inną metodę. Zapłać przy pomocy %s diff --git a/sdk/src/main/res/values-pt/strings.xml b/sdk/src/main/res/values-pt/strings.xml index c9966e9b6..664cfbec8 100644 --- a/sdk/src/main/res/values-pt/strings.xml +++ b/sdk/src/main/res/values-pt/strings.xml @@ -6,9 +6,7 @@ Pagar Será redireccionado para finalizar este pagamento. - Este método de pagamento não está disponível. Não foi possível concluir o seu pagamento. Por favor, verifique os detalhes ou tente outro método de pagamento. - O método de pagamento escolhido não está disponível, por favor tente outro método de pagamento. Pagar com %s diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index a85ff8d91..6f6a83e27 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -6,9 +6,7 @@ Pay You will be redirected to finalise this payment. - This payment method is unavailable. We were unable to process your payment. Please check your payment details or try another payment method. - The requested payment method is not available, please try another payment method. Pay with %s From ac062771d6cf6f11b9abf1369098f28e25c40aea Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 17:24:21 +0300 Subject: [PATCH 27/29] French localization --- sdk/src/main/res/values-fr/strings.xml | 5 +++++ sdk/src/main/res/values/strings.xml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/src/main/res/values-fr/strings.xml b/sdk/src/main/res/values-fr/strings.xml index 41fcea7d6..0615085c0 100644 --- a/sdk/src/main/res/values-fr/strings.xml +++ b/sdk/src/main/res/values-fr/strings.xml @@ -3,6 +3,11 @@ fr + + Payer + Vous serez redirigé pour finaliser ce paiement. + Nous n’avons pas pu finaliser votre paiement. Veuillez vérifier vos informations ou essayer un autre mode de paiement. + Payer avec %s Payer %s diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index 6f6a83e27..2eba97a48 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - + en From ce66be18089dcce2767585fafe3cbd1ce4294c18 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 17:40:28 +0300 Subject: [PATCH 28/29] Fixed FR apostrophes --- sdk/src/main/res/values-fr/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/res/values-fr/strings.xml b/sdk/src/main/res/values-fr/strings.xml index 0615085c0..c36f1e292 100644 --- a/sdk/src/main/res/values-fr/strings.xml +++ b/sdk/src/main/res/values-fr/strings.xml @@ -6,7 +6,7 @@ Payer Vous serez redirigé pour finaliser ce paiement. - Nous n’avons pas pu finaliser votre paiement. Veuillez vérifier vos informations ou essayer un autre mode de paiement. + Nous n\'avons pas pu finaliser votre paiement. Veuillez vérifier vos informations ou essayer un autre mode de paiement. Payer avec %s @@ -33,7 +33,7 @@ Envoyer Annuler Le code CVV de votre carte est incorrect. - Une erreur s’est produite, veuillez réessayer. + Une erreur s\'est produite, veuillez réessayer. Ajouter une nouvelle carte @@ -45,14 +45,14 @@ MM / AA 4242 4242 4242 4242 Adresse de Facturation - Ligne d’adresse %d + Ligne d\'adresse %d Les informations de votre carte sont incorrectes. - La date d’expiration de votre carte est incorrecte. + La date d\'expiration de votre carte est incorrecte. Votre numéro de carte est incorrect. Le nom du porteur de carte est incorrect. Le code CVV de votre carte est incorrect. - Une erreur s’est produite, veuillez réessayer. - La date d’expiration de votre carte ou son code CVV sont incorrects. + Une erreur s\'est produite, veuillez réessayer. + La date d\'expiration de votre carte ou son code CVV sont incorrects. Zone From ac9a4e8bff96532e381486201643dc502474792e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 12 Jul 2024 17:45:14 +0300 Subject: [PATCH 29/29] Apply strings from resources --- .../processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index 20b36f756..7702b53e5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.processout.sdk.R import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.model.response.POBillingAddressCollectionMode import com.processout.sdk.api.model.response.POBillingAddressCollectionMode.* @@ -224,13 +225,13 @@ internal class DynamicCheckoutViewModel private constructor( id = id, state = regularPaymentState( display = paymentMethod.display, - description = "This is redirect.", + description = app.getString(R.string.po_dynamic_checkout_warning_redirect), selected = selected ), content = null, action = POActionState( id = id, - text = "Pay", + text = app.getString(R.string.po_dynamic_checkout_button_pay), primary = true ) ) else null