send: send screen is done.
This commit is contained in:
parent
524b89a11a
commit
a7c7954823
|
@ -2,9 +2,12 @@ package cash.z.android.wallet.extention
|
|||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
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 {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
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 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)
|
||||
}
|
|
@ -53,6 +53,11 @@ class ReceiveFragment : BaseFragment() {
|
|||
text_address_part_8
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
mainActivity.setToolbarShown(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
|
|
@ -227,21 +227,26 @@ class ScanFragment : BaseFragment() {
|
|||
}
|
||||
|
||||
override fun onImageAvailable(image: Image) {
|
||||
System.err.println("camoorah : onImageAvailable: $image width: ${image.width} height: ${image.height}")
|
||||
var firebaseImage = FirebaseVisionImage.fromMediaImage(image, getRotationCompensation(cameraId, mainActivity))
|
||||
barcodeDetector
|
||||
.detectInImage(firebaseImage)
|
||||
.addOnSuccessListener { results ->
|
||||
if (results.isNotEmpty()) {
|
||||
val barcode = results[0]
|
||||
val value = barcode.rawValue
|
||||
onScanSuccess(value!!)
|
||||
// TODO: highlight the barcode
|
||||
var bounds = barcode.boundingBox
|
||||
var corners = barcode.cornerPoints
|
||||
binding.cameraView.setBarcode(barcode)
|
||||
try {
|
||||
System.err.println("camoorah : onImageAvailable: $image width: ${image.width} height: ${image.height}")
|
||||
var firebaseImage =
|
||||
FirebaseVisionImage.fromMediaImage(image, getRotationCompensation(cameraId, mainActivity))
|
||||
barcodeDetector
|
||||
.detectInImage(firebaseImage)
|
||||
.addOnSuccessListener { results ->
|
||||
if (results.isNotEmpty()) {
|
||||
val barcode = results[0]
|
||||
val value = barcode.rawValue
|
||||
onScanSuccess(value!!)
|
||||
// TODO: highlight the 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,15 +23,12 @@ import cash.z.android.wallet.R
|
|||
import cash.z.android.wallet.databinding.FragmentSendBinding
|
||||
import cash.z.android.wallet.extention.*
|
||||
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.wallet.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
* Fragment for sending Zcash.
|
||||
|
@ -39,12 +36,12 @@ import kotlin.math.absoluteValue
|
|||
*/
|
||||
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 usd = R.string.usd_abbreviation.toAppString()
|
||||
|
||||
lateinit var sendPresenter: SendPresenter
|
||||
lateinit var binding: FragmentSendBinding
|
||||
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
@ -74,6 +71,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
|
|||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
mainActivity.setToolbarShown(true)
|
||||
sendPresenter = SendPresenter(this, mainActivity.synchronizer)
|
||||
}
|
||||
|
||||
|
@ -94,7 +92,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
|
|||
// SendView Implementation
|
||||
//
|
||||
|
||||
override fun submit() {
|
||||
override fun exit() {
|
||||
mainActivity.navController.navigate(R.id.nav_home_fragment)
|
||||
}
|
||||
|
||||
|
@ -131,6 +129,10 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
|
|||
binding.textZecValueAvailable.text = availableTextSpan
|
||||
}
|
||||
|
||||
override fun setSendEnabled(isEnabled: Boolean) {
|
||||
binding.buttonSendZec.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// ScanFragment.BarcodeCallback implemenation
|
||||
|
@ -139,7 +141,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
|
|||
override fun onBarcodeScanned(value: String) {
|
||||
exitScanMode()
|
||||
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.
|
||||
*/
|
||||
private fun init() {
|
||||
/* Presenter calls */
|
||||
|
||||
binding.imageSwapCurrency.setOnClickListener {
|
||||
sendPresenter.toggleCurrency()
|
||||
}
|
||||
/* Init - Text Input */
|
||||
|
||||
binding.textValueHeader.apply {
|
||||
afterTextChanged {
|
||||
sendPresenter.headerUpdating(it)
|
||||
}
|
||||
setSelectAllOnFocus(true)
|
||||
afterTextChanged { if (it.isNotEmpty()) sendPresenter.inputHeaderUpdating(it) }
|
||||
doOnDoneOrFocusLost { sendPresenter.inputHeaderUpdated(it) }
|
||||
}
|
||||
|
||||
binding.buttonSendZec.setOnClickListener {
|
||||
sendPresenter.sendPressed()
|
||||
binding.inputZcashAddress.apply {
|
||||
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) */
|
||||
|
||||
binding.textAreaMemo.afterTextChanged {
|
||||
binding.textMemoCharCount.text =
|
||||
"${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
|
||||
}
|
||||
|
||||
binding.imageScanQr.apply {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
binding.dialogSendBackground.setOnClickListener {
|
||||
hideSendDialog()
|
||||
}
|
||||
binding.dialogSubmitButton.setOnClickListener {
|
||||
onSendZec()
|
||||
}
|
||||
|
||||
binding.dialogSendBackground.setOnClickListener { hideSendDialog() }
|
||||
binding.dialogSubmitButton.setOnClickListener { onSendZec() }
|
||||
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)
|
||||
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) {
|
||||
view.context.alert(R.string.send_alert_shortcut_clicked) {
|
||||
binding.inputZcashAddress.setText(SampleProperties.wallet.defaultSendAddress)
|
||||
validateAddressInput()
|
||||
val address = SampleProperties.wallet.defaultSendAddress
|
||||
binding.inputZcashAddress.setText(address)
|
||||
sendPresenter.inputAddressUpdated(address)
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after confirmation dialog is affirmed. Begins the process of actually sending ZEC.
|
||||
*/
|
||||
private fun onSendZec() {
|
||||
setSendEnabled(false)
|
||||
sendPresenter.sendFunds()
|
||||
|
@ -271,6 +282,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
|
|||
private fun hideKeyboard() {
|
||||
mainActivity.getSystemService<InputMethodManager>()
|
||||
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
checkAllInput()
|
||||
}
|
||||
|
||||
private fun hideSendDialog() {
|
||||
|
@ -278,12 +290,28 @@ class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.Barcod
|
|||
binding.groupDialogSend.visibility = View.GONE
|
||||
}
|
||||
|
||||
// note: be careful calling this with `true` that should only happen when all conditions have been validated
|
||||
private fun setSendEnabled(isEnabled: Boolean) {
|
||||
binding.buttonSendZec.isEnabled = isEnabled
|
||||
private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
|
||||
DrawableCompat.setTint(
|
||||
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) {
|
||||
setAddressLineColor()
|
||||
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) {
|
||||
DrawableCompat.setTint(
|
||||
binding.inputZcashAddress.background,
|
||||
ContextCompat.getColor(mainActivity, colorRes)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setAmountError(isError: Boolean) {
|
||||
val color = if (isError) R.color.zcashRed else R.color.text_dark
|
||||
binding.textAmountBackground.setTextColor(color.toAppColor())
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 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")
|
||||
override fun setMemoError(message: String?) {
|
||||
val validColor = R.color.zcashBlack_12.toAppColor()
|
||||
val errorColor = R.color.zcashRed.toAppColor()
|
||||
if (message == null) {
|
||||
binding.dividerMemo.setBackgroundColor(validColor)
|
||||
binding.textMemoCharCount.setTextColor(validColor)
|
||||
binding.textAreaMemo.setTextColor(R.color.text_dark.toAppColor())
|
||||
} else {
|
||||
binding.dividerMemo.setBackgroundColor(errorColor)
|
||||
binding.textMemoCharCount.setTextColor(errorColor)
|
||||
binding.textAreaMemo.setTextColor(errorColor)
|
||||
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
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
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.ui.presenter.Presenter.PresenterView
|
||||
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.ext.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
@ -24,38 +27,57 @@ class SendPresenter(
|
|||
fun setHeaderValue(usdString: String)
|
||||
fun setSubheaderValue(usdString: String, isUsdSelected: Boolean)
|
||||
fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean)
|
||||
fun validateUserInput(): Boolean
|
||||
fun submit()
|
||||
fun exit()
|
||||
|
||||
// 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 requiresValidation = true
|
||||
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
|
||||
//
|
||||
|
||||
override suspend fun start() {
|
||||
Log.e("@TWIG-v", "sendPresenter starting!")
|
||||
// set the currency to zec and update the view, intializing everything to zero
|
||||
toggleCurrency()
|
||||
Twig.sprout("SendPresenter")
|
||||
twig("sendPresenter starting!")
|
||||
// set the currency to zec and update the view, initializing everything to zero
|
||||
inputToggleCurrency()
|
||||
with(view) {
|
||||
balanceJob = launchBalanceBinder(synchronizer.balance())
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
Log.e("@TWIG-v", "sendPresenter stopping!")
|
||||
twig("sendPresenter stopping!")
|
||||
Twig.clip("SendPresenter")
|
||||
balanceJob?.cancel()?.also { balanceJob = null }
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Long>) = launch {
|
||||
Log.e("@TWIG-v", "send balance binder starting!")
|
||||
twig("send balance binder starting!")
|
||||
for (new in channel) {
|
||||
Log.e("@TWIG-v", "send polled a balance item")
|
||||
twig("send polled a balance item")
|
||||
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
|
||||
// also, we need to handle cancellations. So yeah, definitely do this differently
|
||||
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
|
||||
*/
|
||||
fun toggleCurrency() {
|
||||
view.validateUserInput()
|
||||
fun inputToggleCurrency() {
|
||||
// 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)
|
||||
with(sendUiModel) {
|
||||
view.setHeaders(
|
||||
isUsdSelected = isUsdSelected,
|
||||
headerString = if (isUsdSelected) usdValue.toUsdString() else zecValue.convertZatoshiToZecString(),
|
||||
subheaderString = if (isUsdSelected) zecValue.convertZatoshiToZecString() else usdValue.toUsdString()
|
||||
headerString = if (isUsdSelected) usdValue.toUsdString() else zatoshiValue.convertZatoshiToZecString(),
|
||||
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.
|
||||
* 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 ->
|
||||
val subheaderValue = headerValueAsDecimal.convertCurrency(SampleProperties.USD_PER_ZEC, sendUiModel.isUsdSelected)
|
||||
|
||||
|
@ -102,49 +131,77 @@ class SendPresenter(
|
|||
}
|
||||
}
|
||||
|
||||
fun sendPressed() {
|
||||
with(sendUiModel) {
|
||||
view.showSendDialog(
|
||||
zecString = zecValue.convertZatoshiToZecString(),
|
||||
usdString = usdValue.toUsdString(),
|
||||
toAddress = toAddress,
|
||||
hasMemo = !memo.isBlank()
|
||||
)
|
||||
/**
|
||||
* As the user updates the address, update the error that gets displayed in real-time
|
||||
*
|
||||
* @param addressValue the address that the user has typed, so far
|
||||
*/
|
||||
fun inputAddressUpdating(addressValue: String) {
|
||||
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) {
|
||||
if (isUsdSelected) {
|
||||
// amount represents USD
|
||||
val headerString = amount.toUsdString()
|
||||
val usdValue = amount
|
||||
val zecValue = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC)
|
||||
val subheaderString = zecValue.toZecString()
|
||||
sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue)
|
||||
val zatoshiValue = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC).convertZecToZatoshi()
|
||||
val subheaderString = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC).toUsdString()
|
||||
updateModel(sendUiModel.copy(zatoshiValue = zatoshiValue, usdValue = amount))
|
||||
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
|
||||
} else {
|
||||
// amount represents ZEC
|
||||
val headerString = amount.toZecString()
|
||||
val zecValue = amount
|
||||
val usdValue = amount.convertZecToUsd(SampleProperties.USD_PER_ZEC)
|
||||
val subheaderString = usdValue.toUsdString()
|
||||
sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue)
|
||||
println("calling setHeaders with $headerString $subheaderString")
|
||||
updateModel(sendUiModel.copy(zatoshiValue = amount.convertZecToZatoshi(), usdValue = usdValue))
|
||||
twig("calling setHeaders with $headerString $subheaderString")
|
||||
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addressValidated(address: String) {
|
||||
sendUiModel = sendUiModel.copy(toAddress = address)
|
||||
fun inputAddressUpdated(newAddress: String) {
|
||||
if (!validateAddress(newAddress)) return
|
||||
updateModel(sendUiModel.copy(toAddress = newAddress))
|
||||
}
|
||||
|
||||
/**
|
||||
* After the user has typed a memo, validated by the UI, then update the model.
|
||||
*
|
||||
* assert: this method is only called after the memo input has been validated by the UI
|
||||
*/
|
||||
fun memoValidated(sanitizedValue: String) {
|
||||
sendUiModel = sendUiModel.copy(memo = sanitizedValue)
|
||||
fun inputMemoUpdated(newMemo: String) {
|
||||
if (!validateMemo(newMemo)) return
|
||||
updateModel(sendUiModel.copy(memo = newMemo))
|
||||
}
|
||||
|
||||
fun inputSendPressed() {
|
||||
if (requiresValidation && !view.checkAllInput()) return
|
||||
|
||||
with(sendUiModel) {
|
||||
view.showSendDialog(
|
||||
zecString = zatoshiValue.convertZatoshiToZecString(),
|
||||
usdString = usdValue.toUsdString(),
|
||||
toAddress = toAddress,
|
||||
hasMemo = !memo.isBlank()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
var hasBeenUpdated: Boolean = false,
|
||||
val isUsdSelected: Boolean = true,
|
||||
val zecValue: Long? = null,
|
||||
val zatoshiValue: Long? = null,
|
||||
val usdValue: BigDecimal = BigDecimal.ZERO,
|
||||
val toAddress: String = "",
|
||||
val memo: String = ""
|
||||
|
|
|
@ -21,17 +21,18 @@
|
|||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_end="32dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logo_welcome"
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie_welcome_firstrun"
|
||||
android:layout_width="168dp"
|
||||
android:layout_height="168dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.497"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
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
|
||||
android:id="@+id/text_header"
|
||||
|
@ -44,7 +45,7 @@
|
|||
android:textSize="@dimen/text_size_h5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/logo_welcome" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/lottie_welcome_firstrun" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_content"
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
android:background="@color/fragment_receive_background"
|
||||
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 -->
|
||||
<ImageView
|
||||
android:id="@+id/image_shield"
|
||||
|
@ -23,7 +29,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="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_constraintVertical_bias="0.062"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
|
|
|
@ -17,6 +17,11 @@
|
|||
android:layout_height="match_parent"
|
||||
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 -->
|
||||
<!-- -->
|
||||
|
@ -50,7 +55,7 @@
|
|||
android:layout_height="48dp"
|
||||
android:background="@color/zcashWhite"
|
||||
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" />
|
||||
|
||||
<TextView
|
||||
|
@ -78,6 +83,19 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/background_header"
|
||||
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
|
||||
android:id="@+id/button_cancel_scan"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -85,6 +103,7 @@
|
|||
android:layout_marginTop="4dp"
|
||||
android:background="@null"
|
||||
android:backgroundTint="@null"
|
||||
android:elevation="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/cancel"
|
||||
|
@ -118,14 +137,16 @@
|
|||
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
|
||||
app:srcCompat="@drawable/ic_import_export_black" />
|
||||
|
||||
<!-- Input: Header -->
|
||||
<EditText
|
||||
android:id="@+id/text_value_header"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="numberDecimal"
|
||||
android:minWidth="12dp"
|
||||
android:maxLength="8"
|
||||
android:minWidth="12dp"
|
||||
android:text="0"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="@dimen/text_size_h3"
|
||||
|
@ -163,50 +184,51 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:tint="@color/text_dark"
|
||||
tools:visibility="invisible"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toStartOf="@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
|
||||
android:id="@+id/text_dollar_symbol_header"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:text="$"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="18dp"
|
||||
android:includeFontPadding="false"
|
||||
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_constraintEnd_toStartOf="@id/text_value_header"
|
||||
tools:visibility="invisible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dollar_symbol_subheader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_marginRight="2dp"
|
||||
android:includeFontPadding="false"
|
||||
android:text="$"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/text_value_subheader"
|
||||
app:layout_constraintTop_toTopOf="@id/image_zec_symbol_subheader" />
|
||||
|
||||
<!-- Address -->
|
||||
<!-- Input: Address -->
|
||||
<EditText
|
||||
android:id="@+id/input_zcash_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/send_hint_input_zcash_address"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:paddingRight="76dp"
|
||||
android:singleLine="true"
|
||||
android:paddingTop="0dp"
|
||||
app:backgroundTint="@color/zcashBlack_12"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_area_memo"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/transition_active_transaction_bg" />
|
||||
|
||||
|
@ -217,17 +239,22 @@
|
|||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:fontFamily="@font/inconsolata"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/zcashRed"
|
||||
android:textSize="@dimen/text_size_caption"
|
||||
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 -->
|
||||
<ImageView
|
||||
android:id="@+id/image_scan_qr"
|
||||
android:layout_width="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"
|
||||
app:layout_constraintBottom_toBottomOf="@id/input_zcash_address"
|
||||
app:layout_constraintEnd_toEndOf="@id/input_zcash_address"
|
||||
|
@ -238,7 +265,10 @@
|
|||
android:id="@+id/image_address_shortcut"
|
||||
android:layout_width="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_constraintEnd_toStartOf="@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_constraintStart_toStartOf="@id/input_zcash_address" />
|
||||
|
||||
<!-- memo -->
|
||||
<!-- Input: Memo -->
|
||||
<EditText
|
||||
android:id="@+id/text_area_memo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/background_rounded_corners"
|
||||
android:gravity="top|left"
|
||||
android:inputType="textMultiLine"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textAutoComplete"
|
||||
android:maxLength="@integer/memo_max_length"
|
||||
android:padding="16dp"
|
||||
app:backgroundTint="@color/zcashWhite_60"
|
||||
|
@ -318,11 +349,11 @@
|
|||
android:id="@+id/camera_placeholder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
tools:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@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_constraintTop_toTopOf="@id/transition_active_transaction_bg" />
|
||||
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Dialog -->
|
||||
|
|
|
@ -22,17 +22,18 @@
|
|||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_end="32dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logo_welcome"
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie_welcome_sync"
|
||||
android:layout_width="168dp"
|
||||
android:layout_height="168dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.497"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
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
|
||||
android:id="@+id/text_header"
|
||||
|
@ -45,7 +46,7 @@
|
|||
android:textSize="@dimen/text_size_h5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/logo_welcome" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/lottie_welcome_sync" />
|
||||
|
||||
<TextView
|
||||
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
|
@ -3,5 +3,4 @@
|
|||
<integer name="nav_default_slow_transition_duration">400</integer>
|
||||
<integer name="nav_default_quick_transition_duration">100</integer>
|
||||
<integer name="memo_max_length">512</integer>
|
||||
<integer name="z_address_min_length">78</integer>
|
||||
</resources>
|
|
@ -79,6 +79,7 @@
|
|||
<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_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>
|
||||
|
||||
<!-- About -->
|
||||
|
|
|
@ -15,8 +15,8 @@ import javax.inject.Singleton
|
|||
@Module
|
||||
internal object SynchronizerModule {
|
||||
|
||||
// const val MOCK_LOAD_DURATION = 3_000L
|
||||
const val MOCK_LOAD_DURATION = 30_000L
|
||||
const val MOCK_LOAD_DURATION = 3_000L
|
||||
// const val MOCK_LOAD_DURATION = 30_000L
|
||||
const val MOCK_TX_INTERVAL = 20_000L
|
||||
const val MOCK_ACTIVE_TX_STATE_CHANGE_INTERVAL = 5_000L
|
||||
|
||||
|
|
|
@ -10,11 +10,8 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mock
|
||||
import org.mockito.junit.jupiter.MockitoExtension
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@ExtendWith(MockitoExtension::class)
|
||||
internal class SendPresenterTest {
|
||||
|
@ -74,12 +71,12 @@ internal class SendPresenterTest {
|
|||
presenter.toggleCurrency()
|
||||
assertTrue(!presenter.sendUiModel.isUsdSelected, "zec should be selected to avoid testing conversions")
|
||||
presenter.headerValidated("1.1234535".safelyConvertToBigDecimal()!!)
|
||||
assertEquals(112345350, presenter.sendUiModel.zecValue)
|
||||
assertEquals("1.123454", presenter.sendUiModel.zecValue.convertZatoshiToZecString(), "5 is odd, we should round up")
|
||||
assertEquals(112345350, presenter.sendUiModel.zatoshiValue)
|
||||
assertEquals("1.123454", presenter.sendUiModel.zatoshiValue.convertZatoshiToZecString(), "5 is odd, we should round up")
|
||||
|
||||
presenter.headerValidated("1.1234565".safelyConvertToBigDecimal()!!)
|
||||
assertEquals(112345650, presenter.sendUiModel.zecValue)
|
||||
assertEquals("1.123456", presenter.sendUiModel.zecValue.convertZatoshiToZecString(), "6 is even, we should round down")
|
||||
assertEquals(112345650, presenter.sendUiModel.zatoshiValue)
|
||||
assertEquals("1.123456", presenter.sendUiModel.zatoshiValue.convertZatoshiToZecString(), "6 is even, we should round down")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -22,17 +22,18 @@
|
|||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_end="32dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logo_welcome"
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie_welcome_sync"
|
||||
android:layout_width="168dp"
|
||||
android:layout_height="168dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.497"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
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
|
||||
android:id="@+id/text_header"
|
||||
|
@ -45,7 +46,7 @@
|
|||
android:textSize="@dimen/text_size_h5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/logo_welcome" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/lottie_welcome_sync" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_content"
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="z_address_min_length">88</integer>
|
||||
</resources>
|
Loading…
Reference in New Issue