Skip to content

Commit

Permalink
[FX-722] Add support for derivedCardInfo.usState to ElementState
Browse files Browse the repository at this point in the history
…for `ForagePANEditText` (#133)

ElementState previously only cared about values that were common to both
PAN end PIN. We have reached a point where the states will need to
diverage.

* Update unit tests for usState support

* Make ElementState an interface instead of data class

However, Forage X had a conversation around this where we said we would
be OK with risk of changing the type of `ElementState` to something
other than `data class`. This opened the door for us to make
`ElementState` an interface, which is decoupled from the actual Data
Transfer Objects used to ferry the state of the ForageElement.

The rest of the changes in this commit follow from making `ElementState`
an interface. One upside of this approach is that we get rid of
`PinDetails`, which was annoying
  • Loading branch information
devinmorgan authored Dec 5, 2023
1 parent f430751 commit 440d709
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 131 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.joinforage.forage.android

import com.joinforage.forage.android.core.EnvConfig
import com.joinforage.forage.android.core.element.state.ElementState
import com.joinforage.forage.android.core.telemetry.CustomerPerceivedResponseMonitor
import com.joinforage.forage.android.core.telemetry.EventOutcome
import com.joinforage.forage.android.core.telemetry.Log
Expand All @@ -23,7 +24,7 @@ import java.util.UUID
*/
class ForageSDK : ForageSDKInterface {

private fun _getForageConfigOrThrow(element: AbstractForageElement): ForageConfig {
private fun <T : ElementState> _getForageConfigOrThrow(element: AbstractForageElement<T>): ForageConfig {
val context = element.getForageConfig()
return context ?: throw ForageConfigNotSetException(
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
package com.joinforage.forage.android.core.element

import com.joinforage.forage.android.core.element.state.ElementState

internal typealias SimpleElementListener = () -> Unit
internal typealias StatefulElementListener = (state: ElementState) -> Unit
internal typealias StatefulElementListener<T> = (state: T) -> Unit
Original file line number Diff line number Diff line change
@@ -1,20 +1,65 @@
package com.joinforage.forage.android.core.element.state

import com.joinforage.forage.android.core.element.ElementValidationError
import com.joinforage.forage.android.model.USState

data class ElementState(
val isFocused: Boolean,
val isBlurred: Boolean,
val isEmpty: Boolean,
val isValid: Boolean,
val isComplete: Boolean,
interface ElementState {
val isFocused: Boolean
val isBlurred: Boolean
val isEmpty: Boolean
val isValid: Boolean
val isComplete: Boolean
val validationError: ElementValidationError?
)
internal val INITIAL_ELEMENT_STATE = ElementState(
}

interface PinElementState : ElementState

data class PinElementStateDto(
override val isFocused: Boolean,
override val isBlurred: Boolean,
override val isEmpty: Boolean,
override val isValid: Boolean,
override val isComplete: Boolean,
override val validationError: ElementValidationError?
) : PinElementState

internal val INITIAL_PIN_ELEMENT_STATE = PinElementStateDto(
isFocused = false,
isBlurred = true,
isEmpty = true,
isValid = true,
isComplete = false,
validationError = null
)

interface DerivedCardInfo {
val usState: USState?
}

data class DerivedCardInfoDto(
override val usState: USState? = null
) : DerivedCardInfo

interface PanElementState : ElementState {
val derivedCardInfo: DerivedCardInfo // the interface not the DTO
}

data class PanElementStateDto(
override val isFocused: Boolean,
override val isBlurred: Boolean,
override val isEmpty: Boolean,
override val isValid: Boolean,
override val isComplete: Boolean,
override val validationError: ElementValidationError?,
override val derivedCardInfo: DerivedCardInfoDto
) : PanElementState

internal val INITIAL_PAN_ELEMENT_STATE = PanElementStateDto(
isFocused = false,
isBlurred = true,
isEmpty = true,
isValid = true,
isComplete = false,
validationError = null,
derivedCardInfo = DerivedCardInfoDto()
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,37 @@ import com.joinforage.forage.android.core.element.ElementValidationError
import com.joinforage.forage.android.core.element.SimpleElementListener
import com.joinforage.forage.android.core.element.StatefulElementListener

internal abstract class ElementStateManager(
private var isFocused: Boolean,
private var isBlurred: Boolean,
internal abstract class ElementStateManager<T : ElementState>(
// private because only this class should house focus / blur logic
private var _isFocused: Boolean,
private var _isBlurred: Boolean,

// internal because subclasses will define the logic for these
internal var isEmpty: Boolean,
internal var isValid: Boolean,
internal var isComplete: Boolean,
internal var validationError: ElementValidationError?
) {
private var onFocusEventListener: SimpleElementListener? = null
private var onBlurEventListener: SimpleElementListener? = null
internal var onChangeEventListener: StatefulElementListener? = null
internal var onChangeEventListener: StatefulElementListener<T>? = null

internal val isFocused
get() = _isFocused

internal constructor(state: ElementState) : this(
isFocused = state.isFocused,
isBlurred = state.isBlurred,
internal val isBlurred
get() = _isBlurred

internal constructor(state: T) : this(
_isFocused = state.isFocused,
_isBlurred = state.isBlurred,
isEmpty = state.isEmpty,
isValid = state.isValid,
isComplete = state.isComplete,
validationError = state.validationError
)

fun getState(): ElementState {
return ElementState(
isFocused = isFocused,
isBlurred = isBlurred,
isEmpty = isEmpty,
isValid = isValid,
isComplete = isComplete,
validationError = validationError
)
}
internal abstract fun getState(): T

fun setOnFocusEventListener(l: SimpleElementListener) {
onFocusEventListener = l
Expand All @@ -44,13 +44,13 @@ internal abstract class ElementStateManager(
onBlurEventListener = l
}

fun setOnChangeEventListener(l: StatefulElementListener) {
fun setOnChangeEventListener(l: StatefulElementListener<T>) {
onChangeEventListener = l
}

fun changeFocus(hasFocus: Boolean) {
isFocused = hasFocus
isBlurred = !hasFocus
_isFocused = hasFocus
_isBlurred = !hasFocus
if (hasFocus) {
onFocusEventListener?.invoke()
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.joinforage.forage.android.core.element.TooLongEbtPanError
import com.joinforage.forage.android.model.hasInvalidStateIIN
import com.joinforage.forage.android.model.isCorrectLength
import com.joinforage.forage.android.model.missingStateIIN
import com.joinforage.forage.android.model.queryForStateIIN
import com.joinforage.forage.android.model.tooLongForStateIIN
import com.joinforage.forage.android.model.tooShortForStateIIN

Expand Down Expand Up @@ -76,7 +77,11 @@ internal class BalanceCheckErrorCard : WhitelistedCards("5", 14)
internal class NonProdValidEbtCard : WhitelistedCards("9", 4)
internal class EmptyEbtCashBalanceCard : WhitelistedCards("654321")

internal class PanElementStateManager(state: ElementState, private val validators: Array<PanValidator>) : ElementStateManager(state) {
internal class PanElementStateManager(
state: PanElementState,
private val validators: Array<PanValidator>
) : ElementStateManager<PanElementState>(state) {
private var derivedCardInfo = DerivedCardInfoDto()

private fun checkIsValid(cardNumber: String): Boolean {
return validators.any { it.checkIfValid(cardNumber) }
Expand All @@ -89,6 +94,21 @@ internal class PanElementStateManager(state: ElementState, private val validator
.map { it.checkForValidationError(cardNumber) }
.firstOrNull { it != null }
}
private fun getDerivedCardInfo(cardNumber: String): DerivedCardInfoDto {
return DerivedCardInfoDto(queryForStateIIN(cardNumber)?.publicEnum)
}

override fun getState(): PanElementState {
return PanElementStateDto(
isFocused = this.isFocused,
isBlurred = this.isBlurred,
isEmpty = this.isEmpty,
isValid = this.isValid,
isComplete = this.isComplete,
validationError = this.validationError,
derivedCardInfo = this.derivedCardInfo
)
}

fun handleChangeEvent(rawInput: String) {
// because the input may be formatted, we need to
Expand All @@ -97,10 +117,13 @@ internal class PanElementStateManager(state: ElementState, private val validator

// check to see if any of the validators believe the
// card to be valid
this.isValid = checkIsValid(newCardNumber)
this.isComplete = checkIfComplete(newCardNumber)
this.validationError = checkForValidationError(newCardNumber)
this.isEmpty = newCardNumber.isEmpty()
isEmpty = newCardNumber.isEmpty()
isValid = checkIsValid(newCardNumber)
isComplete = checkIfComplete(newCardNumber)
validationError = checkForValidationError(newCardNumber)

// update state details based on newCardNumber
derivedCardInfo = getDerivedCardInfo(newCardNumber)

// invoke the registered listener with the updated state
onChangeEventListener?.invoke(getState())
Expand All @@ -109,14 +132,14 @@ internal class PanElementStateManager(state: ElementState, private val validator
companion object {
fun forEmptyInput(): PanElementStateManager {
return PanElementStateManager(
INITIAL_ELEMENT_STATE,
INITIAL_PAN_ELEMENT_STATE,
arrayOf(StrictEbtValidator())
)
}

fun NON_PROD_forEmptyInput(): PanElementStateManager {
return PanElementStateManager(
INITIAL_ELEMENT_STATE,
INITIAL_PAN_ELEMENT_STATE,
arrayOf(
StrictEbtValidator(),
PaymentCaptureErrorCard(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ package com.joinforage.forage.android.core.element.state
import com.joinforage.forage.android.core.element.IncompleteEbtPinError
import com.joinforage.forage.android.core.element.WrongEbtPinError

internal class PinElementStateManager(state: ElementState) : ElementStateManager(state) {
internal class PinElementStateManager(state: PinElementState) : ElementStateManager<PinElementState>(state) {

override fun getState(): PinElementState {
return PinElementStateDto(
isFocused = this.isFocused,
isBlurred = this.isBlurred,
isEmpty = this.isEmpty,
isValid = this.isValid,
isComplete = this.isComplete,
validationError = this.validationError
)
}

// this function is used for after the submit event
// happens and we learn that the PIN was not correct
Expand Down Expand Up @@ -34,7 +45,7 @@ internal class PinElementStateManager(state: ElementState) : ElementStateManager

companion object {
fun forEmptyInput(): PinElementStateManager {
return PinElementStateManager(INITIAL_ELEMENT_STATE)
return PinElementStateManager(INITIAL_PIN_ELEMENT_STATE)
}
}
}
Loading

0 comments on commit 440d709

Please sign in to comment.