Hush lite wallet for Android
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
10 KiB

package cash.z.ecc.android.ui.profile
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentAwesomeBinding
import cash.z.ecc.android.ext.distribute
import cash.z.ecc.android.ext.invisibleIf
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
override val screen = Report.Screen.AWESOME
private val viewModel: ProfileViewModel by viewModels()
private var lastBalance: WalletBalance? = null
private var initialized: Boolean = false
override fun inflate(inflater: LayoutInflater): FragmentAwesomeBinding =
FragmentAwesomeBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hitAreaExit.onClickNavBack() { tapped(AWESOME_CLOSE) }
binding.hitAreaAddress.setOnClickListener {
tapped(COPY_TRANSPARENT_ADDRESS)
onCopyTransparentAddress()
}
binding.buttonAction.setOnClickListener {
onShieldFundsAction()
}
binding.lottieShielding.visibility = View.GONE
setStatus("Checking balance...")
}
private fun onCopyTransparentAddress() {
resumedScope.launch {
mainActivity?.copyText(viewModel.getTransparentAddress(), "T-Address")
}
}
override fun onResume() {
super.onResume()
if (!initialized) {
resumedScope.launch {
onAddressLoaded(viewModel.getTransparentAddress())
updateBalance()
}
initialized = true
}
}
private fun setStatus(status: String) {
binding.textStatus.text = status
}
@SuppressLint("SetTextI18n")
private fun appendStatus(status: String) {
binding.textStatus.text = "${binding.textStatus.text}$status"
}
private suspend fun updateBalance() {
val utxoCount = viewModel.fetchUtxos()
viewModel.getTransparentBalance().let { balance ->
onBalanceUpdated(balance, utxoCount)
}
}
private fun onAddressLoaded(address: String) {
twig("t-address loaded: $address length: ${address.length}")
// qrecycler.load(address)
// .withQuietZoneSize(3)
// .withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
// .into(binding.receiveQrCode)
address.distribute(2) { i, part ->
setAddressPart(i, part)
}
}
private fun setAddressPart(index: Int, addressPart: String) {
twig("setting t-address for part $index) $addressPart")
val address = when (index) {
0 -> binding.textAddressPart1
1 -> binding.textAddressPart2
else -> throw IllegalArgumentException(
"Unexpected address index $index. Unable to split the t-addr into two parts." +
" Ensure that the address is valid."
)
}
val thinSpace = "\u2005" // 0.25 em space
val textSpan = SpannableString("${index + 1}$thinSpace$addressPart")
textSpan.setSpan(AddressPartNumberSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
address.text = textSpan
}
private fun onShieldFundsAction() {
if (binding.buttonAction.isActivated) {
tapped(AWESOME_SHIELD)
mainActivity?.let { main ->
main.authenticate(
"Shield transparent funds",
getString(R.string.biometric_backup_phrase_title)
) {
onShieldFunds()
}
}
} else {
Toast.makeText(requireContext(), "No balance to shield!", Toast.LENGTH_SHORT).show()
}
}
private fun onDoneAction() {
viewModel.setEasterEggTriggered()
mainActivity?.safeNavigate(R.id.action_nav_awesome_to_nav_history)
}
private fun onShieldFunds() {
twig("onShieldFunds")
lifecycleScope.launchWhenResumed {
twig("launching shield funds job")
viewModel.shieldFunds().onEach {
onPendingTxUpdated(it)
}.launchIn(lifecycleScope)
}
}
private fun onPendingTxUpdated(tx: PendingTransaction) {
twig("shielding transaction updated: $tx")
if (tx == null) return // TODO: maybe log this
try {
tx.toUiModel().let { model ->
binding.apply {
lottieShielding.invisibleIf(!model.showProgress)
buttonAction.isActivated = !model.showProgress || model.canCancel
buttonAction.isEnabled = true
buttonAction.refreshDrawableState()
setStatus(model.status)
appendStatus(model.details.joinToString("\n", "\n\n"))
buttonAction.apply {
text = model.primaryButtonText
setOnClickListener { model.primaryAction() }
}
}
if (model.updateBalance) {
resumedScope.launch {
delay(1000L)
updateBalance()
}
}
}
} catch (t: Throwable) {
val message = "ERROR: error while handling pending transaction update! $t"
twig(message)
mainActivity?.feedback?.report(Report.Error.NonFatal.TxUpdateFailed(t))
mainActivity?.feedback?.report(t)
}
}
private fun onShieldComplete(isSuccess: Boolean) {
binding.lottieShielding.visibility = View.GONE
if (isSuccess) {
Toast.makeText(mainActivity, "Funds shielded successfully!", Toast.LENGTH_SHORT).show()
binding.buttonAction.isEnabled = true
binding.buttonAction.isActivated = true
binding.buttonAction.text = "See Details"
binding.textStatus.text = "Success!\n\nIt may take a while to show up."
binding.buttonAction.setOnClickListener {
mainActivity?.popBackTo(R.id.nav_home)
}
} else {
Toast.makeText(mainActivity, "Failed to shield funds :(", Toast.LENGTH_SHORT).show()
binding.buttonAction.isEnabled = true
binding.buttonAction.text = "Shield Transparent Funds"
binding.textStatus.text = "Failed!"
binding.buttonAction.visibility = View.GONE
}
}
private fun onBalanceUpdated(
balance: WalletBalance = WalletBalance(Zatoshi(0), Zatoshi(0)),
utxoCount: Int = 0
) {
lastBalance = balance
twig("TRANSPARENT BALANCE: ${balance.available} / ${balance.total}")
binding.textStatus.text = if (balance.available.value > 0L) {
binding.buttonAction.isActivated = true
binding.buttonAction.isEnabled = true
"Balance: ᙇ${balance.available.convertZatoshiToZecString(8)}"
} else {
binding.buttonAction.isActivated = false
binding.buttonAction.isEnabled = true
"No available balance found"
}
if (utxoCount > 0) {
appendStatus("\n\nDownloaded $utxoCount ")
appendStatus(if (utxoCount == 1) "transaction!" else "transactions!")
}
balance.pending.takeIf { it.value > 0 }?.let {
appendStatus("\n\n(ᙇ${it.convertZatoshiToZecString()} pending confirmation)")
}
}
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
model.status = "Shielding Cancelled!"
model.updateBalance = true
model.primaryAction = { onShieldFundsAction() }
model.details.add("Cancelled!")
}
isSubmitSuccess() -> {
model.status = "Shielding Success!"
model.primaryButtonText = "Done"
model.primaryAction = { onDoneAction() }
}
isFailure() -> {
model.status = if (isFailedEncoding()) {
"${getString(R.string.send_final_error_encoding)}\n\nPlease note:\nShielding requires funds\nto have 10 confirmations."
} else {
"${getString(R.string.send_final_error_submitting)}\n\n${this.errorMessage}"
}
model.primaryAction = { onShieldFundsAction() }
}
else -> {
model.status = "Shielding ᙇ${lastBalance?.available.convertZatoshiToZecString()}\n\nPlease do not exit this screen!"
model.showProgress = true
if (isCreating()) {
model.canCancel = true
model.details.add("Creating transaction...")
model.primaryButtonText = getString(R.string.send_final_button_primary_cancel)
model.primaryAction = { onCancel(this) }
} else {
model.primaryButtonText = "Shielding Funds..."
if (isCreated()) model.details.add("Submitting transaction...")
}
}
}
}
private fun onCancel(tx: PendingTransaction) {
resumedScope.launch {
viewModel.cancel(tx.id)
}
}
// fields are ordered, as they appear, top-to-bottom in the UI because that makes it easier to reason about each screen state
data class UiModel(
var status: String = "",
val details: MutableSet<String> = linkedSetOf(),
var showProgress: Boolean = false,
var primaryButtonText: String = "Shield Transparent Funds",
var primaryAction: () -> Unit = {},
var canCancel: Boolean = false,
var updateBalance: Boolean = false,
)
}