Home redesign
This commit is contained in:
parent
ec4bc7784f
commit
3ff1df9d0d
|
@ -1,7 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -62,7 +61,6 @@ private fun HiddenStyledBalancePreview() =
|
|||
* @param textColor Optional color to modify the default font color from [textStyle]
|
||||
* @param modifier Modifier to modify the Text UI element as needed
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun StyledBalance(
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
|
||||
@Composable
|
||||
fun ZashiBigIconButton(
|
||||
state: BigIconButtonState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier =
|
||||
modifier
|
||||
.clickable(
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = state.onClick,
|
||||
role = Role.Button,
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = ZashiColors.Surfaces.bgSecondary
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(state.icon),
|
||||
contentDescription = state.text.getValue(),
|
||||
tint = ZashiColors.Text.textPrimary
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = state.text.getValue(),
|
||||
style = ZashiTypography.textXs,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class BigIconButtonState(
|
||||
val text: StringResource,
|
||||
@DrawableRes val icon: Int,
|
||||
val onClick: () -> Unit,
|
||||
)
|
|
@ -4,7 +4,7 @@ import cash.z.ecc.android.sdk.fixture.WalletFixture
|
|||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
||||
internal object SendArgumentsWrapperFixture {
|
||||
val RECIPIENT_ADDRESS =
|
||||
|
@ -14,7 +14,7 @@ internal object SendArgumentsWrapperFixture {
|
|||
)
|
||||
|
||||
fun new(recipientAddress: SerializableAddress? = RECIPIENT_ADDRESS) =
|
||||
SendArguments(
|
||||
Send(
|
||||
recipientAddress = recipientAddress?.toRecipient(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.account
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.account.view.Account
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class AccountTestSetup(
|
||||
private val composeTestRule: ComposeContentTestRule,
|
||||
private val walletSnapshot: WalletSnapshot,
|
||||
) {
|
||||
// TODO [#1282]: Update AccountView Tests #1282
|
||||
// TODO [#1282]: https://github.com/Electric-Coin-Company/zashi-android/issues/1282
|
||||
|
||||
private val onSettingsCount = AtomicInteger(0)
|
||||
private val onHideBalancesCount = AtomicInteger(0)
|
||||
|
||||
fun getOnSettingsCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onSettingsCount.get()
|
||||
}
|
||||
|
||||
fun getOnHideBalancesCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onHideBalancesCount.get()
|
||||
}
|
||||
|
||||
fun getWalletSnapshot(): WalletSnapshot {
|
||||
composeTestRule.waitForIdle()
|
||||
return walletSnapshot
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("TestFunctionName")
|
||||
fun DefaultContent(isHideBalances: Boolean) {
|
||||
Account(
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
goBalances = {},
|
||||
hideStatusDialog = {},
|
||||
isHideBalances = isHideBalances,
|
||||
onContactSupport = {},
|
||||
showStatusDialog = null,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
zashiMainTopAppBarState =
|
||||
ZashiMainTopAppBarStateFixture.new(
|
||||
settingsButton =
|
||||
IconButtonState(
|
||||
icon = R.drawable.ic_app_bar_settings,
|
||||
contentDescription =
|
||||
stringRes(co.electriccoin.zcash.ui.R.string.settings_menu_content_description),
|
||||
) {
|
||||
onSettingsCount.incrementAndGet()
|
||||
},
|
||||
balanceVisibilityButton =
|
||||
IconButtonState(
|
||||
icon = R.drawable.ic_app_bar_balances_hide,
|
||||
contentDescription =
|
||||
stringRes(
|
||||
co.electriccoin.zcash.ui.R.string.hide_balances_content_description
|
||||
),
|
||||
) {
|
||||
onHideBalancesCount.incrementAndGet()
|
||||
},
|
||||
),
|
||||
transactionHistoryWidgetState = TransactionHistoryWidgetStateFixture.new(),
|
||||
isWalletRestoringState = WalletRestoringState.NONE,
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
onStatusClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
fun setDefaultContent(isHideBalances: Boolean = false) {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
DefaultContent(isHideBalances)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.account.integration
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertWidthIsAtLeast
|
||||
import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.account.AccountTag
|
||||
import co.electriccoin.zcash.ui.screen.account.AccountTestSetup
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AccountViewIntegrationTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun newTestSetup(walletSnapshot: WalletSnapshot) =
|
||||
AccountTestSetup(
|
||||
composeTestRule,
|
||||
walletSnapshot,
|
||||
)
|
||||
|
||||
// This is just basic sanity check that we still have UI set up as expected after the state restore
|
||||
@Test
|
||||
@MediumTest
|
||||
fun wallet_snapshot_restoration() {
|
||||
val restorationTester = StateRestorationTester(composeTestRule)
|
||||
val walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
saplingBalance = WalletSnapshotFixture.SAPLING_BALANCE,
|
||||
orchardBalance = WalletSnapshotFixture.ORCHARD_BALANCE,
|
||||
transparentBalance = WalletSnapshotFixture.TRANSPARENT_BALANCE
|
||||
)
|
||||
val testSetup = newTestSetup(walletSnapshot)
|
||||
|
||||
restorationTester.setContent {
|
||||
ZcashTheme {
|
||||
testSetup.DefaultContent(isHideBalances = false)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(WalletSnapshotFixture.SAPLING_BALANCE, testSetup.getWalletSnapshot().saplingBalance)
|
||||
assertEquals(WalletSnapshotFixture.ORCHARD_BALANCE, testSetup.getWalletSnapshot().orchardBalance)
|
||||
assertEquals(WalletSnapshotFixture.TRANSPARENT_BALANCE, testSetup.getWalletSnapshot().transparentBalance)
|
||||
|
||||
restorationTester.emulateSavedInstanceStateRestore()
|
||||
|
||||
assertEquals(WalletSnapshotFixture.SAPLING_BALANCE, testSetup.getWalletSnapshot().saplingBalance)
|
||||
assertEquals(WalletSnapshotFixture.ORCHARD_BALANCE, testSetup.getWalletSnapshot().orchardBalance)
|
||||
assertEquals(WalletSnapshotFixture.TRANSPARENT_BALANCE, testSetup.getWalletSnapshot().transparentBalance)
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountTag.BALANCE_VIEWS).also {
|
||||
it.assertIsDisplayed()
|
||||
it.assertWidthIsAtLeast(1.dp)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.account.view
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.component.CommonTag
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.account.AccountTag
|
||||
import co.electriccoin.zcash.ui.screen.account.AccountTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.send.clickHideBalances
|
||||
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
// TODO [#1194]: Cover Current balances UI widget with tests
|
||||
// TODO [#1194]: https://github.com/Electric-Coin-Company/zashi-android/issues/1194
|
||||
|
||||
class AccountViewTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun check_all_elementary_ui_elements_displayed() {
|
||||
newTestSetup()
|
||||
|
||||
composeTestRule.onNodeWithTag(CommonTag.TOP_APP_BAR)
|
||||
.also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountTag.BALANCE_VIEWS).also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun hamburger_settings_test() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
Assert.assertEquals(0, testSetup.getOnSettingsCount())
|
||||
|
||||
composeTestRule.clickSettingsTopAppBarMenu()
|
||||
|
||||
Assert.assertEquals(1, testSetup.getOnSettingsCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun hide_balances_btn_click_test() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
Assert.assertEquals(0, testSetup.getOnHideBalancesCount())
|
||||
|
||||
composeTestRule.clickHideBalances()
|
||||
|
||||
Assert.assertEquals(1, testSetup.getOnHideBalancesCount())
|
||||
}
|
||||
|
||||
private fun newTestSetup(
|
||||
walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new(),
|
||||
isHideBalances: Boolean = false
|
||||
) = AccountTestSetup(
|
||||
composeTestRule,
|
||||
walletSnapshot = walletSnapshot,
|
||||
).apply {
|
||||
setDefaultContent(isHideBalances)
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
|
||||
import co.electriccoin.zcash.ui.screen.balances.view.Balances
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class BalancesTestSetup(
|
||||
private val composeTestRule: ComposeContentTestRule,
|
||||
private val walletSnapshot: WalletSnapshot,
|
||||
) {
|
||||
private val onSettingsCount = AtomicInteger(0)
|
||||
|
||||
fun getOnSettingsCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onSettingsCount.get()
|
||||
}
|
||||
|
||||
fun getWalletSnapshot(): WalletSnapshot {
|
||||
composeTestRule.waitForIdle()
|
||||
return walletSnapshot
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("TestFunctionName")
|
||||
fun DefaultContent() {
|
||||
Balances(
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
hideStatusDialog = {},
|
||||
isHideBalances = false,
|
||||
showStatusDialog = null,
|
||||
onStatusClick = {},
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
isShowingErrorDialog = false,
|
||||
setShowErrorDialog = {},
|
||||
onContactSupport = {},
|
||||
onShielding = {},
|
||||
shieldState = ShieldState.Available,
|
||||
walletSnapshot = walletSnapshot,
|
||||
walletRestoringState = WalletRestoringState.NONE,
|
||||
zashiMainTopAppBarState =
|
||||
ZashiMainTopAppBarStateFixture.new(
|
||||
settingsButton =
|
||||
IconButtonState(
|
||||
icon = R.drawable.ic_app_bar_settings,
|
||||
contentDescription =
|
||||
stringRes(co.electriccoin.zcash.ui.R.string.settings_menu_content_description),
|
||||
) {
|
||||
onSettingsCount.incrementAndGet()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun setDefaultContent() {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
DefaultContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances.integration
|
||||
|
||||
import androidx.compose.ui.test.assertWidthIsAtLeast
|
||||
import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalancesTag
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalancesTestSetup
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class BalancesViewIntegrationTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun newTestSetup(walletSnapshot: WalletSnapshot) =
|
||||
BalancesTestSetup(
|
||||
composeTestRule,
|
||||
walletSnapshot,
|
||||
)
|
||||
|
||||
// This is just basic sanity check that we still have UI set up as expected after the state restore
|
||||
@Test
|
||||
@MediumTest
|
||||
fun wallet_snapshot_restoration() {
|
||||
val restorationTester = StateRestorationTester(composeTestRule)
|
||||
val walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
status = Synchronizer.Status.SYNCING,
|
||||
progress = PercentDecimal(0.5f)
|
||||
)
|
||||
val testSetup = newTestSetup(walletSnapshot)
|
||||
|
||||
restorationTester.setContent {
|
||||
ZcashTheme {
|
||||
testSetup.DefaultContent()
|
||||
}
|
||||
}
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
|
||||
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
|
||||
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
|
||||
|
||||
restorationTester.emulateSavedInstanceStateRestore()
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
|
||||
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
|
||||
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
|
||||
|
||||
composeTestRule.onNodeWithTag(BalancesTag.STATUS).also {
|
||||
it.assertExists()
|
||||
it.assertWidthIsAtLeast(1.dp)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.totalBalance
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class WalletDisplayValuesTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
fun download_running_test() {
|
||||
val walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
progress = PercentDecimal.ONE_HUNDRED_PERCENT,
|
||||
status = Synchronizer.Status.SYNCING,
|
||||
orchardBalance = WalletSnapshotFixture.ORCHARD_BALANCE,
|
||||
saplingBalance = WalletSnapshotFixture.SAPLING_BALANCE,
|
||||
transparentBalance = WalletSnapshotFixture.TRANSPARENT_BALANCE
|
||||
)
|
||||
val values =
|
||||
WalletDisplayValues.getNextValues(
|
||||
context = getAppContext(),
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
|
||||
assertNotNull(values)
|
||||
assertEquals(1f, values.progress.decimal)
|
||||
assertEquals(walletSnapshot.totalBalance().toZecString(), values.zecAmountText)
|
||||
assertTrue(values.statusText.startsWith(getStringResource(R.string.balances_status_syncing)))
|
||||
// TODO [#578]: Provide Zatoshi -> USD fiat currency formatting
|
||||
// TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578
|
||||
assertEquals(FiatCurrencyConversionRateState.Unavailable, values.fiatCurrencyAmountState)
|
||||
assertEquals(
|
||||
getStringResource(R.string.fiat_currency_conversion_rate_unavailable),
|
||||
values.fiatCurrencyAmountText
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances.view
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.component.CommonTag
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalancesTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.DefaultAsserter.assertEquals
|
||||
|
||||
// TODO [#1227]: Cover Balances UI and logic with tests
|
||||
// TODO [#1227]: https://github.com/Electric-Coin-Company/zashi-android/issues/1227
|
||||
|
||||
class BalancesViewTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun newTestSetup(walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new()) =
|
||||
BalancesTestSetup(
|
||||
composeTestRule,
|
||||
walletSnapshot = walletSnapshot,
|
||||
).apply {
|
||||
setDefaultContent()
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun check_all_elementary_ui_elements_displayed() {
|
||||
newTestSetup()
|
||||
|
||||
composeTestRule.onNodeWithTag(CommonTag.TOP_APP_BAR)
|
||||
.also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun hamburger_settings_test() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals("Failed in comparison", 0, testSetup.getOnSettingsCount())
|
||||
|
||||
composeTestRule.clickSettingsTopAppBarMenu()
|
||||
|
||||
Assert.assertEquals(1, testSetup.getOnSettingsCount())
|
||||
}
|
||||
}
|
|
@ -47,7 +47,7 @@ class ReceiveViewTestSetup(
|
|||
),
|
||||
isLoading = false,
|
||||
),
|
||||
zashiMainTopAppBarState =
|
||||
appBarState =
|
||||
ZashiMainTopAppBarStateFixture.new(
|
||||
settingsButton =
|
||||
IconButtonState(
|
||||
|
|
|
@ -114,11 +114,6 @@ class SendViewTestSetup(
|
|||
onQrScannerOpen = {
|
||||
onScannerCount.incrementAndGet()
|
||||
},
|
||||
goBalances = {
|
||||
// TODO [#1194]: Cover Current balances UI widget with tests
|
||||
// TODO [#1194]: https://github.com/Electric-Coin-Company/zashi-android/issues/1194
|
||||
},
|
||||
isHideBalances = false,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
recipientAddressState = RecipientAddressState("", AddressType.Invalid()),
|
||||
onRecipientAddressChange = {
|
||||
|
@ -137,7 +132,7 @@ class SendViewTestSetup(
|
|||
),
|
||||
setMemoState = {},
|
||||
memoState = MemoState.new(""),
|
||||
walletSnapshot =
|
||||
selectedAccount =
|
||||
WalletSnapshotFixture.new(
|
||||
saplingBalance =
|
||||
WalletBalanceFixture.new(
|
||||
|
|
|
@ -33,10 +33,9 @@ class SendViewIntegrationTest {
|
|||
|
||||
restorationTester.setContent {
|
||||
WrapSend(
|
||||
sendArguments = null,
|
||||
args = null,
|
||||
goToQrScanner = {},
|
||||
goBack = {},
|
||||
goBalances = {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,6 @@ import co.electriccoin.zcash.global.newInstance
|
|||
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouterImpl
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationRouterImpl
|
||||
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
|
||||
|
@ -38,5 +36,4 @@ val coreModule =
|
|||
factory { AndroidConfigurationFactory.new() }
|
||||
|
||||
singleOf(::NavigationRouterImpl) bind NavigationRouter::class
|
||||
singleOf(::HomeTabNavigationRouterImpl) bind HomeTabNavigationRouter::class
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package co.electriccoin.zcash.di
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||
|
@ -29,7 +27,6 @@ val repositoryModule =
|
|||
singleOf(::WalletRepositoryImpl) bind WalletRepository::class
|
||||
singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class
|
||||
singleOf(::ExchangeRateRepositoryImpl) bind ExchangeRateRepository::class
|
||||
singleOf(::BalanceRepositoryImpl) bind BalanceRepository::class
|
||||
singleOf(::FlexaRepositoryImpl) bind FlexaRepository::class
|
||||
singleOf(::BiometricRepositoryImpl) bind BiometricRepository::class
|
||||
singleOf(::KeystoneProposalRepositoryImpl) bind KeystoneProposalRepository::class
|
||||
|
|
|
@ -5,12 +5,12 @@ import co.electriccoin.zcash.ui.common.usecase.ApplyTransactionFulltextFiltersUs
|
|||
import co.electriccoin.zcash.ui.common.usecase.CancelProposalFlowUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ConfirmProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateFlexaTransactionUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneProposalPCZTEncoderUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneShieldProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateOrUpdateTransactionNoteUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateZip321ProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteTransactionNoteUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeriveKeystoneAccountUnifiedAddressUseCase
|
||||
|
@ -33,6 +33,7 @@ import co.electriccoin.zcash.ui.common.usecase.GetTransactionFiltersUseCase
|
|||
import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletStateInformationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiSpendingKeyUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
|
||||
|
@ -55,8 +56,9 @@ import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCa
|
|||
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveTransactionSubmitStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveZashiAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnAddressScannedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnZip321ScannedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ParseKeystonePCZTUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneSignInRequestUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneUrToZashiAccountsUseCase
|
||||
|
@ -114,7 +116,7 @@ val useCaseModule =
|
|||
factoryOf(::ShareImageUseCase)
|
||||
factoryOf(::Zip321BuildUriUseCase)
|
||||
factoryOf(::Zip321ParseUriValidationUseCase)
|
||||
factoryOf(::ObserveWalletStateUseCase)
|
||||
factoryOf(::GetWalletStateInformationUseCase)
|
||||
factoryOf(::IsCoinbaseAvailableUseCase)
|
||||
factoryOf(::GetZashiSpendingKeyUseCase)
|
||||
factoryOf(::ObservePersistableWalletUseCase)
|
||||
|
@ -138,7 +140,8 @@ val useCaseModule =
|
|||
factoryOf(::GetCurrentTransactionsUseCase)
|
||||
factoryOf(::GetCurrentFilteredTransactionsUseCase) onClose ::closeableCallback
|
||||
factoryOf(::CreateProposalUseCase)
|
||||
factoryOf(::CreateZip321ProposalUseCase)
|
||||
factoryOf(::OnZip321ScannedUseCase)
|
||||
factoryOf(::OnAddressScannedUseCase)
|
||||
factoryOf(::CreateKeystoneShieldProposalUseCase)
|
||||
factoryOf(::ParseKeystonePCZTUseCase)
|
||||
factoryOf(::ParseKeystoneSignInRequestUseCase)
|
||||
|
@ -172,4 +175,5 @@ val useCaseModule =
|
|||
factoryOf(::GetMetadataUseCase)
|
||||
factoryOf(::ExportTaxUseCase)
|
||||
factoryOf(::NavigateToTaxExportUseCase)
|
||||
factoryOf(::CreateFlexaTransactionUseCase)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
package co.electriccoin.zcash.di
|
||||
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.OldHomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewModel
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.SelectRecipientViewModel
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettingsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceViewModel
|
||||
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
|
||||
import co.electriccoin.zcash.ui.screen.feedback.viewmodel.FeedbackViewModel
|
||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
|
||||
|
@ -20,7 +23,7 @@ import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel
|
|||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransactionViewModel
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystonePCZTViewModel
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel
|
||||
|
@ -53,7 +56,7 @@ val viewModelModule =
|
|||
module {
|
||||
viewModelOf(::WalletViewModel)
|
||||
viewModelOf(::AuthenticationViewModel)
|
||||
viewModelOf(::HomeViewModel)
|
||||
viewModelOf(::OldHomeViewModel)
|
||||
viewModelOf(::OnboardingViewModel)
|
||||
viewModelOf(::StorageCheckViewModel)
|
||||
viewModelOf(::RestoreViewModel)
|
||||
|
@ -78,17 +81,30 @@ val viewModelModule =
|
|||
viewModelOf(::ReceiveViewModel)
|
||||
viewModelOf(::QrCodeViewModel)
|
||||
viewModelOf(::RequestViewModel)
|
||||
viewModelOf(::IntegrationsViewModel)
|
||||
viewModel { (args: ScanNavigationArgs) ->
|
||||
viewModel { (args: Scan) ->
|
||||
ScanViewModel(
|
||||
args = args,
|
||||
getSynchronizer = get(),
|
||||
zip321ParseUriValidationUseCase = get(),
|
||||
onAddressScanned = get(),
|
||||
zip321Scanned = get()
|
||||
)
|
||||
}
|
||||
viewModelOf(::ScanKeystoneSignInRequestViewModel)
|
||||
viewModelOf(::ScanKeystonePCZTViewModel)
|
||||
viewModelOf(::IntegrationsViewModel)
|
||||
viewModel { (isDialog: Boolean) ->
|
||||
IntegrationsViewModel(
|
||||
isDialog = isDialog,
|
||||
getZcashCurrency = get(),
|
||||
isFlexaAvailableUseCase = get(),
|
||||
isCoinbaseAvailable = get(),
|
||||
observeWalletAccounts = get(),
|
||||
navigationRouter = get(),
|
||||
navigateToCoinbase = get(),
|
||||
getWalletRestoringState = get()
|
||||
)
|
||||
}
|
||||
viewModelOf(::FlexaViewModel)
|
||||
viewModelOf(::SendViewModel)
|
||||
viewModel { (args: SeedNavigationArgs) ->
|
||||
SeedViewModel(
|
||||
|
@ -100,7 +116,7 @@ val viewModelModule =
|
|||
viewModelOf(::FeedbackViewModel)
|
||||
viewModelOf(::SignKeystoneTransactionViewModel)
|
||||
viewModelOf(::AccountListViewModel)
|
||||
viewModelOf(::ZashiMainTopAppBarViewModel)
|
||||
viewModelOf(::ZashiTopAppBarViewModel)
|
||||
viewModel { (args: SelectKeystoneAccount) ->
|
||||
SelectKeystoneAccountViewModel(
|
||||
args = args,
|
||||
|
@ -138,4 +154,6 @@ val viewModelModule =
|
|||
)
|
||||
}
|
||||
viewModelOf(::TaxExportViewModel)
|
||||
viewModelOf(::BalanceViewModel)
|
||||
viewModelOf(::HomeViewModel)
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
package co.electriccoin.zcash.ui
|
||||
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface HomeTabNavigationRouter {
|
||||
fun select(tab: HomeScreenIndex)
|
||||
|
||||
fun observe(): Flow<HomeScreenIndex>
|
||||
}
|
||||
|
||||
class HomeTabNavigationRouterImpl : HomeTabNavigationRouter {
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
private val channel = Channel<HomeScreenIndex>()
|
||||
|
||||
override fun select(tab: HomeScreenIndex) {
|
||||
scope.launch {
|
||||
channel.send(tab)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observe() = channel.receiveAsFlow()
|
||||
}
|
|
@ -35,7 +35,7 @@ import co.electriccoin.zcash.ui.common.model.OnboardingState
|
|||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationUIState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.OldHomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.configuration.RemoteConfig
|
||||
|
@ -67,7 +67,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MainActivity : FragmentActivity() {
|
||||
private val homeViewModel by viewModel<HomeViewModel>()
|
||||
private val oldHomeViewModel by viewModel<OldHomeViewModel>()
|
||||
|
||||
val walletViewModel by viewModel<WalletViewModel>()
|
||||
|
||||
|
@ -132,7 +132,7 @@ class MainActivity : FragmentActivity() {
|
|||
}
|
||||
|
||||
// Note this condition needs to be kept in sync with the condition in MainContent()
|
||||
homeViewModel.configurationFlow.value == null || SecretState.Loading == walletViewModel.secretState.value
|
||||
oldHomeViewModel.configurationFlow.value == null || SecretState.Loading == walletViewModel.secretState.value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,7 @@ class MainActivity : FragmentActivity() {
|
|||
enableEdgeToEdge()
|
||||
setContentCompat {
|
||||
Override(configurationOverrideFlow) {
|
||||
val isHideBalances by homeViewModel.isHideBalances.collectAsStateWithLifecycle()
|
||||
val isHideBalances by oldHomeViewModel.isHideBalances.collectAsStateWithLifecycle()
|
||||
ZcashTheme(
|
||||
balancesAvailable = isHideBalances == false
|
||||
) {
|
||||
|
@ -235,7 +235,7 @@ class MainActivity : FragmentActivity() {
|
|||
|
||||
@Composable
|
||||
private fun MainContent() {
|
||||
val configuration = homeViewModel.configurationFlow.collectAsStateWithLifecycle().value
|
||||
val configuration = oldHomeViewModel.configurationFlow.collectAsStateWithLifecycle().value
|
||||
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
|
||||
|
||||
// Note this condition needs to be kept in sync with the condition in setupSplashScreen()
|
||||
|
@ -295,7 +295,7 @@ class MainActivity : FragmentActivity() {
|
|||
val isEnableBackgroundSyncFlow =
|
||||
run {
|
||||
val isSecretReadyFlow = walletViewModel.secretState.map { it is SecretState.Ready }
|
||||
val isBackgroundSyncEnabledFlow = homeViewModel.isBackgroundSyncEnabled.filterNotNull()
|
||||
val isBackgroundSyncEnabledFlow = oldHomeViewModel.isBackgroundSyncEnabled.filterNotNull()
|
||||
|
||||
isSecretReadyFlow.combine(isBackgroundSyncEnabledFlow) { isSecretReady, isBackgroundSyncEnabled ->
|
||||
isSecretReady && isBackgroundSyncEnabled
|
||||
|
|
|
@ -4,12 +4,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptionsBuilder
|
||||
import androidx.navigation.NavType
|
||||
|
@ -22,17 +22,12 @@ import cash.z.ecc.android.sdk.Synchronizer
|
|||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.spackle.getSerializableCompat
|
||||
import co.electriccoin.zcash.ui.NavigationArgs.ADDRESS_TYPE
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_RECIPIENT_ADDRESS
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_ZIP_321_URI
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.CHOOSE_SERVER
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.DELETE_WALLET
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.EXCHANGE_RATE_OPT_IN
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.EXPORT_PRIVATE_DATA
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.NOT_ENOUGH_SPACE
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
|
||||
|
@ -42,16 +37,13 @@ import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN
|
|||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.isInForeground
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popExitTransition
|
||||
import co.electriccoin.zcash.ui.screen.ExternalUrl
|
||||
import co.electriccoin.zcash.ui.screen.about.WrapAbout
|
||||
import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil
|
||||
import co.electriccoin.zcash.ui.screen.accountlist.AccountList
|
||||
import co.electriccoin.zcash.ui.screen.accountlist.AndroidAccountList
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookArgs
|
||||
|
@ -72,14 +64,21 @@ import co.electriccoin.zcash.ui.screen.exchangerate.optin.AndroidExchangeRateOpt
|
|||
import co.electriccoin.zcash.ui.screen.exchangerate.settings.AndroidSettingsExchangeRateOptIn
|
||||
import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData
|
||||
import co.electriccoin.zcash.ui.screen.feedback.WrapFeedback
|
||||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||
import co.electriccoin.zcash.ui.screen.integrations.WrapIntegrations
|
||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.AndroidHome
|
||||
import co.electriccoin.zcash.ui.screen.home.Home
|
||||
import co.electriccoin.zcash.ui.screen.integrations.AndroidDialogIntegrations
|
||||
import co.electriccoin.zcash.ui.screen.integrations.AndroidIntegrations
|
||||
import co.electriccoin.zcash.ui.screen.integrations.DialogIntegrations
|
||||
import co.electriccoin.zcash.ui.screen.integrations.Integrations
|
||||
import co.electriccoin.zcash.ui.screen.qrcode.WrapQrCode
|
||||
import co.electriccoin.zcash.ui.screen.receive.AndroidReceive
|
||||
import co.electriccoin.zcash.ui.screen.receive.Receive
|
||||
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
||||
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.AndroidReviewTransaction
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystonePCZTRequest
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystoneSignInRequest
|
||||
|
@ -89,7 +88,8 @@ import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
|||
import co.electriccoin.zcash.ui.screen.seed.WrapSeed
|
||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.AndroidSelectKeystoneAccount
|
||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
import co.electriccoin.zcash.ui.screen.send.WrapSend
|
||||
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
|
||||
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.AndroidSignKeystoneTransaction
|
||||
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.SignKeystoneTransaction
|
||||
|
@ -110,8 +110,8 @@ import co.electriccoin.zcash.ui.screen.whatsnew.WrapWhatsNew
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
// TODO [#1297]: Consider: Navigation passing complex data arguments different way
|
||||
|
@ -120,6 +120,8 @@ import org.koin.compose.koinInject
|
|||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
internal fun MainActivity.Navigation() {
|
||||
val navController = LocalNavController.current
|
||||
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||
val navigationRouter = koinInject<NavigationRouter>()
|
||||
|
||||
// Helper properties for triggering the system security UI from callbacks
|
||||
val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) =
|
||||
|
@ -128,69 +130,25 @@ internal fun MainActivity.Navigation() {
|
|||
rememberSaveable { mutableStateOf(false) }
|
||||
val (deleteWalletAuthentication, setDeleteWalletAuthentication) =
|
||||
rememberSaveable { mutableStateOf(false) }
|
||||
val navigationRouter = koinInject<NavigationRouter>()
|
||||
|
||||
val navigator: Navigator = remember { NavigatorImpl(this@Navigation, navController, flexaViewModel) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navigationRouter.observe().collect {
|
||||
when (it) {
|
||||
is NavigationCommand.Forward ->
|
||||
if (it.route is ExternalUrl) {
|
||||
WebBrowserUtil.startActivity(this@Navigation, it.route.url)
|
||||
} else {
|
||||
navController.executeNavigation(route = it.route)
|
||||
}
|
||||
is NavigationCommand.Replace ->
|
||||
if (it.route is ExternalUrl) {
|
||||
navController.popBackStack()
|
||||
WebBrowserUtil.startActivity(this@Navigation, it.route.url)
|
||||
} else {
|
||||
navController.executeNavigation(route = it.route) {
|
||||
popUpTo(navController.currentBackStackEntry?.destination?.id ?: 0) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
is NavigationCommand.ReplaceAll ->
|
||||
if (it.route is ExternalUrl) {
|
||||
navController.popBackStack(
|
||||
route = navController.graph.startDestinationId,
|
||||
inclusive = false
|
||||
)
|
||||
WebBrowserUtil.startActivity(this@Navigation, it.route.url)
|
||||
} else {
|
||||
navController.executeNavigation(route = it.route) {
|
||||
popUpTo(navController.graph.startDestinationId) {
|
||||
inclusive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
is NavigationCommand.NewRoot ->
|
||||
navController.executeNavigation(route = it.route) {
|
||||
popUpTo(navController.graph.startDestinationId) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
NavigationCommand.Back -> navController.popBackStack()
|
||||
|
||||
NavigationCommand.BackToRoot ->
|
||||
navController.popBackStack(
|
||||
destinationId = navController.graph.startDestinationId,
|
||||
inclusive = false
|
||||
)
|
||||
}
|
||||
navigationRouter.observePipeline().collect {
|
||||
navigator.executeCommand(it)
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = HOME,
|
||||
startDestination = Home,
|
||||
enterTransition = { enterTransition() },
|
||||
exitTransition = { exitTransition() },
|
||||
popEnterTransition = { popEnterTransition() },
|
||||
popExitTransition = { popExitTransition() }
|
||||
) {
|
||||
composable(HOME) { backStack ->
|
||||
NavigationHome(navController, backStack)
|
||||
composable<Home> {
|
||||
NavigationHome(navController)
|
||||
}
|
||||
composable(SETTINGS) {
|
||||
WrapSettings()
|
||||
|
@ -287,8 +245,11 @@ internal fun MainActivity.Navigation() {
|
|||
composable(WHATS_NEW) {
|
||||
WrapWhatsNew()
|
||||
}
|
||||
composable(INTEGRATIONS) {
|
||||
WrapIntegrations()
|
||||
composable<Integrations> {
|
||||
AndroidIntegrations()
|
||||
}
|
||||
dialog<DialogIntegrations> {
|
||||
AndroidDialogIntegrations()
|
||||
}
|
||||
composable(EXCHANGE_RATE_OPT_IN) {
|
||||
AndroidExchangeRateOptIn()
|
||||
|
@ -315,18 +276,18 @@ internal fun MainActivity.Navigation() {
|
|||
AndroidAccountList()
|
||||
}
|
||||
composable(
|
||||
route = ScanNavigationArgs.ROUTE,
|
||||
route = Scan.ROUTE,
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument(ScanNavigationArgs.KEY) {
|
||||
type = NavType.EnumType(ScanNavigationArgs::class.java)
|
||||
defaultValue = ScanNavigationArgs.DEFAULT
|
||||
navArgument(Scan.KEY) {
|
||||
type = NavType.EnumType(Scan::class.java)
|
||||
defaultValue = Scan.SEND
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val mode =
|
||||
backStackEntry.arguments
|
||||
?.getSerializableCompat<ScanNavigationArgs>(ScanNavigationArgs.KEY) ?: ScanNavigationArgs.DEFAULT
|
||||
?.getSerializableCompat<Scan>(Scan.KEY) ?: Scan.SEND
|
||||
|
||||
WrapScanValidator(args = mode)
|
||||
}
|
||||
|
@ -438,6 +399,12 @@ internal fun MainActivity.Navigation() {
|
|||
composable<TaxExport> {
|
||||
AndroidTaxExport()
|
||||
}
|
||||
composable<Receive> {
|
||||
AndroidReceive()
|
||||
}
|
||||
composable<Send> {
|
||||
WrapSend(it.toRoute())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,30 +412,10 @@ internal fun MainActivity.Navigation() {
|
|||
* This is the Home screens sub-navigation. We could consider creating a separate sub-navigation graph.
|
||||
*/
|
||||
@Composable
|
||||
private fun MainActivity.NavigationHome(
|
||||
navController: NavHostController,
|
||||
backStack: NavBackStackEntry
|
||||
) {
|
||||
private fun MainActivity.NavigationHome(navController: NavHostController) {
|
||||
val applicationStateProvider: ApplicationStateProvider by inject()
|
||||
|
||||
WrapHome(
|
||||
goScan = { navController.navigateJustOnce(ScanNavigationArgs(ScanNavigationArgs.DEFAULT)) },
|
||||
sendArguments =
|
||||
SendArguments(
|
||||
recipientAddress =
|
||||
backStack.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
|
||||
Json.decodeFromString<SerializableAddress>(it).toRecipient()
|
||||
},
|
||||
zip321Uri = backStack.savedStateHandle.get<String>(SEND_SCAN_ZIP_321_URI),
|
||||
clearForm = backStack.savedStateHandle.get<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM) ?: false
|
||||
).also {
|
||||
// Remove Send screen arguments passed from the Scan or MultipleSubmissionFailure screens if
|
||||
// some exist after we use them
|
||||
backStack.savedStateHandle.remove<String>(SEND_SCAN_RECIPIENT_ADDRESS)
|
||||
backStack.savedStateHandle.remove<String>(SEND_SCAN_ZIP_321_URI)
|
||||
backStack.savedStateHandle.remove<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM)
|
||||
},
|
||||
)
|
||||
AndroidHome()
|
||||
|
||||
val isEnoughSpace by storageCheckViewModel.isEnoughSpace.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -560,25 +507,6 @@ fun NavHostController.navigateJustOnce(
|
|||
}
|
||||
}
|
||||
|
||||
private fun NavHostController.executeNavigation(
|
||||
route: Any,
|
||||
builder: (NavOptionsBuilder.() -> Unit)? = null
|
||||
) {
|
||||
if (route is String) {
|
||||
if (builder == null) {
|
||||
navigate(route)
|
||||
} else {
|
||||
navigate(route, builder)
|
||||
}
|
||||
} else {
|
||||
if (builder == null) {
|
||||
navigate(route)
|
||||
} else {
|
||||
navigate(route, builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops up the current screen from the back stack. Parameter currentRouteToBePopped is meant to be
|
||||
* set only to the current screen so we can easily debounce multiple screen popping from the back stack.
|
||||
|
@ -592,21 +520,13 @@ fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: String) {
|
|||
popBackStack()
|
||||
}
|
||||
|
||||
object NavigationArguments {
|
||||
const val SEND_SCAN_RECIPIENT_ADDRESS = "send_scan_recipient_address"
|
||||
const val SEND_SCAN_ZIP_321_URI = "send_scan_zip_321_uri"
|
||||
const val MULTIPLE_SUBMISSION_CLEAR_FORM = "multiple_submission_clear_form"
|
||||
}
|
||||
|
||||
object NavigationTargets {
|
||||
const val ABOUT = "about"
|
||||
const val ADVANCED_SETTINGS = "advanced_settings"
|
||||
const val DELETE_WALLET = "delete_wallet"
|
||||
const val EXCHANGE_RATE_OPT_IN = "exchange_rate_opt_in"
|
||||
const val EXPORT_PRIVATE_DATA = "export_private_data"
|
||||
const val HOME = "home"
|
||||
const val CHOOSE_SERVER = "choose_server"
|
||||
const val INTEGRATIONS = "integrations"
|
||||
const val NOT_ENOUGH_SPACE = "not_enough_space"
|
||||
const val QR_CODE = "qr_code"
|
||||
const val REQUEST = "request"
|
||||
|
|
|
@ -7,21 +7,37 @@ import kotlinx.coroutines.channels.Channel
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
interface NavigationRouter {
|
||||
fun forward(route: Any)
|
||||
/**
|
||||
* Add [routes] to backstack.
|
||||
*/
|
||||
fun forward(vararg routes: Any)
|
||||
|
||||
fun replace(route: Any)
|
||||
/**
|
||||
* Replace current screen by the first route and add the rest of [routes] to backstack.
|
||||
*/
|
||||
fun replace(vararg routes: Any)
|
||||
|
||||
fun replaceAll(route: Any)
|
||||
|
||||
fun newRoot(route: Any)
|
||||
/**
|
||||
* Pop all screens except for the root and add [routes] to backstack.
|
||||
*/
|
||||
fun replaceAll(vararg routes: Any)
|
||||
|
||||
/**
|
||||
* Pop last screen from backstack.
|
||||
*/
|
||||
fun back()
|
||||
|
||||
fun backTo(route: KClass<*>)
|
||||
|
||||
/**
|
||||
* Pop all screens from backstack except for the root.
|
||||
*/
|
||||
fun backToRoot()
|
||||
|
||||
fun observe(): Flow<NavigationCommand>
|
||||
fun observePipeline(): Flow<NavigationCommand>
|
||||
}
|
||||
|
||||
class NavigationRouterImpl : NavigationRouter {
|
||||
|
@ -29,27 +45,21 @@ class NavigationRouterImpl : NavigationRouter {
|
|||
|
||||
private val channel = Channel<NavigationCommand>()
|
||||
|
||||
override fun forward(route: Any) {
|
||||
override fun forward(vararg routes: Any) {
|
||||
scope.launch {
|
||||
channel.send(NavigationCommand.Forward(route))
|
||||
channel.send(NavigationCommand.Forward(routes.toList()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun replace(route: Any) {
|
||||
override fun replace(vararg routes: Any) {
|
||||
scope.launch {
|
||||
channel.send(NavigationCommand.Replace(route))
|
||||
channel.send(NavigationCommand.Replace(routes.toList()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun replaceAll(route: Any) {
|
||||
override fun replaceAll(vararg routes: Any) {
|
||||
scope.launch {
|
||||
channel.send(NavigationCommand.ReplaceAll(route))
|
||||
}
|
||||
}
|
||||
|
||||
override fun newRoot(route: Any) {
|
||||
scope.launch {
|
||||
channel.send(NavigationCommand.NewRoot(route))
|
||||
channel.send(NavigationCommand.ReplaceAll(routes.toList()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,25 +69,31 @@ class NavigationRouterImpl : NavigationRouter {
|
|||
}
|
||||
}
|
||||
|
||||
override fun backTo(route: KClass<*>) {
|
||||
scope.launch {
|
||||
channel.send(NavigationCommand.BackTo(route))
|
||||
}
|
||||
}
|
||||
|
||||
override fun backToRoot() {
|
||||
scope.launch {
|
||||
channel.send(NavigationCommand.BackToRoot)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observe() = channel.receiveAsFlow()
|
||||
override fun observePipeline() = channel.receiveAsFlow()
|
||||
}
|
||||
|
||||
sealed interface NavigationCommand {
|
||||
data class Forward(val route: Any) : NavigationCommand
|
||||
data class Forward(val routes: List<Any>) : NavigationCommand
|
||||
|
||||
data class Replace(val route: Any) : NavigationCommand
|
||||
data class Replace(val routes: List<Any>) : NavigationCommand
|
||||
|
||||
data class ReplaceAll(val route: Any) : NavigationCommand
|
||||
|
||||
data class NewRoot(val route: Any) : NavigationCommand
|
||||
data class ReplaceAll(val routes: List<Any>) : NavigationCommand
|
||||
|
||||
data object Back : NavigationCommand
|
||||
|
||||
data class BackTo(val route: KClass<*>) : NavigationCommand
|
||||
|
||||
data object BackToRoot : NavigationCommand
|
||||
}
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
package co.electriccoin.zcash.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptionsBuilder
|
||||
import androidx.navigation.serialization.generateHashCode
|
||||
import co.electriccoin.zcash.ui.screen.ExternalUrl
|
||||
import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil
|
||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||
import com.flexa.core.Flexa
|
||||
import com.flexa.spend.buildSpend
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
interface Navigator {
|
||||
fun executeCommand(command: NavigationCommand)
|
||||
}
|
||||
|
||||
class NavigatorImpl(
|
||||
private val activity: ComponentActivity,
|
||||
private val navController: NavHostController,
|
||||
private val flexaViewModel: FlexaViewModel,
|
||||
) : Navigator {
|
||||
override fun executeCommand(command: NavigationCommand) {
|
||||
when (command) {
|
||||
is NavigationCommand.Forward -> forward(command)
|
||||
is NavigationCommand.Replace -> replace(command)
|
||||
is NavigationCommand.ReplaceAll -> replaceAll(command)
|
||||
NavigationCommand.Back -> navController.popBackStack()
|
||||
is NavigationCommand.BackTo -> backTo(command)
|
||||
NavigationCommand.BackToRoot -> backToRoot()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
private fun backTo(command: NavigationCommand.BackTo) {
|
||||
navController.popBackStack(
|
||||
destinationId = command.route.serializer().generateHashCode(),
|
||||
inclusive = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun backToRoot() {
|
||||
navController.popBackStack(
|
||||
destinationId = navController.graph.startDestinationId,
|
||||
inclusive = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun replaceAll(command: NavigationCommand.ReplaceAll) {
|
||||
command.routes.forEachIndexed { index, route ->
|
||||
when (route) {
|
||||
co.electriccoin.zcash.ui.screen.flexa.Flexa -> {
|
||||
if (index == 0) {
|
||||
navController.popBackStack(
|
||||
route = navController.graph.startDestinationId,
|
||||
inclusive = false
|
||||
)
|
||||
}
|
||||
|
||||
if (index != command.routes.lastIndex) {
|
||||
throw UnsupportedOperationException("Flexa can be opened as last screen only")
|
||||
}
|
||||
|
||||
createFlexaFlow(flexaViewModel)
|
||||
}
|
||||
|
||||
is ExternalUrl -> {
|
||||
if (index == 0) {
|
||||
navController.popBackStack(
|
||||
route = navController.graph.startDestinationId,
|
||||
inclusive = false
|
||||
)
|
||||
}
|
||||
|
||||
if (index != command.routes.lastIndex) {
|
||||
throw UnsupportedOperationException("External url can be opened as last screen only")
|
||||
}
|
||||
|
||||
WebBrowserUtil.startActivity(activity, route.url)
|
||||
}
|
||||
|
||||
else -> {
|
||||
navController.executeNavigation(route = route) {
|
||||
if (index == 0) {
|
||||
popUpTo(navController.graph.startDestinationId) {
|
||||
inclusive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replace(command: NavigationCommand.Replace) {
|
||||
command.routes.forEachIndexed { index, route ->
|
||||
when (route) {
|
||||
co.electriccoin.zcash.ui.screen.flexa.Flexa -> {
|
||||
if (index == 0) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
if (index != command.routes.lastIndex) {
|
||||
throw UnsupportedOperationException("Flexa can be opened as last screen only")
|
||||
}
|
||||
|
||||
createFlexaFlow(flexaViewModel)
|
||||
}
|
||||
|
||||
is ExternalUrl -> {
|
||||
if (index == 0) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
if (index != command.routes.lastIndex) {
|
||||
throw UnsupportedOperationException("External url can be opened as last screen only")
|
||||
}
|
||||
|
||||
WebBrowserUtil.startActivity(activity, route.url)
|
||||
}
|
||||
|
||||
else -> {
|
||||
navController.executeNavigation(route = route) {
|
||||
if (index == 0) {
|
||||
popUpTo(navController.currentBackStackEntry?.destination?.id ?: 0) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forward(command: NavigationCommand.Forward) {
|
||||
command.routes.forEach { route ->
|
||||
when (route) {
|
||||
co.electriccoin.zcash.ui.screen.flexa.Flexa -> createFlexaFlow(flexaViewModel)
|
||||
is ExternalUrl -> WebBrowserUtil.startActivity(activity, route.url)
|
||||
else -> navController.executeNavigation(route = route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavHostController.executeNavigation(
|
||||
route: Any,
|
||||
builder: (NavOptionsBuilder.() -> Unit)? = null
|
||||
) {
|
||||
if (route is String) {
|
||||
if (builder == null) {
|
||||
navigate(route)
|
||||
} else {
|
||||
navigate(route, builder)
|
||||
}
|
||||
} else {
|
||||
if (builder == null) {
|
||||
navigate(route)
|
||||
} else {
|
||||
navigate(route, builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFlexaFlow(flexaViewModel: FlexaViewModel) {
|
||||
Flexa.buildSpend()
|
||||
.onTransactionRequest { result ->
|
||||
flexaViewModel.createTransaction(result)
|
||||
}
|
||||
.build()
|
||||
.open(activity)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.common.viewmodel
|
||||
package co.electriccoin.zcash.ui.common.appbar
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -11,12 +11,10 @@ import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
|||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletStateInformationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.AccountSwitchState
|
||||
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.screen.accountlist.AccountList
|
||||
|
@ -31,9 +29,9 @@ import kotlinx.coroutines.flow.flow
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ZashiMainTopAppBarViewModel(
|
||||
class ZashiTopAppBarViewModel(
|
||||
observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase,
|
||||
observeWalletState: ObserveWalletStateUseCase,
|
||||
getWalletStateInformation: GetWalletStateInformationUseCase,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
) : ViewModel() {
|
||||
|
@ -43,7 +41,7 @@ class ZashiMainTopAppBarViewModel(
|
|||
combine(
|
||||
observeSelectedWalletAccount.require(),
|
||||
isHideBalances,
|
||||
observeWalletState()
|
||||
getWalletStateInformation.observe()
|
||||
) { currentAccount, isHideBalances, walletState ->
|
||||
createState(currentAccount, isHideBalances, walletState)
|
||||
}.stateIn(
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
package co.electriccoin.zcash.ui.common.appbar
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.Image
|
||||
|
@ -24,8 +24,11 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState.AccountType
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState.AccountType
|
||||
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiIconButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
|
@ -35,7 +38,7 @@ import co.electriccoin.zcash.ui.design.util.getValue
|
|||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
||||
@Composable
|
||||
fun ZashiMainTopAppBar(
|
||||
fun ZashiTopAppBarWithAccountSelection(
|
||||
state: ZashiMainTopAppBarState?,
|
||||
showHideBalances: Boolean = true
|
||||
) {
|
||||
|
@ -144,7 +147,7 @@ data class AccountSwitchState(
|
|||
@Composable
|
||||
private fun ZashiMainTopAppBarPreview() =
|
||||
ZcashTheme {
|
||||
ZashiMainTopAppBar(
|
||||
ZashiTopAppBarWithAccountSelection(
|
||||
state =
|
||||
ZashiMainTopAppBarState(
|
||||
accountSwitchState =
|
||||
|
@ -163,7 +166,7 @@ private fun ZashiMainTopAppBarPreview() =
|
|||
@Composable
|
||||
private fun KeystoneMainTopAppBarPreview() =
|
||||
ZcashTheme {
|
||||
ZashiMainTopAppBar(
|
||||
ZashiTopAppBarWithAccountSelection(
|
||||
state =
|
||||
ZashiMainTopAppBarState(
|
||||
accountSwitchState =
|
||||
|
@ -182,7 +185,7 @@ private fun KeystoneMainTopAppBarPreview() =
|
|||
@Composable
|
||||
private fun MainTopAppBarWithSubtitlePreview() =
|
||||
ZcashTheme {
|
||||
ZashiMainTopAppBar(
|
||||
ZashiTopAppBarWithAccountSelection(
|
||||
state =
|
||||
ZashiMainTopAppBarState(
|
||||
accountSwitchState =
|
|
@ -0,0 +1,46 @@
|
|||
package co.electriccoin.zcash.ui.common.appbar
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiIconButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.orDark
|
||||
|
||||
@Composable
|
||||
fun ZashiTopAppbar(
|
||||
state: ZashiMainTopAppBarState?,
|
||||
title: StringResource? = null,
|
||||
showHideBalances: Boolean = true,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
ZashiSmallTopAppBar(
|
||||
title = title?.getValue(),
|
||||
subtitle = state?.subtitle?.getValue(),
|
||||
navigationAction = {
|
||||
ZashiTopAppBarBackNavigation(onBack = onBack)
|
||||
},
|
||||
regularActions = {
|
||||
if (state?.balanceVisibilityButton != null && showHideBalances) {
|
||||
Crossfade(state.balanceVisibilityButton, label = "") {
|
||||
ZashiIconButton(it, modifier = Modifier.size(40.dp))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(20.dp))
|
||||
},
|
||||
colors =
|
||||
ZcashTheme.colors.topAppBarColors orDark
|
||||
ZcashTheme.colors.topAppBarColors.copyColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
)
|
||||
}
|
|
@ -1,259 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.extension.toZecStringFull
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.LottieProgress
|
||||
import co.electriccoin.zcash.ui.design.component.Reference
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZecAmountTriple
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.ObserveFiatCurrencyResultFixture
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeBalance
|
||||
|
||||
@Preview(device = Devices.PIXEL_2)
|
||||
@Composable
|
||||
private fun BalanceWidgetPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
@Suppress("MagicNumber")
|
||||
(
|
||||
BalanceWidget(
|
||||
balanceState =
|
||||
BalanceState.Available(
|
||||
totalBalance = Zatoshi(1234567891234567L),
|
||||
spendableBalance = Zatoshi(1234567891234567L),
|
||||
totalShieldedBalance = Zatoshi(0L),
|
||||
exchangeRate = ObserveFiatCurrencyResultFixture.new()
|
||||
),
|
||||
isHideBalances = false,
|
||||
isReferenceToBalances = true,
|
||||
onReferenceClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(device = Devices.PIXEL_2)
|
||||
@Composable
|
||||
private fun BalanceWidgetNotAvailableYetPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
@Suppress("MagicNumber")
|
||||
BalanceWidget(
|
||||
balanceState =
|
||||
BalanceState.Loading(
|
||||
totalBalance = Zatoshi(value = 0L),
|
||||
spendableBalance = Zatoshi(value = 0L),
|
||||
totalShieldedBalance = Zatoshi(0L),
|
||||
exchangeRate = ObserveFiatCurrencyResultFixture.new()
|
||||
),
|
||||
isHideBalances = false,
|
||||
isReferenceToBalances = true,
|
||||
onReferenceClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BalanceWidgetHiddenAmountPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
@Suppress("MagicNumber")
|
||||
BalanceWidget(
|
||||
balanceState =
|
||||
BalanceState.Loading(
|
||||
totalBalance = Zatoshi(0L),
|
||||
spendableBalance = Zatoshi(0L),
|
||||
totalShieldedBalance = Zatoshi(0L),
|
||||
exchangeRate = ObserveFiatCurrencyResultFixture.new()
|
||||
),
|
||||
isHideBalances = true,
|
||||
isReferenceToBalances = true,
|
||||
onReferenceClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface BalanceState {
|
||||
val totalBalance: Zatoshi
|
||||
val totalShieldedBalance: Zatoshi
|
||||
val spendableBalance: Zatoshi
|
||||
val exchangeRate: ExchangeRateState
|
||||
|
||||
data class None(
|
||||
override val exchangeRate: ExchangeRateState
|
||||
) : BalanceState {
|
||||
override val totalBalance: Zatoshi = Zatoshi(0L)
|
||||
override val totalShieldedBalance: Zatoshi = Zatoshi(0L)
|
||||
override val spendableBalance: Zatoshi = Zatoshi(0L)
|
||||
}
|
||||
|
||||
data class Loading(
|
||||
override val totalBalance: Zatoshi,
|
||||
override val spendableBalance: Zatoshi,
|
||||
override val exchangeRate: ExchangeRateState,
|
||||
override val totalShieldedBalance: Zatoshi
|
||||
) : BalanceState
|
||||
|
||||
data class Available(
|
||||
override val totalBalance: Zatoshi,
|
||||
override val spendableBalance: Zatoshi,
|
||||
override val exchangeRate: ExchangeRateState,
|
||||
override val totalShieldedBalance: Zatoshi
|
||||
) : BalanceState
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun BalanceWidget(
|
||||
balanceState: BalanceState,
|
||||
isReferenceToBalances: Boolean,
|
||||
isHideBalances: Boolean,
|
||||
onReferenceClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.wrapContentSize()
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
BalanceWidgetBigLineOnly(
|
||||
isHideBalances = isHideBalances,
|
||||
parts = balanceState.totalBalance.toZecStringFull().asZecAmountTriple()
|
||||
)
|
||||
|
||||
if (balanceState.exchangeRate is ExchangeRateState.Data) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMid))
|
||||
}
|
||||
|
||||
StyledExchangeBalance(
|
||||
zatoshi = balanceState.totalBalance,
|
||||
state = balanceState.exchangeRate,
|
||||
isHideBalances = isHideBalances
|
||||
)
|
||||
|
||||
if (balanceState.exchangeRate is ExchangeRateState.Data) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (isReferenceToBalances) {
|
||||
Reference(
|
||||
text = stringResource(id = co.electriccoin.zcash.ui.R.string.balance_widget_available),
|
||||
onClick = onReferenceClick,
|
||||
fontWeight = FontWeight.Normal,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
vertical = ZcashTheme.dimens.spacingSmall,
|
||||
horizontal = ZcashTheme.dimens.spacingMini,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Body(
|
||||
text = stringResource(id = co.electriccoin.zcash.ui.R.string.balance_widget_available),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
vertical = ZcashTheme.dimens.spacingSmall,
|
||||
horizontal = ZcashTheme.dimens.spacingMini,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
|
||||
|
||||
when (balanceState) {
|
||||
is BalanceState.None, is BalanceState.Loading -> LottieProgress()
|
||||
|
||||
is BalanceState.Available -> {
|
||||
StyledBalance(
|
||||
balanceParts = balanceState.spendableBalance.toZecStringFull().asZecAmountTriple(),
|
||||
isHideBalances = isHideBalances,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.balanceWidgetStyles.third,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.balanceWidgetStyles.fourth
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingMin))
|
||||
|
||||
Body(
|
||||
text = ZcashCurrency.getLocalizedName(LocalContext.current),
|
||||
textFontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BalanceWidgetBigLineOnly(
|
||||
parts: ZecAmountTriple,
|
||||
isHideBalances: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StyledBalance(
|
||||
balanceParts = parts,
|
||||
isHideBalances = isHideBalances,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.balanceWidgetStyles.first,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.balanceWidgetStyles.second
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_zcash_zec_icon),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal.Companion.ONE_HUNDRED_PERCENT
|
||||
import cash.z.ecc.sdk.extension.toPercentageWithDecimal
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
import co.electriccoin.zcash.ui.design.component.SmallLinearProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.isReportable
|
||||
|
||||
@Preview(device = Devices.PIXEL_4_XL)
|
||||
@Composable
|
||||
private fun BalanceWidgetPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SynchronizationStatus(
|
||||
onStatusClick = {},
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SynchronizationStatus(
|
||||
onStatusClick: (StatusAction) -> Unit,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
modifier: Modifier = Modifier,
|
||||
testTag: String? = null,
|
||||
) {
|
||||
val walletDisplayValues =
|
||||
WalletDisplayValues.getNextValues(
|
||||
context = LocalContext.current,
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
) {
|
||||
if (walletDisplayValues.statusText.isNotEmpty()) {
|
||||
BodySmall(
|
||||
text = walletDisplayValues.statusText,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||
.clickable { onStatusClick(walletDisplayValues.statusAction) }
|
||||
.padding(all = ZcashTheme.dimens.spacingSmall)
|
||||
.then(testTag?.let { Modifier.testTag(testTag) } ?: Modifier),
|
||||
)
|
||||
}
|
||||
|
||||
BodySmall(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.balances_status_syncing_percentage,
|
||||
walletSnapshot.progress.toCheckedProgress(walletSnapshot.status)
|
||||
),
|
||||
textFontWeight = FontWeight.Black
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
SmallLinearProgressIndicator(
|
||||
progress = walletSnapshot.progress.decimal,
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val UNCOMPLETED_PERCENTAGE = 0.9999f
|
||||
|
||||
/**
|
||||
* This function ensures that a non finished percentage is returned in case `Synchronizer.Status` is still in the
|
||||
* `SYNCING` state.
|
||||
*
|
||||
* @return String with value 99.99 if the `Synchronizer` is still running, another expected value otherwise.
|
||||
*/
|
||||
private fun PercentDecimal.toCheckedProgress(status: Synchronizer.Status): String =
|
||||
if (status == Synchronizer.Status.SYNCING && this == ONE_HUNDRED_PERCENT) {
|
||||
PercentDecimal(UNCOMPLETED_PERCENTAGE).toPercentageWithDecimal()
|
||||
} else {
|
||||
toPercentageWithDecimal()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusDialog(
|
||||
statusAction: StatusAction.Detailed,
|
||||
onDone: () -> Unit,
|
||||
onReport: (StatusAction.Error) -> Unit,
|
||||
) {
|
||||
AppAlertDialog(
|
||||
title = stringResource(id = R.string.balances_status_error_dialog_title),
|
||||
text = {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = statusAction.details,
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButtonText = stringResource(id = R.string.balances_status_dialog_ok_button),
|
||||
onConfirmButtonClick = onDone,
|
||||
// Add the report button only for the StatusAction.Error type and non-null full stacktrace value
|
||||
dismissButtonText =
|
||||
if (statusAction.isReportable()) {
|
||||
stringResource(id = R.string.balances_status_dialog_report_button)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onDismissButtonClick =
|
||||
if (statusAction.isReportable()) {
|
||||
{ onReport(statusAction as StatusAction.Error) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
|
@ -17,7 +17,6 @@ import co.electriccoin.zcash.spackle.Twig
|
|||
import co.electriccoin.zcash.ui.common.model.SubmitResult
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
|
||||
import co.electriccoin.zcash.ui.screen.balances.DEFAULT_SHIELDING_THRESHOLD
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -92,19 +91,21 @@ class ProposalDataSourceImpl(
|
|||
zip321Uri: String
|
||||
): Zip321TransactionProposal =
|
||||
withContext(Dispatchers.IO) {
|
||||
val synchronizer = synchronizerProvider.getSynchronizer()
|
||||
|
||||
val request =
|
||||
getOrThrow {
|
||||
ZIP321.request(uriString = zip321Uri, validatingRecipients = null)
|
||||
}
|
||||
val payment =
|
||||
when (request) {
|
||||
is ZIP321.ParserResult.Request -> request.paymentRequest.payments[0]
|
||||
else -> throw TransactionProposalNotCreatedException(IllegalArgumentException("Invalid ZIP321 URI"))
|
||||
}
|
||||
|
||||
getOrThrow {
|
||||
val synchronizer = synchronizerProvider.getSynchronizer()
|
||||
|
||||
val request =
|
||||
getOrThrow {
|
||||
ZIP321.request(uriString = zip321Uri, validatingRecipients = null)
|
||||
}
|
||||
val payment =
|
||||
when (request) {
|
||||
is ZIP321.ParserResult.Request -> request.paymentRequest.payments[0]
|
||||
else -> throw TransactionProposalNotCreatedException(
|
||||
IllegalArgumentException("Invalid ZIP321 URI"),
|
||||
)
|
||||
}
|
||||
|
||||
Zip321TransactionProposal(
|
||||
destination =
|
||||
synchronizer
|
||||
|
@ -276,3 +277,5 @@ data class Zip321TransactionProposal(
|
|||
override val memo: Memo,
|
||||
override val proposal: Proposal
|
||||
) : SendTransactionProposal
|
||||
|
||||
private const val DEFAULT_SHIELDING_THRESHOLD = 100000L
|
||||
|
|
|
@ -18,22 +18,7 @@ data class WalletSnapshot(
|
|||
val transparentBalance: Zatoshi,
|
||||
val progress: PercentDecimal,
|
||||
val synchronizerError: SynchronizerError?
|
||||
) {
|
||||
// Note: the wallet's transparent balance is effectively empty if it cannot cover the miner's fee
|
||||
val hasTransparentFunds = transparentBalance.value > 0L
|
||||
|
||||
// Note: the wallet is effectively empty if it cannot cover the miner's fee
|
||||
val hasSaplingFunds = (saplingBalance?.available?.value ?: 0) > 0L
|
||||
|
||||
val hasSaplingBalance = (saplingBalance?.total?.value ?: 0) > 0L
|
||||
|
||||
// Note: the wallet is effectively empty if it cannot cover the miner's fee
|
||||
val hasOrchardFunds = orchardBalance.available.value > 0L
|
||||
|
||||
val hasOrchardBalance = orchardBalance.total.value > 0L
|
||||
|
||||
val isSendEnabled: Boolean get() = hasSaplingFunds && hasOrchardFunds
|
||||
}
|
||||
)
|
||||
|
||||
// TODO [#1370]: WalletSnapshot.canSpend() calculation limitation
|
||||
// TODO [#1370]: https://github.com/Electric-Coin-Company/zashi-android/issues/1370
|
||||
|
@ -43,19 +28,7 @@ fun WalletSnapshot.canSpend(amount: Zatoshi): Boolean = spendableBalance() >= am
|
|||
|
||||
fun WalletSnapshot.totalBalance() = orchardBalance.total + (saplingBalance?.total ?: Zatoshi(0)) + transparentBalance
|
||||
|
||||
fun WalletSnapshot.totalShieldedBalance() = orchardBalance.total + (saplingBalance?.total ?: Zatoshi(0))
|
||||
|
||||
// Note that considering both to be spendable is subject to change.
|
||||
// The user experience could be confusing, and in the future we might prefer to ask users
|
||||
// to transfer their balance to the latest balance type to make it spendable.
|
||||
fun WalletSnapshot.spendableBalance() = orchardBalance.available + (saplingBalance?.available ?: Zatoshi(0))
|
||||
|
||||
// Note that summing both values could be confusing, and we might prefer dividing them in the future
|
||||
fun WalletSnapshot.changePendingBalance() = orchardBalance.changePending + (saplingBalance?.changePending ?: Zatoshi(0))
|
||||
|
||||
fun WalletSnapshot.hasChangePending() = changePendingBalance().value > 0L
|
||||
|
||||
// Note that summing both values could be confusing, and we might prefer dividing them in the future
|
||||
fun WalletSnapshot.valuePendingBalance() = orchardBalance.valuePending + (saplingBalance?.valuePending ?: Zatoshi(0))
|
||||
|
||||
fun WalletSnapshot.hasValuePending() = valuePendingBalance().value > 0L
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.hasChangePending
|
||||
import co.electriccoin.zcash.ui.common.model.hasValuePending
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.totalBalance
|
||||
import co.electriccoin.zcash.ui.common.model.totalShieldedBalance
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
interface BalanceRepository {
|
||||
/**
|
||||
* A flow of the wallet balances state used for the UI layer. It's computed from [WalletSnapshot]'s properties
|
||||
* and provides the result [BalanceState] UI state.
|
||||
*/
|
||||
val state: StateFlow<BalanceState>
|
||||
}
|
||||
|
||||
class BalanceRepositoryImpl(
|
||||
walletRepository: WalletRepository,
|
||||
exchangeRateRepository: ExchangeRateRepository,
|
||||
) : BalanceRepository {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override val state: StateFlow<BalanceState> =
|
||||
combine(
|
||||
walletRepository.currentWalletSnapshot.filterNotNull(),
|
||||
exchangeRateRepository.state,
|
||||
) { snapshot, exchangeRateUsd ->
|
||||
when {
|
||||
// Show the loader only under these conditions:
|
||||
// - Available balance is currently zero AND total balance is non-zero
|
||||
// - And wallet has some ChangePending or ValuePending in progress
|
||||
(
|
||||
snapshot.spendableBalance().value == 0L &&
|
||||
snapshot.totalBalance().value > 0L &&
|
||||
(snapshot.hasChangePending() || snapshot.hasValuePending())
|
||||
) -> {
|
||||
BalanceState.Loading(
|
||||
totalBalance = snapshot.totalBalance(),
|
||||
spendableBalance = snapshot.spendableBalance(),
|
||||
exchangeRate = exchangeRateUsd,
|
||||
totalShieldedBalance = snapshot.totalShieldedBalance()
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
BalanceState.Available(
|
||||
totalBalance = snapshot.totalBalance(),
|
||||
spendableBalance = snapshot.spendableBalance(),
|
||||
exchangeRate = exchangeRateUsd,
|
||||
totalShieldedBalance = snapshot.totalShieldedBalance()
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
scope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
BalanceState.None(ExchangeRateState.OptedOut)
|
||||
)
|
||||
}
|
|
@ -3,10 +3,12 @@ package co.electriccoin.zcash.ui.common.repository
|
|||
import cash.z.ecc.android.sdk.exception.PcztException
|
||||
import cash.z.ecc.android.sdk.model.Pczt
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.ProposalDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.TransactionProposal
|
||||
import co.electriccoin.zcash.ui.common.datasource.TransactionProposalNotCreatedException
|
||||
import co.electriccoin.zcash.ui.common.datasource.Zip321TransactionProposal
|
||||
import co.electriccoin.zcash.ui.common.model.SubmitResult
|
||||
import com.keystone.sdk.KeystoneSDK
|
||||
import com.sparrowwallet.hummingbird.UR
|
||||
|
@ -33,7 +35,7 @@ interface KeystoneProposalRepository {
|
|||
suspend fun createProposal(zecSend: ZecSend)
|
||||
|
||||
@Throws(TransactionProposalNotCreatedException::class)
|
||||
suspend fun createZip321Proposal(zip321Uri: String)
|
||||
suspend fun createZip321Proposal(zip321Uri: String): Zip321TransactionProposal
|
||||
|
||||
@Throws(TransactionProposalNotCreatedException::class)
|
||||
suspend fun createShieldProposal()
|
||||
|
@ -94,8 +96,8 @@ class KeystoneProposalRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun createZip321Proposal(zip321Uri: String) {
|
||||
createProposalInternal {
|
||||
override suspend fun createZip321Proposal(zip321Uri: String): Zip321TransactionProposal {
|
||||
return createProposalInternal {
|
||||
proposalDataSource.createZip321Proposal(
|
||||
account = accountDataSource.getSelectedAccount(),
|
||||
zip321Uri = zip321Uri
|
||||
|
@ -213,15 +215,17 @@ class KeystoneProposalRepositoryImpl(
|
|||
pcztWithSignatures = null
|
||||
}
|
||||
|
||||
private inline fun <T : TransactionProposal> createProposalInternal(block: () -> T) {
|
||||
private inline fun <T : TransactionProposal> createProposalInternal(block: () -> T): T {
|
||||
val proposal =
|
||||
try {
|
||||
block()
|
||||
} catch (e: TransactionProposalNotCreatedException) {
|
||||
Twig.error(e) { "Unable to create proposal" }
|
||||
transactionProposal.update { null }
|
||||
throw e
|
||||
}
|
||||
transactionProposal.update { proposal }
|
||||
return proposal
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.ProposalDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.TransactionProposal
|
||||
import co.electriccoin.zcash.ui.common.datasource.TransactionProposalNotCreatedException
|
||||
import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.Zip321TransactionProposal
|
||||
import co.electriccoin.zcash.ui.common.model.SubmitResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -27,7 +29,7 @@ interface ZashiProposalRepository {
|
|||
suspend fun createProposal(zecSend: ZecSend)
|
||||
|
||||
@Throws(TransactionProposalNotCreatedException::class)
|
||||
suspend fun createZip321Proposal(zip321Uri: String)
|
||||
suspend fun createZip321Proposal(zip321Uri: String): Zip321TransactionProposal
|
||||
|
||||
@Throws(TransactionProposalNotCreatedException::class)
|
||||
suspend fun createShieldProposal()
|
||||
|
@ -63,8 +65,8 @@ class ZashiProposalRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun createZip321Proposal(zip321Uri: String) {
|
||||
createProposalInternal {
|
||||
override suspend fun createZip321Proposal(zip321Uri: String): Zip321TransactionProposal {
|
||||
return createProposalInternal {
|
||||
proposalDataSource.createZip321Proposal(
|
||||
account = accountDataSource.getSelectedAccount(),
|
||||
zip321Uri = zip321Uri
|
||||
|
@ -127,14 +129,16 @@ class ZashiProposalRepositoryImpl(
|
|||
submitState.update { null }
|
||||
}
|
||||
|
||||
private inline fun <T : TransactionProposal> createProposalInternal(block: () -> T) {
|
||||
private inline fun <T : TransactionProposal> createProposalInternal(block: () -> T): T {
|
||||
val proposal =
|
||||
try {
|
||||
block()
|
||||
} catch (e: TransactionProposalNotCreatedException) {
|
||||
Twig.error(e) { "Unable to create proposal" }
|
||||
transactionProposal.update { null }
|
||||
throw e
|
||||
}
|
||||
transactionProposal.update { proposal }
|
||||
return proposal
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.common.usecase
|
|||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
||||
class CancelProposalFlowUseCase(
|
||||
private val keystoneProposalRepository: KeystoneProposalRepository,
|
||||
|
@ -16,6 +17,6 @@ class CancelProposalFlowUseCase(
|
|||
if (clearSendForm) {
|
||||
observeClearSend.requestClear()
|
||||
}
|
||||
navigationRouter.backToRoot()
|
||||
navigationRouter.backTo(Send::class)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.Proposal
|
||||
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.ZecSendExt
|
||||
import cash.z.ecc.android.sdk.model.proposeSend
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.SubmitResult
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRequest
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||
import com.flexa.core.Flexa
|
||||
import com.flexa.spend.Transaction
|
||||
import com.flexa.spend.buildSpend
|
||||
|
||||
class CreateFlexaTransactionUseCase(
|
||||
private val getSynchronizer: GetSynchronizerUseCase,
|
||||
private val getZashiAccount: GetZashiAccountUseCase,
|
||||
private val getSpendingKey: GetZashiSpendingKeyUseCase,
|
||||
private val biometricRepository: BiometricRepository,
|
||||
private val context: Context,
|
||||
) {
|
||||
suspend operator fun invoke(transaction: Result<Transaction>) =
|
||||
runCatching {
|
||||
biometricRepository.requestBiometrics(
|
||||
BiometricRequest(message = stringRes(R.string.integrations_flexa_biometric_message))
|
||||
)
|
||||
Twig.debug { "Getting send transaction proposal" }
|
||||
getSynchronizer()
|
||||
.proposeSend(
|
||||
account = getZashiAccount().sdkAccount,
|
||||
send = getZecSend(transaction.getOrNull())
|
||||
)
|
||||
}.onSuccess { proposal ->
|
||||
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
|
||||
val result = submitTransactions(proposal = proposal, spendingKey = getSpendingKey())
|
||||
when (val output = result.first) {
|
||||
is SubmitResult.Success -> {
|
||||
Twig.debug { "Transaction successful $result" }
|
||||
Flexa.buildSpend()
|
||||
.transactionSent(
|
||||
commerceSessionId = transaction.getOrNull()?.commerceSessionId.orEmpty(),
|
||||
txSignature = result.second.orEmpty()
|
||||
)
|
||||
}
|
||||
|
||||
is SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc -> {
|
||||
Twig.warn { "Transaction grpc failure $result" }
|
||||
Flexa.buildSpend()
|
||||
.transactionSent(
|
||||
commerceSessionId = transaction.getOrNull()?.commerceSessionId.orEmpty(),
|
||||
txSignature = output.result.txIdString()
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Twig.error { "Transaction submission failed" }
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Twig.error(it) { "Transaction proposal failed" }
|
||||
}
|
||||
|
||||
private suspend fun submitTransactions(
|
||||
proposal: Proposal,
|
||||
spendingKey: UnifiedSpendingKey
|
||||
): Pair<SubmitResult, String?> {
|
||||
Twig.debug { "Sending transactions..." }
|
||||
|
||||
val result =
|
||||
runCreateTransactions(
|
||||
synchronizer = getSynchronizer(),
|
||||
spendingKey = spendingKey,
|
||||
proposal = proposal
|
||||
)
|
||||
|
||||
// Triggering the transaction history and balances refresh to be notified immediately
|
||||
// about the wallet's updated state
|
||||
(getSynchronizer() as SdkSynchronizer).run {
|
||||
refreshTransactions()
|
||||
refreshAllBalances()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun runCreateTransactions(
|
||||
synchronizer: Synchronizer,
|
||||
spendingKey: UnifiedSpendingKey,
|
||||
proposal: Proposal
|
||||
): Pair<SubmitResult, String?> {
|
||||
val submitResults = mutableListOf<TransactionSubmitResult>()
|
||||
|
||||
return runCatching {
|
||||
synchronizer.createProposedTransactions(
|
||||
proposal = proposal,
|
||||
usk = spendingKey
|
||||
).collect { submitResult ->
|
||||
Twig.info { "Transaction submit result: $submitResult" }
|
||||
submitResults.add(submitResult)
|
||||
}
|
||||
if (submitResults.find { it is TransactionSubmitResult.Failure } != null) {
|
||||
if (submitResults.size == 1) {
|
||||
// The first transaction submission failed - user might just be able to re-submit the transaction
|
||||
// proposal. Simple error pop up is fine then
|
||||
val result = (submitResults[0] as TransactionSubmitResult.Failure)
|
||||
if (result.grpcError) {
|
||||
SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc(result) to null
|
||||
} else {
|
||||
SubmitResult.SimpleTrxFailure.SimpleTrxFailureSubmit(result) to null
|
||||
}
|
||||
} else {
|
||||
// Any subsequent transaction submission failed - user needs to resolve this manually. Multiple
|
||||
// transaction failure screen presented
|
||||
SubmitResult.MultipleTrxFailure(submitResults) to null
|
||||
}
|
||||
} else {
|
||||
// All transaction submissions were successful
|
||||
SubmitResult.Success(emptyList()) to
|
||||
submitResults.filterIsInstance<TransactionSubmitResult.Success>()
|
||||
.map { it.txIdString() }.firstOrNull()
|
||||
}
|
||||
}.onSuccess {
|
||||
Twig.debug { "Transactions submitted successfully" }
|
||||
}.onFailure {
|
||||
Twig.error(it) { "Transactions submission failed" }
|
||||
}.getOrElse {
|
||||
SubmitResult.SimpleTrxFailure.SimpleTrxFailureOther(it) to null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
private suspend fun getZecSend(transaction: Transaction?): ZecSend {
|
||||
if (transaction == null) throw NullPointerException("Transaction is null")
|
||||
|
||||
val address = transaction.destinationAddress.split(":").last()
|
||||
|
||||
val recipientAddressState =
|
||||
RecipientAddressState.new(
|
||||
address = address,
|
||||
// TODO [#342]: Verify Addresses without Synchronizer
|
||||
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
|
||||
type = getSynchronizer().validateAddress(address)
|
||||
)
|
||||
|
||||
return when (
|
||||
val zecSendValidation =
|
||||
ZecSendExt.new(
|
||||
context = context,
|
||||
destinationString = address,
|
||||
zecString = transaction.amount,
|
||||
// Take memo for a valid non-transparent receiver only
|
||||
memoString = ""
|
||||
)
|
||||
) {
|
||||
is ZecSendExt.ZecSendValidation.Valid ->
|
||||
zecSendValidation.zecSend.copy(
|
||||
destination =
|
||||
when (recipientAddressState.type) {
|
||||
is AddressType.Invalid ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Shielded ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Tex ->
|
||||
WalletAddress.Tex.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Transparent ->
|
||||
WalletAddress.Transparent.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Unified ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
null -> WalletAddress.Unified.new(recipientAddressState.address)
|
||||
}
|
||||
)
|
||||
|
||||
is ZecSendExt.ZecSendValidation.Invalid -> {
|
||||
// We do not expect this validation to fail, so logging is enough here
|
||||
// An error popup could be reasonable here as well
|
||||
Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" }
|
||||
|
||||
throw RuntimeException("Validation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import cash.z.ecc.android.sdk.exception.InitializeException
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
|
||||
import com.keystone.module.ZcashAccount
|
||||
import com.keystone.module.ZcashAccounts
|
||||
|
||||
class CreateKeystoneAccountUseCase(
|
||||
private val accountDataSource: AccountDataSource,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val homeTabNavigationRouter: HomeTabNavigationRouter
|
||||
) {
|
||||
@Throws(InitializeException.ImportAccountException::class)
|
||||
suspend operator fun invoke(
|
||||
|
@ -25,7 +22,6 @@ class CreateKeystoneAccountUseCase(
|
|||
index = account.index.toLong()
|
||||
)
|
||||
accountDataSource.selectAccount(createdAccount)
|
||||
homeTabNavigationRouter.select(HomeScreenIndex.ACCOUNT)
|
||||
navigationRouter.backToRoot()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
||||
|
||||
class CreateZip321ProposalUseCase(
|
||||
private val keystoneProposalRepository: KeystoneProposalRepository,
|
||||
private val zashiProposalRepository: ZashiProposalRepository,
|
||||
private val accountDataSource: AccountDataSource,
|
||||
private val navigationRouter: NavigationRouter
|
||||
) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
suspend operator fun invoke(zip321Uri: String) {
|
||||
try {
|
||||
when (accountDataSource.getSelectedAccount()) {
|
||||
is KeystoneAccount -> {
|
||||
keystoneProposalRepository.createZip321Proposal(zip321Uri)
|
||||
keystoneProposalRepository.createPCZTFromProposal()
|
||||
}
|
||||
is ZashiAccount -> {
|
||||
zashiProposalRepository.createZip321Proposal(zip321Uri)
|
||||
}
|
||||
}
|
||||
navigationRouter.forward(ReviewTransaction)
|
||||
} catch (e: Exception) {
|
||||
keystoneProposalRepository.clear()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,4 +4,6 @@ import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
|||
|
||||
class GetSelectedWalletAccountUseCase(private val accountDataSource: AccountDataSource) {
|
||||
suspend operator fun invoke() = accountDataSource.getSelectedAccount()
|
||||
|
||||
fun observe() = accountDataSource.selectedAccount
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package co.electriccoin.zcash.ui.common.usecase
|
|||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
|
||||
class ObserveWalletStateUseCase(
|
||||
class GetWalletStateInformationUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
) {
|
||||
operator fun invoke() = walletRepository.walletStateInformation
|
||||
fun observe() = walletRepository.walletStateInformation
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
||||
class OnAddressScannedUseCase(
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val prefillSend: PrefillSendUseCase
|
||||
) {
|
||||
operator fun invoke(
|
||||
address: String,
|
||||
addressType: AddressType,
|
||||
scanFlow: Scan
|
||||
) {
|
||||
require(addressType is AddressType.Valid)
|
||||
|
||||
when (scanFlow) {
|
||||
Scan.SEND -> {
|
||||
prefillSend.request(PrefillSendData.FromAddressScan(address = address))
|
||||
navigationRouter.back()
|
||||
}
|
||||
|
||||
Scan.ADDRESS_BOOK -> {
|
||||
navigationRouter.replace(AddContactArgs(address))
|
||||
}
|
||||
|
||||
Scan.HOMEPAGE -> {
|
||||
navigationRouter.replace(
|
||||
Send(
|
||||
address,
|
||||
when (addressType) {
|
||||
AddressType.Shielded -> cash.z.ecc.sdk.model.AddressType.UNIFIED
|
||||
AddressType.Tex -> cash.z.ecc.sdk.model.AddressType.TEX
|
||||
AddressType.Transparent -> cash.z.ecc.sdk.model.AddressType.TRANSPARENT
|
||||
AddressType.Unified -> cash.z.ecc.sdk.model.AddressType.UNIFIED
|
||||
is AddressType.Invalid -> cash.z.ecc.sdk.model.AddressType.UNIFIED
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.sdk.model.AddressType.SAPLING
|
||||
import cash.z.ecc.sdk.model.AddressType.TEX
|
||||
import cash.z.ecc.sdk.model.AddressType.TRANSPARENT
|
||||
import cash.z.ecc.sdk.model.AddressType.UNIFIED
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase.Zip321ParseUriValidation
|
||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan.ADDRESS_BOOK
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan.HOMEPAGE
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan.SEND
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
||||
class OnZip321ScannedUseCase(
|
||||
private val keystoneProposalRepository: KeystoneProposalRepository,
|
||||
private val zashiProposalRepository: ZashiProposalRepository,
|
||||
private val accountDataSource: AccountDataSource,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val prefillSend: PrefillSendUseCase
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
zip321: Zip321ParseUriValidation.Valid,
|
||||
scanFlow: Scan
|
||||
) {
|
||||
if (scanFlow == ADDRESS_BOOK) {
|
||||
navigationRouter.replace(AddContactArgs(zip321.payment.payments[0].recipientAddress.value))
|
||||
} else {
|
||||
createProposal(zip321, scanFlow)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private suspend fun createProposal(
|
||||
zip321: Zip321ParseUriValidation.Valid,
|
||||
scanFlow: Scan
|
||||
) {
|
||||
try {
|
||||
val proposal =
|
||||
when (accountDataSource.getSelectedAccount()) {
|
||||
is KeystoneAccount -> {
|
||||
val result = keystoneProposalRepository.createZip321Proposal(zip321.zip321Uri)
|
||||
keystoneProposalRepository.createPCZTFromProposal()
|
||||
result
|
||||
}
|
||||
|
||||
is ZashiAccount -> {
|
||||
zashiProposalRepository.createZip321Proposal(zip321.zip321Uri)
|
||||
}
|
||||
}
|
||||
|
||||
if (scanFlow == HOMEPAGE) {
|
||||
navigationRouter
|
||||
.replace(
|
||||
Send(
|
||||
recipientAddress = proposal.destination.address,
|
||||
recipientAddressType =
|
||||
when (proposal.destination) {
|
||||
is WalletAddress.Sapling -> SAPLING
|
||||
is WalletAddress.Tex -> TEX
|
||||
is WalletAddress.Transparent -> TRANSPARENT
|
||||
is WalletAddress.Unified -> UNIFIED
|
||||
}
|
||||
),
|
||||
ReviewTransaction
|
||||
)
|
||||
} else if (scanFlow == SEND) {
|
||||
prefillSend.request(
|
||||
PrefillSendData.All(
|
||||
amount = proposal.amount,
|
||||
address = proposal.destination.address,
|
||||
fee = proposal.proposal.totalFeeRequired(),
|
||||
memos = proposal.memo.value.takeIf { it.isNotEmpty() }?.let { listOf(it) }
|
||||
)
|
||||
)
|
||||
navigationRouter.forward(ReviewTransaction)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
keystoneProposalRepository.clear()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
@ -8,14 +9,37 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
class PrefillSendUseCase {
|
||||
private val bus = Channel<DetailedTransactionData>()
|
||||
private val bus = Channel<PrefillSendData>()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
operator fun invoke() = bus.receiveAsFlow()
|
||||
|
||||
fun request(value: DetailedTransactionData) =
|
||||
scope.launch {
|
||||
bus.send(
|
||||
PrefillSendData.All(
|
||||
amount = value.transaction.amount,
|
||||
address = value.recipientAddress?.address,
|
||||
fee = value.transaction.fee,
|
||||
memos = value.memos
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun request(value: PrefillSendData) =
|
||||
scope.launch {
|
||||
bus.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface PrefillSendData {
|
||||
data class All(
|
||||
val amount: Zatoshi,
|
||||
val address: String?,
|
||||
val fee: Zatoshi?,
|
||||
val memos: List<String>?,
|
||||
) : PrefillSendData
|
||||
|
||||
data class FromAddressScan(val address: String) : PrefillSendData
|
||||
}
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
||||
class SendTransactionAgainUseCase(
|
||||
private val prefillSendUseCase: PrefillSendUseCase,
|
||||
private val homeTabNavigationRouter: HomeTabNavigationRouter,
|
||||
private val navigationRouter: NavigationRouter
|
||||
) {
|
||||
operator fun invoke(value: DetailedTransactionData) {
|
||||
homeTabNavigationRouter.select(HomeScreenIndex.SEND)
|
||||
prefillSendUseCase.request(value)
|
||||
navigationRouter.backToRoot()
|
||||
navigationRouter.forward(Send())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
|
||||
import co.electriccoin.zcash.ui.screen.transactiondetail.TransactionDetail
|
||||
|
||||
class ViewTransactionDetailAfterSuccessfulProposalUseCase(
|
||||
private val keystoneProposalRepository: KeystoneProposalRepository,
|
||||
private val zashiProposalRepository: ZashiProposalRepository,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val homeTabNavigationRouter: HomeTabNavigationRouter,
|
||||
private val observeClearSend: ObserveClearSendUseCase,
|
||||
) {
|
||||
operator fun invoke(txId: String) {
|
||||
zashiProposalRepository.clear()
|
||||
keystoneProposalRepository.clear()
|
||||
observeClearSend.requestClear()
|
||||
homeTabNavigationRouter.select(HomeScreenIndex.ACCOUNT)
|
||||
navigationRouter.replaceAll(TransactionDetail(txId))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository
|
||||
|
||||
|
@ -9,12 +8,10 @@ class ViewTransactionsAfterSuccessfulProposalUseCase(
|
|||
private val keystoneProposalRepository: KeystoneProposalRepository,
|
||||
private val zashiProposalRepository: ZashiProposalRepository,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val observeClearSend: ObserveClearSendUseCase
|
||||
) {
|
||||
operator fun invoke() {
|
||||
zashiProposalRepository.clear()
|
||||
keystoneProposalRepository.clear()
|
||||
observeClearSend.requestClear()
|
||||
navigationRouter.newRoot(HOME)
|
||||
navigationRouter.backToRoot()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import PaymentRequest
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.zecdev.zip321.ZIP321
|
||||
|
||||
internal class Zip321ParseUriValidationUseCase(
|
||||
class Zip321ParseUriValidationUseCase(
|
||||
private val getSynchronizerUseCase: GetSynchronizerUseCase
|
||||
) {
|
||||
operator fun invoke(zip321Uri: String) = validateZip321Uri(zip321Uri)
|
||||
|
@ -24,6 +25,7 @@ internal class Zip321ParseUriValidationUseCase(
|
|||
Twig.error { "Address from Zip321 validation failed: ${validation.reason}" }
|
||||
false
|
||||
}
|
||||
|
||||
else -> {
|
||||
validation is AddressType.Valid
|
||||
}
|
||||
|
@ -41,15 +43,19 @@ internal class Zip321ParseUriValidationUseCase(
|
|||
Twig.info { "Payment Request Zip321 validation result: $paymentRequest." }
|
||||
|
||||
return when (paymentRequest) {
|
||||
is ZIP321.ParserResult.Request -> Zip321ParseUriValidation.Valid(zip321Uri)
|
||||
is ZIP321.ParserResult.Request -> Zip321ParseUriValidation.Valid(zip321Uri, paymentRequest.paymentRequest)
|
||||
is ZIP321.ParserResult.SingleAddress ->
|
||||
Zip321ParseUriValidation.SingleAddress(paymentRequest.singleRecipient.value)
|
||||
|
||||
else -> Zip321ParseUriValidation.Invalid
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Zip321ParseUriValidation {
|
||||
data class Valid(val zip321Uri: String) : Zip321ParseUriValidation()
|
||||
sealed class Zip321ParseUriValidation {
|
||||
data class Valid(
|
||||
val zip321Uri: String,
|
||||
val payment: PaymentRequest,
|
||||
) : Zip321ParseUriValidation()
|
||||
|
||||
data class SingleAddress(val address: String) : Zip321ParseUriValidation()
|
||||
|
||||
|
|
|
@ -12,14 +12,11 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeViewModel(
|
||||
private val observeConfiguration: ObserveConfigurationUseCase,
|
||||
class OldHomeViewModel(
|
||||
observeConfiguration: ObserveConfigurationUseCase,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
) : ViewModel() {
|
||||
/**
|
||||
|
@ -28,39 +25,13 @@ class HomeViewModel(
|
|||
val isBackgroundSyncEnabled: StateFlow<Boolean?> =
|
||||
booleanStateFlow(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED)
|
||||
|
||||
/**
|
||||
* A flow of whether keep screen on while syncing is on or off
|
||||
*/
|
||||
val isKeepScreenOnWhileSyncing: StateFlow<Boolean?> =
|
||||
booleanStateFlow(StandardPreferenceKeys.IS_KEEP_SCREEN_ON_DURING_SYNC)
|
||||
|
||||
/**
|
||||
* A flow of whether the app presented the user with restore success screem
|
||||
*/
|
||||
val isRestoreSuccessSeen: StateFlow<Boolean?> =
|
||||
booleanStateFlow(StandardPreferenceKeys.IS_RESTORING_INITIAL_WARNING_SEEN)
|
||||
|
||||
fun setRestoringInitialWarningSeen() {
|
||||
setBooleanPreference(StandardPreferenceKeys.IS_RESTORING_INITIAL_WARNING_SEEN, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow of the wallet balances visibility.
|
||||
*/
|
||||
val isHideBalances: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_HIDE_BALANCES)
|
||||
|
||||
fun showOrHideBalances() {
|
||||
viewModelScope.launch {
|
||||
setBooleanPreference(StandardPreferenceKeys.IS_HIDE_BALANCES, isHideBalances.filterNotNull().first().not())
|
||||
}
|
||||
}
|
||||
|
||||
val configurationFlow: StateFlow<Configuration?> = observeConfiguration()
|
||||
|
||||
//
|
||||
// PRIVATE HELPERS
|
||||
//
|
||||
|
||||
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
|
||||
flow<Boolean?> {
|
||||
emitAll(default.observe(standardPreferenceProvider()))
|
||||
|
@ -69,13 +40,4 @@ class HomeViewModel(
|
|||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
null
|
||||
)
|
||||
|
||||
private fun setBooleanPreference(
|
||||
default: BooleanPreferenceDefault,
|
||||
newState: Boolean
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
default.putValue(standardPreferenceProvider(), newState)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ import co.electriccoin.zcash.ui.common.model.OnboardingState
|
|||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||
|
@ -42,7 +41,6 @@ import kotlinx.coroutines.launch
|
|||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
class WalletViewModel(
|
||||
application: Application,
|
||||
balanceRepository: BalanceRepository,
|
||||
private val walletCoordinator: WalletCoordinator,
|
||||
private val walletRepository: WalletRepository,
|
||||
private val exchangeRateRepository: ExchangeRateRepository,
|
||||
|
@ -55,8 +53,6 @@ class WalletViewModel(
|
|||
) : AndroidViewModel(application) {
|
||||
val synchronizer = walletRepository.synchronizer
|
||||
|
||||
val walletRestoringState = walletRepository.walletRestoringState
|
||||
|
||||
val walletStateInformation = walletRepository.walletStateInformation
|
||||
|
||||
val secretState: StateFlow<SecretState> = walletRepository.secretState
|
||||
|
@ -67,12 +63,6 @@ class WalletViewModel(
|
|||
|
||||
val exchangeRateUsd = exchangeRateRepository.state
|
||||
|
||||
val balanceState = balanceRepository.state
|
||||
|
||||
fun refreshExchangeRateUsd() {
|
||||
exchangeRateRepository.refreshExchangeRateUsd()
|
||||
}
|
||||
|
||||
fun optInExchangeRateUsd(optIn: Boolean) {
|
||||
exchangeRateRepository.optInExchangeRateUsd(optIn)
|
||||
}
|
||||
|
@ -111,21 +101,6 @@ class WalletViewModel(
|
|||
walletRepository.persistOnboardingState(onboardingState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously notes that the wallet has completed the initial wallet restoring block synchronization run.
|
||||
*
|
||||
* Note that in the current SDK implementation, we don't have any information about the block synchronization
|
||||
* state from the SDK, and thus, we need to note the wallet restoring state here on the client side.
|
||||
*/
|
||||
fun persistWalletRestoringState(walletRestoringState: WalletRestoringState) {
|
||||
viewModelScope.launch {
|
||||
StandardPreferenceKeys.WALLET_RESTORING_STATE.putValue(
|
||||
standardPreferenceProvider(),
|
||||
walletRestoringState.toNumber()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun persistExistingWalletWithSeedPhrase(
|
||||
network: ZcashNetwork,
|
||||
seedPhrase: SeedPhrase,
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
package co.electriccoin.zcash.ui.fixture
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceState
|
||||
|
||||
object BalanceStateFixture {
|
||||
private const val BALANCE_VALUE = 0L
|
||||
|
||||
val TOTAL_BALANCE = Zatoshi(BALANCE_VALUE)
|
||||
val TOTAL_SHIELDED_BALANCE = Zatoshi(BALANCE_VALUE)
|
||||
val SPENDABLE_BALANCE = Zatoshi(BALANCE_VALUE)
|
||||
|
||||
fun new(
|
||||
totalBalance: Zatoshi = TOTAL_BALANCE,
|
||||
totalShieldedBalance: Zatoshi = TOTAL_SHIELDED_BALANCE,
|
||||
spendableBalance: Zatoshi = SPENDABLE_BALANCE,
|
||||
exchangeRate: ExchangeRateState = ObserveFiatCurrencyResultFixture.new()
|
||||
) = BalanceState.Available(
|
||||
totalBalance = totalBalance,
|
||||
spendableBalance = spendableBalance,
|
||||
exchangeRate = exchangeRate,
|
||||
totalShieldedBalance = totalShieldedBalance
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.fixture
|
||||
|
||||
import co.electriccoin.zcash.ui.common.appbar.AccountSwitchState
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.AccountSwitchState
|
||||
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
||||
object ZashiMainTopAppBarStateFixture {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.account
|
||||
|
||||
/**
|
||||
* These are only used for automated testing.
|
||||
*/
|
||||
object AccountTag {
|
||||
const val BALANCE_VIEWS = "balance_views"
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.account
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.screen.account.view.Account
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
|
||||
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
|
||||
import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType
|
||||
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetViewModel
|
||||
import co.electriccoin.zcash.ui.util.EmailUtil
|
||||
import co.electriccoin.zcash.ui.util.PlayStoreUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun WrapAccount(goBalances: () -> Unit) {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
|
||||
val homeViewModel = koinActivityViewModel<HomeViewModel>()
|
||||
|
||||
val topAppBarViewModel = koinActivityViewModel<ZashiMainTopAppBarViewModel>()
|
||||
|
||||
val supportViewModel = koinActivityViewModel<SupportViewModel>()
|
||||
|
||||
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
||||
|
||||
val balanceState = walletViewModel.balanceState.collectAsStateWithLifecycle().value
|
||||
|
||||
val walletSnapshot = walletViewModel.currentWalletSnapshot.collectAsStateWithLifecycle().value
|
||||
|
||||
val isHideBalances = homeViewModel.isHideBalances.collectAsStateWithLifecycle().value ?: false
|
||||
|
||||
val supportInfo = supportViewModel.supportInfo.collectAsStateWithLifecycle().value
|
||||
|
||||
val topAppBarState by topAppBarViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
|
||||
|
||||
WrapAccount(
|
||||
balanceState = balanceState,
|
||||
goBalances = goBalances,
|
||||
isHideBalances = isHideBalances,
|
||||
supportInfo = supportInfo,
|
||||
synchronizer = synchronizer,
|
||||
walletSnapshot = walletSnapshot,
|
||||
zashiMainTopAppBarState = topAppBarState,
|
||||
walletRestoringState = walletRestoringState,
|
||||
)
|
||||
|
||||
// For benchmarking purposes
|
||||
activity.reportFullyDrawn()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@VisibleForTesting
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
internal fun WrapAccount(
|
||||
balanceState: BalanceState,
|
||||
goBalances: () -> Unit,
|
||||
isHideBalances: Boolean,
|
||||
synchronizer: Synchronizer?,
|
||||
supportInfo: SupportInfo?,
|
||||
walletSnapshot: WalletSnapshot?,
|
||||
zashiMainTopAppBarState: ZashiMainTopAppBarState?,
|
||||
walletRestoringState: WalletRestoringState,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val transactionHistoryWidgetViewModel = koinViewModel<TransactionHistoryWidgetViewModel>()
|
||||
|
||||
val transactionHistoryWidgetState by transactionHistoryWidgetViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
// We could also improve this by `rememberSaveable` to preserve the dialog after a configuration change. But the
|
||||
// dialog dismissing in such cases is not critical, and it would require creating StatusAction custom Saver
|
||||
val showStatusDialog = remember { mutableStateOf<StatusAction.Detailed?>(null) }
|
||||
|
||||
if (null == synchronizer || null == walletSnapshot) {
|
||||
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
||||
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
|
||||
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
|
||||
CircularScreenProgressIndicator()
|
||||
} else {
|
||||
Account(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
showStatusDialog = showStatusDialog.value,
|
||||
hideStatusDialog = { showStatusDialog.value = null },
|
||||
onContactSupport = { status ->
|
||||
val fullMessage =
|
||||
EmailUtil.formatMessage(
|
||||
body = status.fullStackTrace,
|
||||
supportInfo = supportInfo?.toSupportString(SupportInfoType.entries.toSet())
|
||||
)
|
||||
val mailIntent =
|
||||
EmailUtil.newMailActivityIntent(
|
||||
context.getString(R.string.support_email_address),
|
||||
context.getString(R.string.app_name),
|
||||
fullMessage
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
runCatching {
|
||||
context.startActivity(mailIntent)
|
||||
}.onFailure {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.unable_to_open_email)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
goBalances = goBalances,
|
||||
snackbarHostState = snackbarHostState,
|
||||
zashiMainTopAppBarState = zashiMainTopAppBarState,
|
||||
transactionHistoryWidgetState = transactionHistoryWidgetState,
|
||||
isWalletRestoringState = walletRestoringState,
|
||||
onStatusClick = { status ->
|
||||
when (status) {
|
||||
is StatusAction.Detailed -> showStatusDialog.value = status
|
||||
StatusAction.AppUpdate -> {
|
||||
openPlayStoreAppSite(
|
||||
context = context,
|
||||
snackbarHostState = snackbarHostState,
|
||||
scope = scope
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// No action required
|
||||
}
|
||||
}
|
||||
},
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPlayStoreAppSite(
|
||||
context: Context,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val storeIntent = PlayStoreUtil.newActivityIntent(context)
|
||||
runCatching {
|
||||
context.startActivity(storeIntent)
|
||||
}.onFailure {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.unable_to_open_play_store)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.account
|
||||
|
||||
/**
|
||||
* These are only used for automated testing.
|
||||
*/
|
||||
object HistoryTag {
|
||||
const val TRANSACTION_LIST = "transaction_list"
|
||||
const val TRANSACTION_ITEM_TITLE = "transaction_item_title"
|
||||
const val TRANSACTION_ID = "transaction_id"
|
||||
const val PROGRESS = "progress_bar"
|
||||
}
|
|
@ -1,280 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.account.view
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.compose.StatusDialog
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
|
||||
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.account.AccountTag
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeOptIn
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetState
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.createTransactionHistoryWidgets
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun HistoryLoadingComposablePreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
Account(
|
||||
balanceState =
|
||||
BalanceStateFixture.new(
|
||||
exchangeRate = ExchangeRateState.OptIn(onDismissClick = {})
|
||||
),
|
||||
isHideBalances = false,
|
||||
goBalances = {},
|
||||
hideStatusDialog = {},
|
||||
onContactSupport = {},
|
||||
showStatusDialog = null,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new(),
|
||||
transactionHistoryWidgetState = TransactionHistoryWidgetStateFixture.new(),
|
||||
onStatusClick = {},
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
isWalletRestoringState = WalletRestoringState.SYNCING,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
@PreviewScreens
|
||||
private fun HistoryListComposablePreview() {
|
||||
ZcashTheme {
|
||||
@Suppress("MagicNumber")
|
||||
Account(
|
||||
balanceState =
|
||||
BalanceState.Available(
|
||||
totalBalance = Zatoshi(value = 123_000_000L),
|
||||
spendableBalance = Zatoshi(value = 123_000_000L),
|
||||
totalShieldedBalance = Zatoshi(value = 123_000_000L),
|
||||
exchangeRate =
|
||||
ExchangeRateState.Data(
|
||||
isLoading = false,
|
||||
isRefreshEnabled = true,
|
||||
currencyConversion =
|
||||
FiatCurrencyConversion(
|
||||
timestamp = Clock.System.now(),
|
||||
priceOfZec = 25.0
|
||||
)
|
||||
) {}
|
||||
),
|
||||
isHideBalances = false,
|
||||
goBalances = {},
|
||||
hideStatusDialog = {},
|
||||
onContactSupport = {},
|
||||
showStatusDialog = null,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new(),
|
||||
transactionHistoryWidgetState = TransactionHistoryWidgetStateFixture.new(),
|
||||
onStatusClick = {},
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
isWalletRestoringState = WalletRestoringState.SYNCING,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun Account(
|
||||
balanceState: BalanceState,
|
||||
goBalances: () -> Unit,
|
||||
isHideBalances: Boolean,
|
||||
hideStatusDialog: () -> Unit,
|
||||
onContactSupport: (StatusAction.Error) -> Unit,
|
||||
showStatusDialog: StatusAction.Detailed?,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
zashiMainTopAppBarState: ZashiMainTopAppBarState?,
|
||||
transactionHistoryWidgetState: TransactionHistoryWidgetState,
|
||||
isWalletRestoringState: WalletRestoringState,
|
||||
onStatusClick: (StatusAction) -> Unit,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
) {
|
||||
BlankBgScaffold(
|
||||
topBar = {
|
||||
ZashiMainTopAppBar(zashiMainTopAppBarState)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState)
|
||||
},
|
||||
) { paddingValues ->
|
||||
AccountMainContent(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
goBalances = goBalances,
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
top = paddingValues.calculateTopPadding() + ZashiDimensions.Spacing.spacingLg,
|
||||
// We intentionally do not set the bottom and horizontal paddings here. Those are set by the
|
||||
// underlying transaction history composable
|
||||
),
|
||||
paddingValues = paddingValues,
|
||||
transactionHistoryWidgetState = transactionHistoryWidgetState,
|
||||
isWalletRestoringState = isWalletRestoringState,
|
||||
onStatusClick = onStatusClick,
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
|
||||
// Show synchronization status popup
|
||||
if (showStatusDialog != null) {
|
||||
StatusDialog(
|
||||
statusAction = showStatusDialog,
|
||||
onDone = hideStatusDialog,
|
||||
onReport = { status ->
|
||||
hideStatusDialog()
|
||||
onContactSupport(status)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList", "ModifierNotUsedAtRoot", "LongMethod")
|
||||
private fun AccountMainContent(
|
||||
balanceState: BalanceState,
|
||||
goBalances: () -> Unit,
|
||||
isHideBalances: Boolean,
|
||||
transactionHistoryWidgetState: TransactionHistoryWidgetState,
|
||||
isWalletRestoringState: WalletRestoringState,
|
||||
onStatusClick: (StatusAction) -> Unit,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
modifier: Modifier = Modifier,
|
||||
paddingValues: PaddingValues = PaddingValues(),
|
||||
) {
|
||||
Box {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
val bottomPadding =
|
||||
animateDpAsState(
|
||||
targetValue =
|
||||
if (balanceState.exchangeRate is ExchangeRateState.OptIn) {
|
||||
112.dp
|
||||
} else {
|
||||
0.dp
|
||||
},
|
||||
animationSpec =
|
||||
if (balanceState.exchangeRate is ExchangeRateState.OptIn) {
|
||||
snap()
|
||||
} else {
|
||||
spring(visibilityThreshold = .1.dp)
|
||||
},
|
||||
label = "bottom padding animation"
|
||||
)
|
||||
|
||||
BalancesStatus(
|
||||
balanceState = balanceState,
|
||||
goBalances = goBalances,
|
||||
isHideBalances = isHideBalances,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
|
||||
end = ZcashTheme.dimens.screenHorizontalSpacingRegular,
|
||||
bottom = bottomPadding.value
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
createRestoringProgressView(
|
||||
onStatusClick = onStatusClick,
|
||||
walletRestoringState = isWalletRestoringState,
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
|
||||
createTransactionHistoryWidgets(
|
||||
state = transactionHistoryWidgetState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = balanceState.exchangeRate is ExchangeRateState.OptIn,
|
||||
enter = EnterTransition.None,
|
||||
exit = fadeOut() + slideOutVertically(),
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(100.dp + paddingValues.calculateTopPadding()))
|
||||
StyledExchangeOptIn(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
state =
|
||||
(balanceState.exchangeRate as? ExchangeRateState.OptIn) ?: ExchangeRateState.OptIn(
|
||||
onDismissClick = {},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BalancesStatus(
|
||||
balanceState: BalanceState,
|
||||
goBalances: () -> Unit,
|
||||
isHideBalances: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
modifier.then(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(AccountTag.BALANCE_VIEWS)
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
BalanceWidget(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
isReferenceToBalances = true,
|
||||
onReferenceClick = goBalances,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.account.view
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.common.compose.SynchronizationStatus
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalancesTag
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
|
||||
|
||||
internal fun LazyListScope.createRestoringProgressView(
|
||||
onStatusClick: (StatusAction) -> Unit,
|
||||
walletRestoringState: WalletRestoringState,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
) {
|
||||
if (walletRestoringState == WalletRestoringState.RESTORING) {
|
||||
item {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillParentMaxWidth()
|
||||
.background(color = ZcashTheme.colors.historySyncingColor)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
// Do not calculate and use the app update information here, as the sync bar won't be displayed after
|
||||
// the wallet is fully restored
|
||||
SynchronizationStatus(
|
||||
onStatusClick = onStatusClick,
|
||||
testTag = BalancesTag.STATUS,
|
||||
walletSnapshot = walletSnapshot,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = ZcashTheme.dimens.spacingDefault)
|
||||
.animateContentSize()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -104,6 +104,11 @@ class AccountListViewModel(
|
|||
bottomSheetHiddenResponse.first()
|
||||
}
|
||||
|
||||
private fun onBottomSheetHidden() =
|
||||
viewModelScope.launch {
|
||||
bottomSheetHiddenResponse.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onAccountClicked(account: WalletAccount) =
|
||||
viewModelScope.launch {
|
||||
selectWalletAccount(account) { hideBottomSheet() }
|
||||
|
@ -115,11 +120,6 @@ class AccountListViewModel(
|
|||
navigationRouter.forward(ConnectKeystone)
|
||||
}
|
||||
|
||||
private fun onBottomSheetHidden() =
|
||||
viewModelScope.launch {
|
||||
bottomSheetHiddenResponse.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onBack() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
|
|
|
@ -15,7 +15,7 @@ import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookItem
|
|||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookState
|
||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.contact.UpdateContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
|
@ -85,7 +85,7 @@ class AddressBookViewModel(
|
|||
|
||||
private fun onAddContactManuallyClick() = navigationRouter.forward(AddContactArgs(null))
|
||||
|
||||
private fun onScanContactClick() = navigationRouter.forward(ScanNavigationArgs(ScanNavigationArgs.ADDRESS_BOOK))
|
||||
private fun onScanContactClick() = navigationRouter.forward(Scan(Scan.ADDRESS_BOOK))
|
||||
}
|
||||
|
||||
internal const val ADDRESS_MAX_LENGTH = 20
|
||||
|
|
|
@ -20,7 +20,7 @@ import co.electriccoin.zcash.ui.design.util.stringRes
|
|||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookItem
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookState
|
||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
|
@ -174,5 +174,5 @@ class SelectRecipientViewModel(
|
|||
|
||||
private fun onAddContactManuallyClick() = navigationRouter.forward(AddContactArgs(null))
|
||||
|
||||
private fun onScanContactClick() = navigationRouter.forward(ScanNavigationArgs(ScanNavigationArgs.ADDRESS_BOOK))
|
||||
private fun onScanContactClick() = navigationRouter.forward(Scan(Scan.ADDRESS_BOOK))
|
||||
}
|
||||
|
|
|
@ -1,340 +0,0 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.balances
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.SubmitResult
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneShieldProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiSpendingKeyUseCase
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
|
||||
import co.electriccoin.zcash.ui.screen.balances.view.Balances
|
||||
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
|
||||
import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType
|
||||
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
|
||||
import co.electriccoin.zcash.ui.util.EmailUtil
|
||||
import co.electriccoin.zcash.ui.util.PlayStoreUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
internal fun WrapBalances() {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
|
||||
val createTransactionsViewModel = koinActivityViewModel<CreateTransactionsViewModel>()
|
||||
|
||||
val homeViewModel = koinActivityViewModel<HomeViewModel>()
|
||||
|
||||
val supportViewModel = koinActivityViewModel<SupportViewModel>()
|
||||
|
||||
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
||||
|
||||
val walletSnapshot = walletViewModel.currentWalletSnapshot.collectAsStateWithLifecycle().value
|
||||
|
||||
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
|
||||
|
||||
val isHideBalances = homeViewModel.isHideBalances.collectAsStateWithLifecycle().value ?: false
|
||||
|
||||
val balanceState = walletViewModel.balanceState.collectAsStateWithLifecycle().value
|
||||
|
||||
val supportInfo = supportViewModel.supportInfo.collectAsStateWithLifecycle().value
|
||||
|
||||
val zashiMainTopAppBarViewModel = koinActivityViewModel<ZashiMainTopAppBarViewModel>()
|
||||
|
||||
val zashiMainTopAppBarState = zashiMainTopAppBarViewModel.state.collectAsStateWithLifecycle().value
|
||||
|
||||
WrapBalances(
|
||||
balanceState = balanceState,
|
||||
createTransactionsViewModel = createTransactionsViewModel,
|
||||
isHideBalances = isHideBalances,
|
||||
lifecycleScope = activity.lifecycleScope,
|
||||
supportInfo = supportInfo,
|
||||
synchronizer = synchronizer,
|
||||
walletSnapshot = walletSnapshot,
|
||||
walletRestoringState = walletRestoringState,
|
||||
zashiMainTopAppBarState = zashiMainTopAppBarState
|
||||
)
|
||||
}
|
||||
|
||||
const val DEFAULT_SHIELDING_THRESHOLD = 100000L
|
||||
|
||||
// This function should be refactored into smaller chunks
|
||||
@Composable
|
||||
@VisibleForTesting
|
||||
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
|
||||
internal fun WrapBalances(
|
||||
balanceState: BalanceState,
|
||||
createTransactionsViewModel: CreateTransactionsViewModel,
|
||||
lifecycleScope: CoroutineScope,
|
||||
isHideBalances: Boolean,
|
||||
supportInfo: SupportInfo?,
|
||||
synchronizer: Synchronizer?,
|
||||
walletSnapshot: WalletSnapshot?,
|
||||
walletRestoringState: WalletRestoringState,
|
||||
zashiMainTopAppBarState: ZashiMainTopAppBarState?
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val (shieldState, setShieldState) =
|
||||
rememberSaveable(walletSnapshot?.isZashi, stateSaver = ShieldState.Saver) { mutableStateOf(ShieldState.None) }
|
||||
|
||||
// Keep the state always up-to-date with the latest transparent balance
|
||||
LaunchedEffect(shieldState, walletSnapshot) {
|
||||
setShieldState(updateTransparentBalanceState(shieldState, walletSnapshot))
|
||||
}
|
||||
|
||||
val (isShowingErrorDialog, setShowErrorDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun showShieldingSuccess() {
|
||||
setShieldState(ShieldState.Shielded)
|
||||
Toast.makeText(context, context.getString(R.string.balances_shielding_successful), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
suspend fun showShieldingError(shieldingState: ShieldState) {
|
||||
Twig.error { "Shielding proposal failed with: $shieldingState" }
|
||||
|
||||
// Adding the extra delay before notifying UI for a better UX
|
||||
@Suppress("MagicNumber")
|
||||
delay(1000)
|
||||
|
||||
setShieldState(shieldingState)
|
||||
setShowErrorDialog(true)
|
||||
}
|
||||
|
||||
// We could also improve this by `rememberSaveable` to preserve the dialog after a configuration change. But the
|
||||
// dialog dismissing in such cases is not critical, and it would require creating StatusAction custom Saver
|
||||
val showStatusDialog = remember { mutableStateOf<StatusAction.Detailed?>(null) }
|
||||
|
||||
val getZashiSpendingKey = koinInject<GetZashiSpendingKeyUseCase>()
|
||||
val getSelectedWalletAccount = koinInject<GetSelectedWalletAccountUseCase>()
|
||||
val createKeystoneShieldProposal = koinInject<CreateKeystoneShieldProposalUseCase>()
|
||||
|
||||
if (null == synchronizer || null == walletSnapshot) {
|
||||
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
||||
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
|
||||
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
|
||||
CircularScreenProgressIndicator()
|
||||
} else {
|
||||
Balances(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
isShowingErrorDialog = isShowingErrorDialog,
|
||||
setShowErrorDialog = setShowErrorDialog,
|
||||
showStatusDialog = showStatusDialog.value,
|
||||
hideStatusDialog = { showStatusDialog.value = null },
|
||||
snackbarHostState = snackbarHostState,
|
||||
onShielding = {
|
||||
lifecycleScope.launch {
|
||||
when (val account = getSelectedWalletAccount()) {
|
||||
is KeystoneAccount -> {
|
||||
try {
|
||||
createKeystoneShieldProposal()
|
||||
} catch (_: Exception) {
|
||||
showShieldingError(
|
||||
ShieldState.Failed(
|
||||
error =
|
||||
context.getString(
|
||||
R.string.balances_shielding_dialog_error_text_below_threshold
|
||||
),
|
||||
stackTrace = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is ZashiAccount -> {
|
||||
val spendingKey = getZashiSpendingKey()
|
||||
setShieldState(ShieldState.Running)
|
||||
|
||||
Twig.debug { "Shielding transparent funds" }
|
||||
|
||||
runCatching {
|
||||
synchronizer.proposeShielding(
|
||||
account = account.sdkAccount,
|
||||
shieldingThreshold = Zatoshi(DEFAULT_SHIELDING_THRESHOLD),
|
||||
// Using empty string for memo to clear the default memo prefix value defined in
|
||||
// the SDK
|
||||
memo = "",
|
||||
// Using null will select whichever of the account's trans. receivers has funds
|
||||
// to shield
|
||||
transparentReceiver = null
|
||||
)
|
||||
}.onSuccess { newProposal ->
|
||||
Twig.info { "Shielding proposal result: ${newProposal?.toPrettyString()}" }
|
||||
|
||||
if (newProposal == null) {
|
||||
showShieldingError(
|
||||
ShieldState.Failed(
|
||||
error =
|
||||
context.getString(
|
||||
R.string.balances_shielding_dialog_error_text_below_threshold
|
||||
),
|
||||
stackTrace = ""
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val result =
|
||||
createTransactionsViewModel.runCreateTransactions(
|
||||
synchronizer = synchronizer,
|
||||
spendingKey = spendingKey,
|
||||
proposal = newProposal
|
||||
)
|
||||
|
||||
// Triggering the transaction history and balances refresh to be notified
|
||||
// immediately about the wallet's updated state
|
||||
(synchronizer as SdkSynchronizer).run {
|
||||
refreshTransactions()
|
||||
refreshAllBalances()
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is SubmitResult.Success -> {
|
||||
Twig.info { "Shielding transaction done successfully" }
|
||||
showShieldingSuccess()
|
||||
}
|
||||
is SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc -> {
|
||||
Twig.warn { "Shielding transaction failed" }
|
||||
showShieldingError(ShieldState.FailedGrpc)
|
||||
}
|
||||
is SubmitResult.SimpleTrxFailure -> {
|
||||
Twig.warn { "Shielding transaction failed" }
|
||||
showShieldingError(
|
||||
ShieldState.Failed(
|
||||
error = result.toErrorMessage(),
|
||||
stackTrace = result.toErrorStacktrace()
|
||||
)
|
||||
)
|
||||
}
|
||||
is SubmitResult.MultipleTrxFailure -> {
|
||||
Twig.warn {
|
||||
"Shielding failed with multi-transactions-submission-error handling"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
showShieldingError(
|
||||
ShieldState.Failed(
|
||||
error = it.message ?: "Unknown error",
|
||||
stackTrace = it.stackTraceToString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onStatusClick = { status ->
|
||||
when (status) {
|
||||
is StatusAction.Detailed -> showStatusDialog.value = status
|
||||
StatusAction.AppUpdate -> {
|
||||
openPlayStoreAppSite(
|
||||
context = context,
|
||||
snackbarHostState = snackbarHostState,
|
||||
scope = scope
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// No action required
|
||||
}
|
||||
}
|
||||
},
|
||||
onContactSupport = { error ->
|
||||
val fullMessage =
|
||||
EmailUtil.formatMessage(
|
||||
body = error,
|
||||
supportInfo = supportInfo?.toSupportString(SupportInfoType.entries.toSet())
|
||||
)
|
||||
val mailIntent =
|
||||
EmailUtil.newMailActivityIntent(
|
||||
context.getString(R.string.support_email_address),
|
||||
context.getString(R.string.app_name),
|
||||
fullMessage
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
runCatching {
|
||||
context.startActivity(mailIntent)
|
||||
}.onFailure {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.unable_to_open_email)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
shieldState = shieldState,
|
||||
walletSnapshot = walletSnapshot,
|
||||
walletRestoringState = walletRestoringState,
|
||||
zashiMainTopAppBarState = zashiMainTopAppBarState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTransparentBalanceState(
|
||||
currentShieldState: ShieldState,
|
||||
walletSnapshot: WalletSnapshot?
|
||||
): ShieldState {
|
||||
return when {
|
||||
(walletSnapshot == null) -> currentShieldState
|
||||
(walletSnapshot.transparentBalance >= Zatoshi(DEFAULT_SHIELDING_THRESHOLD) && currentShieldState.isEnabled()) ->
|
||||
ShieldState.Available
|
||||
else -> currentShieldState
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPlayStoreAppSite(
|
||||
context: Context,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val storeIntent = PlayStoreUtil.newActivityIntent(context)
|
||||
runCatching {
|
||||
context.startActivity(storeIntent)
|
||||
}.onFailure {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.unable_to_open_play_store)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
|
||||
sealed interface BalanceState {
|
||||
val totalBalance: Zatoshi
|
||||
val spendableBalance: Zatoshi
|
||||
val exchangeRate: ExchangeRateState
|
||||
|
||||
data class None(
|
||||
override val exchangeRate: ExchangeRateState
|
||||
) : BalanceState {
|
||||
override val totalBalance: Zatoshi = Zatoshi(0L)
|
||||
override val spendableBalance: Zatoshi = Zatoshi(0L)
|
||||
}
|
||||
|
||||
data class Loading(
|
||||
override val totalBalance: Zatoshi,
|
||||
override val spendableBalance: Zatoshi,
|
||||
override val exchangeRate: ExchangeRateState,
|
||||
) : BalanceState
|
||||
|
||||
data class Available(
|
||||
override val totalBalance: Zatoshi,
|
||||
override val spendableBalance: Zatoshi,
|
||||
override val exchangeRate: ExchangeRateState,
|
||||
) : BalanceState
|
||||
}
|
|
@ -3,6 +3,6 @@ package co.electriccoin.zcash.ui.screen.balances
|
|||
/**
|
||||
* These are only used for automated testing.
|
||||
*/
|
||||
object BalancesTag {
|
||||
const val STATUS = "status"
|
||||
object BalanceTag {
|
||||
const val BALANCE_VIEWS = "balance_views"
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class BalanceViewModel(
|
||||
accountDataSource: AccountDataSource,
|
||||
exchangeRateRepository: ExchangeRateRepository,
|
||||
) : ViewModel() {
|
||||
val state: StateFlow<BalanceState> =
|
||||
combine(
|
||||
accountDataSource.selectedAccount.filterNotNull(),
|
||||
exchangeRateRepository.state,
|
||||
) { account, exchangeRateUsd ->
|
||||
when {
|
||||
(
|
||||
account.spendableBalance.value == 0L &&
|
||||
account.totalBalance.value > 0L &&
|
||||
(account.hasChangePending || account.hasValuePending)
|
||||
) -> {
|
||||
BalanceState.Loading(
|
||||
totalBalance = account.totalBalance,
|
||||
spendableBalance = account.spendableBalance,
|
||||
exchangeRate = exchangeRateUsd,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
BalanceState.Available(
|
||||
totalBalance = account.totalBalance,
|
||||
spendableBalance = account.spendableBalance,
|
||||
exchangeRate = exchangeRateUsd,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
BalanceState.None(ExchangeRateState.OptedOut)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.extension.toZecStringFull
|
||||
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZecAmountTriple
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.fixture.ObserveFiatCurrencyResultFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceTag.BALANCE_VIEWS
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeBalance
|
||||
|
||||
@Composable
|
||||
fun BalanceWidget(
|
||||
balanceState: BalanceState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.wrapContentSize()
|
||||
.then(modifier)
|
||||
.testTag(BALANCE_VIEWS),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
BalanceWidgetHeader(
|
||||
parts = balanceState.totalBalance.toZecStringFull().asZecAmountTriple()
|
||||
)
|
||||
|
||||
if (balanceState.exchangeRate is ExchangeRateState.Data) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
StyledExchangeBalance(
|
||||
zatoshi = balanceState.totalBalance,
|
||||
state = balanceState.exchangeRate,
|
||||
)
|
||||
|
||||
if (balanceState.exchangeRate is ExchangeRateState.Data) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BalanceWidgetHeader(
|
||||
parts: ZecAmountTriple,
|
||||
modifier: Modifier = Modifier,
|
||||
isHideBalances: Boolean = LocalBalancesAvailable.current.not(),
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StyledBalance(
|
||||
balanceParts = parts,
|
||||
isHideBalances = isHideBalances,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZashiTypography.header2.copy(fontWeight = FontWeight.SemiBold),
|
||||
leastSignificantPart = ZashiTypography.textXs.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_zcash_zec_icon),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun BalanceWidgetPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
BalanceWidget(
|
||||
balanceState =
|
||||
BalanceState.Available(
|
||||
totalBalance = Zatoshi(1234567891234567L),
|
||||
spendableBalance = Zatoshi(1234567891234567L),
|
||||
exchangeRate = ObserveFiatCurrencyResultFixture.new()
|
||||
),
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances.model
|
||||
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
|
||||
sealed class ShieldState {
|
||||
data object None : ShieldState()
|
||||
|
||||
data object Available : ShieldState()
|
||||
|
||||
data object Running : ShieldState()
|
||||
|
||||
data object Shielded : ShieldState()
|
||||
|
||||
data class Failed(
|
||||
val error: String,
|
||||
val stackTrace: String,
|
||||
) : ShieldState()
|
||||
|
||||
data object FailedGrpc : ShieldState()
|
||||
|
||||
fun isEnabled() = this != Running && this !is Failed && this != Shielded
|
||||
|
||||
companion object {
|
||||
private const val TYPE_NONE = "none" // $NON-NLS
|
||||
private const val TYPE_AVAILABLE = "available" // $NON-NLS
|
||||
private const val TYPE_RUNNING = "running" // $NON-NLS
|
||||
private const val TYPE_SHIELDED = "shielded" // $NON-NLS
|
||||
private const val TYPE_FAILED = "failed" // $NON-NLS
|
||||
private const val TYPE_FAILED_GRPC = "failed_grpc" // $NON-NLS
|
||||
|
||||
private const val KEY_TYPE = "type" // $NON-NLS
|
||||
private const val KEY_ERROR = "error" // $NON-NLS
|
||||
private const val KEY_STACKTRACE = "stacktrace" // $NON-NLS
|
||||
|
||||
internal val Saver
|
||||
get() =
|
||||
run {
|
||||
mapSaver(
|
||||
save = { it.toSaverMap() },
|
||||
restore = {
|
||||
if (it.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val sendStageString = (it[KEY_TYPE] as String)
|
||||
when (sendStageString) {
|
||||
TYPE_NONE -> None
|
||||
TYPE_AVAILABLE -> Available
|
||||
TYPE_RUNNING -> Running
|
||||
TYPE_SHIELDED -> Shielded
|
||||
TYPE_FAILED ->
|
||||
Failed(
|
||||
error = (it[KEY_ERROR] as String),
|
||||
stackTrace = (it[KEY_STACKTRACE] as String)
|
||||
)
|
||||
TYPE_FAILED_GRPC -> FailedGrpc
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ShieldState.toSaverMap(): HashMap<String, String> {
|
||||
val saverMap = HashMap<String, String>()
|
||||
when (this) {
|
||||
None -> saverMap[KEY_TYPE] = TYPE_NONE
|
||||
Available -> saverMap[KEY_TYPE] = TYPE_AVAILABLE
|
||||
Running -> saverMap[KEY_TYPE] = TYPE_RUNNING
|
||||
Shielded -> saverMap[KEY_TYPE] = TYPE_SHIELDED
|
||||
is Failed -> {
|
||||
saverMap[KEY_TYPE] = TYPE_FAILED
|
||||
saverMap[KEY_ERROR] = this.error
|
||||
saverMap[KEY_STACKTRACE] = this.stackTrace
|
||||
}
|
||||
FailedGrpc -> saverMap[KEY_TYPE] = TYPE_FAILED_GRPC
|
||||
}
|
||||
|
||||
return saverMap
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances.model
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.android.sdk.model.Locale
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.toFiatCurrencyState
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.totalBalance
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.STACKTRACE_LIMIT
|
||||
|
||||
data class WalletDisplayValues(
|
||||
val progress: PercentDecimal,
|
||||
val zecAmountText: String,
|
||||
val statusText: String,
|
||||
val statusAction: StatusAction = StatusAction.None,
|
||||
val fiatCurrencyAmountState: FiatCurrencyConversionRateState,
|
||||
val fiatCurrencyAmountText: String
|
||||
) {
|
||||
companion object {
|
||||
@Suppress("LongMethod")
|
||||
internal fun getNextValues(
|
||||
context: Context,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
): WalletDisplayValues {
|
||||
var progress = PercentDecimal.ZERO_PERCENT
|
||||
val zecAmountText = walletSnapshot.totalBalance().toZecString()
|
||||
var statusText = ""
|
||||
var statusAction: StatusAction = StatusAction.None
|
||||
|
||||
val fiatCurrencyAmountState =
|
||||
walletSnapshot.spendableBalance().toFiatCurrencyState(
|
||||
null,
|
||||
Locale.getDefault(),
|
||||
)
|
||||
var fiatCurrencyAmountText = getFiatCurrencyRateValue(context, fiatCurrencyAmountState)
|
||||
|
||||
when (walletSnapshot.status) {
|
||||
Synchronizer.Status.INITIALIZING, Synchronizer.Status.SYNCING -> {
|
||||
progress = walletSnapshot.progress
|
||||
// We add "so far" to the amount
|
||||
if (fiatCurrencyAmountState != FiatCurrencyConversionRateState.Unavailable) {
|
||||
fiatCurrencyAmountText =
|
||||
context.getString(
|
||||
R.string.balances_status_syncing_amount_suffix,
|
||||
fiatCurrencyAmountText
|
||||
)
|
||||
}
|
||||
statusText = context.getString(R.string.balances_status_syncing)
|
||||
statusAction = StatusAction.Syncing
|
||||
}
|
||||
Synchronizer.Status.SYNCED -> {
|
||||
statusText = context.getString(R.string.balances_status_synced)
|
||||
statusAction = StatusAction.Synced
|
||||
}
|
||||
Synchronizer.Status.DISCONNECTED -> {
|
||||
statusText =
|
||||
context.getString(
|
||||
R.string.balances_status_error_simple,
|
||||
context.getString(R.string.app_name)
|
||||
)
|
||||
statusAction =
|
||||
StatusAction.Disconnected(
|
||||
details = context.getString(R.string.balances_status_error_dialog_connection)
|
||||
)
|
||||
}
|
||||
Synchronizer.Status.STOPPED -> {
|
||||
statusText = context.getString(R.string.balances_status_syncing)
|
||||
statusAction =
|
||||
StatusAction.Stopped(
|
||||
details = context.getString(R.string.balances_status_dialog_stopped)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// More detailed error message
|
||||
walletSnapshot.synchronizerError?.let {
|
||||
statusText =
|
||||
context.getString(
|
||||
R.string.balances_status_error_simple,
|
||||
context.getString(R.string.app_name)
|
||||
)
|
||||
statusAction =
|
||||
StatusAction.Error(
|
||||
details =
|
||||
context.getString(
|
||||
R.string.balances_status_error_dialog_cause,
|
||||
walletSnapshot.synchronizerError.getCauseMessage()
|
||||
?: context.getString(R.string.balances_status_error_dialog_cause_unknown),
|
||||
walletSnapshot.synchronizerError.getStackTrace(limit = STACKTRACE_LIMIT)
|
||||
?: context.getString(R.string.balances_status_error_dialog_stacktrace_unknown)
|
||||
),
|
||||
fullStackTrace = walletSnapshot.synchronizerError.getStackTrace(limit = null)
|
||||
)
|
||||
}
|
||||
|
||||
return WalletDisplayValues(
|
||||
progress = progress,
|
||||
zecAmountText = zecAmountText,
|
||||
statusAction = statusAction,
|
||||
statusText = statusText,
|
||||
fiatCurrencyAmountState = fiatCurrencyAmountState,
|
||||
fiatCurrencyAmountText = fiatCurrencyAmountText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StatusAction {
|
||||
data object None : StatusAction()
|
||||
|
||||
data object Syncing : StatusAction()
|
||||
|
||||
data object Synced : StatusAction()
|
||||
|
||||
data object AppUpdate : StatusAction()
|
||||
|
||||
sealed class Detailed(open val details: String) : StatusAction()
|
||||
|
||||
data class Disconnected(override val details: String) : Detailed(details)
|
||||
|
||||
data class Stopped(override val details: String) : Detailed(details)
|
||||
|
||||
data class Error(
|
||||
override val details: String,
|
||||
val fullStackTrace: String?
|
||||
) : Detailed(details)
|
||||
}
|
||||
|
||||
fun StatusAction.isReportable() = this is StatusAction.Error && fullStackTrace != null
|
||||
|
||||
private fun getFiatCurrencyRateValue(
|
||||
context: Context,
|
||||
fiatCurrencyAmountState: FiatCurrencyConversionRateState
|
||||
): String {
|
||||
return fiatCurrencyAmountState.let { state ->
|
||||
when (state) {
|
||||
is FiatCurrencyConversionRateState.Current -> state.formattedFiatValue
|
||||
is FiatCurrencyConversionRateState.Stale -> state.formattedFiatValue
|
||||
is FiatCurrencyConversionRateState.Unavailable -> {
|
||||
context.getString(R.string.fiat_currency_conversion_rate_unavailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,629 +0,0 @@
|
|||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.balances.view
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.extension.DEFAULT_FEE
|
||||
import cash.z.ecc.sdk.extension.toZecStringFull
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.compose.StatusDialog
|
||||
import co.electriccoin.zcash.ui.common.compose.SynchronizationStatus
|
||||
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.changePendingBalance
|
||||
import co.electriccoin.zcash.ui.common.model.hasChangePending
|
||||
import co.electriccoin.zcash.ui.common.model.hasValuePending
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.valuePendingBalance
|
||||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.CircularSmallProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.Reference
|
||||
import co.electriccoin.zcash.ui.design.component.Small
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiModal
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalancesTag
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
|
||||
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ComposableBalancesPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
Balances(
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
isHideBalances = false,
|
||||
isShowingErrorDialog = false,
|
||||
hideStatusDialog = {},
|
||||
showStatusDialog = null,
|
||||
setShowErrorDialog = {},
|
||||
onShielding = {},
|
||||
onStatusClick = {},
|
||||
onContactSupport = {},
|
||||
shieldState = ShieldState.Available,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
walletRestoringState = WalletRestoringState.NONE,
|
||||
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ComposableBalancesShieldDarkPreview() {
|
||||
ZcashTheme(forceDarkMode = true) {
|
||||
Balances(
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
isHideBalances = false,
|
||||
isShowingErrorDialog = true,
|
||||
hideStatusDialog = {},
|
||||
showStatusDialog = null,
|
||||
setShowErrorDialog = {},
|
||||
onShielding = {},
|
||||
onStatusClick = {},
|
||||
onContactSupport = {},
|
||||
shieldState = ShieldState.Available,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
walletRestoringState = WalletRestoringState.NONE,
|
||||
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview("BalancesShieldErrorDialog")
|
||||
@Composable
|
||||
private fun ComposableBalancesShieldErrorDialogPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface {
|
||||
ShieldingErrorDialog(
|
||||
state = ShieldState.Failed("Test Error Text", "Test Error Stacktrace"),
|
||||
onDone = {},
|
||||
onReport = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
fun Balances(
|
||||
balanceState: BalanceState,
|
||||
isHideBalances: Boolean,
|
||||
isShowingErrorDialog: Boolean,
|
||||
hideStatusDialog: () -> Unit,
|
||||
onContactSupport: (String?) -> Unit,
|
||||
onShielding: () -> Unit,
|
||||
onStatusClick: (StatusAction) -> Unit,
|
||||
showStatusDialog: StatusAction.Detailed?,
|
||||
setShowErrorDialog: (Boolean) -> Unit,
|
||||
shieldState: ShieldState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
walletSnapshot: WalletSnapshot?,
|
||||
walletRestoringState: WalletRestoringState,
|
||||
zashiMainTopAppBarState: ZashiMainTopAppBarState?
|
||||
) {
|
||||
BlankBgScaffold(
|
||||
topBar = {
|
||||
ZashiMainTopAppBar(zashiMainTopAppBarState)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState)
|
||||
},
|
||||
) { paddingValues ->
|
||||
if (null == walletSnapshot) {
|
||||
CircularScreenProgressIndicator()
|
||||
} else {
|
||||
BalancesMainContent(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
onShielding = onShielding,
|
||||
onStatusClick = onStatusClick,
|
||||
walletSnapshot = walletSnapshot,
|
||||
shieldState = shieldState,
|
||||
modifier =
|
||||
Modifier.scaffoldPadding(paddingValues),
|
||||
walletRestoringState = walletRestoringState
|
||||
)
|
||||
|
||||
// Show synchronization status popup
|
||||
if (showStatusDialog != null) {
|
||||
StatusDialog(
|
||||
statusAction = showStatusDialog,
|
||||
onDone = hideStatusDialog,
|
||||
onReport = { status ->
|
||||
hideStatusDialog()
|
||||
onContactSupport(status.fullStackTrace)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Show shielding error popup
|
||||
if (isShowingErrorDialog) {
|
||||
when (shieldState) {
|
||||
is ShieldState.Failed -> {
|
||||
ShieldingErrorDialog(
|
||||
state = shieldState,
|
||||
onDone = { setShowErrorDialog(false) },
|
||||
onReport = { state ->
|
||||
setShowErrorDialog(false)
|
||||
onContactSupport(state.stackTrace)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ShieldState.FailedGrpc -> {
|
||||
ShieldingErrorGrpcDialog(
|
||||
onDone = { setShowErrorDialog(false) }
|
||||
)
|
||||
}
|
||||
|
||||
else -> { // Nothing to do now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShieldingErrorDialog(
|
||||
state: ShieldState.Failed,
|
||||
onDone: () -> Unit,
|
||||
onReport: (ShieldState.Failed) -> Unit,
|
||||
) {
|
||||
AppAlertDialog(
|
||||
title = stringResource(id = R.string.balances_shielding_dialog_error_title),
|
||||
text = {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.balances_shielding_dialog_error_text),
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
)
|
||||
|
||||
if (state.error.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
Text(
|
||||
text = state.error,
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButtonText = stringResource(id = R.string.balances_shielding_dialog_error_btn),
|
||||
onConfirmButtonClick = onDone,
|
||||
dismissButtonText = stringResource(id = R.string.balances_shielding_dialog_report_btn),
|
||||
onDismissButtonClick = { onReport(state) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShieldingErrorGrpcDialog(onDone: () -> Unit) {
|
||||
AppAlertDialog(
|
||||
title = stringResource(id = R.string.balances_shielding_dialog_error_grpc_title),
|
||||
text = {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.balances_shielding_dialog_error_grpc_text),
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButtonText = stringResource(id = R.string.balances_shielding_dialog_error_grpc_btn),
|
||||
onConfirmButtonClick = onDone
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun BalancesMainContent(
|
||||
balanceState: BalanceState,
|
||||
isHideBalances: Boolean,
|
||||
onShielding: () -> Unit,
|
||||
onStatusClick: (StatusAction) -> Unit,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
shieldState: ShieldState,
|
||||
walletRestoringState: WalletRestoringState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
BalanceWidget(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
isReferenceToBalances = false,
|
||||
onReferenceClick = {}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
|
||||
HorizontalDivider(
|
||||
color = ZcashTheme.colors.tertiaryDividerColor,
|
||||
thickness = ZcashTheme.dimens.divider
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
BalancesOverview(
|
||||
isHideBalances = isHideBalances,
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
|
||||
TransparentBalancePanel(
|
||||
isHideBalances = isHideBalances,
|
||||
onShielding = onShielding,
|
||||
shieldState = shieldState,
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
|
||||
if (walletRestoringState == WalletRestoringState.RESTORING) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
Small(
|
||||
text = stringResource(id = R.string.balances_status_restoring_text),
|
||||
textFontWeight = FontWeight.Medium,
|
||||
color = ZcashTheme.colors.textFieldWarning,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = ZcashTheme.dimens.spacingSmall)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
|
||||
}
|
||||
|
||||
SynchronizationStatus(
|
||||
onStatusClick = onStatusClick,
|
||||
testTag = BalancesTag.STATUS,
|
||||
walletSnapshot = walletSnapshot,
|
||||
modifier = Modifier.animateContentSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TransparentBalancePanel(
|
||||
isHideBalances: Boolean,
|
||||
onShielding: () -> Unit,
|
||||
shieldState: ShieldState,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
) {
|
||||
var showHelpPanel by rememberSaveable { mutableStateOf(false) }
|
||||
ZashiModal {
|
||||
Column {
|
||||
TransparentBalanceRow(
|
||||
isHideBalances = isHideBalances,
|
||||
isProgressbarVisible = shieldState == ShieldState.Running,
|
||||
onHelpClick = { showHelpPanel = !showHelpPanel },
|
||||
walletSnapshot = walletSnapshot
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
ZashiButton(
|
||||
onClick = onShielding,
|
||||
text = stringResource(R.string.balances_transparent_balance_shield),
|
||||
enabled = shieldState == ShieldState.Available,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
BodySmall(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.balances_transparent_balance_fee,
|
||||
DEFAULT_FEE
|
||||
),
|
||||
textFontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
if (showHelpPanel) {
|
||||
TransparentBalanceHelpPanel(
|
||||
onHideHelpPanel = { showHelpPanel = !showHelpPanel }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TransparentBalanceRow(
|
||||
isHideBalances: Boolean,
|
||||
isProgressbarVisible: Boolean,
|
||||
onHelpClick: () -> Unit,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// To keep both elements together in relatively sized row
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// Apply common click listener
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.smallRippleEffectCorner))
|
||||
.clickable { onHelpClick() }
|
||||
) {
|
||||
BodySmall(text = stringResource(id = R.string.balances_transparent_balance).uppercase())
|
||||
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_help_question_mark),
|
||||
contentDescription = stringResource(id = R.string.balances_transparent_help_content_description),
|
||||
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingXtiny)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
StyledBalance(
|
||||
balanceParts = walletSnapshot.transparentBalance.toZecStringFull().asZecAmountTriple(),
|
||||
isHideBalances = isHideBalances,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
textColor = ZcashTheme.colors.textDescriptionDark
|
||||
)
|
||||
|
||||
if (isProgressbarVisible) {
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
|
||||
Box(Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)) {
|
||||
CircularSmallProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TransparentBalanceHelpPanel(onHideHelpPanel: () -> Unit) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(color = ZashiColors.Modals.surfacePrimary)
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val appName = stringResource(id = R.string.app_name)
|
||||
val currencyName = ZcashCurrency.getLocalizedName(LocalContext.current)
|
||||
BodySmall(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.balances_transparent_balance_help,
|
||||
appName,
|
||||
currencyName
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
Reference(
|
||||
text = stringResource(id = R.string.balances_transparent_balance_help_close).uppercase(),
|
||||
onClick = onHideHelpPanel,
|
||||
textStyle = ZcashTheme.extendedTypography.referenceSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BalancesOverview(
|
||||
walletSnapshot: WalletSnapshot,
|
||||
isHideBalances: Boolean,
|
||||
) {
|
||||
Column {
|
||||
SpendableBalanceRow(isHideBalances, walletSnapshot)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
ChangePendingRow(isHideBalances, walletSnapshot)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
// aka value pending
|
||||
PendingTransactionsRow(isHideBalances, walletSnapshot)
|
||||
}
|
||||
}
|
||||
|
||||
const val TEXT_PART_WIDTH_RATIO = 0.6f
|
||||
|
||||
@Composable
|
||||
fun SpendableBalanceRow(
|
||||
isHideBalances: Boolean,
|
||||
walletSnapshot: WalletSnapshot
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BodySmall(
|
||||
text = stringResource(id = R.string.balances_shielded_spendable).uppercase(),
|
||||
modifier = Modifier.fillMaxWidth(TEXT_PART_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
StyledBalance(
|
||||
balanceParts = walletSnapshot.spendableBalance().toZecStringFull().asZecAmountTriple(),
|
||||
isHideBalances = isHideBalances,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
textColor = ZcashTheme.colors.textPrimary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.balance_shield),
|
||||
contentDescription = null,
|
||||
// The same size as the following progress bars
|
||||
modifier = Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangePendingRow(
|
||||
isHideBalances: Boolean,
|
||||
walletSnapshot: WalletSnapshot
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BodySmall(
|
||||
text = stringResource(id = R.string.balances_change_pending).uppercase(),
|
||||
modifier = Modifier.fillMaxWidth(TEXT_PART_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
StyledBalance(
|
||||
balanceParts = walletSnapshot.changePendingBalance().toZecStringFull().asZecAmountTriple(),
|
||||
isHideBalances = isHideBalances,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
textColor = ZcashTheme.colors.textDescriptionDark
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Box(Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)) {
|
||||
if (walletSnapshot.hasChangePending()) {
|
||||
CircularSmallProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PendingTransactionsRow(
|
||||
isHideBalances: Boolean,
|
||||
walletSnapshot: WalletSnapshot
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BodySmall(
|
||||
text = stringResource(id = R.string.balances_pending_transactions).uppercase(),
|
||||
modifier = Modifier.fillMaxWidth(TEXT_PART_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
StyledBalance(
|
||||
balanceParts = walletSnapshot.valuePendingBalance().toZecStringFull().asZecAmountTriple(),
|
||||
isHideBalances = isHideBalances,
|
||||
textStyle =
|
||||
StyledBalanceDefaults.textStyles(
|
||||
mostSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
leastSignificantPart = ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
textColor = ZcashTheme.colors.textDescriptionDark
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Box(Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)) {
|
||||
if (walletSnapshot.hasValuePending()) {
|
||||
CircularSmallProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@ import co.electriccoin.zcash.ui.design.component.BlankSurface
|
|||
import co.electriccoin.zcash.ui.design.component.LottieProgress
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
|
@ -56,7 +57,6 @@ fun StyledExchangeBalance(
|
|||
zatoshi: Zatoshi,
|
||||
state: ExchangeRateState,
|
||||
modifier: Modifier = Modifier,
|
||||
isHideBalances: Boolean = false,
|
||||
hiddenBalancePlaceholder: StringResource =
|
||||
stringRes(co.electriccoin.zcash.ui.design.R.string.hide_balance_placeholder),
|
||||
textColor: Color = ZashiColors.Text.textPrimary,
|
||||
|
@ -75,9 +75,9 @@ fun StyledExchangeBalance(
|
|||
style = style,
|
||||
textColor = textColor,
|
||||
zatoshi = zatoshi,
|
||||
isHideBalances = isHideBalances,
|
||||
state = state,
|
||||
hiddenBalancePlaceholder = hiddenBalancePlaceholder
|
||||
hiddenBalancePlaceholder = hiddenBalancePlaceholder,
|
||||
isHideBalance = LocalBalancesAvailable.current.not()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -97,9 +97,9 @@ private fun ExchangeAvailableRateLabelInternal(
|
|||
style: TextStyle,
|
||||
textColor: Color,
|
||||
zatoshi: Zatoshi,
|
||||
isHideBalances: Boolean,
|
||||
state: ExchangeRateState.Data,
|
||||
hiddenBalancePlaceholder: StringResource,
|
||||
isHideBalance: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isEnabled = !state.isLoading && state.isRefreshEnabled
|
||||
|
@ -114,7 +114,7 @@ private fun ExchangeAvailableRateLabelInternal(
|
|||
textColor = textColor,
|
||||
) {
|
||||
Text(
|
||||
text = createExchangeRateText(state, isHideBalances, hiddenBalancePlaceholder, zatoshi),
|
||||
text = createExchangeRateText(state, hiddenBalancePlaceholder, zatoshi, isHideBalance),
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
color = textColor
|
||||
|
@ -148,9 +148,9 @@ private fun ExchangeAvailableRateLabelInternal(
|
|||
@Composable
|
||||
internal fun createExchangeRateText(
|
||||
state: ExchangeRateState.Data,
|
||||
isHideBalances: Boolean,
|
||||
hiddenBalancePlaceholder: StringResource,
|
||||
zatoshi: Zatoshi
|
||||
zatoshi: Zatoshi,
|
||||
isHideBalances: Boolean
|
||||
): String {
|
||||
val currencySymbol = state.fiatCurrency.symbol
|
||||
val text =
|
||||
|
@ -268,7 +268,6 @@ private fun ExchangeRateButton(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun DefaultPreview() =
|
||||
|
@ -276,7 +275,6 @@ private fun DefaultPreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = false,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state =
|
||||
|
@ -294,7 +292,6 @@ private fun DefaultPreview() =
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun DefaultNoRefreshPreview() =
|
||||
|
@ -302,7 +299,6 @@ private fun DefaultNoRefreshPreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = false,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state =
|
||||
|
@ -321,7 +317,6 @@ private fun DefaultNoRefreshPreview() =
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun HiddenPreview() =
|
||||
|
@ -329,7 +324,6 @@ private fun HiddenPreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = true,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state =
|
||||
|
@ -347,7 +341,6 @@ private fun HiddenPreview() =
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun HiddenStalePreview() =
|
||||
|
@ -355,7 +348,6 @@ private fun HiddenStalePreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = true,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state =
|
||||
|
@ -374,7 +366,6 @@ private fun HiddenStalePreview() =
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun LoadingPreview() =
|
||||
|
@ -382,7 +373,6 @@ private fun LoadingPreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = false,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state = ObserveFiatCurrencyResultFixture.new()
|
||||
|
@ -391,7 +381,6 @@ private fun LoadingPreview() =
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun LoadingEmptyPreview() =
|
||||
|
@ -399,7 +388,6 @@ private fun LoadingEmptyPreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = false,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state = ObserveFiatCurrencyResultFixture.new(currencyConversion = null)
|
||||
|
@ -408,7 +396,6 @@ private fun LoadingEmptyPreview() =
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun LoadingStalePreview() =
|
||||
|
@ -416,7 +403,6 @@ private fun LoadingStalePreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = false,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state =
|
||||
|
@ -429,7 +415,6 @@ private fun LoadingStalePreview() =
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun StalePreview() =
|
||||
|
@ -437,7 +422,6 @@ private fun StalePreview() =
|
|||
BlankSurface {
|
||||
Column {
|
||||
StyledExchangeBalance(
|
||||
isHideBalances = false,
|
||||
modifier = Modifier,
|
||||
zatoshi = Zatoshi(1),
|
||||
state =
|
||||
|
|
|
@ -32,7 +32,7 @@ fun StyledExchangeLabel(
|
|||
if (!state.isStale && state.currencyConversion != null) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = createExchangeRateText(state, isHideBalances, hiddenBalancePlaceholder, zatoshi),
|
||||
text = createExchangeRateText(state, hiddenBalancePlaceholder, zatoshi, isHideBalances),
|
||||
maxLines = 1,
|
||||
color = textColor,
|
||||
style = style,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.screen.flexa
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object Flexa
|
|
@ -0,0 +1,16 @@
|
|||
package co.electriccoin.zcash.ui.screen.flexa
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateFlexaTransactionUseCase
|
||||
import com.flexa.spend.Transaction
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FlexaViewModel(
|
||||
private val createFlexaTransaction: CreateFlexaTransactionUseCase
|
||||
) : ViewModel() {
|
||||
fun createTransaction(transaction: Result<Transaction>) =
|
||||
viewModelScope.launch {
|
||||
createFlexaTransaction(transaction)
|
||||
}
|
||||
}
|
|
@ -2,229 +2,36 @@
|
|||
|
||||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.isSynced
|
||||
import co.electriccoin.zcash.ui.design.component.RestoreScreenBrightness
|
||||
import co.electriccoin.zcash.ui.screen.account.WrapAccount
|
||||
import co.electriccoin.zcash.ui.screen.balances.WrapBalances
|
||||
import co.electriccoin.zcash.ui.screen.home.model.TabItem
|
||||
import co.electriccoin.zcash.ui.screen.home.view.Home
|
||||
import co.electriccoin.zcash.ui.screen.receive.WrapReceive
|
||||
import co.electriccoin.zcash.ui.screen.send.WrapSend
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceViewModel
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetViewModel
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun WrapHome(
|
||||
goScan: () -> Unit,
|
||||
sendArguments: SendArguments
|
||||
) {
|
||||
internal fun AndroidHome() {
|
||||
val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>()
|
||||
val balanceViewModel = koinActivityViewModel<BalanceViewModel>()
|
||||
val homeViewModel = koinActivityViewModel<HomeViewModel>()
|
||||
val transactionHistoryWidgetViewModel = koinActivityViewModel<TransactionHistoryWidgetViewModel>()
|
||||
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val appBarState by topAppBarViewModel.state.collectAsStateWithLifecycle()
|
||||
val balanceState by balanceViewModel.state.collectAsStateWithLifecycle()
|
||||
val state by homeViewModel.state.collectAsStateWithLifecycle()
|
||||
val transactionWidgetState by transactionHistoryWidgetViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val isKeepScreenOnWhileSyncing = homeViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
|
||||
|
||||
val walletSnapshot = walletViewModel.currentWalletSnapshot.collectAsStateWithLifecycle().value
|
||||
|
||||
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
|
||||
|
||||
// Once the wallet is fully synced and still in restoring state, persist the new state
|
||||
if (walletSnapshot?.status?.isSynced() == true && walletRestoringState.isRunningRestoring()) {
|
||||
walletViewModel.persistWalletRestoringState(WalletRestoringState.SYNCING)
|
||||
}
|
||||
|
||||
// TODO [#1523]: Refactor RestoreSuccess screen navigation
|
||||
// TODO [#1523]: https://github.com/Electric-Coin-Company/zashi-android/issues/1523
|
||||
val isRestoreSuccessSeen = homeViewModel.isRestoreSuccessSeen.collectAsStateWithLifecycle().value
|
||||
|
||||
var isShowingRestoreSuccess by rememberSaveable { mutableStateOf(false) }
|
||||
val setShowingRestoreSuccess = {
|
||||
homeViewModel.setRestoringInitialWarningSeen()
|
||||
isShowingRestoreSuccess = false
|
||||
}
|
||||
|
||||
// Show initial restore success screen
|
||||
isRestoreSuccessSeen?.let { restoreSuccessSeen ->
|
||||
if (!restoreSuccessSeen && walletRestoringState == WalletRestoringState.RESTORING) {
|
||||
LaunchedEffect(key1 = isShowingRestoreSuccess) {
|
||||
// Adding an extra little delay before displaying for a better UX
|
||||
@Suppress("MagicNumber")
|
||||
delay(1500)
|
||||
isShowingRestoreSuccess = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WrapHome(
|
||||
goScan = goScan,
|
||||
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
|
||||
isShowingRestoreSuccess = isShowingRestoreSuccess,
|
||||
sendArguments = sendArguments,
|
||||
setShowingRestoreSuccess = setShowingRestoreSuccess,
|
||||
walletSnapshot = walletSnapshot,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
internal fun WrapHome(
|
||||
goScan: () -> Unit,
|
||||
isKeepScreenOnWhileSyncing: Boolean?,
|
||||
isShowingRestoreSuccess: Boolean,
|
||||
sendArguments: SendArguments,
|
||||
setShowingRestoreSuccess: () -> Unit,
|
||||
walletSnapshot: WalletSnapshot?,
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val homeTabNavigationRouter = koinInject<HomeTabNavigationRouter>()
|
||||
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val pagerState =
|
||||
rememberPagerState(
|
||||
initialPage = 0,
|
||||
initialPageOffsetFraction = 0f,
|
||||
pageCount = { 4 }
|
||||
state?.let {
|
||||
HomeView(
|
||||
appBarState = appBarState,
|
||||
balanceState = balanceState,
|
||||
state = it,
|
||||
transactionWidgetState = transactionWidgetState
|
||||
)
|
||||
|
||||
val homeGoBack: () -> Unit by remember(pagerState.currentPage, scope) {
|
||||
derivedStateOf {
|
||||
{
|
||||
when (pagerState.currentPage) {
|
||||
HomeScreenIndex.ACCOUNT.pageIndex -> activity.finish()
|
||||
HomeScreenIndex.SEND.pageIndex,
|
||||
HomeScreenIndex.RECEIVE.pageIndex,
|
||||
HomeScreenIndex.BALANCES.pageIndex ->
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(HomeScreenIndex.ACCOUNT.pageIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
homeTabNavigationRouter.observe().collect {
|
||||
pagerState.scrollToPage(it.pageIndex)
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
homeGoBack()
|
||||
}
|
||||
|
||||
// Reset the screen brightness for all pages except Receive which maintain the screen brightness by itself
|
||||
if (pagerState.currentPage != HomeScreenIndex.RECEIVE.pageIndex) {
|
||||
RestoreScreenBrightness()
|
||||
}
|
||||
|
||||
val tabs =
|
||||
persistentListOf(
|
||||
TabItem(
|
||||
index = HomeScreenIndex.ACCOUNT,
|
||||
title = stringResource(id = R.string.home_tab_account),
|
||||
testTag = HomeTag.TAB_ACCOUNT,
|
||||
screenContent = {
|
||||
WrapAccount(
|
||||
goBalances = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(HomeScreenIndex.BALANCES.pageIndex)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
),
|
||||
TabItem(
|
||||
index = HomeScreenIndex.SEND,
|
||||
title = stringResource(id = R.string.home_tab_send),
|
||||
testTag = HomeTag.TAB_SEND,
|
||||
screenContent = {
|
||||
WrapSend(
|
||||
sendArguments = sendArguments,
|
||||
goToQrScanner = goScan,
|
||||
goBack = homeGoBack,
|
||||
goBalances = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(HomeScreenIndex.BALANCES.pageIndex)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
),
|
||||
TabItem(
|
||||
index = HomeScreenIndex.RECEIVE,
|
||||
title = stringResource(id = R.string.home_tab_receive),
|
||||
testTag = HomeTag.TAB_RECEIVE,
|
||||
screenContent = {
|
||||
WrapReceive()
|
||||
}
|
||||
),
|
||||
TabItem(
|
||||
index = HomeScreenIndex.BALANCES,
|
||||
title = stringResource(id = R.string.home_tab_balances),
|
||||
testTag = HomeTag.TAB_BALANCES,
|
||||
screenContent = {
|
||||
WrapBalances()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Home(
|
||||
subScreens = tabs,
|
||||
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
|
||||
isShowingRestoreSuccess = isShowingRestoreSuccess,
|
||||
setShowingRestoreSuccess = setShowingRestoreSuccess,
|
||||
walletSnapshot = walletSnapshot,
|
||||
pagerState = pagerState,
|
||||
)
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
if (pagerState.currentPage == HomeScreenIndex.SEND.pageIndex) {
|
||||
walletViewModel.refreshExchangeRateUsd()
|
||||
} else {
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum of the Home screen sub-screens
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
enum class HomeScreenIndex(val pageIndex: Int) {
|
||||
ACCOUNT(0),
|
||||
SEND(1),
|
||||
RECEIVE(2),
|
||||
BALANCES(3)
|
||||
}
|
||||
@Serializable
|
||||
object Home
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||
|
||||
data class HomeState(
|
||||
val receiveButton: BigIconButtonState,
|
||||
val sendButton: BigIconButtonState,
|
||||
val scanButton: BigIconButtonState,
|
||||
val moreButton: BigIconButtonState,
|
||||
)
|
|
@ -1,11 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
/**
|
||||
* These are only used for automated testing.
|
||||
*/
|
||||
object HomeTag {
|
||||
const val TAB_ACCOUNT = "tab_account"
|
||||
const val TAB_SEND = "tab_send"
|
||||
const val TAB_RECEIVE = "tab_receive"
|
||||
const val TAB_BALANCES = "tab_balances"
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarWithAccountSelection
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiBigIconButton
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceState
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeOptIn
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetState
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.createTransactionHistoryWidgets
|
||||
|
||||
@Composable
|
||||
internal fun HomeView(
|
||||
appBarState: ZashiMainTopAppBarState?,
|
||||
balanceState: BalanceState,
|
||||
transactionWidgetState: TransactionHistoryWidgetState,
|
||||
state: HomeState
|
||||
) {
|
||||
BlankBgScaffold(
|
||||
topBar = { ZashiTopAppBarWithAccountSelection(appBarState) }
|
||||
) { paddingValues ->
|
||||
Content(
|
||||
modifier = Modifier.padding(top = paddingValues.calculateTopPadding() + 24.dp),
|
||||
paddingValues = paddingValues,
|
||||
transactionHistoryWidgetState = transactionWidgetState,
|
||||
balanceState = balanceState,
|
||||
state = state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
transactionHistoryWidgetState: TransactionHistoryWidgetState,
|
||||
paddingValues: PaddingValues,
|
||||
balanceState: BalanceState,
|
||||
state: HomeState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
BalanceWidget(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
|
||||
end = ZcashTheme.dimens.screenHorizontalSpacingRegular,
|
||||
),
|
||||
balanceState = balanceState,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.scaffoldPadding(paddingValues, top = 0.dp, bottom = 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.receiveButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.sendButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.scanButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.moreButton,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
createTransactionHistoryWidgets(
|
||||
state = transactionHistoryWidgetState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = balanceState.exchangeRate is ExchangeRateState.OptIn,
|
||||
enter = EnterTransition.None,
|
||||
exit = fadeOut() + slideOutVertically(),
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(66.dp + paddingValues.calculateTopPadding()))
|
||||
StyledExchangeOptIn(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
state =
|
||||
(balanceState.exchangeRate as? ExchangeRateState.OptIn) ?: ExchangeRateState.OptIn(
|
||||
onDismissClick = {},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun Preview() =
|
||||
ZcashTheme {
|
||||
HomeView(
|
||||
appBarState = ZashiMainTopAppBarStateFixture.new(),
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
transactionWidgetState = TransactionHistoryWidgetStateFixture.new(),
|
||||
state =
|
||||
HomeState(
|
||||
receiveButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
sendButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
scanButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
moreButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.integrations.DialogIntegrations
|
||||
import co.electriccoin.zcash.ui.screen.receive.Receive
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class HomeViewModel(
|
||||
private val navigationRouter: NavigationRouter,
|
||||
) : ViewModel() {
|
||||
val state: StateFlow<HomeState?> =
|
||||
MutableStateFlow(
|
||||
HomeState(
|
||||
receiveButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Receive"),
|
||||
icon = R.drawable.ic_home_receive,
|
||||
onClick = ::onReceiveButtonClick,
|
||||
),
|
||||
sendButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Send"),
|
||||
icon = R.drawable.ic_home_send,
|
||||
onClick = ::onSendButtonClick,
|
||||
),
|
||||
scanButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Scan"),
|
||||
icon = R.drawable.ic_home_scan,
|
||||
onClick = ::onScanButtonClick,
|
||||
),
|
||||
moreButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("More"),
|
||||
icon = R.drawable.ic_home_more,
|
||||
onClick = ::onMoreButtonClick,
|
||||
),
|
||||
)
|
||||
).asStateFlow()
|
||||
|
||||
private fun onMoreButtonClick() {
|
||||
navigationRouter.forward(DialogIntegrations)
|
||||
}
|
||||
|
||||
private fun onSendButtonClick() {
|
||||
navigationRouter.forward(Send())
|
||||
}
|
||||
|
||||
private fun onReceiveButtonClick() {
|
||||
navigationRouter.forward(Receive)
|
||||
}
|
||||
|
||||
private fun onScanButtonClick() {
|
||||
navigationRouter.forward(Scan(Scan.HOMEPAGE))
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.model
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex
|
||||
|
||||
data class TabItem(
|
||||
val index: HomeScreenIndex,
|
||||
val title: String,
|
||||
val testTag: String,
|
||||
val screenContent: @Composable () -> Unit
|
||||
)
|
|
@ -1,180 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.view
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PageSize
|
||||
import androidx.compose.foundation.pager.PagerDefaults
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.DividerDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.TabRowDefaults
|
||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.NavigationTabText
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.home.model.TabItem
|
||||
import co.electriccoin.zcash.ui.screen.restoresuccess.WrapRestoreSuccess
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Preview("Home")
|
||||
@Composable
|
||||
private fun ComposablePreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface {
|
||||
Home(
|
||||
isKeepScreenOnWhileSyncing = false,
|
||||
isShowingRestoreSuccess = false,
|
||||
setShowingRestoreSuccess = {},
|
||||
subScreens = persistentListOf(),
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun Home(
|
||||
isKeepScreenOnWhileSyncing: Boolean?,
|
||||
isShowingRestoreSuccess: Boolean,
|
||||
setShowingRestoreSuccess: () -> Unit,
|
||||
subScreens: ImmutableList<TabItem>,
|
||||
walletSnapshot: WalletSnapshot?,
|
||||
pagerState: PagerState =
|
||||
rememberPagerState(
|
||||
initialPage = 0,
|
||||
initialPageOffsetFraction = 0f,
|
||||
pageCount = { subScreens.size }
|
||||
)
|
||||
) {
|
||||
HomeContent(
|
||||
pagerState = pagerState,
|
||||
subScreens = subScreens,
|
||||
)
|
||||
|
||||
if (isShowingRestoreSuccess) {
|
||||
WrapRestoreSuccess(onDone = setShowingRestoreSuccess)
|
||||
}
|
||||
|
||||
if (isKeepScreenOnWhileSyncing == true &&
|
||||
walletSnapshot?.status == Synchronizer.Status.SYNCING
|
||||
) {
|
||||
DisableScreenTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun HomeContent(
|
||||
pagerState: PagerState,
|
||||
subScreens: ImmutableList<TabItem>
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
ConstraintLayout {
|
||||
val (pager, tabRow) = createRefs()
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
pageSpacing = 0.dp,
|
||||
pageSize = PageSize.Fill,
|
||||
pageNestedScrollConnection =
|
||||
PagerDefaults.pageNestedScrollConnection(
|
||||
state = pagerState,
|
||||
orientation = Orientation.Horizontal
|
||||
),
|
||||
pageContent = { index ->
|
||||
subScreens[index].screenContent()
|
||||
},
|
||||
key = { index ->
|
||||
subScreens[index].title
|
||||
},
|
||||
modifier =
|
||||
Modifier.constrainAs(pager) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(tabRow.top)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
width = Dimension.fillToConstraints
|
||||
height = Dimension.fillToConstraints
|
||||
}
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.constrainAs(tabRow) {
|
||||
top.linkTo(pager.bottom)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
width = Dimension.fillToConstraints
|
||||
height = Dimension.wrapContent
|
||||
}
|
||||
) {
|
||||
HorizontalDivider(
|
||||
thickness = DividerDefaults.Thickness,
|
||||
color = ZcashTheme.colors.primaryDividerColor
|
||||
)
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
// Don't use the predefined divider, as its fixed position is below the tabs bar
|
||||
divider = {},
|
||||
indicator = { tabPositions ->
|
||||
TabRowDefaults.SecondaryIndicator(
|
||||
modifier =
|
||||
Modifier
|
||||
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
|
||||
.padding(horizontal = ZcashTheme.dimens.spacingDefault),
|
||||
color = ZcashTheme.colors.complementaryColor
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
)
|
||||
) {
|
||||
subScreens.forEachIndexed { index, item ->
|
||||
val selected = index == pagerState.currentPage
|
||||
|
||||
NavigationTabText(
|
||||
text = item.title,
|
||||
selected = selected,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
horizontal = ZcashTheme.dimens.spacingXtiny,
|
||||
vertical = ZcashTheme.dimens.spacingDefault
|
||||
)
|
||||
.testTag(item.testTag)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations
|
||||
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.screen.integrations.view.IntegrationsDialogView
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AndroidDialogIntegrations() {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val parent = LocalView.current.parent
|
||||
val viewModel = koinViewModel<IntegrationsViewModel> { parametersOf(true) }
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(enabled = state != null) {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
|
||||
}
|
||||
|
||||
state?.let {
|
||||
IntegrationsDialogView(
|
||||
state = it,
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
it.onBack()
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
sheetState.show()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.hideBottomSheetRequest.collect {
|
||||
sheetState.hide()
|
||||
state?.onBottomSheetHidden?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object DialogIntegrations
|
|
@ -2,39 +2,25 @@ package co.electriccoin.zcash.ui.screen.integrations
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.integrations.view.Integrations
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import com.flexa.core.Flexa
|
||||
import com.flexa.spend.buildSpend
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
internal fun WrapIntegrations() {
|
||||
val activity = LocalActivity.current
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val viewModel = koinViewModel<IntegrationsViewModel>()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
fun AndroidIntegrations() {
|
||||
val walletViewModel = koinViewModel<WalletViewModel>()
|
||||
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.flexaNavigationCommand.collect {
|
||||
Flexa.buildSpend()
|
||||
.onTransactionRequest {
|
||||
viewModel.onFlexaResultCallback(it)
|
||||
}
|
||||
.build()
|
||||
.open(activity)
|
||||
}
|
||||
}
|
||||
val viewModel = koinViewModel<IntegrationsViewModel> { parametersOf(false) }
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler {
|
||||
viewModel.onBack()
|
||||
BackHandler(enabled = state != null) {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
|
||||
state?.let {
|
||||
|
@ -44,3 +30,6 @@ internal fun WrapIntegrations() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object Integrations
|
||||
|
|
|
@ -8,4 +8,5 @@ data class IntegrationsState(
|
|||
val disabledInfo: StringResource?,
|
||||
val onBack: () -> Unit,
|
||||
val items: ImmutableList<ZashiListItemState>,
|
||||
val onBottomSheetHidden: () -> Unit,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiModalBottomSheet
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
internal fun IntegrationsDialogView(
|
||||
onDismissRequest: () -> Unit,
|
||||
sheetState: SheetState,
|
||||
state: IntegrationsState
|
||||
) {
|
||||
ZashiModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
content = {
|
||||
BottomSheetContent(state)
|
||||
},
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(state: IntegrationsState) {
|
||||
Column {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = stringResource(R.string.integrations_dialog_more_options),
|
||||
style = ZashiTypography.textXl,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
IntegrationItems(state, contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun IntegrationSettings() =
|
||||
ZcashTheme {
|
||||
IntegrationsDialogView(
|
||||
onDismissRequest = {},
|
||||
sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
skipHiddenState = true,
|
||||
skipPartiallyExpanded = true,
|
||||
initialValue = SheetValue.Expanded,
|
||||
confirmValueChange = { true }
|
||||
),
|
||||
state =
|
||||
IntegrationsState(
|
||||
onBack = {},
|
||||
disabledInfo = stringRes("Disabled info"),
|
||||
items =
|
||||
persistentListOf(
|
||||
ZashiListItemState(
|
||||
icon = R.drawable.ic_integrations_coinbase,
|
||||
title = stringRes("Coinbase"),
|
||||
subtitle = stringRes("subtitle"),
|
||||
onClick = {}
|
||||
),
|
||||
ZashiListItemState(
|
||||
title = stringRes(R.string.integrations_flexa),
|
||||
subtitle = stringRes(R.string.integrations_flexa),
|
||||
icon = R.drawable.ic_integrations_flexa,
|
||||
onClick = {}
|
||||
),
|
||||
ZashiListItemState(
|
||||
title = stringRes(R.string.integrations_keystone),
|
||||
subtitle = stringRes(R.string.integrations_keystone_subtitle),
|
||||
icon = R.drawable.ic_integrations_keystone,
|
||||
onClick = {}
|
||||
),
|
||||
),
|
||||
onBottomSheetHidden = {}
|
||||
),
|
||||
)
|
||||
}
|
|
@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.screen.integrations.view
|
|||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -64,24 +65,7 @@ fun Integrations(
|
|||
.verticalScroll(rememberScrollState())
|
||||
.scaffoldScrollPadding(paddingValues),
|
||||
) {
|
||||
state.items.forEachIndexed { index, item ->
|
||||
ZashiListItem(
|
||||
state = item,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
leading = {
|
||||
ZashiListItemDefaults.LeadingItem(
|
||||
modifier = Modifier.size(40.dp),
|
||||
icon = item.icon,
|
||||
contentDescription = item.title.getValue()
|
||||
)
|
||||
},
|
||||
)
|
||||
if (index != state.items.lastIndex) {
|
||||
ZashiHorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
IntegrationItems(state)
|
||||
|
||||
state.disabledInfo?.let {
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
|
@ -114,6 +98,32 @@ fun Integrations(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IntegrationItems(
|
||||
state: IntegrationsState,
|
||||
contentPadding: PaddingValues = ZashiListItemDefaults.contentPadding
|
||||
) {
|
||||
state.items.forEachIndexed { index, item ->
|
||||
ZashiListItem(
|
||||
state = item,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
leading = {
|
||||
ZashiListItemDefaults.LeadingItem(
|
||||
modifier = Modifier.size(40.dp),
|
||||
icon = item.icon,
|
||||
contentDescription = item.title.getValue()
|
||||
)
|
||||
},
|
||||
contentPadding = contentPadding
|
||||
)
|
||||
if (index != state.items.lastIndex) {
|
||||
ZashiHorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisabledInfo(it: StringResource) {
|
||||
Row(
|
||||
|
@ -187,7 +197,8 @@ private fun IntegrationSettings() =
|
|||
icon = R.drawable.ic_integrations_keystone,
|
||||
onClick = {}
|
||||
),
|
||||
)
|
||||
),
|
||||
onBottomSheetHidden = {}
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
|
|
@ -1,85 +1,60 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.Proposal
|
||||
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.WalletAddress
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.ZecSendExt
|
||||
import cash.z.ecc.android.sdk.model.proposeSend
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.SubmitResult
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRequest
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiSpendingKeyUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
|
||||
import co.electriccoin.zcash.ui.screen.flexa.Flexa
|
||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||
import com.flexa.core.Flexa
|
||||
import com.flexa.spend.Transaction
|
||||
import com.flexa.spend.buildSpend
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IntegrationsViewModel(
|
||||
getZcashCurrency: GetZcashCurrencyProvider,
|
||||
observeWalletState: ObserveWalletStateUseCase,
|
||||
getWalletRestoringState: GetWalletRestoringStateUseCase,
|
||||
isFlexaAvailableUseCase: IsFlexaAvailableUseCase,
|
||||
isCoinbaseAvailable: IsCoinbaseAvailableUseCase,
|
||||
private val getSynchronizer: GetSynchronizerUseCase,
|
||||
private val getZashiAccount: GetZashiAccountUseCase,
|
||||
private val isFlexaAvailable: IsFlexaAvailableUseCase,
|
||||
private val getSpendingKey: GetZashiSpendingKeyUseCase,
|
||||
private val context: Context,
|
||||
private val biometricRepository: BiometricRepository,
|
||||
observeWalletAccounts: ObserveWalletAccountsUseCase,
|
||||
private val isDialog: Boolean,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val navigateToCoinbase: NavigateToCoinbaseUseCase,
|
||||
private val observeWalletAccounts: ObserveWalletAccountsUseCase,
|
||||
) : ViewModel() {
|
||||
val flexaNavigationCommand = MutableSharedFlow<Unit>()
|
||||
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
|
||||
|
||||
private val isEnabled =
|
||||
observeWalletState()
|
||||
.map {
|
||||
it != TopAppBarSubTitleState.Restoring
|
||||
}
|
||||
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
|
||||
|
||||
private val isRestoring = getWalletRestoringState.observe().map { it == WalletRestoringState.RESTORING }
|
||||
|
||||
val state =
|
||||
combine(
|
||||
isFlexaAvailableUseCase.observe(),
|
||||
isCoinbaseAvailable.observe(),
|
||||
isEnabled,
|
||||
isRestoring,
|
||||
observeWalletAccounts()
|
||||
) { isFlexaAvailable, isCoinbaseAvailable, isEnabled, accounts ->
|
||||
) { isFlexaAvailable, isCoinbaseAvailable, isRestoring, accounts ->
|
||||
IntegrationsState(
|
||||
disabledInfo = stringRes(R.string.integrations_disabled_info).takeIf { isEnabled.not() },
|
||||
disabledInfo =
|
||||
stringRes(R.string.integrations_disabled_info)
|
||||
.takeIf { isRestoring },
|
||||
onBack = ::onBack,
|
||||
items =
|
||||
listOfNotNull(
|
||||
|
@ -98,9 +73,9 @@ class IntegrationsViewModel(
|
|||
ZashiListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from
|
||||
// the UI in the Testnet build
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = isRestoring.not(),
|
||||
icon =
|
||||
if (isEnabled) {
|
||||
if (isRestoring.not()) {
|
||||
R.drawable.ic_integrations_flexa
|
||||
} else {
|
||||
R.drawable.ic_integrations_flexa_disabled
|
||||
|
@ -115,7 +90,8 @@ class IntegrationsViewModel(
|
|||
icon = R.drawable.ic_integrations_keystone,
|
||||
onClick = ::onConnectKeystoneClick
|
||||
).takeIf { accounts.orEmpty().none { it is KeystoneAccount } },
|
||||
).toImmutableList()
|
||||
).toImmutableList(),
|
||||
onBottomSheetHidden = ::onBottomSheetHidden
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
|
@ -123,186 +99,40 @@ class IntegrationsViewModel(
|
|||
initialValue = null
|
||||
)
|
||||
|
||||
fun onBack() = navigationRouter.back()
|
||||
private fun onBack() = navigationRouter.back()
|
||||
|
||||
private suspend fun hideBottomSheet() {
|
||||
if (isDialog) {
|
||||
hideBottomSheetRequest.emit(Unit)
|
||||
bottomSheetHiddenResponse.first()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBottomSheetHidden() =
|
||||
viewModelScope.launch {
|
||||
bottomSheetHiddenResponse.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onBuyWithCoinbaseClicked() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
navigateToCoinbase()
|
||||
}
|
||||
|
||||
private fun onConnectKeystoneClick() = navigationRouter.forward(ConnectKeystone)
|
||||
private fun onConnectKeystoneClick() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
navigationRouter.forward(ConnectKeystone)
|
||||
}
|
||||
|
||||
private fun onFlexaClicked() =
|
||||
viewModelScope.launch {
|
||||
if (isFlexaAvailable()) {
|
||||
flexaNavigationCommand.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun onFlexaResultCallback(transaction: Result<Transaction>) =
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
biometricRepository.requestBiometrics(
|
||||
BiometricRequest(message = stringRes(R.string.integrations_flexa_biometric_message))
|
||||
)
|
||||
Twig.debug { "Getting send transaction proposal" }
|
||||
getSynchronizer()
|
||||
.proposeSend(
|
||||
account = getZashiAccount().sdkAccount,
|
||||
send = getZecSend(transaction.getOrNull())
|
||||
)
|
||||
}.onSuccess { proposal ->
|
||||
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
|
||||
val result = submitTransactions(proposal = proposal, spendingKey = getSpendingKey())
|
||||
when (val output = result.first) {
|
||||
is SubmitResult.Success -> {
|
||||
Twig.debug { "Transaction successful $result" }
|
||||
Flexa.buildSpend()
|
||||
.transactionSent(
|
||||
commerceSessionId = transaction.getOrNull()?.commerceSessionId.orEmpty(),
|
||||
txSignature = result.second.orEmpty()
|
||||
)
|
||||
}
|
||||
is SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc -> {
|
||||
Twig.warn { "Transaction grpc failure $result" }
|
||||
Flexa.buildSpend()
|
||||
.transactionSent(
|
||||
commerceSessionId = transaction.getOrNull()?.commerceSessionId.orEmpty(),
|
||||
txSignature = output.result.txIdString()
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Twig.error { "Transaction submission failed" }
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Twig.error(it) { "Transaction proposal failed" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun submitTransactions(
|
||||
proposal: Proposal,
|
||||
spendingKey: UnifiedSpendingKey
|
||||
): Pair<SubmitResult, String?> {
|
||||
Twig.debug { "Sending transactions..." }
|
||||
|
||||
val result =
|
||||
runCreateTransactions(
|
||||
synchronizer = getSynchronizer(),
|
||||
spendingKey = spendingKey,
|
||||
proposal = proposal
|
||||
)
|
||||
|
||||
// Triggering the transaction history and balances refresh to be notified immediately
|
||||
// about the wallet's updated state
|
||||
(getSynchronizer() as SdkSynchronizer).run {
|
||||
refreshTransactions()
|
||||
refreshAllBalances()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun runCreateTransactions(
|
||||
synchronizer: Synchronizer,
|
||||
spendingKey: UnifiedSpendingKey,
|
||||
proposal: Proposal
|
||||
): Pair<SubmitResult, String?> {
|
||||
val submitResults = mutableListOf<TransactionSubmitResult>()
|
||||
|
||||
return runCatching {
|
||||
synchronizer.createProposedTransactions(
|
||||
proposal = proposal,
|
||||
usk = spendingKey
|
||||
).collect { submitResult ->
|
||||
Twig.info { "Transaction submit result: $submitResult" }
|
||||
submitResults.add(submitResult)
|
||||
}
|
||||
if (submitResults.find { it is TransactionSubmitResult.Failure } != null) {
|
||||
if (submitResults.size == 1) {
|
||||
// The first transaction submission failed - user might just be able to re-submit the transaction
|
||||
// proposal. Simple error pop up is fine then
|
||||
val result = (submitResults[0] as TransactionSubmitResult.Failure)
|
||||
if (result.grpcError) {
|
||||
SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc(result) to null
|
||||
} else {
|
||||
SubmitResult.SimpleTrxFailure.SimpleTrxFailureSubmit(result) to null
|
||||
}
|
||||
} else {
|
||||
// Any subsequent transaction submission failed - user needs to resolve this manually. Multiple
|
||||
// transaction failure screen presented
|
||||
SubmitResult.MultipleTrxFailure(submitResults) to null
|
||||
}
|
||||
if (isDialog) {
|
||||
hideBottomSheet()
|
||||
navigationRouter.replace(Flexa)
|
||||
} else {
|
||||
// All transaction submissions were successful
|
||||
SubmitResult.Success(emptyList()) to
|
||||
submitResults.filterIsInstance<TransactionSubmitResult.Success>()
|
||||
.map { it.txIdString() }.firstOrNull()
|
||||
}
|
||||
}.onSuccess {
|
||||
Twig.debug { "Transactions submitted successfully" }
|
||||
}.onFailure {
|
||||
Twig.error(it) { "Transactions submission failed" }
|
||||
}.getOrElse {
|
||||
SubmitResult.SimpleTrxFailure.SimpleTrxFailureOther(it) to null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
private suspend fun getZecSend(transaction: Transaction?): ZecSend {
|
||||
if (transaction == null) throw NullPointerException("Transaction is null")
|
||||
|
||||
val address = transaction.destinationAddress.split(":").last()
|
||||
|
||||
val recipientAddressState =
|
||||
RecipientAddressState.new(
|
||||
address = address,
|
||||
// TODO [#342]: Verify Addresses without Synchronizer
|
||||
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
|
||||
type = getSynchronizer().validateAddress(address)
|
||||
)
|
||||
|
||||
return when (
|
||||
val zecSendValidation =
|
||||
ZecSendExt.new(
|
||||
context = context,
|
||||
destinationString = address,
|
||||
zecString = transaction.amount,
|
||||
// Take memo for a valid non-transparent receiver only
|
||||
memoString = ""
|
||||
)
|
||||
) {
|
||||
is ZecSendExt.ZecSendValidation.Valid ->
|
||||
zecSendValidation.zecSend.copy(
|
||||
destination =
|
||||
when (recipientAddressState.type) {
|
||||
is AddressType.Invalid ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Shielded ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Tex ->
|
||||
WalletAddress.Tex.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Transparent ->
|
||||
WalletAddress.Transparent.new(recipientAddressState.address)
|
||||
|
||||
AddressType.Unified ->
|
||||
WalletAddress.Unified.new(recipientAddressState.address)
|
||||
|
||||
null -> WalletAddress.Unified.new(recipientAddressState.address)
|
||||
}
|
||||
)
|
||||
|
||||
is ZecSendExt.ZecSendValidation.Invalid -> {
|
||||
// We do not expect this validation to fail, so logging is enough here
|
||||
// An error popup could be reasonable here as well
|
||||
Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" }
|
||||
|
||||
throw RuntimeException("Validation failed")
|
||||
hideBottomSheet()
|
||||
navigationRouter.forward(Flexa)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,23 +2,28 @@
|
|||
|
||||
package co.electriccoin.zcash.ui.screen.receive
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.screen.receive.view.ReceiveView
|
||||
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
||||
|
||||
@Composable
|
||||
internal fun WrapReceive() {
|
||||
internal fun AndroidReceive() {
|
||||
val receiveViewModel = koinActivityViewModel<ReceiveViewModel>()
|
||||
val receiveState by receiveViewModel.state.collectAsStateWithLifecycle()
|
||||
val topAppBarViewModel = koinActivityViewModel<ZashiMainTopAppBarViewModel>()
|
||||
val zashiMainTopAppBarState by topAppBarViewModel.state.collectAsStateWithLifecycle()
|
||||
val state by receiveViewModel.state.collectAsStateWithLifecycle()
|
||||
val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>()
|
||||
val appBarState by topAppBarViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler {
|
||||
state.onBack()
|
||||
}
|
||||
|
||||
ReceiveView(
|
||||
state = receiveState,
|
||||
zashiMainTopAppBarState = zashiMainTopAppBarState,
|
||||
state = state,
|
||||
appBarState = appBarState,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.screen.receive
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object Receive
|
|
@ -4,7 +4,8 @@ import co.electriccoin.zcash.ui.design.util.StringResource
|
|||
|
||||
data class ReceiveState(
|
||||
val items: List<ReceiveAddressState>?,
|
||||
val isLoading: Boolean
|
||||
val isLoading: Boolean,
|
||||
val onBack: () -> Unit
|
||||
)
|
||||
|
||||
data class ReceiveAddressState(
|
||||
|
|
|
@ -31,16 +31,17 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppbar
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressState
|
||||
|
@ -49,7 +50,7 @@ import co.electriccoin.zcash.ui.screen.receive.model.ReceiveState
|
|||
@Composable
|
||||
internal fun ReceiveView(
|
||||
state: ReceiveState,
|
||||
zashiMainTopAppBarState: ZashiMainTopAppBarState?,
|
||||
appBarState: ZashiMainTopAppBarState?,
|
||||
) {
|
||||
when {
|
||||
state.items.isNullOrEmpty() && state.isLoading -> {
|
||||
|
@ -59,15 +60,20 @@ internal fun ReceiveView(
|
|||
else -> {
|
||||
BlankBgScaffold(
|
||||
topBar = {
|
||||
ZashiMainTopAppBar(state = zashiMainTopAppBarState, showHideBalances = false)
|
||||
ZashiTopAppbar(
|
||||
title = null,
|
||||
state = appBarState,
|
||||
showHideBalances = false,
|
||||
onBack = state.onBack
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
ReceiveContents(
|
||||
items = state.items.orEmpty(),
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
Modifier.scaffoldScrollPadding(
|
||||
paddingValues = paddingValues,
|
||||
top = paddingValues.calculateTopPadding()
|
||||
// We intentionally do not set the rest paddings, those are set by the underlying composable
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -266,8 +272,13 @@ private fun ReceiveIconButton(
|
|||
private fun LoadingPreview() =
|
||||
ZcashTheme(forceDarkMode = true) {
|
||||
ReceiveView(
|
||||
state = ReceiveState(items = null, isLoading = true),
|
||||
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new()
|
||||
state =
|
||||
ReceiveState(
|
||||
items = null,
|
||||
isLoading = true,
|
||||
onBack = {}
|
||||
),
|
||||
appBarState = ZashiMainTopAppBarStateFixture.new()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -303,8 +314,9 @@ private fun ZashiPreview() =
|
|||
onClick = {}
|
||||
)
|
||||
),
|
||||
isLoading = false
|
||||
isLoading = false,
|
||||
onBack = {}
|
||||
),
|
||||
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new()
|
||||
appBarState = ZashiMainTopAppBarStateFixture.new()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -55,12 +55,18 @@ class ReceiveViewModel(
|
|||
onClick = { onAddressClick(1) }
|
||||
),
|
||||
),
|
||||
isLoading = false
|
||||
isLoading = false,
|
||||
onBack = ::onBack
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = ReceiveState(items = null, isLoading = true)
|
||||
initialValue =
|
||||
ReceiveState(
|
||||
items = null,
|
||||
isLoading = true,
|
||||
onBack = ::onBack
|
||||
)
|
||||
)
|
||||
|
||||
init {
|
||||
|
@ -71,6 +77,8 @@ class ReceiveViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onBack() = navigationRouter.back()
|
||||
|
||||
private fun createAddressState(
|
||||
account: WalletAccount,
|
||||
address: String,
|
||||
|
@ -96,6 +104,7 @@ class ReceiveViewModel(
|
|||
} else {
|
||||
stringRes(R.string.receive_wallet_address_transparent_keystone)
|
||||
}
|
||||
|
||||
is ZashiAccount ->
|
||||
if (type == ReceiveAddressType.Unified) {
|
||||
stringRes(R.string.receive_wallet_address_shielded)
|
||||
|
|
|
@ -27,7 +27,6 @@ import androidx.compose.ui.unit.sp
|
|||
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
|
||||
import cash.z.ecc.sdk.extension.toZecStringFull
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceWidgetBigLineOnly
|
||||
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
|
@ -51,6 +50,7 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
|||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetHeader
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeLabel
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
|
@ -340,7 +340,7 @@ private fun AmountWidget(state: AmountState) {
|
|||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
}
|
||||
BalanceWidgetBigLineOnly(
|
||||
BalanceWidgetHeader(
|
||||
parts = state.amount.toZecStringFull().asZecAmountTriple(),
|
||||
isHideBalances = false
|
||||
)
|
||||
|
|
|
@ -3,21 +3,17 @@ package co.electriccoin.zcash.ui.screen.scan
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_RECIPIENT_ADDRESS
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_SCAN_ZIP_321_URI
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.popBackStackJustOnce
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResultState
|
||||
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
|
||||
import co.electriccoin.zcash.ui.util.SettingsUtil
|
||||
|
@ -26,7 +22,7 @@ import org.koin.androidx.compose.koinViewModel
|
|||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
internal fun WrapScanValidator(args: ScanNavigationArgs) {
|
||||
internal fun WrapScanValidator(args: Scan) {
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
@ -38,26 +34,7 @@ internal fun WrapScanValidator(args: ScanNavigationArgs) {
|
|||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler {
|
||||
navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.navigateBack.collect { scanResult ->
|
||||
navController.previousBackStackEntry?.savedStateHandle?.apply {
|
||||
when (scanResult) {
|
||||
is ScanResultState.Address -> set(SEND_SCAN_RECIPIENT_ADDRESS, scanResult.address)
|
||||
is ScanResultState.Zip321Uri -> set(SEND_SCAN_ZIP_321_URI, scanResult.zip321Uri)
|
||||
}
|
||||
}
|
||||
navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.navigateCommand.collect {
|
||||
navController.popBackStack()
|
||||
navController.navigate(it)
|
||||
}
|
||||
navController.popBackStackJustOnce(Scan.ROUTE)
|
||||
}
|
||||
|
||||
if (synchronizer == null) {
|
||||
|
@ -69,7 +46,7 @@ internal fun WrapScanValidator(args: ScanNavigationArgs) {
|
|||
Scan(
|
||||
snackbarHostState = snackbarHostState,
|
||||
validationResult = state,
|
||||
onBack = { navController.popBackStackJustOnce(ScanNavigationArgs.ROUTE) },
|
||||
onBack = { navController.popBackStackJustOnce(Scan.ROUTE) },
|
||||
onScanned = {
|
||||
viewModel.onScanned(it)
|
||||
},
|
||||
|
@ -95,8 +72,9 @@ internal fun WrapScanValidator(args: ScanNavigationArgs) {
|
|||
}
|
||||
}
|
||||
|
||||
enum class ScanNavigationArgs {
|
||||
DEFAULT,
|
||||
enum class Scan {
|
||||
HOMEPAGE,
|
||||
SEND,
|
||||
ADDRESS_BOOK;
|
||||
|
||||
companion object {
|
||||
|
@ -104,6 +82,6 @@ enum class ScanNavigationArgs {
|
|||
const val KEY = "mode"
|
||||
const val ROUTE = "$PATH/{$KEY}"
|
||||
|
||||
operator fun invoke(mode: ScanNavigationArgs) = "$PATH/${mode.name}"
|
||||
operator fun invoke(mode: Scan) = "$PATH/${mode.name}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.scan.model
|
||||
|
||||
sealed class ScanResultState {
|
||||
data class Address(val address: String) : ScanResultState()
|
||||
|
||||
data class Zip321Uri(val zip321Uri: String) : ScanResultState()
|
||||
}
|
|
@ -3,34 +3,27 @@ package co.electriccoin.zcash.ui.screen.scan.viewmodel
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnAddressScannedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnZip321ScannedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase.Zip321ParseUriValidation
|
||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs.ADDRESS_BOOK
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs.DEFAULT
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanResultState
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
internal class ScanViewModel(
|
||||
private val args: ScanNavigationArgs,
|
||||
private val args: Scan,
|
||||
private val getSynchronizer: GetSynchronizerUseCase,
|
||||
private val zip321ParseUriValidationUseCase: Zip321ParseUriValidationUseCase,
|
||||
private val onAddressScanned: OnAddressScannedUseCase,
|
||||
private val zip321Scanned: OnZip321ScannedUseCase
|
||||
) : ViewModel() {
|
||||
val navigateBack = MutableSharedFlow<ScanResultState>()
|
||||
|
||||
val navigateCommand = MutableSharedFlow<String>()
|
||||
|
||||
var state = MutableStateFlow(ScanValidationState.NONE)
|
||||
val state = MutableStateFlow(ScanValidationState.NONE)
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
|
@ -40,77 +33,58 @@ internal class ScanViewModel(
|
|||
viewModelScope.launch {
|
||||
mutex.withLock {
|
||||
if (!hasBeenScannedSuccessfully) {
|
||||
val addressValidationResult = getSynchronizer().validateAddress(result)
|
||||
val zip321ValidationResult = zip321ParseUriValidationUseCase(result)
|
||||
runCatching {
|
||||
val zip321ValidationResult = zip321ParseUriValidationUseCase(result)
|
||||
val addressValidationResult = getSynchronizer().validateAddress(result)
|
||||
|
||||
when {
|
||||
zip321ValidationResult is Zip321ParseUriValidation.Valid ->
|
||||
{
|
||||
hasBeenScannedSuccessfully = true
|
||||
state.update { ScanValidationState.VALID }
|
||||
navigateBack.emit(ScanResultState.Zip321Uri(zip321ValidationResult.zip321Uri))
|
||||
}
|
||||
zip321ValidationResult is Zip321ParseUriValidation.SingleAddress ->
|
||||
{
|
||||
hasBeenScannedSuccessfully = true
|
||||
val singleAddressValidation =
|
||||
getSynchronizer()
|
||||
.validateAddress(zip321ValidationResult.address)
|
||||
when (singleAddressValidation) {
|
||||
is AddressType.Invalid -> {
|
||||
state.update { ScanValidationState.INVALID }
|
||||
}
|
||||
else -> {
|
||||
state.update { ScanValidationState.VALID }
|
||||
processAddress(zip321ValidationResult.address, singleAddressValidation)
|
||||
}
|
||||
}
|
||||
}
|
||||
addressValidationResult is AddressType.Valid ->
|
||||
{
|
||||
hasBeenScannedSuccessfully = true
|
||||
state.update { ScanValidationState.VALID }
|
||||
processAddress(result, addressValidationResult)
|
||||
}
|
||||
else -> {
|
||||
hasBeenScannedSuccessfully = false
|
||||
state.update { ScanValidationState.INVALID }
|
||||
when {
|
||||
zip321ValidationResult is Zip321ParseUriValidation.Valid ->
|
||||
onZip321Scanned(zip321ValidationResult)
|
||||
|
||||
zip321ValidationResult is Zip321ParseUriValidation.SingleAddress ->
|
||||
onZip321SingleAddressScanned(zip321ValidationResult)
|
||||
|
||||
addressValidationResult is AddressType.Valid ->
|
||||
onAddressScanned(result, addressValidationResult)
|
||||
|
||||
else -> onInvalidScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processAddress(
|
||||
address: String,
|
||||
addressType: AddressType
|
||||
private fun onInvalidScan() {
|
||||
hasBeenScannedSuccessfully = false
|
||||
state.update { ScanValidationState.INVALID }
|
||||
}
|
||||
|
||||
private fun onAddressScanned(
|
||||
result: String,
|
||||
addressValidationResult: AddressType
|
||||
) {
|
||||
require(addressType is AddressType.Valid)
|
||||
hasBeenScannedSuccessfully = true
|
||||
state.update { ScanValidationState.VALID }
|
||||
onAddressScanned(result, addressValidationResult, args)
|
||||
}
|
||||
|
||||
val serializableAddress =
|
||||
SerializableAddress(
|
||||
address = address,
|
||||
type = addressType
|
||||
)
|
||||
|
||||
when (args) {
|
||||
DEFAULT -> {
|
||||
navigateBack.emit(
|
||||
ScanResultState.Address(
|
||||
Json.encodeToString(
|
||||
SerializableAddress.serializer(),
|
||||
serializableAddress
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ADDRESS_BOOK -> {
|
||||
navigateCommand.emit(AddContactArgs(serializableAddress.address))
|
||||
}
|
||||
private suspend fun onZip321SingleAddressScanned(zip321ValidationResult: Zip321ParseUriValidation.SingleAddress) {
|
||||
hasBeenScannedSuccessfully = true
|
||||
val singleAddressValidation = getSynchronizer().validateAddress(zip321ValidationResult.address)
|
||||
if (singleAddressValidation is AddressType.Invalid) {
|
||||
state.update { ScanValidationState.INVALID }
|
||||
} else {
|
||||
state.update { ScanValidationState.VALID }
|
||||
onAddressScanned(zip321ValidationResult.address, singleAddressValidation, args)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onZip321Scanned(zip321ValidationResult: Zip321ParseUriValidation.Valid) {
|
||||
hasBeenScannedSuccessfully = true
|
||||
state.update { ScanValidationState.VALID }
|
||||
zip321Scanned(zip321ValidationResult, args)
|
||||
}
|
||||
|
||||
fun onScannedError() =
|
||||
viewModelScope.launch {
|
||||
mutex.withLock {
|
||||
|
|
|
@ -19,22 +19,25 @@ import cash.z.ecc.android.sdk.model.ZecSend
|
|||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveClearSendUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.PrefillSendData
|
||||
import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceState
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceViewModel
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
|
||||
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -43,45 +46,39 @@ import org.koin.compose.koinInject
|
|||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun WrapSend(
|
||||
sendArguments: SendArguments?,
|
||||
goToQrScanner: () -> Unit,
|
||||
goBack: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
) {
|
||||
internal fun WrapSend(args: Send) {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
val navigationRouter = koinInject<NavigationRouter>()
|
||||
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
|
||||
val homeViewModel = koinActivityViewModel<HomeViewModel>()
|
||||
val balanceViewModel = koinActivityViewModel<BalanceViewModel>()
|
||||
|
||||
val accountDataSource = koinInject<AccountDataSource>()
|
||||
|
||||
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
|
||||
|
||||
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
||||
|
||||
val walletSnapshot = walletViewModel.currentWalletSnapshot.collectAsStateWithLifecycle().value
|
||||
val selectedAccount = accountDataSource.selectedAccount.collectAsStateWithLifecycle(null).value
|
||||
|
||||
val monetarySeparators = MonetarySeparators.current(Locale.getDefault())
|
||||
|
||||
val balanceState = walletViewModel.balanceState.collectAsStateWithLifecycle().value
|
||||
|
||||
val isHideBalances = homeViewModel.isHideBalances.collectAsStateWithLifecycle().value ?: false
|
||||
val balanceState = balanceViewModel.state.collectAsStateWithLifecycle().value
|
||||
|
||||
val exchangeRateState = walletViewModel.exchangeRateUsd.collectAsStateWithLifecycle().value
|
||||
|
||||
WrapSend(
|
||||
balanceState = balanceState,
|
||||
exchangeRateState = exchangeRateState,
|
||||
isHideBalances = isHideBalances,
|
||||
goToQrScanner = goToQrScanner,
|
||||
goBack = goBack,
|
||||
goBalances = goBalances,
|
||||
goToQrScanner = { navigationRouter.forward(Scan(Scan.SEND)) },
|
||||
goBack = { navigationRouter.back() },
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
monetarySeparators = monetarySeparators,
|
||||
sendArguments = sendArguments,
|
||||
sendArguments = args,
|
||||
synchronizer = synchronizer,
|
||||
walletSnapshot = walletSnapshot
|
||||
selectedAccount = selectedAccount
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -91,15 +88,13 @@ internal fun WrapSend(
|
|||
internal fun WrapSend(
|
||||
balanceState: BalanceState,
|
||||
exchangeRateState: ExchangeRateState,
|
||||
isHideBalances: Boolean,
|
||||
goToQrScanner: () -> Unit,
|
||||
goBack: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
hasCameraFeature: Boolean,
|
||||
monetarySeparators: MonetarySeparators,
|
||||
sendArguments: SendArguments?,
|
||||
sendArguments: Send,
|
||||
synchronizer: Synchronizer?,
|
||||
walletSnapshot: WalletSnapshot?,
|
||||
selectedAccount: WalletAccount?,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
@ -115,7 +110,7 @@ internal fun WrapSend(
|
|||
|
||||
val sendAddressBookState by viewModel.sendAddressBookState.collectAsStateWithLifecycle()
|
||||
|
||||
val topAppBarViewModel = koinActivityViewModel<ZashiMainTopAppBarViewModel>()
|
||||
val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>()
|
||||
|
||||
val zashiMainTopAppBarState by topAppBarViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -131,27 +126,20 @@ internal fun WrapSend(
|
|||
val observeClearSend = koinInject<ObserveClearSendUseCase>()
|
||||
val prefillSend = koinInject<PrefillSendUseCase>()
|
||||
|
||||
if (sendArguments?.recipientAddress != null) {
|
||||
if (sendArguments.recipientAddress != null && sendArguments.recipientAddressType != null) {
|
||||
viewModel.onRecipientAddressChanged(
|
||||
RecipientAddressState.new(
|
||||
sendArguments.recipientAddress.address,
|
||||
sendArguments.recipientAddress.type
|
||||
sendArguments.recipientAddress,
|
||||
when (sendArguments.recipientAddressType) {
|
||||
cash.z.ecc.sdk.model.AddressType.UNIFIED -> AddressType.Unified
|
||||
cash.z.ecc.sdk.model.AddressType.TRANSPARENT -> AddressType.Transparent
|
||||
cash.z.ecc.sdk.model.AddressType.SAPLING -> AddressType.Shielded
|
||||
cash.z.ecc.sdk.model.AddressType.TEX -> AddressType.Tex
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Zip321 Uri scan result processing
|
||||
if (sendArguments?.zip321Uri != null &&
|
||||
synchronizer != null
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.onCreateZecSend321Click(
|
||||
zip321Uri = sendArguments.zip321Uri,
|
||||
setSendStage = setSendStage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Amount computation:
|
||||
val (amountState, setAmountState) =
|
||||
rememberSaveable(stateSaver = AmountState.Saver) {
|
||||
|
@ -226,51 +214,48 @@ internal fun WrapSend(
|
|||
|
||||
LaunchedEffect(Unit) {
|
||||
prefillSend().collect {
|
||||
val type = synchronizer?.validateAddress(it.recipientAddress?.address.orEmpty())
|
||||
setSendStage(SendStage.Form)
|
||||
setZecSend(null)
|
||||
viewModel.onRecipientAddressChanged(
|
||||
RecipientAddressState.new(
|
||||
address = it.recipientAddress?.address.orEmpty(),
|
||||
type = type
|
||||
)
|
||||
)
|
||||
when (it) {
|
||||
is PrefillSendData.All -> {
|
||||
val type = synchronizer?.validateAddress(it.address.orEmpty())
|
||||
setSendStage(SendStage.Form)
|
||||
setZecSend(null)
|
||||
viewModel.onRecipientAddressChanged(
|
||||
RecipientAddressState.new(
|
||||
address = it.address.orEmpty(),
|
||||
type = type
|
||||
)
|
||||
)
|
||||
|
||||
val fee = it.transaction.fee
|
||||
val value = if (fee == null) it.transaction.amount else it.transaction.amount - fee
|
||||
val fee = it.fee
|
||||
val value = if (fee == null) it.amount else it.amount - fee
|
||||
|
||||
setAmountState(
|
||||
AmountState.newFromZec(
|
||||
context = context,
|
||||
value = value.convertZatoshiToZecString(),
|
||||
monetarySeparators = monetarySeparators,
|
||||
isTransparentOrTextRecipient = type == AddressType.Transparent,
|
||||
fiatValue = amountState.fiatValue,
|
||||
exchangeRateState = exchangeRateState
|
||||
)
|
||||
)
|
||||
setMemoState(MemoState.new(it.memos?.firstOrNull().orEmpty()))
|
||||
setAmountState(
|
||||
AmountState.newFromZec(
|
||||
context = context,
|
||||
value = value.convertZatoshiToZecString(),
|
||||
monetarySeparators = monetarySeparators,
|
||||
isTransparentOrTextRecipient = type == AddressType.Transparent,
|
||||
fiatValue = amountState.fiatValue,
|
||||
exchangeRateState = exchangeRateState
|
||||
)
|
||||
)
|
||||
setMemoState(MemoState.new(it.memos?.firstOrNull().orEmpty()))
|
||||
}
|
||||
is PrefillSendData.FromAddressScan -> {
|
||||
val type = synchronizer?.validateAddress(it.address)
|
||||
setSendStage(SendStage.Form)
|
||||
setZecSend(null)
|
||||
viewModel.onRecipientAddressChanged(
|
||||
RecipientAddressState.new(
|
||||
address = it.address,
|
||||
type = type
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clearing form from the previous navigation destination if required
|
||||
if (sendArguments?.clearForm == true) {
|
||||
setSendStage(SendStage.Form)
|
||||
setZecSend(null)
|
||||
viewModel.onRecipientAddressChanged(RecipientAddressState.new("", null))
|
||||
setAmountState(
|
||||
AmountState.newFromZec(
|
||||
context = context,
|
||||
monetarySeparators = monetarySeparators,
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
isTransparentOrTextRecipient = false,
|
||||
exchangeRateState = exchangeRateState
|
||||
)
|
||||
)
|
||||
setMemoState(MemoState.new(""))
|
||||
}
|
||||
|
||||
val onBackAction = {
|
||||
when (sendStage) {
|
||||
SendStage.Form -> goBack()
|
||||
|
@ -282,7 +267,7 @@ internal fun WrapSend(
|
|||
}
|
||||
}
|
||||
|
||||
if (null == synchronizer || null == walletSnapshot) {
|
||||
if (null == synchronizer || null == selectedAccount) {
|
||||
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
||||
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
|
||||
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
|
||||
|
@ -290,7 +275,6 @@ internal fun WrapSend(
|
|||
} else {
|
||||
Send(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
sendStage = sendStage,
|
||||
onCreateZecSend = { newZecSend ->
|
||||
viewModel.onCreateZecSendClick(
|
||||
|
@ -299,6 +283,8 @@ internal fun WrapSend(
|
|||
)
|
||||
},
|
||||
onBack = onBackAction,
|
||||
onQrScannerOpen = goToQrScanner,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
recipientAddressState = recipientAddressState,
|
||||
onRecipientAddressChange = {
|
||||
scope.launch {
|
||||
|
@ -312,14 +298,11 @@ internal fun WrapSend(
|
|||
)
|
||||
}
|
||||
},
|
||||
memoState = memoState,
|
||||
setMemoState = setMemoState,
|
||||
amountState = amountState,
|
||||
setAmountState = setAmountState,
|
||||
onQrScannerOpen = goToQrScanner,
|
||||
goBalances = goBalances,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
walletSnapshot = walletSnapshot,
|
||||
amountState = amountState,
|
||||
setMemoState = setMemoState,
|
||||
memoState = memoState,
|
||||
selectedAccount = selectedAccount,
|
||||
exchangeRateState = exchangeRateState,
|
||||
sendAddressBookState = sendAddressBookState,
|
||||
zashiMainTopAppBarState = zashiMainTopAppBarState
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package co.electriccoin.zcash.ui.screen.send
|
||||
|
||||
import cash.z.ecc.sdk.model.AddressType
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Send(
|
||||
val recipientAddress: String? = null,
|
||||
val recipientAddressType: AddressType? = null,
|
||||
)
|
|
@ -6,7 +6,6 @@ import cash.z.ecc.android.sdk.model.ZecSend
|
|||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateZip321ProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
|
||||
|
@ -34,7 +33,6 @@ class SendViewModel(
|
|||
private val observeContactByAddress: ObserveContactByAddressUseCase,
|
||||
private val observeContactPicked: ObserveContactPickedUseCase,
|
||||
private val createProposal: CreateProposalUseCase,
|
||||
private val createKeystoneZip321TransactionProposal: CreateZip321ProposalUseCase,
|
||||
private val observeWalletAccounts: ObserveWalletAccountsUseCase,
|
||||
private val navigateToAddressBook: NavigateToAddressBookUseCase
|
||||
) : ViewModel() {
|
||||
|
@ -142,17 +140,4 @@ class SendViewModel(
|
|||
Twig.error(e) { "Error creating proposal" }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
fun onCreateZecSend321Click(
|
||||
zip321Uri: String,
|
||||
setSendStage: (SendStage) -> Unit,
|
||||
) = viewModelScope.launch {
|
||||
try {
|
||||
createKeystoneZip321TransactionProposal(zip321Uri)
|
||||
} catch (e: Exception) {
|
||||
setSendStage(SendStage.SendFailure(e.cause?.message ?: e.message ?: ""))
|
||||
Twig.error(e) { "Error creating proposal" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.model
|
||||
|
||||
data class SendArguments(
|
||||
val recipientAddress: RecipientAddressState? = null,
|
||||
val zip321Uri: String? = null,
|
||||
val clearForm: Boolean = false,
|
||||
)
|
|
@ -57,27 +57,25 @@ import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
|||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.canSpend
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppbar
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTextField
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceState
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
||||
import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
||||
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
||||
|
@ -92,11 +90,11 @@ import java.util.Locale
|
|||
private fun PreviewSendForm() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
Send(
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
sendStage = SendStage.Form,
|
||||
onCreateZecSend = {},
|
||||
onBack = {},
|
||||
onQrScannerOpen = {},
|
||||
goBalances = {},
|
||||
hasCameraFeature = true,
|
||||
recipientAddressState = RecipientAddressState("invalid_address", AddressType.Invalid()),
|
||||
onRecipientAddressChange = {},
|
||||
|
@ -109,9 +107,7 @@ private fun PreviewSendForm() {
|
|||
),
|
||||
setMemoState = {},
|
||||
memoState = MemoState.new("Test message "),
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
isHideBalances = false,
|
||||
selectedAccount = null,
|
||||
exchangeRateState = ExchangeRateState.OptedOut,
|
||||
sendAddressBookState =
|
||||
SendAddressBookState(
|
||||
|
@ -129,11 +125,11 @@ private fun PreviewSendForm() {
|
|||
private fun SendFormTransparentAddressPreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
Send(
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
sendStage = SendStage.Form,
|
||||
onCreateZecSend = {},
|
||||
onBack = {},
|
||||
onQrScannerOpen = {},
|
||||
goBalances = {},
|
||||
hasCameraFeature = true,
|
||||
recipientAddressState =
|
||||
RecipientAddressState(
|
||||
|
@ -150,9 +146,7 @@ private fun SendFormTransparentAddressPreview() {
|
|||
),
|
||||
setMemoState = {},
|
||||
memoState = MemoState.new("Test message"),
|
||||
walletSnapshot = WalletSnapshotFixture.new(),
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
isHideBalances = false,
|
||||
selectedAccount = null,
|
||||
exchangeRateState = ExchangeRateState.OptedOut,
|
||||
sendAddressBookState =
|
||||
SendAddressBookState(
|
||||
|
@ -171,12 +165,10 @@ private fun SendFormTransparentAddressPreview() {
|
|||
@Composable
|
||||
fun Send(
|
||||
balanceState: BalanceState,
|
||||
isHideBalances: Boolean,
|
||||
sendStage: SendStage,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onQrScannerOpen: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
hasCameraFeature: Boolean,
|
||||
recipientAddressState: RecipientAddressState,
|
||||
onRecipientAddressChange: (String) -> Unit,
|
||||
|
@ -184,40 +176,39 @@ fun Send(
|
|||
amountState: AmountState,
|
||||
setMemoState: (MemoState) -> Unit,
|
||||
memoState: MemoState,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
selectedAccount: WalletAccount?,
|
||||
exchangeRateState: ExchangeRateState,
|
||||
sendAddressBookState: SendAddressBookState,
|
||||
zashiMainTopAppBarState: ZashiMainTopAppBarState?,
|
||||
) {
|
||||
if (selectedAccount == null) {
|
||||
return
|
||||
}
|
||||
|
||||
BlankBgScaffold(topBar = {
|
||||
ZashiMainTopAppBar(zashiMainTopAppBarState)
|
||||
ZashiTopAppbar(
|
||||
title = null,
|
||||
state = zashiMainTopAppBarState,
|
||||
onBack = onBack
|
||||
)
|
||||
}) { paddingValues ->
|
||||
SendMainContent(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
walletSnapshot = walletSnapshot,
|
||||
selectedAccount = selectedAccount,
|
||||
exchangeRateState = exchangeRateState,
|
||||
onBack = onBack,
|
||||
sendStage = sendStage,
|
||||
onCreateZecSend = onCreateZecSend,
|
||||
sendStage = sendStage,
|
||||
onQrScannerOpen = onQrScannerOpen,
|
||||
recipientAddressState = recipientAddressState,
|
||||
onRecipientAddressChange = onRecipientAddressChange,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
amountState = amountState,
|
||||
setAmountState = setAmountState,
|
||||
memoState = memoState,
|
||||
setMemoState = setMemoState,
|
||||
onQrScannerOpen = onQrScannerOpen,
|
||||
goBalances = goBalances,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding() + ZashiDimensions.Spacing.spacingLg,
|
||||
bottom = ZashiDimensions.Spacing.spacing4xl,
|
||||
start = ZashiDimensions.Spacing.spacing3xl,
|
||||
end = ZashiDimensions.Spacing.spacing3xl
|
||||
),
|
||||
exchangeRateState = exchangeRateState,
|
||||
sendState = sendAddressBookState
|
||||
sendState = sendAddressBookState,
|
||||
modifier = Modifier.scaffoldPadding(paddingValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -226,11 +217,9 @@ fun Send(
|
|||
@Composable
|
||||
private fun SendMainContent(
|
||||
balanceState: BalanceState,
|
||||
isHideBalances: Boolean,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
selectedAccount: WalletAccount,
|
||||
exchangeRateState: ExchangeRateState,
|
||||
onBack: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
sendStage: SendStage,
|
||||
onQrScannerOpen: () -> Unit,
|
||||
|
@ -249,9 +238,9 @@ private fun SendMainContent(
|
|||
|
||||
SendForm(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
walletSnapshot = walletSnapshot,
|
||||
selectedAccount = selectedAccount,
|
||||
recipientAddressState = recipientAddressState,
|
||||
exchangeRateState = exchangeRateState,
|
||||
onRecipientAddressChange = onRecipientAddressChange,
|
||||
amountState = amountState,
|
||||
setAmountState = setAmountState,
|
||||
|
@ -259,11 +248,9 @@ private fun SendMainContent(
|
|||
setMemoState = setMemoState,
|
||||
onCreateZecSend = onCreateZecSend,
|
||||
onQrScannerOpen = onQrScannerOpen,
|
||||
goBalances = goBalances,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
modifier = modifier,
|
||||
exchangeRateState = exchangeRateState,
|
||||
sendState = sendState
|
||||
sendState = sendState,
|
||||
modifier = modifier
|
||||
)
|
||||
|
||||
if (sendStage is SendStage.SendFailure) {
|
||||
|
@ -284,8 +271,7 @@ private fun SendMainContent(
|
|||
@Composable
|
||||
private fun SendForm(
|
||||
balanceState: BalanceState,
|
||||
isHideBalances: Boolean,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
selectedAccount: WalletAccount,
|
||||
recipientAddressState: RecipientAddressState,
|
||||
exchangeRateState: ExchangeRateState,
|
||||
onRecipientAddressChange: (String) -> Unit,
|
||||
|
@ -295,7 +281,6 @@ private fun SendForm(
|
|||
setMemoState: (MemoState) -> Unit,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
onQrScannerOpen: () -> Unit,
|
||||
goBalances: () -> Unit,
|
||||
hasCameraFeature: Boolean,
|
||||
sendState: SendAddressBookState,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -313,10 +298,7 @@ private fun SendForm(
|
|||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
BalanceWidget(
|
||||
balanceState = balanceState,
|
||||
isHideBalances = isHideBalances,
|
||||
isReferenceToBalances = true,
|
||||
onReferenceClick = goBalances
|
||||
balanceState = balanceState
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
@ -364,7 +346,7 @@ private fun SendForm(
|
|||
} ?: false,
|
||||
monetarySeparators = monetarySeparators,
|
||||
setAmountState = setAmountState,
|
||||
walletSnapshot = walletSnapshot,
|
||||
selectedAccount = selectedAccount,
|
||||
exchangeRateState = exchangeRateState
|
||||
)
|
||||
|
||||
|
@ -388,7 +370,7 @@ private fun SendForm(
|
|||
memoState = memoState,
|
||||
onCreateZecSend = onCreateZecSend,
|
||||
recipientAddressState = recipientAddressState,
|
||||
walletSnapshot = walletSnapshot,
|
||||
selectedAccount = selectedAccount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -400,7 +382,7 @@ fun SendButton(
|
|||
memoState: MemoState,
|
||||
onCreateZecSend: (ZecSend) -> Unit,
|
||||
recipientAddressState: RecipientAddressState,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
selectedAccount: WalletAccount,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
@ -411,7 +393,7 @@ fun SendButton(
|
|||
recipientAddressState.address.isNotEmpty() &&
|
||||
amountState is AmountState.Valid &&
|
||||
amountState.value.isNotBlank() &&
|
||||
walletSnapshot.canSpend(amountState.zatoshi) &&
|
||||
selectedAccount.canSpend(amountState.zatoshi) &&
|
||||
// A valid memo is necessary only for non-transparent recipient
|
||||
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
|
||||
|
||||
|
@ -602,7 +584,7 @@ fun SendFormAmountTextField(
|
|||
monetarySeparators: MonetarySeparators,
|
||||
exchangeRateState: ExchangeRateState,
|
||||
setAmountState: (AmountState) -> Unit,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
selectedAccount: WalletAccount,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
@ -621,7 +603,7 @@ fun SendFormAmountTextField(
|
|||
}
|
||||
|
||||
is AmountState.Valid -> {
|
||||
if (walletSnapshot.spendableBalance() < amountState.zatoshi) {
|
||||
if (selectedAccount.spendableBalance < amountState.zatoshi) {
|
||||
stringResource(id = R.string.send_amount_insufficient_balance)
|
||||
} else {
|
||||
null
|
||||
|
|
|
@ -8,7 +8,6 @@ import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
|
|||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
|
||||
import co.electriccoin.zcash.ui.R
|
||||
|
@ -27,6 +26,7 @@ import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
|||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookArgs
|
||||
import co.electriccoin.zcash.ui.screen.integrations.Integrations
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.SettingsState
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.SettingsTroubleshootingState
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingItemState
|
||||
|
@ -223,7 +223,7 @@ class SettingsViewModel(
|
|||
|
||||
fun onBack() = navigationRouter.back()
|
||||
|
||||
private fun onIntegrationsClick() = navigationRouter.forward(INTEGRATIONS)
|
||||
private fun onIntegrationsClick() = navigationRouter.forward(Integrations)
|
||||
|
||||
private fun onAdvancedSettingsClick() = navigationRouter.forward(ADVANCED_SETTINGS)
|
||||
|
||||
|
|
|
@ -6,13 +6,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
fun AndroidTaxExport() {
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val walletSnapshot = walletViewModel.currentWalletSnapshot.collectAsStateWithLifecycle().value
|
||||
val viewModel = koinViewModel<TaxExportViewModel> { parametersOf(walletSnapshot?.isZashi) }
|
||||
val viewModel = koinViewModel<TaxExportViewModel>()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle()
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package co.electriccoin.zcash.ui.screen.taxexport
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
|
||||
data class TaxExportState(
|
||||
val text: StringResource,
|
||||
val exportButton: ButtonState,
|
||||
val isZashiAccount: Boolean,
|
||||
val onBack: () -> Unit,
|
||||
)
|
||||
|
|
|
@ -24,6 +24,7 @@ import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreenSizes
|
|||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
||||
|
@ -86,15 +87,7 @@ private fun Content(
|
|||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.tax_export_message,
|
||||
if (state.isZashiAccount) {
|
||||
stringResource(R.string.zashi_wallet_name)
|
||||
} else {
|
||||
stringResource(R.string.keystone_wallet_name)
|
||||
}
|
||||
),
|
||||
text = state.text.getValue(),
|
||||
style = ZashiTypography.textSm,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
|
@ -121,7 +114,11 @@ private fun ExportPrivateDataPreview() =
|
|||
text = stringRes(R.string.tax_export_export_button),
|
||||
onClick = {}
|
||||
),
|
||||
isZashiAccount = true,
|
||||
text =
|
||||
stringRes(
|
||||
R.string.tax_export_message,
|
||||
stringResource(R.string.zashi_wallet_name)
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue