forked from fekt/hush-android-wallet
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.
236 lines
9.8 KiB
236 lines
9.8 KiB
package cash.z.ecc.android.ui.send
|
|
|
|
import android.content.Context
|
|
import android.os.Bundle
|
|
import android.text.format.DateUtils
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.widget.Toast
|
|
import androidx.core.view.isVisible
|
|
import androidx.fragment.app.viewModels
|
|
import androidx.lifecycle.lifecycleScope
|
|
import cash.z.ecc.android.R
|
|
import cash.z.ecc.android.databinding.FragmentAutoShieldBinding
|
|
import cash.z.ecc.android.ext.goneIf
|
|
import cash.z.ecc.android.ext.invisibleIf
|
|
import cash.z.ecc.android.ext.requireApplicationContext
|
|
import cash.z.ecc.android.feedback.Report
|
|
import cash.z.ecc.android.preference.Preferences
|
|
import cash.z.ecc.android.preference.model.get
|
|
import cash.z.ecc.android.preference.model.put
|
|
import cash.z.ecc.android.sdk.db.entity.*
|
|
import cash.z.ecc.android.sdk.ext.collectWith
|
|
import cash.z.ecc.android.ui.base.BaseFragment
|
|
import cash.z.ecc.android.util.twig
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.launchIn
|
|
import kotlinx.coroutines.flow.onEach
|
|
import java.time.Clock
|
|
|
|
class AutoShieldFragment : BaseFragment<FragmentAutoShieldBinding>() {
|
|
override val screen = Report.Screen.AUTO_SHIELD_FINAL
|
|
|
|
private val viewModel: AutoShieldViewModel by viewModels()
|
|
|
|
private val uiModels = MutableStateFlow(UiModel())
|
|
|
|
override fun inflate(inflater: LayoutInflater): FragmentAutoShieldBinding =
|
|
FragmentAutoShieldBinding.inflate(inflater)
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
if (null == savedInstanceState) {
|
|
setAutoshield(requireApplicationContext())
|
|
}
|
|
}
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
super.onViewCreated(view, savedInstanceState)
|
|
binding.backButtonHitArea.setOnClickListener {
|
|
onExit().also { tapped(Report.Tap.AUTO_SHIELD_FINAL_CLOSE) }
|
|
}
|
|
mainActivity?.preventBackPress(this)
|
|
uiModels.collectWith(lifecycleScope, ::updateUi)
|
|
}
|
|
|
|
override fun onAttach(context: Context) {
|
|
super.onAttach(context)
|
|
mainActivity?.apply {
|
|
viewModel.shieldFunds().onEach { p: PendingTransaction ->
|
|
try {
|
|
uiModels.value = p.toUiModel()
|
|
} 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)
|
|
}
|
|
}.launchIn(lifecycleScope)
|
|
}
|
|
}
|
|
|
|
private fun updateUi(uiModel: UiModel) = uiModel.apply {
|
|
if (isResumed) {
|
|
// if this is the first success
|
|
if (!binding.lottieSuccess.isVisible && showSuccess) {
|
|
mainActivity?.vibrateSuccess()
|
|
}
|
|
|
|
binding.backButton.goneIf(!showCloseIcon)
|
|
binding.textTitle.text = title
|
|
binding.lottieShielding.invisibleIf(!showShielding)
|
|
if (pauseShielding) binding.lottieShielding.pauseAnimation()
|
|
binding.lottieSuccess.invisibleIf(!showSuccess)
|
|
binding.imageFailed.invisibleIf(!isFailure)
|
|
binding.textStatus.text = statusMessage
|
|
|
|
binding.textStatus.text = when {
|
|
showStatusDetails && showStatusMessage -> statusDetails
|
|
showStatusDetails -> statusDetails
|
|
showStatusMessage -> statusMessage
|
|
else -> ""
|
|
}
|
|
|
|
binding.buttonPrimary.text = primaryButtonText
|
|
binding.buttonPrimary.setOnClickListener { primaryAction() }
|
|
binding.buttonMoreInfo.text = moreInfoButtonText
|
|
binding.buttonMoreInfo.goneIf(!showMoreInfoButton)
|
|
binding.buttonMoreInfo.setOnClickListener { moreInfoAction() }
|
|
|
|
if (showSuccess) {
|
|
if (viewModel.updateAutoshieldAchievement()) {
|
|
mainActivity?.showSnackbar("Achievement unlocked! Golden Zebra.", "View") {
|
|
mainActivity?.safeNavigate(R.id.action_nav_shield_final_to_profile)
|
|
Toast.makeText(mainActivity, "Your Zebra is now yellow because you are great", Toast.LENGTH_LONG).show()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun onExit() {
|
|
mainActivity?.safeNavigate(R.id.action_nav_shield_final_to_nav_home)
|
|
}
|
|
|
|
private fun onCancel(tx: PendingTransaction) {
|
|
viewModel.cancel(tx.id)
|
|
}
|
|
|
|
private fun onSeeDetails() {
|
|
mainActivity?.safeNavigate(R.id.action_nav_shield_final_to_nav_history)
|
|
}
|
|
|
|
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
|
|
when {
|
|
isCancelled() -> {
|
|
model.title = getString(R.string.send_final_result_cancelled)
|
|
model.pauseShielding = true
|
|
model.primaryButtonText = getString(R.string.send_final_button_primary_back)
|
|
model.primaryAction = { mainActivity?.navController?.popBackStack() }
|
|
}
|
|
isSubmitSuccess() -> {
|
|
model.showCloseIcon = true
|
|
model.title = getString(R.string.send_final_button_primary_sent)
|
|
model.showShielding = false
|
|
model.showSuccess = true
|
|
model.primaryButtonText = getString(R.string.done)
|
|
model.primaryAction = ::onExit
|
|
model.showMoreInfoButton = true
|
|
model.moreInfoButtonText = getString(R.string.send_final_button_primary_details)
|
|
model.moreInfoAction = ::onSeeDetails
|
|
}
|
|
isFailure() -> {
|
|
model.showCloseIcon = true
|
|
model.title =
|
|
if (isFailedEncoding()) getString(R.string.send_final_error_encoding) else getString(
|
|
R.string.send_final_error_submitting
|
|
)
|
|
model.showShielding = false
|
|
model.showSuccess = false
|
|
model.isFailure = true
|
|
model.showStatusDetails = false
|
|
model.primaryButtonText = getString(R.string.translated_button_back)
|
|
model.primaryAction = { mainActivity?.navController?.popBackStack() }
|
|
model.showMoreInfoButton = errorMessage != null
|
|
model.moreInfoButtonText = getString(R.string.send_more_info)
|
|
model.moreInfoAction = {
|
|
showMoreInfo(errorMessage ?: "No details available")
|
|
}
|
|
}
|
|
isCreating() -> {
|
|
model.showCloseIcon = false
|
|
model.primaryButtonText = getString(R.string.send_final_button_primary_cancel)
|
|
model.showStatusMessage = true
|
|
model.statusMessage = "Creating transaction..."
|
|
model.primaryAction = { onCancel(this) }
|
|
}
|
|
isCreated() -> {
|
|
model.showStatusMessage = true
|
|
model.statusMessage = "Submitting transaction..."
|
|
model.primaryButtonText = getString(R.string.translated_button_back)
|
|
model.primaryAction = { mainActivity?.navController?.popBackStack() }
|
|
}
|
|
else -> {
|
|
model.primaryButtonText = getString(R.string.translated_button_back)
|
|
model.primaryAction = { mainActivity?.navController?.popBackStack() }
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun showMoreInfo(info: String) {
|
|
val current = uiModels.value
|
|
uiModels.value = current.copy(
|
|
showMoreInfoButton = true,
|
|
moreInfoButtonText = getString(R.string.done),
|
|
moreInfoAction = ::onExit,
|
|
showStatusMessage = false,
|
|
showStatusDetails = true,
|
|
statusDetails = info
|
|
)
|
|
}
|
|
|
|
// 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 showCloseIcon: Boolean = false,
|
|
var title: String = "Shielding Now!",
|
|
var showShielding: Boolean = true,
|
|
var pauseShielding: Boolean = false,
|
|
var showSuccess: Boolean = false,
|
|
var isFailure: Boolean = false,
|
|
var statusMessage: String = "",
|
|
var statusDetails: String = "",
|
|
var showStatusDetails: Boolean = false,
|
|
var showStatusMessage: Boolean = true,
|
|
var primaryButtonText: String = "Cancel",
|
|
var primaryAction: () -> Unit = {},
|
|
var moreInfoButtonText: String = "",
|
|
var showMoreInfoButton: Boolean = false,
|
|
var moreInfoAction: () -> Unit = {},
|
|
)
|
|
|
|
companion object {
|
|
private const val maxAutoshieldFrequency: Long = 30 * DateUtils.MINUTE_IN_MILLIS
|
|
|
|
/**
|
|
* @param clock Optionally allows injecting a clock, in order to make this testable.
|
|
*/
|
|
fun canAutoshield(context: Context, clock: Clock = Clock.systemUTC()): Boolean {
|
|
val currentEpochMillis = clock.millis()
|
|
val lastAutoshieldEpochMillis = Preferences.lastAutoshieldingEpochMillis.get(context)
|
|
|
|
val isLastAutoshieldOld = (currentEpochMillis - lastAutoshieldEpochMillis) > maxAutoshieldFrequency
|
|
// Prevent a corner case where a user with a clock in the future during one autoshielding prompt
|
|
// could prevent all subsequent autoshielding prompts.
|
|
val isTimeTraveling = lastAutoshieldEpochMillis > currentEpochMillis
|
|
|
|
return isLastAutoshieldOld || isTimeTraveling
|
|
}
|
|
|
|
/**
|
|
* @param clock Optionally allows injecting a clock, in order to make this testable.
|
|
*/
|
|
private fun setAutoshield(context: Context, clock: Clock = Clock.systemUTC()) =
|
|
Preferences.lastAutoshieldingEpochMillis.put(context, clock.millis())
|
|
}
|
|
}
|
|
|