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

Create ForageVaultWrapper #144

Merged
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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
## Table of contents

<!--ts-->

- [Overview](#overview)
- [Installation](#installation)
- [UI Components](#ui-components)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.joinforage.forage.android

import android.content.res.TypedArray
import com.joinforage.forage.android.network.model.ForageApiResponse
import com.joinforage.forage.android.network.model.ForageError
import com.joinforage.forage.android.network.model.SQSError
Expand Down Expand Up @@ -30,3 +31,8 @@ internal fun sqsMessageToError(sqsError: SQSError): ForageApiResponse.Failure {
internal fun HttpUrl.Builder.addTrailingSlash(): HttpUrl.Builder {
return this.addPathSegment("")
}

internal fun TypedArray.getBoxCornerRadius(styleIndex: Int, defaultBoxCornerRadius: Float): Float {
val styledBoxCornerRadius = getDimension(styleIndex, 0f)
return if (styledBoxCornerRadius == 0f) defaultBoxCornerRadius else styledBoxCornerRadius
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.LinearLayout
import com.basistheory.android.view.TextElement
import com.basistheory.android.view.mask.ElementMask
Expand Down Expand Up @@ -106,6 +107,10 @@ internal class BTVaultWrapper @JvmOverloads constructor(
return _internalTextElement
}

override fun getForageTextElement(): EditText {
throw RuntimeException("Unimplemented for this vault!")
}

override fun getUnderlying(): View {
return _internalTextElement
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ForagePINEditText @JvmOverloads constructor(
private val _linearLayout: LinearLayout
private val btVaultWrapper: BTVaultWrapper
private val vgsVaultWrapper: VGSVaultWrapper
private val forageVaultWrapper: ForageVaultWrapper

/**
* The `vault` property acts as an abstraction for the actual code
Expand Down Expand Up @@ -69,16 +70,18 @@ class ForagePINEditText @JvmOverloads constructor(

// at this point in time, we do not know the environment and
// we are operating and thus do not know whether to add
// BTVaultWrapper or VGSVaultWrapper to the UI.
// But that's OK. We can hedge and instantiate both of them
// BTVaultWrapper, VGSVaultWrapper, or ForageVaultWrapper to the UI.
// But that's OK. We can hedge and instantiate all of them.
// Then, within setForageConfig, once we know the environment
// and are thus able to initial LaunchDarkly and find out
// whether to use BT or VGS. So, below we are hedging
// whether to use BT or VGS. So, below we are hedging.
btVaultWrapper = BTVaultWrapper(context, attrs, defStyleAttr)
vgsVaultWrapper = VGSVaultWrapper(context, attrs, defStyleAttr)
// ensure both wrappers init with the
forageVaultWrapper = ForageVaultWrapper(context, attrs, defStyleAttr)
// ensure all wrappers init with the
// same typeface (or the attributes)
btVaultWrapper.typeface = vgsVaultWrapper.typeface
forageVaultWrapper.typeface = vgsVaultWrapper.typeface

val elementWidth: Int = getDimensionPixelSize(R.styleable.ForagePINEditText_elementWidth, ViewGroup.LayoutParams.MATCH_PARENT)
val elementHeight: Int = getDimensionPixelSize(R.styleable.ForagePINEditText_elementHeight, ViewGroup.LayoutParams.WRAP_CONTENT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.joinforage.forage.android.ui

import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.text.InputFilter
import android.text.InputType
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.widget.EditText
import android.widget.LinearLayout
import com.basistheory.android.view.TextElement
import com.joinforage.forage.android.R
import com.joinforage.forage.android.core.element.state.PinElementStateManager
import com.verygoodsecurity.vgscollect.widget.VGSEditText

internal class ForageVaultWrapper @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : VaultWrapper(context, attrs, defStyleAttr) {
private val _editText: EditText
override val manager: PinElementStateManager = PinElementStateManager.forEmptyInput()

init {
context.obtainStyledAttributes(attrs, R.styleable.ForagePINEditText, defStyleAttr, 0)
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

Check warning

Code scanning / Android Lint

Mismatched Styleable/Custom View Name Warning

By convention, the custom view (ForageVaultWrapper) and the declare-styleable (ForagePINEditText) should have the same name (various editor features rely on this convention)

Check warning

Code scanning / Android Lint

Mismatched Styleable/Custom View Name Warning

By convention, the custom view (ForageVaultWrapper) and the declare-styleable (ForagePINEditText) should have the same name (various editor features rely on this convention)
Dismissed Show dismissed Hide dismissed
.apply {
try {
val parsedStyles = parseStyles(context, attrs)

_editText = EditText(context, null, parsedStyles.textInputLayoutStyleAttribute).apply {
layoutParams =
LinearLayout.LayoutParams(
parsedStyles.inputWidth,
parsedStyles.inputHeight
)

setTextIsSelectable(true)
isSingleLine = true

val maxLength = 4
filters = arrayOf(InputFilter.LengthFilter(maxLength))

if (parsedStyles.textColor != Color.BLACK) {
setTextColor(parsedStyles.textColor)
}

if (parsedStyles.textSize != -1f) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, parsedStyles.textSize)
}

inputType =
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD

gravity = Gravity.CENTER
hint = parsedStyles.hint
setHintTextColor(parsedStyles.hintTextColor)

val customBackground = GradientDrawable().apply {
setPaddingRelative(20, 20, 20, 20)
shape = GradientDrawable.RECTANGLE
cornerRadii = floatArrayOf(
parsedStyles.boxCornerRadiusTopStart,
parsedStyles.boxCornerRadiusTopStart,
parsedStyles.boxCornerRadiusTopEnd,
parsedStyles.boxCornerRadiusTopEnd,
parsedStyles.boxCornerRadiusBottomStart,
parsedStyles.boxCornerRadiusBottomStart,
parsedStyles.boxCornerRadiusBottomEnd,
parsedStyles.boxCornerRadiusBottomEnd
)
setStroke(5, parsedStyles.boxStrokeColor)
setColor(parsedStyles.boxBackgroundColor)
}
background = customBackground
}

_editText.setOnFocusChangeListener { _, hasFocus ->
manager.changeFocus(hasFocus)
}
val pinTextWatcher = PinTextWatcher(_editText)
pinTextWatcher.onInputChangeEvent { isComplete, isEmpty ->
manager.handleChangeEvent(isComplete, isEmpty)
}
_editText.addTextChangedListener(pinTextWatcher)
} finally {
recycle()
}
}
}

override fun clearText() {
_editText.setText("")
}

override fun getForageTextElement(): EditText {
return _editText
}

override fun getTextElement(): TextElement {
throw RuntimeException("Unimplemented for this vault!")
}

override fun getVGSEditText(): VGSEditText {
throw RuntimeException("Unimplemented for this vault!")
}

override fun getUnderlying(): EditText {
return _editText
}

override var typeface: Typeface?
get() = _editText.typeface
set(value) {
if (value != null) {
_editText.typeface = value
}
}

override fun setTextColor(textColor: Int) {
_editText.setTextColor(textColor)
}

override fun setTextSize(textSize: Float) {
_editText.textSize = textSize
}

override fun setHint(hint: String) {
_editText.hint = hint
}

override fun setHintTextColor(hintTextColor: Int) {
_editText.setHintTextColor(hintTextColor)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice strategy for separating the PIN validation logic from the PIN rendering logic!

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.joinforage.forage.android.ui

import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText

internal class PinTextWatcher(
private val editText: EditText
) : TextWatcher {
private var onInputChangeEvent: ((Boolean, Boolean) -> Unit)? = null

fun onInputChangeEvent(callback: (Boolean, Boolean) -> Unit) {
onInputChangeEvent = callback
}

override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
// a no-op for now
}

override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
// a no-op for now
}

override fun afterTextChanged(editable: Editable) {
val isValidAndComplete = editText.length() == 4
val isEmpty = editText.length() == 0
onInputChangeEvent?.invoke(isValidAndComplete, isEmpty)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.ViewGroup
import android.widget.EditText
import android.widget.LinearLayout
import com.basistheory.android.view.TextElement
import com.joinforage.forage.android.core.element.state.PinElementStateManager
Expand Down Expand Up @@ -96,7 +97,7 @@ internal class VGSVaultWrapper @JvmOverloads constructor(
// up into separate focus and blur listeners. This requires
// that we pass a single listener to VGS on init that uses
// mutable references to listeners so that setting the focus
// would not remove the blur listener and vice versea
// would not remove the blur listener and vice versa
Copy link
Contributor

Choose a reason for hiding this comment

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

omg embarrassing - thank you for catching and fixing!

_internalEditText.setOnFocusChangeListener { _, hasFocus ->
manager.changeFocus(hasFocus)
}
Expand Down Expand Up @@ -127,6 +128,10 @@ internal class VGSVaultWrapper @JvmOverloads constructor(
throw RuntimeException("Unimplemented for this vault!")
}

override fun getForageTextElement(): EditText {
throw RuntimeException("Unimplemented for this vault!")
}

Copy link
Contributor

Choose a reason for hiding this comment

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

No action required right now but the work we're doing here is surfacing that we probably have some tech debt around the relationship between VaultWrapper and ForagePINEdiText that we'll want to pay down at some point.

That we don't want BTVaultWrapper or VGSVaultWrapper to implement getForageTextElement, indicates that we should not be making getForageTextElement an abstract method on the VaultWrapper class. VaultWrapper already declares getTextElement and getVGSEditText as abstract methods even though they too really should not be living on VaultWrapper since they are specific to BT / VGS wrappers. After poking around the code, it's clear that ForagePINEditText is implemented in a way that expects all vaults expose a getTextElement and getVGSEditText. ForagePINEditTexts dependency on these methods means the fix won't be trivial and so we should probably defer paying this debt down to a later ticket.

One way that I can see to fix this delema is to create an abstraction over the underlying editable text field that BT, VGS, and Forage expose so that there is a unified way of calling things like:

  • getUnderlyingView()
  • clearText()
  • etc

But anyhow, my next step is to make a Linear ticket to capture this and share the link here once I do. But, like a said before, I think what you're doing here is fine and won't break or block anything.

override fun getUnderlying(): VGSEditText {
return _internalEditText
}
Expand Down
Copy link
Contributor

@devinmorgan devinmorgan Jan 5, 2024

Choose a reason for hiding this comment

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

Introducing the data class ParsedStyles definitely made it easier to read the render logic inside ForageVaultWrapper - good idea!

Curious why we put ParsedStyles inside of VaultWrapper instead of keeping it local to ForageVaultWrapper? From what I can tell, this ParsedStyles does not seem to be used by BTVaultWrapper or VGSVaultWrapper and wanted to better understand.

At any rate, where ParsedStyles and it's related logic lives should not hold up merging in this me PR as it's internal code that we can revisit at any point

Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
package com.joinforage.forage.android.ui

import android.content.Context
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.Typeface
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.FrameLayout
import com.basistheory.android.view.TextElement
import com.joinforage.forage.android.R
import com.joinforage.forage.android.core.element.SimpleElementListener
import com.joinforage.forage.android.core.element.StatefulElementListener
import com.joinforage.forage.android.core.element.state.PinElementState
import com.joinforage.forage.android.core.element.state.PinElementStateManager
import com.joinforage.forage.android.getBoxCornerRadius
import com.verygoodsecurity.vgscollect.widget.VGSEditText

internal data class ParsedStyles(
val textInputLayoutStyleAttribute: Int,
val boxStrokeColor: Int,
val boxBackgroundColor: Int,
val boxCornerRadiusTopStart: Float,
val boxCornerRadiusTopEnd: Float,
val boxCornerRadiusBottomStart: Float,
val boxCornerRadiusBottomEnd: Float,
val hint: String?,
val hintTextColor: ColorStateList?,
val inputWidth: Int,
val inputHeight: Int,
val textSize: Float,
val textColor: Int
)

internal abstract class VaultWrapper @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
Expand All @@ -38,37 +60,40 @@
abstract fun getUnderlying(): View
abstract fun getVGSEditText(): VGSEditText
abstract fun getTextElement(): TextElement
abstract fun getForageTextElement(): EditText

fun parseStyles(context: Context, attrs: AttributeSet?): ParsedStyles {
val defaultRadius = resources.getDimension(R.dimen.default_horizontal_field)
val typedArray: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ForagePINEditText)

Check warning

Code scanning / Android Lint

Mismatched Styleable/Custom View Name Warning

By convention, the custom view (VaultWrapper) and the declare-styleable (ForagePINEditText) should have the same name (various editor features rely on this convention)
val boxCornerRadius = typedArray.getDimension(R.styleable.ForagePINEditText_boxCornerRadius, defaultRadius)

try {
return ParsedStyles(
textInputLayoutStyleAttribute = typedArray.getResourceId(R.styleable.ForagePINEditText_pinInputLayoutStyle, 0),
boxStrokeColor = typedArray.getColor(R.styleable.ForagePINEditText_pinBoxStrokeColor, getThemeAccentColor(context)),
boxBackgroundColor = typedArray.getColor(R.styleable.ForagePINEditText_boxBackgroundColor, Color.TRANSPARENT),
boxCornerRadiusTopStart = typedArray.getBoxCornerRadius(R.styleable.ForagePINEditText_boxCornerRadiusTopStart, boxCornerRadius),
boxCornerRadiusTopEnd = typedArray.getBoxCornerRadius(R.styleable.ForagePINEditText_boxCornerRadiusTopEnd, boxCornerRadius),
boxCornerRadiusBottomStart = typedArray.getBoxCornerRadius(R.styleable.ForagePINEditText_boxCornerRadiusBottomStart, boxCornerRadius),
boxCornerRadiusBottomEnd = typedArray.getBoxCornerRadius(R.styleable.ForagePINEditText_boxCornerRadiusBottomEnd, boxCornerRadius),
hint = typedArray.getString(R.styleable.ForagePINEditText_hint),
hintTextColor = typedArray.getColorStateList(R.styleable.ForagePINEditText_hintTextColor),
inputWidth = typedArray.getDimensionPixelSize(R.styleable.ForagePINEditText_inputWidth, ViewGroup.LayoutParams.MATCH_PARENT),
inputHeight = typedArray.getDimensionPixelSize(R.styleable.ForagePINEditText_inputHeight, ViewGroup.LayoutParams.WRAP_CONTENT),
textSize = typedArray.getDimension(R.styleable.ForagePINEditText_textSize, -1f),
textColor = typedArray.getColor(R.styleable.ForagePINEditText_textColor, Color.BLACK)
)
} finally {
typedArray.recycle()
}
}

fun getThemeAccentColor(context: Context): Int {
val outValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.colorAccent, outValue, true)
return outValue.data
}

fun TypedArray.getBoxCornerRadiusBottomStart(boxCornerRadius: Float): Float {
val boxCornerRadiusBottomStart =
getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxCornerRadiusBottomStart, 0f)
return if (boxCornerRadiusBottomStart == 0f) boxCornerRadius else boxCornerRadiusBottomStart
}

fun TypedArray.getBoxCornerRadiusTopEnd(boxCornerRadius: Float): Float {
val boxCornerRadiusTopEnd =
getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxCornerRadiusTopEnd, 0f)
return if (boxCornerRadiusTopEnd == 0f) boxCornerRadius else boxCornerRadiusTopEnd
}

fun TypedArray.getBoxCornerRadiusBottomEnd(boxCornerRadius: Float): Float {
val boxCornerRadiusBottomEnd =
getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxCornerRadiusBottomEnd, 0f)
return if (boxCornerRadiusBottomEnd == 0f) boxCornerRadius else boxCornerRadiusBottomEnd
}

fun TypedArray.getBoxCornerRadiusTopStart(boxCornerRadius: Float): Float {
val boxCornerRadiusTopStart =
getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxCornerRadiusTopStart, 0f)
return if (boxCornerRadiusTopStart == 0f) boxCornerRadius else boxCornerRadiusTopStart
}

fun setOnFocusEventListener(l: SimpleElementListener) {
manager.setOnFocusEventListener(l)
}
Expand Down
Loading
Loading