2018-11-12 10:38:37 -08:00
package cash.z.android.wallet.ui.fragment
2019-02-16 00:47:39 -08:00
import android.annotation.SuppressLint
2019-01-15 07:49:43 -08:00
import android.graphics.Typeface
2018-11-12 10:38:37 -08:00
import android.os.Bundle
2019-01-15 07:49:43 -08:00
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
2018-11-12 10:38:37 -08:00
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
2019-02-07 19:18:32 -08:00
import android.view.inputmethod.InputMethodManager
2019-02-16 00:47:39 -08:00
import androidx.annotation.ColorRes
import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
2019-02-07 19:18:32 -08:00
import androidx.core.content.getSystemService
2019-02-16 00:47:39 -08:00
import androidx.core.graphics.drawable.DrawableCompat
2019-01-15 07:49:43 -08:00
import androidx.core.text.toSpannable
2019-02-01 19:35:45 -08:00
import androidx.databinding.DataBindingUtil
2019-02-17 21:05:40 -08:00
import androidx.fragment.app.Fragment
2019-02-16 00:47:39 -08:00
import cash.z.android.wallet.BuildConfig
2018-11-12 10:38:37 -08:00
import cash.z.android.wallet.R
2019-02-01 19:35:45 -08:00
import cash.z.android.wallet.databinding.FragmentSendBinding
2019-02-16 00:47:39 -08:00
import cash.z.android.wallet.extention.*
2019-02-14 17:26:56 -08:00
import cash.z.android.wallet.sample.SampleProperties
2018-11-13 21:16:53 -08:00
import cash.z.android.wallet.ui.activity.MainActivity
2019-01-31 12:44:25 -08:00
import cash.z.android.wallet.ui.presenter.SendPresenter
2019-02-17 21:05:40 -08:00
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
2019-01-31 12:44:25 -08:00
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
2019-01-14 00:09:02 -08:00
import java.text.DecimalFormat
2019-02-16 00:47:39 -08:00
import kotlin.math.absoluteValue
2018-11-12 10:38:37 -08:00
/ * *
2018-12-10 08:38:03 -08:00
* Fragment for sending Zcash .
2018-11-12 10:38:37 -08:00
*
* /
2019-02-17 21:05:40 -08:00
class SendFragment : BaseFragment ( ) , SendPresenter . SendView , ScanFragment . BarcodeCallback {
2018-11-12 10:38:37 -08:00
2019-01-31 12:44:25 -08:00
lateinit var sendPresenter : SendPresenter
2019-02-01 19:35:45 -08:00
lateinit var binding : FragmentSendBinding
2019-01-31 12:44:25 -08:00
2019-02-16 00:47:39 -08:00
private val zec = R . string . zec _abbreviation . toAppString ( )
private val usd = R . string . usd _abbreviation . toAppString ( )
2019-01-14 00:09:02 -08:00
2019-02-17 21:05:40 -08:00
//
// Lifecycle
//
2018-11-12 10:38:37 -08:00
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ? ,
savedInstanceState : Bundle ?
) : View ? {
2019-02-01 19:35:45 -08:00
return DataBindingUtil . inflate < FragmentSendBinding > (
inflater , R . layout . fragment _send , container , false
) . let {
binding = it
it . root
}
2018-11-12 10:38:37 -08:00
}
2019-02-17 21:05:40 -08:00
override fun onAttachFragment ( childFragment : Fragment ? ) {
super . onAttachFragment ( childFragment )
( childFragment as ? ScanFragment ) ?. barcodeCallback = this
}
2018-11-13 21:16:53 -08:00
override fun onViewCreated ( view : View , savedInstanceState : Bundle ? ) {
super . onViewCreated ( view , savedInstanceState )
2019-02-16 00:47:39 -08:00
init ( )
2019-01-14 00:09:02 -08:00
}
2019-02-17 21:05:40 -08:00
override fun onActivityCreated ( savedInstanceState : Bundle ? ) {
super . onActivityCreated ( savedInstanceState )
sendPresenter = SendPresenter ( this , mainActivity . synchronizer )
}
override fun onResume ( ) {
super . onResume ( )
launch {
sendPresenter . start ( )
}
}
override fun onPause ( ) {
super . onPause ( )
sendPresenter . stop ( )
}
//
// SendView Implementation
//
override fun submit ( ) {
mainActivity . navController . navigate ( R . id . nav _home _fragment )
}
override fun setHeaders ( isUsdSelected : Boolean , headerString : String , subheaderString : String ) {
showCurrencySymbols ( isUsdSelected )
setHeaderValue ( headerString )
setSubheaderValue ( subheaderString , isUsdSelected )
}
override fun setHeaderValue ( value : String ) {
binding . textValueHeader . setText ( value )
}
@SuppressLint ( " SetTextI18n " ) // SetTextI18n lint logic has errors and does not recognize that the entire string contains variables, formatted per locale and loaded from string resources.
override fun setSubheaderValue ( value : String , isUsdSelected : Boolean ) {
val subheaderLabel = if ( isUsdSelected ) zec else usd
binding . textValueSubheader . text = " $value $subheaderLabel " //ignore SetTextI18n error here because it is invalid
}
override fun showSendDialog ( zecString : String , usdString : String , toAddress : String , hasMemo : Boolean ) {
hideKeyboard ( )
setSendEnabled ( false ) // partially because we need to lower the button elevation
binding . dialogTextTitle . text = getString ( R . string . send _dialog _title , zecString , zec , usdString )
binding . dialogTextAddress . text = toAddress
binding . dialogTextMemoIncluded . visibility = if ( hasMemo ) View . VISIBLE else View . GONE
binding . groupDialogSend . visibility = View . VISIBLE
}
2019-02-19 05:22:45 -08:00
override fun updateBalance ( new : Long ) {
2019-02-17 21:05:40 -08:00
// TODO: use a formatted string resource here
val availableTextSpan = " ${new.convertZatoshiToZecString(8)} $zec Available " . toSpannable ( )
availableTextSpan . setSpan ( ForegroundColorSpan ( R . color . colorPrimary . toAppColor ( ) ) , availableTextSpan . length - " Available " . length , availableTextSpan . length , Spanned . SPAN _EXCLUSIVE _EXCLUSIVE )
availableTextSpan . setSpan ( StyleSpan ( Typeface . BOLD ) , 0 , 6 , Spanned . SPAN _EXCLUSIVE _EXCLUSIVE )
binding . textZecValueAvailable . text = availableTextSpan
}
//
// ScanFragment.BarcodeCallback implemenation
//
override fun onBarcodeScanned ( value : String ) {
exitScanMode ( )
binding . inputZcashAddress . setText ( value )
validateAddressInput ( )
}
//
// Internal View Logic
//
/ * *
* Initialize view logic only . Click listeners , text change handlers and tooltips .
* /
2019-02-16 00:47:39 -08:00
private fun init ( ) {
2019-02-17 21:05:40 -08:00
/* Presenter calls */
2019-02-01 19:35:45 -08:00
binding . imageSwapCurrency . setOnClickListener {
2019-02-17 21:05:40 -08:00
sendPresenter . toggleCurrency ( )
2019-01-14 00:09:02 -08:00
}
2019-02-16 00:47:39 -08:00
binding . textValueHeader . apply {
afterTextChanged {
2019-02-17 21:05:40 -08:00
sendPresenter . headerUpdating ( it )
2019-01-14 00:09:02 -08:00
}
}
2019-02-17 21:05:40 -08:00
binding . buttonSendZec . setOnClickListener {
sendPresenter . sendPressed ( )
}
/* Non-Presenter calls (UI-only logic) */
2019-02-01 19:35:45 -08:00
binding . textAreaMemo . afterTextChanged {
2019-02-16 00:47:39 -08:00
binding . textMemoCharCount . text =
" ${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)} "
2019-01-14 00:09:02 -08:00
}
2019-02-17 21:05:40 -08:00
binding . imageScanQr . apply {
2019-02-16 00:47:39 -08:00
TooltipCompat . setTooltipText ( this , context . getString ( R . string . send _tooltip _scan _qr ) )
}
2019-02-17 21:05:40 -08:00
2019-02-16 00:47:39 -08:00
binding . imageAddressShortcut ?. apply {
if ( BuildConfig . DEBUG ) {
2019-02-17 21:05:40 -08:00
visibility = View . VISIBLE
2019-02-16 00:47:39 -08:00
TooltipCompat . setTooltipText ( this , context . getString ( R . string . send _tooltip _address _shortcut ) )
setOnClickListener ( :: onPasteShortcutAddress )
} else {
visibility = View . GONE
}
}
2019-02-01 19:35:45 -08:00
binding . dialogSendBackground . setOnClickListener {
hideSendDialog ( )
}
2019-02-01 22:42:23 -08:00
binding . dialogSubmitButton . setOnClickListener {
2019-02-17 21:05:40 -08:00
onSendZec ( )
2019-01-31 12:44:25 -08:00
}
2019-02-14 17:26:56 -08:00
2019-02-17 21:05:40 -08:00
binding . imageScanQr . setOnClickListener ( :: onScanQrCode )
2019-02-04 11:11:22 -08:00
2019-02-17 21:05:40 -08:00
// allow background taps to dismiss the keyboard and clear focus
binding . contentFragmentSend . setOnClickListener {
it ?. findFocus ( ) ?. clearFocus ( )
validateUserInput ( )
hideKeyboard ( )
2019-02-02 14:47:08 -08:00
}
2019-02-01 08:10:43 -08:00
2019-02-17 21:05:40 -08:00
binding . buttonSendZec . text = getString ( R . string . send _button _label , zec )
setSendEnabled ( false )
2019-01-31 12:44:25 -08:00
}
2019-02-17 21:05:40 -08:00
private fun showCurrencySymbols ( isUsdSelected : Boolean ) {
// visibility has some kind of bug that appears to be related to layout groups. So using alpha instead since our API level is high enough to support that
if ( isUsdSelected ) {
binding . textDollarSymbolHeader . alpha = 1.0f
binding . imageZecSymbolSubheader . alpha = 1.0f
binding . imageZecSymbolHeader . alpha = 0.0f
binding . textDollarSymbolSubheader . alpha = 0.0f
2019-02-16 00:47:39 -08:00
} else {
2019-02-17 21:05:40 -08:00
binding . imageZecSymbolHeader . alpha = 1.0f
binding . textDollarSymbolSubheader . alpha = 1.0f
binding . textDollarSymbolHeader . alpha = 0.0f
binding . imageZecSymbolSubheader . alpha = 0.0f
2019-02-16 00:47:39 -08:00
}
}
private fun onScanQrCode ( view : View ) {
hideKeyboard ( )
val fragment = ScanFragment ( )
val ft = childFragmentManager . beginTransaction ( )
. add ( R . id . camera _placeholder , fragment , " camera_fragment " )
2019-02-17 21:05:40 -08:00
. addToBackStack ( " camera_fragment_scanning " )
2019-02-16 00:47:39 -08:00
. commit ( )
2019-02-17 21:05:40 -08:00
binding . groupHiddenDuringScan . visibility = View . INVISIBLE
binding . buttonCancelScan . apply {
visibility = View . VISIBLE
animate ( ) . alpha ( 1.0f ) . apply {
duration = 3000L
}
setOnClickListener {
exitScanMode ( )
}
}
2019-02-16 00:47:39 -08:00
}
// 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.
private fun onPasteShortcutAddress ( view : View ) {
view . context . alert ( R . string . send _alert _shortcut _clicked ) {
binding . inputZcashAddress . setText ( SampleProperties . wallet . defaultSendAddress )
2019-02-17 21:05:40 -08:00
validateAddressInput ( )
2019-02-16 00:47:39 -08:00
hideKeyboard ( )
2019-01-14 00:09:02 -08:00
}
}
2018-11-12 10:38:37 -08:00
2019-01-14 00:09:02 -08:00
private fun onSendZec ( ) {
2019-01-31 12:44:25 -08:00
setSendEnabled ( false )
2019-02-17 21:05:40 -08:00
sendPresenter . sendFunds ( )
2019-01-14 00:09:02 -08:00
}
2019-01-31 12:44:25 -08:00
2019-02-17 21:05:40 -08:00
private fun exitScanMode ( ) {
val cameraFragment = childFragmentManager . findFragmentByTag ( " camera_fragment " )
if ( cameraFragment != null ) {
val ft = childFragmentManager . beginTransaction ( )
. remove ( cameraFragment )
. commit ( )
}
binding . buttonCancelScan . visibility = View . GONE
binding . groupHiddenDuringScan . visibility = View . VISIBLE
2019-02-01 19:35:45 -08:00
}
2019-02-16 00:47:39 -08:00
private fun hideKeyboard ( ) {
mainActivity . getSystemService < InputMethodManager > ( )
?. hideSoftInputFromWindow ( view ?. windowToken , InputMethodManager . HIDE _NOT _ALWAYS )
}
2019-02-01 19:35:45 -08:00
private fun hideSendDialog ( ) {
setSendEnabled ( true )
binding . groupDialogSend . visibility = View . GONE
}
2019-02-17 21:05:40 -08:00
// note: be careful calling this with `true` that should only happen when all conditions have been validated
2019-02-01 19:35:45 -08:00
private fun setSendEnabled ( isEnabled : Boolean ) {
binding . buttonSendZec . isEnabled = isEnabled
2019-02-17 21:05:40 -08:00
}
private fun setAddressError ( message : String ? ) {
if ( message == null ) {
setAddressLineColor ( )
binding . textAddressError . text = null
binding . textAddressError . visibility = View . GONE
2019-01-31 12:44:25 -08:00
} else {
2019-02-17 21:05:40 -08:00
setAddressLineColor ( R . color . zcashRed )
binding . textAddressError . text = message
binding . textAddressError . visibility = View . VISIBLE
setSendEnabled ( false )
2019-01-31 12:44:25 -08:00
}
}
2019-02-17 21:05:40 -08:00
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 " )
setSendEnabled ( false )
setAmountError ( true )
false
}
}
// TODO: come back to this test code later and fix the shared element transitions
//
// fun submitWithSharedElements() {
// var extras = with(binding) {
// listOf(dialogSendBackground, dialogSendContents, dialogTextTitle, dialogTextAddress)
// .map{ it to it.transitionName }
// .let { FragmentNavigatorExtras(*it.toTypedArray()) }
// }
// val extras = FragmentNavigatorExtras(
// binding.dialogSendContents to binding.dialogSendContents.transitionName,
// binding.dialogTextTitle to getString(R.string.transition_active_transaction_title),
// binding.dialogTextAddress to getString(R.string.transition_active_transaction_address),
// binding.dialogSendBackground to getString(R.string.transition_active_transaction_background)
// )
//
// mainActivity.navController.navigate(R.id.nav_home_fragment,
// null,
// null,
// extras)
// }
2019-01-31 12:44:25 -08:00
}
@Module
abstract class SendFragmentModule {
@ContributesAndroidInjector
abstract fun contributeSendFragment ( ) : SendFragment
2018-11-12 10:38:37 -08:00
}