diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index eecc055af..f308f7d75 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -8,6 +8,41 @@
+
+
+
diff --git a/build.gradle b/build.gradle
index 3a42ea54e..69e4530bd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,7 +2,7 @@
buildscript {
ext {
- androidGradlePluginVersion = '8.9.2'
+ androidGradlePluginVersion = '8.10.0'
kotlinVersion = '2.1.20'
kspVersion = '2.1.20-1.0.32'
dokkaVersion = '1.9.20'
diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/POField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/POField.kt
index 78f6da4c1..28c9221f5 100644
--- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/POField.kt
+++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/POField.kt
@@ -156,6 +156,7 @@ object POField {
@Composable
internal fun ContainerBox(
style: StateStyle,
+ enabled: Boolean,
isDropdown: Boolean
) {
Box(
@@ -170,7 +171,7 @@ object POField {
shape = style.shape
)
.clip(style.shape)
- .conditional(isDropdown) {
+ .conditional(enabled && isDropdown) {
clickable(
onClick = {},
interactionSource = remember { MutableInteractionSource() },
diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt
index 27c63a36d..b5f945dce 100644
--- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt
+++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt
@@ -45,6 +45,7 @@ fun PODropdownField(
modifier: Modifier = Modifier,
fieldStyle: POField.Style = POField.default,
menuStyle: PODropdownField.MenuStyle = PODropdownField.defaultMenu,
+ enabled: Boolean = true,
isError: Boolean = false,
placeholderText: String? = null
) {
@@ -55,7 +56,11 @@ fun PODropdownField(
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
- onExpandedChange = { expanded = it }
+ onExpandedChange = {
+ if (enabled) {
+ expanded = it
+ }
+ }
) {
var isFocused by remember { mutableStateOf(false) }
val fieldStateStyle = fieldStyle.stateStyle(isError = isError, isFocused = isFocused)
@@ -69,7 +74,7 @@ fun PODropdownField(
isFocused = it.isFocused
},
style = fieldStyle,
- enabled = true,
+ enabled = enabled,
readOnly = true,
isDropdown = true,
isError = isError,
diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt
index 485a60ffc..6d053b190 100644
--- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt
+++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt
@@ -23,6 +23,7 @@ fun POLabeledDropdownField(
fieldStyle: POField.Style = POField.default,
menuStyle: PODropdownField.MenuStyle = PODropdownField.defaultMenu,
labelsStyle: POFieldLabels.Style = POFieldLabels.default,
+ enabled: Boolean = true,
isError: Boolean = false,
placeholderText: String? = null
) {
@@ -38,6 +39,7 @@ fun POLabeledDropdownField(
modifier = modifier,
fieldStyle = fieldStyle,
menuStyle = menuStyle,
+ enabled = enabled,
isError = isError,
placeholderText = placeholderText
)
diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/radio/PORadioGroup.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/radio/PORadioGroup.kt
index 4b2482d67..e27aa6a77 100644
--- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/radio/PORadioGroup.kt
+++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/radio/PORadioGroup.kt
@@ -39,7 +39,7 @@ fun PORadioGroup(
modifier = modifier
) {
availableValues.elements.forEach {
- val onClick = remember { { onValueChange(it.value) } }
+ val onClick = { onValueChange(it.value) }
Row(
modifier = Modifier
.fillMaxWidth()
diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/text/POTextField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/text/POTextField.kt
index c436443dd..7f37a7b78 100644
--- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/text/POTextField.kt
+++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/text/POTextField.kt
@@ -103,6 +103,7 @@ fun POTextField(
container = {
ContainerBox(
style = stateStyle,
+ enabled = enabled,
isDropdown = isDropdown
)
}
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 99d61dc00..70f043702 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
@@ -27,6 +27,11 @@ import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.*
import com.processout.sdk.ui.card.tokenization.CardTokenizationInteractorState.*
import com.processout.sdk.ui.card.tokenization.CardTokenizationSideEffect.CardScanner
import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode.*
+import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationEligibilityRequest
+import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationEligibilityResponse
+import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility
+import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.Eligible
+import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.NotEligible
import com.processout.sdk.ui.core.state.POAvailableValue
import com.processout.sdk.ui.shared.extension.currentAppLocale
import com.processout.sdk.ui.shared.extension.findBy
@@ -59,7 +64,7 @@ internal class CardTokenizationInteractor(
) : BaseInteractor() {
private companion object {
- const val IIN_LENGTH = 6
+ const val IIN_LENGTH = 8
const val EXPIRATION_DATE_PART_LENGTH = 2
const val CARD_SCANNER_DELAY_MS = 350L
}
@@ -81,11 +86,12 @@ internal class CardTokenizationInteractor(
private val cardNumberInputFilter = CardNumberInputFilter()
private val cardExpirationInputFilter = CardExpirationInputFilter()
+ private var issuerInformationJob: Job? = null
+
+ private var latestEligibilityRequest: CardTokenizationEligibilityRequest? = null
private var latestPreferredSchemeRequest: POCardTokenizationPreferredSchemeRequest? = null
private var latestShouldContinueRequest: POCardTokenizationShouldContinueRequest? = null
- private var issuerInformationJob: Job? = null
-
//region Initialization
fun start() {
@@ -96,11 +102,12 @@ internal class CardTokenizationInteractor(
interactorScope.launch {
POLogger.info("Starting card tokenization.")
dispatch(WillStart)
- collectFailure()
- initAddressFields()
- collectPreferredScheme()
handleCompletion()
shouldContinueOnFailure()
+ collectFailure()
+ collectEligibility()
+ collectPreferredScheme()
+ initAddressFields()
POLogger.info("Card tokenization is started: waiting for user input.")
dispatch(DidStart)
}
@@ -241,7 +248,7 @@ internal class CardTokenizationInteractor(
if (isTextChanged) {
POLogger.debug(message = "Field is edited by the user: %s", id)
dispatch(ParametersChanged)
- if (areAllFieldsValid()) {
+ if (isSubmitAllowed()) {
_state.update {
it.copy(
submitAllowed = true,
@@ -273,6 +280,21 @@ internal class CardTokenizationInteractor(
}
} else field
+ private fun updateAllFields(enabled: Boolean) {
+ _state.update {
+ it.copy(
+ cardFields = it.cardFields.map { field ->
+ field.copy(enabled = enabled)
+ },
+ addressFields = it.addressFields.map { field ->
+ field.copy(enabled = enabled)
+ },
+ preferredSchemeField = it.preferredSchemeField.copy(enabled = enabled),
+ saveCardField = it.saveCardField.copy(enabled = enabled)
+ )
+ }
+ }
+
private fun updateCardFields(card: POScannedCard) {
POLogger.debug("Updating card field values with the scanned card: $card.")
updateFieldValue(
@@ -334,17 +356,23 @@ internal class CardTokenizationInteractor(
//endregion
- //region Issuer Information & Preferred Scheme
+ //region Issuer Information
private fun updateIssuerInformation(cardNumber: String, previousCardNumber: String) {
val iin = iin(cardNumber)
if (iin == iin(previousCardNumber)) {
return
}
- updateState(
- issuerInformation = localIssuerInformation(iin),
- preferredScheme = null
- )
+ val localIssuerInformation = localIssuerInformation(iin)
+ _state.update {
+ it.copy(
+ submitAllowed = areAllFieldsValid(),
+ errorMessage = null,
+ issuerInformation = localIssuerInformation,
+ eligibility = Eligible()
+ )
+ }
+ updatePreferredScheme(scheme = localIssuerInformation?.scheme)
if (iin.length == IIN_LENGTH) {
updateIssuerInformation(iin)
}
@@ -361,7 +389,8 @@ internal class CardTokenizationInteractor(
issuerInformationJob?.cancel()
issuerInformationJob = interactorScope.launch {
fetchIssuerInformation(iin)?.let { issuerInformation ->
- requestPreferredScheme(issuerInformation)
+ _state.update { it.copy(issuerInformation = issuerInformation) }
+ requestEligibility(iin, issuerInformation)
}
}
}
@@ -375,15 +404,105 @@ internal class CardTokenizationInteractor(
)
}.getOrNull()
- private suspend fun requestPreferredScheme(issuerInformation: POCardIssuerInformation) {
- val request = POCardTokenizationPreferredSchemeRequest(issuerInformation)
- latestPreferredSchemeRequest = request
- if (legacyEventDispatcher?.subscribedForPreferredSchemeRequest() == true) {
- legacyEventDispatcher.send(request)
- } else {
+ //endregion
+
+ //region Eligibility
+
+ private fun requestEligibility(
+ iin: String,
+ issuerInformation: POCardIssuerInformation
+ ) {
+ interactorScope.launch {
+ val request = CardTokenizationEligibilityRequest(
+ iin = iin,
+ issuerInformation = issuerInformation
+ )
+ latestEligibilityRequest = request
eventDispatcher.send(request)
+ POLogger.info("Requested to evaluate card eligibility: [iin=%s] [issuerInformation=%s]", iin, issuerInformation)
+ }
+ }
+
+ private fun collectEligibility() {
+ eventDispatcher.subscribeForResponse(
+ coroutineScope = interactorScope
+ ) { response ->
+ if (response.uuid == latestEligibilityRequest?.uuid) {
+ latestEligibilityRequest = null
+ handleEligibility(response.eligibility)
+ }
+ }
+ }
+
+ private fun handleEligibility(eligibility: POCardTokenizationEligibility) {
+ _state.update { it.copy(eligibility = eligibility) }
+ POLogger.info("Card eligibility: %s", eligibility)
+ if (eligibility is NotEligible) {
+ val errorMessage = eligibility.failure?.localizedMessage
+ ?: app.getString(R.string.po_card_tokenization_error_eligibility)
+ _state.update {
+ it.copy(
+ cardFields = it.cardFields.map { field ->
+ validatedField(
+ field = field,
+ invalidFieldIds = setOf(CardFieldId.NUMBER)
+ )
+ },
+ focusedFieldId = CardFieldId.NUMBER,
+ submitAllowed = false,
+ pendingSubmit = false,
+ submitting = false,
+ errorMessage = errorMessage
+ )
+ }
+ updateAllFields(enabled = true)
+ }
+ val eligibleSchemes = _state.value.eligibleSchemes
+ if (eligibleSchemes.size > 1) {
+ _state.value.issuerInformation?.let {
+ requestPreferredScheme(issuerInformation = it)
+ return
+ }
+ } else {
+ eligibleSchemes.firstOrNull()?.let {
+ updatePreferredScheme(scheme = it)
+ }
+ }
+ if (_state.value.pendingSubmit) {
+ _state.update { it.copy(pendingSubmit = false) }
+ submit()
+ }
+ }
+
+ private val CardTokenizationInteractorState.eligibleSchemes: List
+ get() = when (eligibility) {
+ is Eligible ->
+ if (eligibility.scheme != null) {
+ listOf(eligibility.scheme)
+ } else {
+ listOfNotNull(
+ issuerInformation?.scheme,
+ issuerInformation?.coScheme
+ )
+ }
+ is NotEligible -> emptyList()
+ }
+
+ //endregion
+
+ //region Preferred Scheme
+
+ private fun requestPreferredScheme(issuerInformation: POCardIssuerInformation) {
+ interactorScope.launch {
+ val request = POCardTokenizationPreferredSchemeRequest(issuerInformation)
+ latestPreferredSchemeRequest = request
+ if (legacyEventDispatcher?.subscribedForPreferredSchemeRequest() == true) {
+ legacyEventDispatcher.send(request)
+ } else {
+ eventDispatcher.send(request)
+ }
+ POLogger.info("Requested to choose preferred scheme by issuer information: %s", issuerInformation)
}
- POLogger.info("Requested to choose preferred scheme by issuer information: %s", issuerInformation)
}
private fun collectPreferredScheme() {
@@ -402,32 +521,32 @@ internal class CardTokenizationInteractor(
private fun handlePreferredScheme(response: POCardTokenizationPreferredSchemeResponse) {
if (response.uuid == latestPreferredSchemeRequest?.uuid) {
latestPreferredSchemeRequest = null
- updateState(
- issuerInformation = response.issuerInformation,
- preferredScheme = response.preferredScheme
- )
+ updatePreferredScheme(scheme = response.preferredScheme)
+ if (_state.value.pendingSubmit) {
+ _state.update { it.copy(pendingSubmit = false) }
+ submit()
+ }
}
}
- private fun updateState(
- issuerInformation: POCardIssuerInformation?,
- preferredScheme: String?
- ) {
- val availableSchemes = listOfNotNull(
- availableScheme(issuerInformation?.scheme),
- availableScheme(issuerInformation?.coScheme)
- )
+ private fun updatePreferredScheme(scheme: String?) {
+ val eligibleSchemes = _state.value.eligibleSchemes.mapNotNull { availableScheme(scheme = it) }
+ val preferredScheme = availableScheme(scheme)?.let {
+ if (it in eligibleSchemes) it else {
+ POLogger.warn("Preferred scheme is not eligible: %s", it.value)
+ eligibleSchemes.firstOrNull()
+ }
+ }
_state.update {
it.copy(
- issuerInformation = issuerInformation,
preferredSchemeField = it.preferredSchemeField.copy(
- value = TextFieldValue(text = preferredScheme ?: String()),
- availableValues = availableSchemes,
- shouldCollect = configuration.preferredScheme != null && availableSchemes.size > 1
+ value = TextFieldValue(text = preferredScheme?.value ?: String()),
+ availableValues = eligibleSchemes,
+ shouldCollect = configuration.preferredScheme != null && eligibleSchemes.size > 1
)
)
}
- POLogger.info("State updated: [issuerInformation=%s] [preferredScheme=%s]", issuerInformation, preferredScheme)
+ POLogger.info("Preferred scheme updated: %s", preferredScheme?.value)
}
private fun availableScheme(scheme: String?): POAvailableValue? =
@@ -569,7 +688,21 @@ internal class CardTokenizationInteractor(
//region Submit
private fun submit() {
- if (!areAllFieldsValid()) {
+ if (_state.value.pendingSubmit) {
+ return
+ }
+ if (issuerInformationJob?.isActive == true || latestEligibilityRequest != null) {
+ _state.update {
+ it.copy(
+ pendingSubmit = true,
+ submitting = true,
+ errorMessage = null
+ )
+ }
+ updateAllFields(enabled = false)
+ return
+ }
+ if (!isSubmitAllowed()) {
POLogger.debug("Ignored attempt to tokenize the card with invalid values.")
return
}
@@ -580,15 +713,18 @@ internal class CardTokenizationInteractor(
errorMessage = null
)
}
+ updateAllFields(enabled = false)
tokenize(tokenizationRequest())
}
+ private fun isSubmitAllowed() = _state.value.eligibility is Eligible && areAllFieldsValid()
+
+ private fun areAllFieldsValid(): Boolean = allFields().all { it.isValid }
+
private fun allFields(): List = with(_state.value) {
cardFields + addressFields + preferredSchemeField + saveCardField
}
- private fun areAllFieldsValid(): Boolean = allFields().all { it.isValid }
-
//endregion
//region Tokenization Request
@@ -853,29 +989,32 @@ internal class CardTokenizationInteractor(
errorMessage: String?
) {
val cardFields = _state.value.cardFields.map { field ->
- validatedField(invalidFieldIds, field)
+ validatedField(field, invalidFieldIds)
}
val addressFields = _state.value.addressFields.map { field ->
- validatedField(invalidFieldIds, field)
+ validatedField(field, invalidFieldIds)
}
- val preferredSchemeField = validatedField(invalidFieldIds, _state.value.preferredSchemeField)
- val saveCardField = validatedField(invalidFieldIds, _state.value.saveCardField)
+ val preferredSchemeField = validatedField(_state.value.preferredSchemeField, invalidFieldIds)
+ val saveCardField = validatedField(_state.value.saveCardField, invalidFieldIds)
val allFields = cardFields + addressFields + preferredSchemeField + saveCardField
val firstInvalidFieldId = allFields.find { !it.isValid }?.id
_state.update { state ->
state.copy(
cardFields = cardFields,
addressFields = addressFields,
+ preferredSchemeField = preferredSchemeField,
+ saveCardField = saveCardField,
focusedFieldId = firstInvalidFieldId ?: state.focusedFieldId,
submitAllowed = allFields.all { it.isValid },
submitting = false,
errorMessage = errorMessage
)
}
+ updateAllFields(enabled = true)
POLogger.info(message = "Recovered after the failure: %s", failure)
}
- private fun validatedField(invalidFieldIds: Set, field: Field): Field =
+ private fun validatedField(field: Field, invalidFieldIds: Set): Field =
if (invalidFieldIds.contains(field.id)) {
field.copy(
isValid = false,
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt
index 1d303a694..ec69ad221 100644
--- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt
@@ -3,6 +3,8 @@ package com.processout.sdk.ui.card.tokenization
import androidx.compose.ui.text.input.TextFieldValue
import com.processout.sdk.api.model.response.POCard
import com.processout.sdk.api.model.response.POCardIssuerInformation
+import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility
+import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.Eligible
import com.processout.sdk.ui.core.state.POAvailableValue
import com.processout.sdk.ui.shared.provider.address.AddressSpecification
@@ -18,10 +20,12 @@ internal data class CardTokenizationInteractorState(
val secondaryActionId: String,
val cardScannerActionId: String,
val submitAllowed: Boolean = true,
+ val pendingSubmit: Boolean = false,
val submitting: Boolean = false,
val errorMessage: String? = null,
val addressSpecification: AddressSpecification? = null,
val issuerInformation: POCardIssuerInformation? = null,
+ val eligibility: POCardTokenizationEligibility = Eligible(),
val tokenizedCard: POCard? = null
) {
@@ -29,6 +33,7 @@ internal data class CardTokenizationInteractorState(
val id: String,
val value: TextFieldValue = TextFieldValue(),
val availableValues: List? = null,
+ val enabled: Boolean = true,
val isValid: Boolean = true,
val shouldCollect: Boolean = true
)
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt
index 8fa0b596d..0878f5da7 100644
--- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt
@@ -326,12 +326,14 @@ private fun RadioField(
PORadioGroup(
value = state.value.text,
onValueChange = {
- onEvent(
- FieldValueChanged(
- id = state.id,
- value = TextFieldValue(text = it)
+ if (state.enabled) {
+ onEvent(
+ FieldValueChanged(
+ id = state.id,
+ value = TextFieldValue(text = it)
+ )
)
- )
+ }
},
availableValues = state.availableValues ?: POImmutableList(emptyList()),
modifier = modifier,
@@ -369,6 +371,7 @@ private fun DropdownField(
},
fieldStyle = fieldStyle,
menuStyle = menuStyle,
+ enabled = state.enabled,
isError = state.isError,
placeholderText = state.placeholder
)
@@ -385,16 +388,17 @@ private fun CheckboxField(
text = state.title ?: String(),
checked = state.value.text.toBooleanStrictOrNull() ?: false,
onCheckedChange = {
- onEvent(
- FieldValueChanged(
- id = state.id,
- value = TextFieldValue(text = it.toString())
+ if (state.enabled) {
+ onEvent(
+ FieldValueChanged(
+ id = state.id,
+ value = TextFieldValue(text = it.toString())
+ )
)
- )
+ }
},
modifier = modifier,
style = style,
- enabled = state.enabled,
isError = state.isError
)
}
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 c3192964d..841f254bf 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
@@ -126,6 +126,7 @@ internal class CardTokenizationViewModel private constructor(
id = state.cardScannerActionId,
text = it.text ?: app.getString(R.string.po_card_tokenization_button_scan),
primary = false,
+ enabled = !state.submitting,
icon = it.icon ?: PODrawableImage(
resId = com.processout.sdk.ui.R.drawable.po_icon_camera,
renderingMode = POImageRenderingMode.TEMPLATE
@@ -420,6 +421,7 @@ internal class CardTokenizationViewModel private constructor(
value = field.value,
placeholder = placeholder,
iconResId = iconResId,
+ enabled = field.enabled,
isError = !field.isValid,
forceTextDirectionLtr = forceTextDirectionLtr,
inputFilter = inputFilter,
@@ -434,7 +436,8 @@ internal class CardTokenizationViewModel private constructor(
FieldState(
id = field.id,
value = field.value,
- availableValues = POImmutableList(field.availableValues ?: emptyList())
+ availableValues = POImmutableList(field.availableValues ?: emptyList()),
+ enabled = field.enabled
)
)
@@ -443,7 +446,8 @@ internal class CardTokenizationViewModel private constructor(
FieldState(
id = field.id,
value = field.value,
- availableValues = POImmutableList(field.availableValues ?: emptyList())
+ availableValues = POImmutableList(field.availableValues ?: emptyList()),
+ enabled = field.enabled
)
)
@@ -455,6 +459,7 @@ internal class CardTokenizationViewModel private constructor(
id = field.id,
value = field.value,
title = title,
+ enabled = field.enabled,
isError = !field.isValid
)
)
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt
index 66dbd355f..72d68ae7a 100644
--- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationLauncher.kt
@@ -15,7 +15,9 @@ import com.processout.sdk.api.model.request.POCardTokenizationShouldContinueRequ
import com.processout.sdk.api.model.response.POCard
import com.processout.sdk.api.model.response.toResponse
import com.processout.sdk.core.ProcessOutActivityResult
+import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationEligibilityRequest
import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationDelegate
+import com.processout.sdk.ui.card.tokenization.delegate.toResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -114,6 +116,7 @@ class POCardTokenizationLauncher private constructor(
init {
dispatchEvents()
dispatchTokenizedCard()
+ dispatchEligibility()
dispatchPreferredScheme()
dispatchShouldContinue()
}
@@ -138,6 +141,20 @@ class POCardTokenizationLauncher private constructor(
}
}
+ private fun dispatchEligibility() {
+ eventDispatcher.subscribeForRequest(
+ coroutineScope = scope
+ ) { request ->
+ scope.launch {
+ val eligibility = delegate.evaluateEligibility(
+ iin = request.iin,
+ issuerInformation = request.issuerInformation
+ )
+ eventDispatcher.send(request.toResponse(eligibility))
+ }
+ }
+ }
+
private fun dispatchPreferredScheme() {
eventDispatcher.subscribeForRequest(
coroutineScope = scope
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/CardTokenizationEligibility.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/CardTokenizationEligibility.kt
new file mode 100644
index 000000000..ca507e550
--- /dev/null
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/CardTokenizationEligibility.kt
@@ -0,0 +1,20 @@
+package com.processout.sdk.ui.card.tokenization.delegate
+
+import com.processout.sdk.api.dispatcher.POEventDispatcher
+import com.processout.sdk.api.model.response.POCardIssuerInformation
+import java.util.UUID
+
+internal data class CardTokenizationEligibilityRequest(
+ override val uuid: UUID = UUID.randomUUID(),
+ val iin: String,
+ val issuerInformation: POCardIssuerInformation
+) : POEventDispatcher.Request
+
+internal data class CardTokenizationEligibilityResponse(
+ override val uuid: UUID,
+ val eligibility: POCardTokenizationEligibility
+) : POEventDispatcher.Response
+
+internal fun CardTokenizationEligibilityRequest.toResponse(
+ eligibility: POCardTokenizationEligibility
+) = CardTokenizationEligibilityResponse(uuid, eligibility)
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/POCardTokenizationDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/POCardTokenizationDelegate.kt
index e82df41db..d4b2bc6b8 100644
--- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/POCardTokenizationDelegate.kt
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/POCardTokenizationDelegate.kt
@@ -4,6 +4,7 @@ import com.processout.sdk.api.model.event.POCardTokenizationEvent
import com.processout.sdk.api.model.response.POCard
import com.processout.sdk.api.model.response.POCardIssuerInformation
import com.processout.sdk.core.ProcessOutResult
+import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.Eligible
/**
* Delegate that allows to handle events during card tokenization.
@@ -32,6 +33,17 @@ interface POCardTokenizationDelegate {
saveCard: Boolean
): ProcessOutResult = ProcessOutResult.Success(Unit)
+ /**
+ * Allows to evaluate card eligibility for tokenization based on issuer information.
+ *
+ * @param[iin] Issuer identification number.
+ * @param[issuerInformation] Resolved issuer information.
+ */
+ suspend fun evaluateEligibility(
+ iin: String,
+ issuerInformation: POCardIssuerInformation
+ ): POCardTokenizationEligibility = Eligible()
+
/**
* Allows to choose default preferred card scheme based on issuer information.
* Primary card scheme is used by default.
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/POCardTokenizationEligibility.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/POCardTokenizationEligibility.kt
new file mode 100644
index 000000000..808860209
--- /dev/null
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/delegate/POCardTokenizationEligibility.kt
@@ -0,0 +1,24 @@
+package com.processout.sdk.ui.card.tokenization.delegate
+
+import com.processout.sdk.core.ProcessOutResult
+
+/**
+ * Represents card eligibility for tokenization.
+ */
+sealed class POCardTokenizationEligibility {
+
+ /**
+ * Indicates that the card is eligible for tokenization, optionally restricted to the specific card scheme.
+ */
+ data class Eligible(
+ val scheme: String? = null
+ ) : POCardTokenizationEligibility()
+
+ /**
+ * Indicates that the card is not eligible for tokenization.
+ * You may provide a failure with the [ProcessOutResult.Failure.localizedMessage] that will be shown directly to the user.
+ */
+ data class NotEligible(
+ val failure: ProcessOutResult.Failure? = null
+ ) : POCardTokenizationEligibility()
+}
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt
index c9a1d4296..dae218dc9 100644
--- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutLauncher.kt
@@ -17,6 +17,9 @@ import com.processout.sdk.api.service.proxy3ds.POProxy3DSServiceRequest.*
import com.processout.sdk.api.service.proxy3ds.POProxy3DSServiceResponse
import com.processout.sdk.core.POUnit
import com.processout.sdk.core.ProcessOutActivityResult
+import com.processout.sdk.ui.card.tokenization.delegate.CardTokenizationEligibilityRequest
+import com.processout.sdk.ui.card.tokenization.delegate.POCardTokenizationEligibility.Eligible
+import com.processout.sdk.ui.card.tokenization.delegate.toResponse
import com.processout.sdk.ui.checkout.delegate.*
import com.processout.sdk.ui.core.annotation.ProcessOutInternalApi
import com.processout.sdk.ui.napm.delegate.PONativeAlternativePaymentEvent
@@ -89,6 +92,7 @@ class PODynamicCheckoutLauncher private constructor(
dispatchEvents()
dispatchInvoice()
dispatchInvoiceAuthorizationRequest()
+ dispatchCardEligibility()
dispatchPreferredScheme()
dispatchDefaultValues()
dispatchAlternativePaymentConfiguration()
@@ -139,6 +143,16 @@ class PODynamicCheckoutLauncher private constructor(
}
}
+ private fun dispatchCardEligibility() {
+ eventDispatcher.subscribeForRequest(
+ coroutineScope = scope
+ ) { request ->
+ scope.launch {
+ eventDispatcher.send(request.toResponse(eligibility = Eligible()))
+ }
+ }
+ }
+
private fun dispatchPreferredScheme() {
eventDispatcher.subscribeForRequest(
coroutineScope = scope
diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt
index 31a1c49de..9905056b0 100644
--- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt
+++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt
@@ -279,13 +279,15 @@ private fun RadioField(
PORadioGroup(
value = state.value.text,
onValueChange = {
- onEvent(
- FieldValueChanged(
- paymentMethodId = id,
- fieldId = state.id,
- value = TextFieldValue(text = it)
+ if (state.enabled) {
+ onEvent(
+ FieldValueChanged(
+ paymentMethodId = id,
+ fieldId = state.id,
+ value = TextFieldValue(text = it)
+ )
)
- )
+ }
},
availableValues = state.availableValues ?: POImmutableList(emptyList()),
modifier = modifier,
@@ -326,6 +328,7 @@ private fun DropdownField(
},
fieldStyle = fieldStyle,
menuStyle = menuStyle,
+ enabled = state.enabled,
isError = state.isError,
placeholderText = state.placeholder
)
@@ -343,17 +346,18 @@ private fun CheckboxField(
text = state.title ?: String(),
checked = state.value.text.toBooleanStrictOrNull() ?: false,
onCheckedChange = {
- onEvent(
- FieldValueChanged(
- paymentMethodId = id,
- fieldId = state.id,
- value = TextFieldValue(text = it.toString())
+ if (state.enabled) {
+ onEvent(
+ FieldValueChanged(
+ paymentMethodId = id,
+ fieldId = state.id,
+ value = TextFieldValue(text = it.toString())
+ )
)
- )
+ }
},
modifier = modifier,
style = style,
- enabled = state.enabled,
isError = state.isError
)
}