send: send screen is done.

This commit is contained in:
Kevin Gorham 2019-02-21 01:37:09 -05:00 committed by Kevin Gorham
parent 524b89a11a
commit a7c7954823
17 changed files with 447 additions and 216 deletions

View File

@ -2,9 +2,12 @@ package cash.z.android.wallet.extention
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import androidx.core.content.getSystemService
fun EditText.afterTextChanged(block: (String) -> Unit) { inline fun EditText.afterTextChanged(crossinline block: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher { this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
block.invoke(s.toString()) block.invoke(s.toString())
@ -13,4 +16,30 @@ fun EditText.afterTextChanged(block: (String) -> Unit) {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}) })
}
inline fun EditText.doOnDone(crossinline block: (String) -> Unit) {
setOnEditorActionListener { v, actionId, _ ->
return@setOnEditorActionListener if ((actionId == EditorInfo.IME_ACTION_DONE)) {
v.clearFocus()
// v.clearComposingText()
v.context.getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(v.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
block(this.text.toString())
true
} else {
false
}
}
}
inline fun EditText.doOnFocusLost(crossinline block: (String) -> Unit) {
setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) block(this.text.toString())
}
}
inline fun EditText.doOnDoneOrFocusLost(crossinline block: (String) -> Unit) {
doOnDone(block)
doOnFocusLost(block)
} }

View File

@ -53,6 +53,11 @@ class ReceiveFragment : BaseFragment() {
text_address_part_8 text_address_part_8
) )
} }
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainActivity.setToolbarShown(true)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()

View File

@ -227,21 +227,26 @@ class ScanFragment : BaseFragment() {
} }
override fun onImageAvailable(image: Image) { override fun onImageAvailable(image: Image) {
System.err.println("camoorah : onImageAvailable: $image width: ${image.width} height: ${image.height}") try {
var firebaseImage = FirebaseVisionImage.fromMediaImage(image, getRotationCompensation(cameraId, mainActivity)) System.err.println("camoorah : onImageAvailable: $image width: ${image.width} height: ${image.height}")
barcodeDetector var firebaseImage =
.detectInImage(firebaseImage) FirebaseVisionImage.fromMediaImage(image, getRotationCompensation(cameraId, mainActivity))
.addOnSuccessListener { results -> barcodeDetector
if (results.isNotEmpty()) { .detectInImage(firebaseImage)
val barcode = results[0] .addOnSuccessListener { results ->
val value = barcode.rawValue if (results.isNotEmpty()) {
onScanSuccess(value!!) val barcode = results[0]
// TODO: highlight the barcode val value = barcode.rawValue
var bounds = barcode.boundingBox onScanSuccess(value!!)
var corners = barcode.cornerPoints // TODO: highlight the barcode
binding.cameraView.setBarcode(barcode) var bounds = barcode.boundingBox
var corners = barcode.cornerPoints
binding.cameraView.setBarcode(barcode)
}
} }
} } catch (t: Throwable) {
System.err.println("camoorah : error while processing onImageAvailable: $t\n\tcaused by: ${t.cause}")
}
} }
} }

View File

@ -23,15 +23,12 @@ import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentSendBinding import cash.z.android.wallet.databinding.FragmentSendBinding
import cash.z.android.wallet.extention.* import cash.z.android.wallet.extention.*
import cash.z.android.wallet.sample.SampleProperties import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.ui.activity.MainActivity
import cash.z.android.wallet.ui.presenter.SendPresenter import cash.z.android.wallet.ui.presenter.SendPresenter
import cash.z.wallet.sdk.ext.convertZatoshiToZecString import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.DecimalFormat
import kotlin.math.absoluteValue
/** /**
* Fragment for sending Zcash. * Fragment for sending Zcash.
@ -39,12 +36,12 @@ import kotlin.math.absoluteValue
*/ */
class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.BarcodeCallback { class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.BarcodeCallback {
lateinit var sendPresenter: SendPresenter
lateinit var binding: FragmentSendBinding
private val zec = R.string.zec_abbreviation.toAppString() private val zec = R.string.zec_abbreviation.toAppString()
private val usd = R.string.usd_abbreviation.toAppString() private val usd = R.string.usd_abbreviation.toAppString()
lateinit var sendPresenter: SendPresenter
lateinit var binding: FragmentSendBinding
// //
// Lifecycle // Lifecycle
@ -74,6 +71,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
mainActivity.setToolbarShown(true)
sendPresenter = SendPresenter(this, mainActivity.synchronizer) sendPresenter = SendPresenter(this, mainActivity.synchronizer)
} }
@ -94,7 +92,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
// SendView Implementation // SendView Implementation
// //
override fun submit() { override fun exit() {
mainActivity.navController.navigate(R.id.nav_home_fragment) mainActivity.navController.navigate(R.id.nav_home_fragment)
} }
@ -131,6 +129,10 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
binding.textZecValueAvailable.text = availableTextSpan binding.textZecValueAvailable.text = availableTextSpan
} }
override fun setSendEnabled(isEnabled: Boolean) {
binding.buttonSendZec.isEnabled = isEnabled
}
// //
// ScanFragment.BarcodeCallback implemenation // ScanFragment.BarcodeCallback implemenation
@ -139,7 +141,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
override fun onBarcodeScanned(value: String) { override fun onBarcodeScanned(value: String) {
exitScanMode() exitScanMode()
binding.inputZcashAddress.setText(value) binding.inputZcashAddress.setText(value)
validateAddressInput() sendPresenter.inputAddressUpdated(value)
} }
@ -151,29 +153,48 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
* Initialize view logic only. Click listeners, text change handlers and tooltips. * Initialize view logic only. Click listeners, text change handlers and tooltips.
*/ */
private fun init() { private fun init() {
/* Presenter calls */
binding.imageSwapCurrency.setOnClickListener { /* Init - Text Input */
sendPresenter.toggleCurrency()
}
binding.textValueHeader.apply { binding.textValueHeader.apply {
afterTextChanged { setSelectAllOnFocus(true)
sendPresenter.headerUpdating(it) afterTextChanged { if (it.isNotEmpty()) sendPresenter.inputHeaderUpdating(it) }
} doOnDoneOrFocusLost { sendPresenter.inputHeaderUpdated(it) }
} }
binding.buttonSendZec.setOnClickListener { binding.inputZcashAddress.apply {
sendPresenter.sendPressed() afterTextChanged { if (it.isNotEmpty()) sendPresenter.inputAddressUpdating(it) }
doOnDoneOrFocusLost { sendPresenter.inputAddressUpdated(it) }
}
binding.textAreaMemo.apply {
afterTextChanged {
if (it.isNotEmpty()) sendPresenter.inputMemoUpdating(it)
binding.textMemoCharCount.text = "${text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
}
doOnDoneOrFocusLost { sendPresenter.inputMemoUpdated(it) }
}
/* Init - Taps */
binding.imageSwapCurrency.setOnClickListener {
// validate the amount before we toggle (or else we lose their uncommitted change)
sendPresenter.inputHeaderUpdated(binding.textValueHeader.text.toString())
sendPresenter.inputToggleCurrency()
}
binding.buttonSendZec.setOnClickListener{
exitScanMode()
sendPresenter.inputSendPressed()
}
// allow background taps to dismiss the keyboard and clear focus
binding.contentFragmentSend.setOnClickListener {
sendPresenter.invalidate()
hideKeyboard()
} }
/* Non-Presenter calls (UI-only logic) */ /* Non-Presenter calls (UI-only logic) */
binding.textAreaMemo.afterTextChanged {
binding.textMemoCharCount.text =
"${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
}
binding.imageScanQr.apply { binding.imageScanQr.apply {
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr)) TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
} }
@ -187,23 +208,9 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
visibility = View.GONE visibility = View.GONE
} }
} }
binding.dialogSendBackground.setOnClickListener { hideSendDialog() }
binding.dialogSendBackground.setOnClickListener { binding.dialogSubmitButton.setOnClickListener { onSendZec() }
hideSendDialog()
}
binding.dialogSubmitButton.setOnClickListener {
onSendZec()
}
binding.imageScanQr.setOnClickListener(::onScanQrCode) binding.imageScanQr.setOnClickListener(::onScanQrCode)
// allow background taps to dismiss the keyboard and clear focus
binding.contentFragmentSend.setOnClickListener {
it?.findFocus()?.clearFocus()
validateUserInput()
hideKeyboard()
}
binding.buttonSendZec.text = getString(R.string.send_button_label, zec) binding.buttonSendZec.text = getString(R.string.send_button_label, zec)
setSendEnabled(false) setSendEnabled(false)
} }
@ -243,15 +250,19 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
} }
} }
// TODO: possibly move this behavior to only live in the debug build. Perhaps with a viewholder that I just delegate to. Then inject the holder here. // TODO: possibly move this behavior to only live in the debug build. Perhaps with a viewholder that I just delegate to. Then inject the holder in this class with production verstion getting an empty implementation that just hides the icon.
private fun onPasteShortcutAddress(view: View) { private fun onPasteShortcutAddress(view: View) {
view.context.alert(R.string.send_alert_shortcut_clicked) { view.context.alert(R.string.send_alert_shortcut_clicked) {
binding.inputZcashAddress.setText(SampleProperties.wallet.defaultSendAddress) val address = SampleProperties.wallet.defaultSendAddress
validateAddressInput() binding.inputZcashAddress.setText(address)
sendPresenter.inputAddressUpdated(address)
hideKeyboard() hideKeyboard()
} }
} }
/**
* Called after confirmation dialog is affirmed. Begins the process of actually sending ZEC.
*/
private fun onSendZec() { private fun onSendZec() {
setSendEnabled(false) setSendEnabled(false)
sendPresenter.sendFunds() sendPresenter.sendFunds()
@ -271,6 +282,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
private fun hideKeyboard() { private fun hideKeyboard() {
mainActivity.getSystemService<InputMethodManager>() mainActivity.getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS) ?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
checkAllInput()
} }
private fun hideSendDialog() { private fun hideSendDialog() {
@ -278,12 +290,28 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
binding.groupDialogSend.visibility = View.GONE binding.groupDialogSend.visibility = View.GONE
} }
// note: be careful calling this with `true` that should only happen when all conditions have been validated private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
private fun setSendEnabled(isEnabled: Boolean) { DrawableCompat.setTint(
binding.buttonSendZec.isEnabled = isEnabled binding.inputZcashAddress.background,
ContextCompat.getColor(mainActivity, colorRes)
)
} }
private fun setAddressError(message: String?) {
/* Error handling */
override fun setAmountError(message: String?) {
if (message == null) {
binding.textValueError.visibility = View.GONE
binding.textValueError.text = null
} else {
binding.textValueError.text = message
binding.textValueError.visibility = View.VISIBLE
setSendEnabled(false)
}
}
override fun setAddressError(message: String?) {
if (message == null) { if (message == null) {
setAddressLineColor() setAddressLineColor()
binding.textAddressError.text = null binding.textAddressError.text = null
@ -296,74 +324,36 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
} }
} }
private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) { override fun setMemoError(message: String?) {
DrawableCompat.setTint( val validColor = R.color.zcashBlack_12.toAppColor()
binding.inputZcashAddress.background, val errorColor = R.color.zcashRed.toAppColor()
ContextCompat.getColor(mainActivity, colorRes) if (message == null) {
) binding.dividerMemo.setBackgroundColor(validColor)
} binding.textMemoCharCount.setTextColor(validColor)
binding.textAreaMemo.setTextColor(R.color.text_dark.toAppColor())
private fun setAmountError(isError: Boolean) { } else {
val color = if (isError) R.color.zcashRed else R.color.text_dark binding.dividerMemo.setBackgroundColor(errorColor)
binding.textAmountBackground.setTextColor(color.toAppColor()) binding.textMemoCharCount.setTextColor(errorColor)
} binding.textAreaMemo.setTextColor(errorColor)
//
// Validation
//
override fun validateUserInput(): Boolean {
val allValid = validateAddressInput() && validateAmountInput() && validateMemo()
setSendEnabled(allValid)
return allValid
}
/**
* Validate the memo input and update presenter when valid.
*
* @return true when the memo is valid
*/
private fun validateMemo(): Boolean {
val memo = binding.textAreaMemo.text.toString()
return memo.all { it.isLetterOrDigit() }.also { if (it) sendPresenter.memoValidated(memo) }
}
/**
* Validate the address input and update presenter when valid.
*
* @return true when the address is valid
*/
private fun validateAddressInput(): Boolean {
var isValid = false
val address = binding.inputZcashAddress.text.toString()
if (address.isNotEmpty() && address.length < R.integer.z_address_min_length.toAppInt()) setAddressError(R.string.send_error_address_too_short.toAppString())
else if (address.any { !it.isLetterOrDigit() }) setAddressError(R.string.send_error_address_invalid_char.toAppString())
else setAddressError(null).also { isValid = true; sendPresenter.addressValidated(address) }
return isValid
}
/**
* Validate the amount input and update the presenter when valid.
*
* @return true when the amount is valid
*/
private fun validateAmountInput(): Boolean {
return try {
val amount = binding.textValueHeader.text.toString().safelyConvertToBigDecimal()!!
sendPresenter.headerValidated(amount)
setAmountError(false)
true
} catch (t: Throwable) {
Toaster.short("Invalid ZEC or USD value")
setSendEnabled(false) setSendEnabled(false)
setAmountError(true)
false
} }
} }
/**
* Validate all input. This is essentially the same as extracting a model out of the view and validating it with the
* presenter. Basically, this needs to happen anytime something is edited, in order to try and enable Send. Right
* now this method is called 1) any time the model is updated with valid input, 2) anytime the keyboard is hidden,
* and 3) anytime send is pressed. It also triggers the only logic that can set "requiresValidation" to false.
*/
override fun checkAllInput(): Boolean {
with(binding) {
return sendPresenter.validateAll(
headerValue = textValueHeader.text.toString(),
toAddress = inputZcashAddress.text.toString(),
memo = textAreaMemo.text.toString()
)
}
}
// TODO: come back to this test code later and fix the shared element transitions // TODO: come back to this test code later and fix the shared element transitions

View File

@ -1,10 +1,13 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import android.util.Log import cash.z.android.wallet.R
import cash.z.android.wallet.extention.toAppInt
import cash.z.android.wallet.extention.toAppString
import cash.z.android.wallet.sample.SampleProperties import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.data.Synchronizer import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.data.twig import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.data.Twig
import cash.z.wallet.sdk.ext.* import cash.z.wallet.sdk.ext.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -24,38 +27,57 @@ class SendPresenter(
fun setHeaderValue(usdString: String) fun setHeaderValue(usdString: String)
fun setSubheaderValue(usdString: String, isUsdSelected: Boolean) fun setSubheaderValue(usdString: String, isUsdSelected: Boolean)
fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean) fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean)
fun validateUserInput(): Boolean fun exit()
fun submit()
// error handling
fun setAmountError(message: String?)
fun setAddressError(message: String?)
fun setMemoError(message: String?)
fun setSendEnabled(isEnabled: Boolean)
fun checkAllInput(): Boolean
} }
/**
* We require the user to send more than this amount. Right now, we just use the miner's fee as a minimum but other
* lower bounds may also be useful for validation.
*/
private val minimumZatoshiAllowed = 10_000L
private var balanceJob: Job? = null private var balanceJob: Job? = null
private var requiresValidation = true
var sendUiModel = SendUiModel() var sendUiModel = SendUiModel()
// TODO: find the best set of characters here. Possibly add something to the rust layer to help with this.
private val validMemoChars = " \t\n\r.?!,\"':;-_=+@#%*"
// //
// LifeCycle // LifeCycle
// //
override suspend fun start() { override suspend fun start() {
Log.e("@TWIG-v", "sendPresenter starting!") Twig.sprout("SendPresenter")
// set the currency to zec and update the view, intializing everything to zero twig("sendPresenter starting!")
toggleCurrency() // set the currency to zec and update the view, initializing everything to zero
inputToggleCurrency()
with(view) { with(view) {
balanceJob = launchBalanceBinder(synchronizer.balance()) balanceJob = launchBalanceBinder(synchronizer.balance())
} }
} }
override fun stop() { override fun stop() {
Log.e("@TWIG-v", "sendPresenter stopping!") twig("sendPresenter stopping!")
Twig.clip("SendPresenter")
balanceJob?.cancel()?.also { balanceJob = null } balanceJob?.cancel()?.also { balanceJob = null }
} }
fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Long>) = launch { fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Long>) = launch {
Log.e("@TWIG-v", "send balance binder starting!") twig("send balance binder starting!")
for (new in channel) { for (new in channel) {
Log.e("@TWIG-v", "send polled a balance item") twig("send polled a balance item")
bind(new) bind(new)
} }
Log.e("@TWIG-v", "send balance binder exiting!") twig("send balance binder exiting!")
} }
@ -67,31 +89,38 @@ class SendPresenter(
//TODO: prehaps grab the activity scope or let the sycnchronizer have scope and make that function not suspend //TODO: prehaps grab the activity scope or let the sycnchronizer have scope and make that function not suspend
// also, we need to handle cancellations. So yeah, definitely do this differently // also, we need to handle cancellations. So yeah, definitely do this differently
GlobalScope.launch { GlobalScope.launch {
synchronizer.sendToAddress(sendUiModel.zecValue!!, sendUiModel.toAddress) synchronizer.sendToAddress(sendUiModel.zatoshiValue!!, sendUiModel.toAddress)
} }
view.submit() view.exit()
} }
//
// User Input
//
/** /**
* Called when the user has tapped on the button for toggling currency, swapping zec for usd * Called when the user has tapped on the button for toggling currency, swapping zec for usd
*/ */
fun toggleCurrency() { fun inputToggleCurrency() {
view.validateUserInput() // tricky: this is not really a model update, instead it is a byproduct of using `isUsdSelected` for the
// currency instead of strong types. There are several todo's to fix that. if we update the model here then
// the UI will think the user took action and display errors prematurely.
sendUiModel = sendUiModel.copy(isUsdSelected = !sendUiModel.isUsdSelected) sendUiModel = sendUiModel.copy(isUsdSelected = !sendUiModel.isUsdSelected)
with(sendUiModel) { with(sendUiModel) {
view.setHeaders( view.setHeaders(
isUsdSelected = isUsdSelected, isUsdSelected = isUsdSelected,
headerString = if (isUsdSelected) usdValue.toUsdString() else zecValue.convertZatoshiToZecString(), headerString = if (isUsdSelected) usdValue.toUsdString() else zatoshiValue.convertZatoshiToZecString(),
subheaderString = if (isUsdSelected) zecValue.convertZatoshiToZecString() else usdValue.toUsdString() subheaderString = if (isUsdSelected) zatoshiValue.convertZatoshiToZecString() else usdValue.toUsdString()
) )
} }
} }
/** /**
* As the user is typing the header string, update the subheader string. Do not modify our own internal model yet. * As the user is typing the header string, update the subheader string. Do not modify our own internal model yet.
* Internal model is only updated after [headerValidated] is called. * Internal model is only modified after [headerUpdated] is called (with valid data).
*/ */
fun headerUpdating(headerValue: String) { fun inputHeaderUpdating(headerValue: String) {
headerValue.safelyConvertToBigDecimal()?.let { headerValueAsDecimal -> headerValue.safelyConvertToBigDecimal()?.let { headerValueAsDecimal ->
val subheaderValue = headerValueAsDecimal.convertCurrency(SampleProperties.USD_PER_ZEC, sendUiModel.isUsdSelected) val subheaderValue = headerValueAsDecimal.convertCurrency(SampleProperties.USD_PER_ZEC, sendUiModel.isUsdSelected)
@ -102,49 +131,77 @@ class SendPresenter(
} }
} }
fun sendPressed() { /**
with(sendUiModel) { * As the user updates the address, update the error that gets displayed in real-time
view.showSendDialog( *
zecString = zecValue.convertZatoshiToZecString(), * @param addressValue the address that the user has typed, so far
usdString = usdValue.toUsdString(), */
toAddress = toAddress, fun inputAddressUpdating(addressValue: String) {
hasMemo = !memo.isBlank() validateAddress(addressValue, true)
) }
/**
* As the user updates the memo, update the error that gets displayed in real-time
*
* @param memoValue the memo that the user has typed, so far
*/
fun inputMemoUpdating(memoValue: String) {
// treat the memo a little differently because it is more likely for the user to go back and edit invalid chars
// and we want the send button to be active the moment that happens
if(validateMemo(memoValue)) {
updateModel(sendUiModel.copy(memo = memoValue))
} }
} }
fun headerValidated(amount: BigDecimal) { /**
* Called when the user has completed their update to the header value, typically on focus change.
*/
fun inputHeaderUpdated(amountString: String) {
if (!validateAmount(amountString)) return
// either USD or ZEC -- TODO: use strong typing (and polymorphism) instead of isUsdSelected checks
val amount = amountString.safelyConvertToBigDecimal()!! // we've already validated this as not null and it's immutable
with(sendUiModel) { with(sendUiModel) {
if (isUsdSelected) { if (isUsdSelected) {
// amount represents USD
val headerString = amount.toUsdString() val headerString = amount.toUsdString()
val usdValue = amount val zatoshiValue = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC).convertZecToZatoshi()
val zecValue = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC) val subheaderString = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC).toUsdString()
val subheaderString = zecValue.toZecString() updateModel(sendUiModel.copy(zatoshiValue = zatoshiValue, usdValue = amount))
sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue)
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString) view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
} else { } else {
// amount represents ZEC
val headerString = amount.toZecString() val headerString = amount.toZecString()
val zecValue = amount
val usdValue = amount.convertZecToUsd(SampleProperties.USD_PER_ZEC) val usdValue = amount.convertZecToUsd(SampleProperties.USD_PER_ZEC)
val subheaderString = usdValue.toUsdString() val subheaderString = usdValue.toUsdString()
sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue) updateModel(sendUiModel.copy(zatoshiValue = amount.convertZecToZatoshi(), usdValue = usdValue))
println("calling setHeaders with $headerString $subheaderString") twig("calling setHeaders with $headerString $subheaderString")
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString) view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
} }
} }
} }
fun addressValidated(address: String) { fun inputAddressUpdated(newAddress: String) {
sendUiModel = sendUiModel.copy(toAddress = address) if (!validateAddress(newAddress)) return
updateModel(sendUiModel.copy(toAddress = newAddress))
} }
/** fun inputMemoUpdated(newMemo: String) {
* After the user has typed a memo, validated by the UI, then update the model. if (!validateMemo(newMemo)) return
* updateModel(sendUiModel.copy(memo = newMemo))
* assert: this method is only called after the memo input has been validated by the UI }
*/
fun memoValidated(sanitizedValue: String) { fun inputSendPressed() {
sendUiModel = sendUiModel.copy(memo = sanitizedValue) if (requiresValidation && !view.checkAllInput()) return
with(sendUiModel) {
view.showSendDialog(
zecString = zatoshiValue.convertZatoshiToZecString(),
usdString = usdValue.toUsdString(),
toAddress = toAddress,
hasMemo = !memo.isBlank()
)
}
} }
fun bind(newZecBalance: Long) { fun bind(newZecBalance: Long) {
@ -154,9 +211,119 @@ class SendPresenter(
} }
} }
fun updateModel(newModel: SendUiModel) {
sendUiModel = newModel.apply { hasBeenUpdated = true }
// now that we have new data, check and see if we can clear errors and re-enable the send button
if (requiresValidation) view.checkAllInput()
}
//
// Validation
//
/**
* Called after any user interaction. This is a potential time that errors should be shown, but only if data has
* already been entered. The view should call this method on focus change.
*/
fun invalidate() {
requiresValidation = true
}
/**
* Validates the given memo, ensuring that it does not contain unsupported characters. For now, we're very
* restrictive until we define more clear requirements for the values that can safely be entered in this field
* without introducing security risks.
*
* @param memo the memo to consider for validation
*
* @return true when the memo contains valid characters, which includes being blank
*/
private fun validateMemo(memo: String): Boolean {
return if (memo.all { it.isLetterOrDigit() || it in validMemoChars }) {
view.setMemoError(null)
true
} else {
view.setMemoError("Only letters and numbers are allowed in memo at this time")
requiresValidation = true
false
}
}
/**
* Validates the given address
*
* @param toAddress the address to consider for validation
* @param ignoreLength whether to ignore the length while validating, this is helpful when the user is still
* actively typing the address
*/
private fun validateAddress(toAddress: String, ignoreLength: Boolean = false): Boolean {
// TODO: later expose a method in the synchronizer for validating addresses.
// Right now it's not available so we do it ourselves
return if (!ignoreLength && sendUiModel.hasBeenUpdated && toAddress.length < 20) {// arbitrary length for now
view.setAddressError(R.string.send_error_address_too_short.toAppString())
requiresValidation = true
false
} else if (!toAddress.startsWith("zt") && !toAddress.startsWith("zs")) {
view.setAddressError(R.string.send_error_address_invalid_contents.toAppString())
requiresValidation = true
false
} else if (toAddress.any { !it.isLetterOrDigit() }) {
view.setAddressError(R.string.send_error_address_invalid_char.toAppString())
requiresValidation = true
false
} else {
view.setAddressError(null)
true
}
}
/**
* Validates the given amount, calling the related `showError` methods on the view, when appropriate
*
* @param amount the amount to consider for validation, for now this can be either USD or ZEC. In the future we will
* will separate those into types.
*
* @return true when the given amount is valid and all errors have been cleared on the view
*/
private fun validateAmount(amountString: String): Boolean {
if (!sendUiModel.hasBeenUpdated) return true // don't mark zero as bad until the model has been updated
var amount = amountString.safelyConvertToBigDecimal()
// no need to convert when we know it's null
return if (amount == null ) {
validateZatoshiAmount(null)
} else {
val zecAmount =
if (sendUiModel.isUsdSelected) amount.convertUsdToZec(SampleProperties.USD_PER_ZEC) else amount
validateZatoshiAmount(zecAmount.convertZecToZatoshi())
}
}
private fun validateZatoshiAmount(zatoshiValue: Long?): Boolean {
return if (zatoshiValue == null || zatoshiValue <= minimumZatoshiAllowed) {
view.setAmountError("Please specify a larger amount")
requiresValidation = true
false
} else {
view.setAmountError(null)
true
}
}
fun validateAll(headerValue: String, toAddress: String, memo: String): Boolean {
val isValid = validateAmount(headerValue)
&& validateAddress(toAddress)
&& validateMemo(memo)
requiresValidation = !isValid
view.setSendEnabled(isValid)
return isValid
}
data class SendUiModel( data class SendUiModel(
var hasBeenUpdated: Boolean = false,
val isUsdSelected: Boolean = true, val isUsdSelected: Boolean = true,
val zecValue: Long? = null, val zatoshiValue: Long? = null,
val usdValue: BigDecimal = BigDecimal.ZERO, val usdValue: BigDecimal = BigDecimal.ZERO,
val toAddress: String = "", val toAddress: String = "",
val memo: String = "" val memo: String = ""

View File

@ -21,17 +21,18 @@
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintGuide_end="32dp" /> app:layout_constraintGuide_end="32dp" />
<ImageView <com.airbnb.lottie.LottieAnimationView
android:id="@+id/logo_welcome" android:id="@+id/lottie_welcome_firstrun"
android:layout_width="168dp" android:layout_width="168dp"
android:layout_height="168dp" android:layout_height="168dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.17" app:layout_constraintVertical_bias="0.17"
app:srcCompat="@drawable/ic_first_run" /> app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_welcome_sync"/>
<TextView <TextView
android:id="@+id/text_header" android:id="@+id/text_header"
@ -44,7 +45,7 @@
android:textSize="@dimen/text_size_h5" android:textSize="@dimen/text_size_h5"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logo_welcome" /> app:layout_constraintTop_toBottomOf="@+id/lottie_welcome_firstrun" />
<TextView <TextView
android:id="@+id/text_content" android:id="@+id/text_content"

View File

@ -13,6 +13,12 @@
android:background="@color/fragment_receive_background" android:background="@color/fragment_receive_background"
tools:context=".ui.fragment.ReceiveFragment"> tools:context=".ui.fragment.ReceiveFragment">
<include
android:id="@+id/main_app_bar"
layout="@layout/include_main_app_bar"
android:visibility="invisible"
tools:ignore="MissingConstraints" />
<!-- Shield Background --> <!-- Shield Background -->
<ImageView <ImageView
android:id="@+id/image_shield" android:id="@+id/image_shield"
@ -23,7 +29,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_toolbar" app:layout_constraintTop_toBottomOf="@+id/main_app_bar"
app:layout_constraintDimensionRatio="H,1:1.1676" app:layout_constraintDimensionRatio="H,1:1.1676"
app:layout_constraintVertical_bias="0.062" app:layout_constraintVertical_bias="0.062"
app:layout_constraintWidth_default="percent" app:layout_constraintWidth_default="percent"

View File

@ -17,6 +17,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/fragment_send_background"> android:background="@color/fragment_send_background">
<include
android:id="@+id/main_app_bar"
layout="@layout/include_main_app_bar"
android:visibility="invisible"
tools:ignore="MissingConstraints" />
<!-- --> <!-- -->
<!-- Guidelines --> <!-- Guidelines -->
<!-- --> <!-- -->
@ -50,7 +55,7 @@
android:layout_height="48dp" android:layout_height="48dp"
android:background="@color/zcashWhite" android:background="@color/zcashWhite"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_toolbar" app:layout_constraintTop_toBottomOf="@+id/main_app_bar"
app:layout_constraintVertical_chainStyle="spread" /> app:layout_constraintVertical_chainStyle="spread" />
<TextView <TextView
@ -78,6 +83,19 @@
app:layout_constraintTop_toBottomOf="@id/background_header" app:layout_constraintTop_toBottomOf="@id/background_header"
app:layout_constraintVertical_chainStyle="spread_inside" /> app:layout_constraintVertical_chainStyle="spread_inside" />
<TextView
android:id="@+id/text_value_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:fontFamily="@font/inconsolata"
android:textColor="@color/zcashRed"
android:textSize="@dimen/text_size_caption"
app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg"
app:layout_constraintTop_toBottomOf="@id/transition_active_transaction_bg"
tools:text="invalid amount of zec" />
<Button <Button
android:id="@+id/button_cancel_scan" android:id="@+id/button_cancel_scan"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -85,6 +103,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:background="@null" android:background="@null"
android:backgroundTint="@null" android:backgroundTint="@null"
android:elevation="0dp"
android:minWidth="0dp" android:minWidth="0dp"
android:minHeight="0dp" android:minHeight="0dp"
android:text="@string/cancel" android:text="@string/cancel"
@ -118,14 +137,16 @@
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg" app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
app:srcCompat="@drawable/ic_import_export_black" /> app:srcCompat="@drawable/ic_import_export_black" />
<!-- Input: Header -->
<EditText <EditText
android:id="@+id/text_value_header" android:id="@+id/text_value_header"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@null" android:background="@null"
android:imeOptions="actionDone"
android:inputType="numberDecimal" android:inputType="numberDecimal"
android:minWidth="12dp"
android:maxLength="8" android:maxLength="8"
android:minWidth="12dp"
android:text="0" android:text="0"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="@dimen/text_size_h3" android:textSize="@dimen/text_size_h3"
@ -163,50 +184,51 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:tint="@color/text_dark" android:tint="@color/text_dark"
tools:visibility="invisible"
app:layout_constraintDimensionRatio="H,1:1" app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/text_value_subheader" app:layout_constraintEnd_toStartOf="@id/text_value_subheader"
app:layout_constraintTop_toTopOf="@id/text_value_subheader" app:layout_constraintTop_toTopOf="@id/text_value_subheader"
app:srcCompat="@drawable/ic_zec_symbol" /> app:srcCompat="@drawable/ic_zec_symbol"
tools:visibility="invisible" />
<TextView <TextView
android:id="@+id/text_dollar_symbol_header" android:id="@+id/text_dollar_symbol_header"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="$" android:text="$"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="18dp" android:textSize="18dp"
android:includeFontPadding="false"
android:textStyle="bold" android:textStyle="bold"
tools:visibility="invisible"
app:layout_constraintEnd_toStartOf="@id/text_value_header"
app:layout_constraintBottom_toBottomOf="@id/image_zec_symbol_header" app:layout_constraintBottom_toBottomOf="@id/image_zec_symbol_header"
/> app:layout_constraintEnd_toStartOf="@id/text_value_header"
tools:visibility="invisible" />
<TextView <TextView
android:id="@+id/text_dollar_symbol_subheader" android:id="@+id/text_dollar_symbol_subheader"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:includeFontPadding="false"
android:layout_marginRight="2dp" android:layout_marginRight="2dp"
android:includeFontPadding="false"
android:text="$" android:text="$"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="8dp" android:textSize="8dp"
app:layout_constraintEnd_toStartOf="@id/text_value_subheader" app:layout_constraintEnd_toStartOf="@id/text_value_subheader"
app:layout_constraintTop_toTopOf="@id/image_zec_symbol_subheader" /> app:layout_constraintTop_toTopOf="@id/image_zec_symbol_subheader" />
<!-- Address --> <!-- Input: Address -->
<EditText <EditText
android:id="@+id/input_zcash_address" android:id="@+id/input_zcash_address"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/send_hint_input_zcash_address" android:hint="@string/send_hint_input_zcash_address"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions"
android:paddingRight="76dp" android:paddingRight="76dp"
android:singleLine="true" android:singleLine="true"
android:paddingTop="0dp"
app:backgroundTint="@color/zcashBlack_12" app:backgroundTint="@color/zcashBlack_12"
app:layout_constraintBottom_toTopOf="@id/text_area_memo" app:layout_constraintBottom_toTopOf="@id/text_area_memo"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end" app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@id/guideline_content_start" app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/transition_active_transaction_bg" /> app:layout_constraintTop_toBottomOf="@id/transition_active_transaction_bg" />
@ -217,17 +239,22 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:fontFamily="@font/inconsolata" android:fontFamily="@font/inconsolata"
android:includeFontPadding="false"
android:textColor="@color/zcashRed" android:textColor="@color/zcashRed"
android:textSize="@dimen/text_size_caption" android:textSize="@dimen/text_size_caption"
app:layout_constraintStart_toStartOf="@id/input_zcash_address" app:layout_constraintStart_toStartOf="@id/input_zcash_address"
app:layout_constraintTop_toBottomOf="@id/input_zcash_address" /> app:layout_constraintTop_toBottomOf="@id/input_zcash_address"
tools:text="invalid address" />
<!-- Scan QR code --> <!-- Scan QR code -->
<ImageView <ImageView
android:id="@+id/image_scan_qr" android:id="@+id/image_scan_qr"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginRight="8dp" android:paddingStart="6dp"
android:paddingTop="10dp"
android:paddingEnd="12dp"
android:paddingBottom="24dp"
android:tint="@color/zcashBlack_87" android:tint="@color/zcashBlack_87"
app:layout_constraintBottom_toBottomOf="@id/input_zcash_address" app:layout_constraintBottom_toBottomOf="@id/input_zcash_address"
app:layout_constraintEnd_toEndOf="@id/input_zcash_address" app:layout_constraintEnd_toEndOf="@id/input_zcash_address"
@ -238,7 +265,10 @@
android:id="@+id/image_address_shortcut" android:id="@+id/image_address_shortcut"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginRight="16dp" android:paddingStart="20dp"
android:paddingTop="10dp"
android:paddingEnd="6dp"
android:paddingBottom="24dp"
app:layout_constraintBottom_toBottomOf="@id/image_scan_qr" app:layout_constraintBottom_toBottomOf="@id/image_scan_qr"
app:layout_constraintEnd_toStartOf="@id/image_scan_qr" app:layout_constraintEnd_toStartOf="@id/image_scan_qr"
app:layout_constraintTop_toTopOf="@id/image_scan_qr" app:layout_constraintTop_toTopOf="@id/image_scan_qr"
@ -252,14 +282,15 @@
app:layout_constraintBottom_toTopOf="@id/input_zcash_address" app:layout_constraintBottom_toTopOf="@id/input_zcash_address"
app:layout_constraintStart_toStartOf="@id/input_zcash_address" /> app:layout_constraintStart_toStartOf="@id/input_zcash_address" />
<!-- memo --> <!-- Input: Memo -->
<EditText <EditText
android:id="@+id/text_area_memo" android:id="@+id/text_area_memo"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:background="@drawable/background_rounded_corners" android:background="@drawable/background_rounded_corners"
android:gravity="top|left" android:gravity="top|left"
android:inputType="textMultiLine" android:imeOptions="actionDone"
android:inputType="textAutoComplete"
android:maxLength="@integer/memo_max_length" android:maxLength="@integer/memo_max_length"
android:padding="16dp" android:padding="16dp"
app:backgroundTint="@color/zcashWhite_60" app:backgroundTint="@color/zcashWhite_60"
@ -318,11 +349,11 @@
android:id="@+id/camera_placeholder" android:id="@+id/camera_placeholder"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
tools:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg" app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg" app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg"
app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg" app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg"
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg" /> app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
tools:visibility="gone" />
<!-- --> <!-- -->
<!-- Dialog --> <!-- Dialog -->

View File

@ -22,17 +22,18 @@
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintGuide_end="32dp" /> app:layout_constraintGuide_end="32dp" />
<ImageView <com.airbnb.lottie.LottieAnimationView
android:id="@+id/logo_welcome" android:id="@+id/lottie_welcome_sync"
android:layout_width="168dp" android:layout_width="168dp"
android:layout_height="168dp" android:layout_height="168dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.17" app:layout_constraintVertical_bias="0.17"
app:srcCompat="@drawable/ic_sync" /> app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_welcome_firstrun"/>
<TextView <TextView
android:id="@+id/text_header" android:id="@+id/text_header"
@ -45,7 +46,7 @@
android:textSize="@dimen/text_size_h5" android:textSize="@dimen/text_size_h5"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logo_welcome" /> app:layout_constraintTop_toBottomOf="@+id/lottie_welcome_sync" />
<TextView <TextView
android:id="@+id/text_content" android:id="@+id/text_content"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,5 +3,4 @@
<integer name="nav_default_slow_transition_duration">400</integer> <integer name="nav_default_slow_transition_duration">400</integer>
<integer name="nav_default_quick_transition_duration">100</integer> <integer name="nav_default_quick_transition_duration">100</integer>
<integer name="memo_max_length">512</integer> <integer name="memo_max_length">512</integer>
<integer name="z_address_min_length">78</integer>
</resources> </resources>

View File

@ -79,6 +79,7 @@
<string name="send_dialog_title">Send %1$s %2$s ($%3$s)?</string> <string name="send_dialog_title">Send %1$s %2$s ($%3$s)?</string>
<string name="send_alert_shortcut_clicked">Paste a valid sample address for testing?</string> <string name="send_alert_shortcut_clicked">Paste a valid sample address for testing?</string>
<string name="send_error_address_too_short">Address is too short.</string> <string name="send_error_address_too_short">Address is too short.</string>
<string name="send_error_address_invalid_contents">Address appears to be invalid.</string>
<string name="send_error_address_invalid_char">Address contains invalid characters.</string> <string name="send_error_address_invalid_char">Address contains invalid characters.</string>
<!-- About --> <!-- About -->

View File

@ -15,8 +15,8 @@ import javax.inject.Singleton
@Module @Module
internal object SynchronizerModule { internal object SynchronizerModule {
// const val MOCK_LOAD_DURATION = 3_000L const val MOCK_LOAD_DURATION = 3_000L
const val MOCK_LOAD_DURATION = 30_000L // const val MOCK_LOAD_DURATION = 30_000L
const val MOCK_TX_INTERVAL = 20_000L const val MOCK_TX_INTERVAL = 20_000L
const val MOCK_ACTIVE_TX_STATE_CHANGE_INTERVAL = 5_000L const val MOCK_ACTIVE_TX_STATE_CHANGE_INTERVAL = 5_000L

View File

@ -10,11 +10,8 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoExtension
import java.math.BigDecimal
import java.math.RoundingMode
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
internal class SendPresenterTest { internal class SendPresenterTest {
@ -74,12 +71,12 @@ internal class SendPresenterTest {
presenter.toggleCurrency() presenter.toggleCurrency()
assertTrue(!presenter.sendUiModel.isUsdSelected, "zec should be selected to avoid testing conversions") assertTrue(!presenter.sendUiModel.isUsdSelected, "zec should be selected to avoid testing conversions")
presenter.headerValidated("1.1234535".safelyConvertToBigDecimal()!!) presenter.headerValidated("1.1234535".safelyConvertToBigDecimal()!!)
assertEquals(112345350, presenter.sendUiModel.zecValue) assertEquals(112345350, presenter.sendUiModel.zatoshiValue)
assertEquals("1.123454", presenter.sendUiModel.zecValue.convertZatoshiToZecString(), "5 is odd, we should round up") assertEquals("1.123454", presenter.sendUiModel.zatoshiValue.convertZatoshiToZecString(), "5 is odd, we should round up")
presenter.headerValidated("1.1234565".safelyConvertToBigDecimal()!!) presenter.headerValidated("1.1234565".safelyConvertToBigDecimal()!!)
assertEquals(112345650, presenter.sendUiModel.zecValue) assertEquals(112345650, presenter.sendUiModel.zatoshiValue)
assertEquals("1.123456", presenter.sendUiModel.zecValue.convertZatoshiToZecString(), "6 is even, we should round down") assertEquals("1.123456", presenter.sendUiModel.zatoshiValue.convertZatoshiToZecString(), "6 is even, we should round down")
} }
@Test @Test

View File

@ -22,17 +22,18 @@
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintGuide_end="32dp" /> app:layout_constraintGuide_end="32dp" />
<ImageView <com.airbnb.lottie.LottieAnimationView
android:id="@+id/logo_welcome" android:id="@+id/lottie_welcome_sync"
android:layout_width="168dp" android:layout_width="168dp"
android:layout_height="168dp" android:layout_height="168dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.17" app:layout_constraintVertical_bias="0.17"
app:srcCompat="@drawable/ic_sync" /> app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_welcome_firstrun"/>
<TextView <TextView
android:id="@+id/text_header" android:id="@+id/text_header"
@ -45,7 +46,7 @@
android:textSize="@dimen/text_size_h5" android:textSize="@dimen/text_size_h5"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logo_welcome" /> app:layout_constraintTop_toBottomOf="@+id/lottie_welcome_sync" />
<TextView <TextView
android:id="@+id/text_content" android:id="@+id/text_content"

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="z_address_min_length">88</integer>
</resources>