Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FX-1460] Insert ForageVaultElement into PIN class hierarchy #267

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.joinforage.forage.android.core.services.forageapi.paymentmethod

import com.joinforage.forage.android.core.services.getStringOrNull
import com.joinforage.forage.android.core.ui.element.state.USState
import com.joinforage.forage.android.core.ui.element.state.pan.USState
import org.json.JSONObject

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,30 @@ import com.joinforage.forage.android.core.services.telemetry.Log
import com.joinforage.forage.android.core.services.vault.AbstractVaultSubmitter
import com.joinforage.forage.android.core.ui.element.SimpleElementListener
import com.joinforage.forage.android.core.ui.element.StatefulElementListener
import com.joinforage.forage.android.core.ui.element.state.PinElementState
import com.joinforage.forage.android.core.ui.element.state.PinElementStateManager
import com.joinforage.forage.android.core.ui.element.state.FocusState
import com.joinforage.forage.android.core.ui.element.state.pin.PinEditTextState
import com.joinforage.forage.android.core.ui.element.state.pin.PinInputState

internal abstract class VaultWrapper @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
abstract var typeface: Typeface?
abstract val vaultType: VaultType

protected var focusState = FocusState.forEmptyInput()
protected var inputState = PinInputState.forEmptyInput()

// mutable references to event listeners. We use mutable
// references because the implementations of our vaults
// require that we are only able to ever pass a single
// monolithic event within init call. This is mutability
// allows us simulate setting and overwriting a listener
// with every set call
internal abstract val manager: PinElementStateManager
abstract val vaultType: VaultType
var onFocusEventListener: SimpleElementListener? = null
var onBlurEventListener: SimpleElementListener? = null
var onChangeEventListener: StatefulElementListener<PinEditTextState>? = null

abstract fun clearText()

Expand All @@ -51,15 +57,6 @@ internal abstract class VaultWrapper @JvmOverloads constructor(
return outValue.data
}

fun setOnFocusEventListener(l: SimpleElementListener) {
manager.setOnFocusEventListener(l)
}

fun setOnBlurEventListener(l: SimpleElementListener) {
manager.setOnBlurEventListener(l)
}

fun setOnChangeEventListener(l: StatefulElementListener<PinElementState>) {
manager.setOnChangeEventListener(l)
}
val pinEditTextState: PinEditTextState
get() = PinEditTextState.from(focusState, inputState)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import com.joinforage.forage.android.R
import com.joinforage.forage.android.core.services.EnvConfig
import com.joinforage.forage.android.core.services.ForageConfigNotSetException
import com.joinforage.forage.android.core.services.telemetry.Log
import com.joinforage.forage.android.core.ui.element.state.PanElementState
import com.joinforage.forage.android.core.ui.element.state.PanElementStateManager
import com.joinforage.forage.android.core.ui.element.state.FocusState
import com.joinforage.forage.android.core.ui.element.state.pan.PanEditTextState
import com.joinforage.forage.android.core.ui.element.state.pan.PanInputState
import com.joinforage.forage.android.core.ui.getBoxCornerRadiusBottomEnd
import com.joinforage.forage.android.core.ui.getBoxCornerRadiusBottomStart
import com.joinforage.forage.android.core.ui.getBoxCornerRadiusTopEnd
Expand Down Expand Up @@ -61,26 +62,32 @@ abstract class ForagePanElement @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.foragePanEditTextStyle
) : LinearLayout(context, attrs, defStyleAttr), ForageElement<PanElementState>, EditTextElement, DynamicEnvElement {
) : LinearLayout(context, attrs, defStyleAttr), ForageElement<PanEditTextState>, EditTextElement, DynamicEnvElement {
private val textInputEditText: TextInputEditText
private val textInputLayout: TextInputLayout

private var onFocusEventListener: SimpleElementListener? = null
private var onBlurEventListener: SimpleElementListener? = null
private var onChangeEventListener: StatefulElementListener<PanEditTextState>? = null
private var focusState = FocusState.forEmptyInput()

/**
* The `manager` property acts as an abstraction for the actual code
* The `inputState` property acts as an abstraction for the actual code
* in ForagePANEditText, allowing it to work with a non-nullable
* result determined by the choice between `prod` or `sandbox`.
* This choice requires knowledge of the environment,which is determined
* by `forageConfig` set on this instance.
*
* The underlying value for `manager` is stored in `_SET_ONLY_manager`.
* This backing property is set only after `ForageConfig` has been
* initialized for this instance. If `manager` is accessed before
* `_SET_ONLY_manager` is set, a runtime exception is thrown.
* The underlying value for `inputState` is stored in
* `_SET_ONLY_inputState`. This backing property is set only after
* `ForageConfig` has been initialized for this instance. If `manager`
* is accessed before `_SET_ONLY_inputState` is set, a runtime exception
* is thrown.
*/
private var _SET_ONLY_manager: PanElementStateManager? = null
private val manager: PanElementStateManager
private var _SET_ONLY_inputState: PanInputState? = null
private val inputState: PanInputState
get() {
if (_SET_ONLY_manager == null) {
if (_SET_ONLY_inputState == null) {
throw ForageConfigNotSetException(
"""You are attempting invoke a method a ForageElement before setting
it's ForageConfig. Make sure to call
Expand All @@ -89,7 +96,7 @@ abstract class ForagePanElement @JvmOverloads constructor(
""".trimIndent()
)
}
return _SET_ONLY_manager!!
return _SET_ONLY_inputState!!
}

override var typeface: Typeface? = null
Expand Down Expand Up @@ -198,12 +205,12 @@ abstract class ForagePanElement @JvmOverloads constructor(
val logger = Log.getInstance()
logger.initializeDD(context, forageConfig)

_SET_ONLY_manager = if (EnvConfig.inProd(forageConfig)) {
_SET_ONLY_inputState = if (EnvConfig.inProd(forageConfig)) {
// strictly support only valid Ebt PAN numbers
PanElementStateManager.forEmptyInput()
PanInputState.forEmptyInput()
} else {
// allows whitelist of special Ebt PAN numbers
PanElementStateManager.NON_PROD_forEmptyInput()
PanInputState.NON_PROD_forEmptyInput()
}

// register FormatPanTextWatcher to keep the format up to date
Expand All @@ -214,7 +221,8 @@ abstract class ForagePanElement @JvmOverloads constructor(
// decide when its appropriate to invoke the user-registered callback
val formatPanTextWatcher = FormatPanTextWatcher(textInputEditText)
formatPanTextWatcher.onFormattedChangeEvent { formattedCardNumber ->
manager.handleChangeEvent(formattedCardNumber)
_SET_ONLY_inputState = inputState.handleChangeEvent(formattedCardNumber)
onChangeEventListener?.invoke(getElementState())
}
textInputEditText.addTextChangedListener(formatPanTextWatcher)

Expand Down Expand Up @@ -246,7 +254,11 @@ abstract class ForagePanElement @JvmOverloads constructor(
// over will continue to correctly call the developer's
// focus/blur events
textInputEditText.setOnFocusChangeListener { _, hasFocus ->
manager.changeFocus(hasFocus)
focusState = focusState.changeFocus(hasFocus)
focusState.fireEvent(
onFocusEventListener = onFocusEventListener,
onBlurEventListener = onBlurEventListener
)
}
}

Expand All @@ -258,20 +270,21 @@ abstract class ForagePanElement @JvmOverloads constructor(
// Therefore we expose novel set listener methods instead of
// overriding the convention setOn*Listener
override fun setOnFocusEventListener(l: SimpleElementListener) {
manager.setOnFocusEventListener(l)
onFocusEventListener = l
restartFocusChangeListener()
}
override fun setOnBlurEventListener(l: SimpleElementListener) {
manager.setOnBlurEventListener(l)
onBlurEventListener = l
restartFocusChangeListener()
}
override fun setOnChangeEventListener(l: StatefulElementListener<PanElementState>) {
manager.setOnChangeEventListener(l)
override fun setOnChangeEventListener(l: StatefulElementListener<PanEditTextState>) {
onChangeEventListener = l
}

override fun getElementState(): PanElementState {
return manager.getState()
}
override fun getElementState(): PanEditTextState = PanEditTextState.from(
focusState,
inputState
)
Comment on lines 272 to +287
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods expose the PanElementState to the public surface area, which forces us to make PanElementState public 😕


override fun setTextColor(textColor: Int) {
// no-ops for now
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import android.widget.LinearLayout
import com.joinforage.forage.android.R
import com.joinforage.forage.android.core.services.VaultType
import com.joinforage.forage.android.core.ui.VaultWrapper
import com.joinforage.forage.android.core.ui.element.state.PinElementState
import com.joinforage.forage.android.core.ui.element.state.pin.PinEditTextState

/**
* A [ForageElement] that securely collects a card PIN. You need a [ForagePINEditText] to call
Expand Down Expand Up @@ -49,7 +49,7 @@ abstract class ForagePinElement @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.foragePanEditTextStyle
) : LinearLayout(context, attrs, defStyleAttr), ForageElement<PinElementState>, EditTextElement {
) : ForageVaultElement<PinEditTextState>(context, attrs, defStyleAttr), EditTextElement {
protected val _linearLayout: LinearLayout
internal abstract val vault: VaultWrapper

Expand Down Expand Up @@ -86,18 +86,16 @@ abstract class ForagePinElement @JvmOverloads constructor(
// Therefore we expose novel set listener methods instead of
// overriding the convention setOn*Listener
override fun setOnFocusEventListener(l: SimpleElementListener) {
vault.setOnFocusEventListener(l)
vault.onFocusEventListener = l
}
override fun setOnBlurEventListener(l: SimpleElementListener) {
vault.setOnBlurEventListener(l)
vault.onBlurEventListener = l
}
override fun setOnChangeEventListener(l: StatefulElementListener<PinElementState>) {
vault.setOnChangeEventListener(l)
override fun setOnChangeEventListener(l: StatefulElementListener<PinEditTextState>) {
vault.onChangeEventListener = l
}

override fun getElementState(): PinElementState {
return vault.manager.getState()
}
override fun getElementState(): PinEditTextState = vault.pinEditTextState

internal fun getVaultType(): VaultType {
return vault.vaultType
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.joinforage.forage.android.core.ui.element

import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import com.joinforage.forage.android.R
import com.joinforage.forage.android.core.services.EnvConfig
import com.joinforage.forage.android.core.services.telemetry.Log
import com.joinforage.forage.android.core.services.vault.AbstractVaultSubmitter
import com.joinforage.forage.android.core.ui.element.state.ElementState

abstract class ForageVaultElement<T : ElementState> @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.foragePanEditTextStyle
) : LinearLayout(context, attrs, defStyleAttr), ForageElement<T> {
internal abstract fun getVaultSubmitter(
envConfig: EnvConfig,
logger: Log
): AbstractVaultSubmitter
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.joinforage.forage.android.core.ui.element.state

/**
* An interface that represents the `EditText` state of the [ForageElement][com.joinforage.forage.android.core.ui.ForageElement]
* as the customer interacts with it.
* @property isFocused Whether the Element is in focus.
* @property isBlurred Whether the Element is blurred.
*/
interface EditTextState : ElementState {
val isFocused: Boolean
val isBlurred: Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,88 +2,20 @@ package com.joinforage.forage.android.core.ui.element.state

import com.joinforage.forage.android.core.ui.element.ElementValidationError

/**
* An interface that represents the state of the [ForageElement][com.joinforage.forage.android.ui.ForageElement]
* as the customer interacts with it.
* @property isFocused Whether the Element is in focus.
* @property isBlurred Whether the Element is blurred.
* @property isEmpty Whether the input value of the Element is empty.
* @property isValid Whether the input text fails any validation checks, with the exception of the
* target length constraint.
* @property isComplete Whether the text field of the Element is ready to submit. This is `true`
* when all input value validation constraints are satisfied.
* @property validationError An [ElementValidationError], if `isValid` is `false`.
/**
* An interface that represents the state of the [ForageElement][com.joinforage.forage.android.core.ui.ForageElement]
* as the customer interacts with it.
* @property isEmpty Whether the input value of the Element is empty.
* @property isValid Whether the input text fails any validation checks, with the exception of the
* target length constraint.
* @property isComplete Whether the text field of the Element is ready to submit. This is `true`
* when all input value validation constraints are satisfied.
* @property validationError An [ElementValidationError], if `isValid` is `false`.
* @see [EditTextState]
*/
interface ElementState {
val isFocused: Boolean
val isBlurred: Boolean
val isEmpty: Boolean
val isValid: Boolean
val isComplete: Boolean
val validationError: ElementValidationError?
}

/**
* An interface that represents the state of a
* [ForagePINEditText][com.joinforage.forage.android.ui.ForagePINEditText] Element.
*/
interface PinElementState : ElementState

internal 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 = false,
isComplete = false,
validationError = null
)

/**
* An interface that represents information that Forage gets from the card.
* This includes the [USState] that issued the card.
*/
interface DerivedCardInfo {
val usState: USState?
}

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

/**
* An interface that represents the state of a
* [ForagePANEditText][com.joinforage.forage.android.ui.ForagePANEditText] Element.
* @property derivedCardInfo The [DerivedCardInfo] for the card number submitted to the Element.
*/
interface PanElementState : ElementState {
val derivedCardInfo: DerivedCardInfo // the interface not the DTO
}

internal 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()
)
Loading
Loading