Business logic implementation

This commit is contained in:
Milan Cerovsky 2024-12-01 13:25:43 +01:00
parent 088b2c82a5
commit c8ac10fcdc
17 changed files with 647 additions and 28 deletions

View File

@ -0,0 +1,23 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.sdk.model.WalletAddress
enum class AddressType {
UNIFIED, TRANSPARENT, SAPLING, TEX;
suspend fun toWalletAddress(address: String) = when (this) {
UNIFIED -> WalletAddress.Unified.new(address)
TRANSPARENT -> WalletAddress.Transparent.new(address)
SAPLING -> WalletAddress.Sapling.new(address)
TEX -> WalletAddress.Tex.new(address)
}
companion object {
fun fromWalletAddress(walletAddress: WalletAddress) = when (walletAddress) {
is WalletAddress.Sapling -> SAPLING
is WalletAddress.Tex -> TEX
is WalletAddress.Transparent -> TRANSPARENT
is WalletAddress.Unified -> UNIFIED
}
}
}

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -21,7 +22,7 @@ import co.electriccoin.zcash.ui.design.util.stringRes
@Composable @Composable
fun ZashiBottomBar( fun ZashiBottomBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable () -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Surface( Surface(
shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp), shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp),

View File

@ -38,9 +38,9 @@ class SendViewIntegrationTest {
goToQrScanner = {}, goToQrScanner = {},
goBack = {}, goBack = {},
goBalances = {}, goBalances = {},
goSettings = {},
goPaymentRequest = { _, _ -> }, goPaymentRequest = { _, _ -> },
goSendConfirmation = {}, goSendConfirmation = {},
goReviewKeystoneTransaction = {},
) )
} }

View File

@ -7,7 +7,7 @@ import org.junit.Test
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class SupportInfoTest { class SupportFinancialInfoStateTest {
@Test @Test
fun filter_time() = fun filter_time() =
runTest { runTest {

View File

@ -8,8 +8,10 @@ import co.electriccoin.zcash.ui.common.usecase.DeriveKeystoneAccountUnifiedAddre
import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase
import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetLoadedExchangeRateUseCase
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSupportUseCase import co.electriccoin.zcash.ui.common.usecase.GetSupportUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
@ -100,4 +102,6 @@ val useCaseModule =
factoryOf(::CreateKeystoneAccountUseCase) factoryOf(::CreateKeystoneAccountUseCase)
factoryOf(::DeriveKeystoneAccountUnifiedAddressUseCase) factoryOf(::DeriveKeystoneAccountUnifiedAddressUseCase)
factoryOf(::DecodeUrToZashiAccountsUseCase) factoryOf(::DecodeUrToZashiAccountsUseCase)
factoryOf(::GetLoadedExchangeRateUseCase)
factoryOf(::GetSelectedWalletAccountUseCase)
} }

View File

@ -22,6 +22,8 @@ import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewKeystoneTransaction
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewKeystoneTransactionViewModel
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel
@ -113,4 +115,11 @@ val viewModelModule =
decodeUrToZashiAccounts = get() decodeUrToZashiAccounts = get()
) )
} }
viewModel { (args: ReviewKeystoneTransaction) ->
ReviewKeystoneTransactionViewModel(
args = args,
observeContactByAddress = get(),
getLoadedExchangeRate = get(),
)
}
} }

View File

@ -93,6 +93,8 @@ import co.electriccoin.zcash.ui.screen.paymentrequest.model.PaymentRequestArgume
import co.electriccoin.zcash.ui.screen.qrcode.WrapQrCode import co.electriccoin.zcash.ui.screen.qrcode.WrapQrCode
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
import co.electriccoin.zcash.ui.screen.request.WrapRequest import co.electriccoin.zcash.ui.screen.request.WrapRequest
import co.electriccoin.zcash.ui.screen.reviewtransaction.AndroidReviewKeystoneTransaction
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewKeystoneTransaction
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystoneNavigationArgs import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystoneNavigationArgs
@ -394,9 +396,11 @@ internal fun MainActivity.Navigation() {
composable(ConnectKeystoneArgs.PATH) { composable(ConnectKeystoneArgs.PATH) {
AndroidConnectKeystone() AndroidConnectKeystone()
} }
composable<SelectKeystoneAccount> { backStackEntry -> composable<SelectKeystoneAccount> {
val args = backStackEntry.toRoute<SelectKeystoneAccount>() AndroidSelectKeystoneAccount(it.toRoute())
AndroidSelectKeystoneAccount(args) }
composable<ReviewKeystoneTransaction> {
AndroidReviewKeystoneTransaction(it.toRoute())
} }
} }
} }
@ -417,6 +421,9 @@ private fun MainActivity.NavigationHome(
} }
navController.navigateJustOnce(SEND_CONFIRMATION) navController.navigateJustOnce(SEND_CONFIRMATION)
}, },
goReviewKeystoneTransaction = {
navController.navigate(it)
},
goPaymentRequest = { zecSend, zip321Uri -> goPaymentRequest = { zecSend, zip321Uri ->
navController.currentBackStackEntry?.savedStateHandle?.let { handle -> navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
fillInHandleForPaymentRequest(handle, zecSend, zip321Uri) fillInHandleForPaymentRequest(handle, zecSend, zip321Uri)

View File

@ -1,6 +1,8 @@
package co.electriccoin.zcash.ui.common.datasource package co.electriccoin.zcash.ui.common.datasource
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletCoordinator import cash.z.ecc.android.sdk.WalletCoordinator
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.PersistableWallet import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
@ -14,6 +16,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -23,10 +26,12 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
interface AccountDataSource { interface AccountDataSource {
@ -73,7 +78,7 @@ class AccountDataSourceImpl(
if (synchronizer == null || walletBalances == null || persistableWallet == null) { if (synchronizer == null || walletBalances == null || persistableWallet == null) {
null null
} else { } else {
synchronizer.getAccounts().mapIndexed { index, account -> synchronizer.getAccountsSafe().mapIndexed { index, account ->
val balance = walletBalances.getValue(account) val balance = walletBalances.getValue(account)
val spendingKey = deriveSpendingKey(persistableWallet) val spendingKey = deriveSpendingKey(persistableWallet)
@ -91,12 +96,27 @@ class AccountDataSourceImpl(
) )
} }
} }
}.stateIn( }.flowOn(Dispatchers.Default)
.stateIn(
scope = scope, scope = scope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT, Duration.ZERO), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT, Duration.ZERO),
initialValue = null initialValue = null
) )
private suspend fun Synchronizer.getAccountsSafe(): List<Account> {
var accounts: List<Account>? = null
while (accounts == null) {
try {
accounts = getAccounts()
} catch (_: Throwable) {
delay(1.seconds)
}
}
return accounts
}
private suspend fun deriveSpendingKey(persistableWallet: PersistableWallet): UnifiedSpendingKey? { private suspend fun deriveSpendingKey(persistableWallet: PersistableWallet): UnifiedSpendingKey? {
// crashes currently // crashes currently
// val bip39Seed = // val bip39Seed =

View File

@ -0,0 +1,17 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import kotlinx.coroutines.flow.first
class GetLoadedExchangeRateUseCase(
private val exchangeRateRepository: ExchangeRateRepository
) {
suspend operator fun invoke() = exchangeRateRepository.state.first {
when (it) {
is ExchangeRateState.Data -> it.isLoading
is ExchangeRateState.OptIn -> true
ExchangeRateState.OptedOut -> true
}
}
}

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
class GetSelectedWalletAccountUseCase(private val accountDataSource: AccountDataSource) {
suspend operator fun invoke() = accountDataSource.getSelectedAccount()
}

View File

@ -31,6 +31,7 @@ import co.electriccoin.zcash.ui.screen.balances.WrapBalances
import co.electriccoin.zcash.ui.screen.home.model.TabItem import co.electriccoin.zcash.ui.screen.home.model.TabItem
import co.electriccoin.zcash.ui.screen.home.view.Home import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.receive.WrapReceive import co.electriccoin.zcash.ui.screen.receive.WrapReceive
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewKeystoneTransaction
import co.electriccoin.zcash.ui.screen.send.WrapSend import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.send.model.SendArguments import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -43,6 +44,7 @@ internal fun WrapHome(
goMultiTrxSubmissionFailure: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit,
goScan: () -> Unit, goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit,
goPaymentRequest: (ZecSend, String) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit,
sendArguments: SendArguments sendArguments: SendArguments
) { ) {
@ -92,7 +94,8 @@ internal fun WrapHome(
isShowingRestoreSuccess = isShowingRestoreSuccess, isShowingRestoreSuccess = isShowingRestoreSuccess,
sendArguments = sendArguments, sendArguments = sendArguments,
setShowingRestoreSuccess = setShowingRestoreSuccess, setShowingRestoreSuccess = setShowingRestoreSuccess,
walletSnapshot = walletSnapshot walletSnapshot = walletSnapshot,
goReviewKeystoneTransaction = goReviewKeystoneTransaction,
) )
} }
@ -102,6 +105,7 @@ internal fun WrapHome(
goMultiTrxSubmissionFailure: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit,
goScan: () -> Unit, goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit,
goPaymentRequest: (ZecSend, String) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit,
isKeepScreenOnWhileSyncing: Boolean?, isKeepScreenOnWhileSyncing: Boolean?,
isShowingRestoreSuccess: Boolean, isShowingRestoreSuccess: Boolean,
@ -180,7 +184,8 @@ internal fun WrapHome(
}, },
goSendConfirmation = goSendConfirmation, goSendConfirmation = goSendConfirmation,
goPaymentRequest = goPaymentRequest, goPaymentRequest = goPaymentRequest,
sendArguments = sendArguments sendArguments = sendArguments,
goReviewKeystoneTransaction = goReviewKeystoneTransaction
) )
} }
), ),

View File

@ -0,0 +1,23 @@
package co.electriccoin.zcash.ui.screen.reviewtransaction
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@Composable
fun AndroidReviewKeystoneTransaction(args: ReviewKeystoneTransaction) {
val viewModel = koinViewModel<ReviewKeystoneTransactionViewModel> { parametersOf(args) }
val state by viewModel.state.collectAsStateWithLifecycle()
BackHandler {
state?.onBack
}
state?.let {
ReviewTransactionView(it)
}
}

View File

@ -0,0 +1,20 @@
package co.electriccoin.zcash.ui.screen.reviewtransaction
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.model.AddressType
import kotlinx.serialization.Serializable
@Serializable
data class ReviewKeystoneTransaction(
val addressString: String,
val addressType: AddressType,
val amountLong: Long,
val memoString: String?
) {
val amount: Zatoshi
get() = Zatoshi(amountLong)
val memo: Memo?
get() = memoString?.let { Memo(it) }
}

View File

@ -0,0 +1,78 @@
package co.electriccoin.zcash.ui.screen.reviewtransaction
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.common.usecase.GetLoadedExchangeRateUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.util.stringRes
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class ReviewKeystoneTransactionViewModel(
args: ReviewKeystoneTransaction,
observeContactByAddress: ObserveContactByAddressUseCase,
private val getLoadedExchangeRate: GetLoadedExchangeRateUseCase
) : ViewModel() {
val state = observeContactByAddress(args.addressString).map { addressBookContact ->
ReviewTransactionState(
title = stringRes("Review"),
items = listOfNotNull(
AmountState(
title = stringRes("Total Amount"),
amount = args.amount,
exchangeRate = getLoadedExchangeRate(),
),
ReceiverState(
title = stringRes("Sending to"),
name = addressBookContact?.name?.let { stringRes(it) },
address = stringRes(args.addressString)
),
SenderState(
title = stringRes("Sending from"),
icon = R.drawable.ic_item_keystone,
name = stringRes("Keystone wallet"),
),
FinancialInfoState(
title = stringRes("Amount"),
amount = args.amount
),
FinancialInfoState(
title = stringRes("Fee"),
amount = args.amount
),
args.memo?.let {
MessageState(
title = stringRes("Message"),
message = stringRes(it.value)
)
}
),
primaryButton = ButtonState(
stringRes("Confirm with Keystone"),
onClick = ::onConfirmClick
),
negativeButton = ButtonState(
stringRes("Cancel"),
onClick = ::onCancelClick
),
onBack = ::onBack,
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
private fun onBack() {
TODO("Not yet implemented")
}
private fun onCancelClick() {
TODO("Not yet implemented")
}
private fun onConfirmClick() {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,44 @@
package co.electriccoin.zcash.ui.screen.reviewtransaction
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.util.StringResource
data class ReviewTransactionState(
val title: StringResource,
val items: List<ReviewTransactionItemState>,
val primaryButton: ButtonState,
val negativeButton: ButtonState,
val onBack: () -> Unit,
)
sealed interface ReviewTransactionItemState
data class AmountState(
val title: StringResource,
val amount: Zatoshi,
val exchangeRate: ExchangeRateState,
) : ReviewTransactionItemState
data class ReceiverState(
val title: StringResource,
val name: StringResource?,
val address: StringResource,
) : ReviewTransactionItemState
data class SenderState(
val title: StringResource,
val icon: Int,
val name: StringResource
) : ReviewTransactionItemState
data class FinancialInfoState(
val title: StringResource,
val amount: Zatoshi,
) : ReviewTransactionItemState
data class MessageState(
val title: StringResource,
val message: StringResource
) : ReviewTransactionItemState

View File

@ -0,0 +1,321 @@
package co.electriccoin.zcash.ui.screen.reviewtransaction
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
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
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
import co.electriccoin.zcash.ui.design.component.TextFieldState
import co.electriccoin.zcash.ui.design.component.ZashiBottomBar
import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiTextField
import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults
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.getValue
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeLabel
import kotlinx.datetime.Clock
@Composable
fun ReviewTransactionView(state: ReviewTransactionState) {
BlankBgScaffold(
topBar = {
ZashiSmallTopAppBar(
title = state.title.getValue()
)
}
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
.scaffoldPadding(it)
) {
state.items.forEachIndexed { index, item ->
when (item) {
is AmountState -> {
AmountWidget(item)
}
is ReceiverState -> {
Spacer(Modifier.height(24.dp))
ReceiverWidget(item)
}
is SenderState -> {
Spacer(Modifier.height(20.dp))
SenderWidget(item)
Spacer(Modifier.height(16.dp))
}
is FinancialInfoState -> {
Spacer(Modifier.height(16.dp))
FinancialInfoWidget(item)
}
is MessageState -> {
Spacer(Modifier.height(16.dp))
MessageWidget(item)
}
}
}
Spacer(Modifier.height(32.dp))
}
BottomBar(state)
}
}
}
@Composable
fun SenderWidget(state: SenderState) {
Column {
Text(
text = state.title.getValue(),
style = ZashiTypography.textSm,
color = ZashiColors.Text.textTertiary,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(6.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.size(32.dp),
painter = painterResource(id = state.icon),
contentDescription = null
)
Spacer(Modifier.width(16.dp))
Text(
text = state.name.getValue(),
style = ZashiTypography.textMd,
color = ZashiColors.Text.textPrimary,
fontWeight = FontWeight.SemiBold
)
}
}
}
@Composable
fun ReceiverWidget(state: ReceiverState) {
Column {
Text(
state.title.getValue(),
style = ZashiTypography.textSm,
color = ZashiColors.Text.textTertiary,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(8.dp))
if (state.name != null) {
Text(
state.name.getValue(),
style = ZashiTypography.textSm,
color = ZashiColors.Inputs.Filled.label,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(4.dp))
}
Text(
state.address.getValue(),
style = ZashiTypography.textXs,
color = ZashiColors.Text.textPrimary
)
}
}
@Composable
fun MessageWidget(state: MessageState) {
Column {
Text(
state.title.getValue(),
style = ZashiTypography.textSm,
fontWeight = FontWeight.Medium,
color = ZashiColors.Text.textTertiary
)
Spacer(modifier = Modifier.height(8.dp))
ZashiTextField(
state = TextFieldState(value = state.message, isEnabled = false) {},
modifier =
Modifier
.fillMaxWidth(),
colors =
ZashiTextFieldDefaults.defaultColors(
disabledTextColor = ZashiColors.Inputs.Filled.text,
disabledHintColor = ZashiColors.Inputs.Disabled.hint,
disabledBorderColor = Color.Unspecified,
disabledContainerColor = ZashiColors.Inputs.Disabled.bg,
disabledPlaceholderColor = ZashiColors.Inputs.Disabled.text,
),
minLines = 4
)
}
}
@Composable
fun FinancialInfoWidget(state: FinancialInfoState) {
Row {
Text(
modifier = Modifier.weight(1f),
text = state.title.getValue(),
style = ZashiTypography.textSm,
fontWeight = FontWeight.Medium,
color = ZashiColors.Text.textTertiary
)
StyledBalance(
balanceParts = state.amount.toZecStringFull().asZecAmountTriple(),
isHideBalances = false,
textStyle =
StyledBalanceDefaults.textStyles(
mostSignificantPart = ZashiTypography.textSm.copy(fontWeight = FontWeight.SemiBold),
leastSignificantPart = ZashiTypography.textXxs.copy(fontWeight = FontWeight.SemiBold, fontSize = 8.sp)
),
)
}
}
@Composable
fun AmountWidget(state: AmountState) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.title.getValue(),
style = ZashiTypography.textSm,
color = ZashiColors.Text.textPrimary
)
BalanceWidgetBigLineOnly(
parts = state.amount.toZecStringFull().asZecAmountTriple(),
isHideBalances = false
)
StyledExchangeLabel(
zatoshi = state.amount,
state = state.exchangeRate,
isHideBalances = false,
style = ZashiTypography.textMd.copy(fontWeight = FontWeight.SemiBold),
textColor = ZashiColors.Text.textPrimary
)
}
}
@Composable
private fun BottomBar(state: ReviewTransactionState) {
ZashiBottomBar {
ZashiButton(
state = state.primaryButton,
modifier =
Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth()
)
ZashiButton(
state = state.negativeButton,
colors = ZashiButtonDefaults.secondaryColors(),
modifier =
Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth()
)
}
}
@PreviewScreens
@Composable
private fun Preview() = ZcashTheme {
ReviewTransactionView(
state = ReviewTransactionState(
title = stringRes("Review"),
items = listOf(
AmountState(
title = stringRes("Total Amount"),
amount = ZatoshiFixture.new(),
exchangeRate = ExchangeRateState.Data(
currencyConversion = FiatCurrencyConversion(
timestamp = Clock.System.now(),
priceOfZec = 50.0
),
isLoading = false,
isStale = false,
isRefreshEnabled = false,
onRefresh = {}
),
),
ReceiverState(
title = stringRes("Total Amount"),
name = stringRes("Receiver Name"),
address = stringRes("Receiver Address")
),
SenderState(
title = stringRes("Sending from"),
icon = R.drawable.ic_item_keystone,
name = stringRes("Keystone wallet"),
),
FinancialInfoState(
title = stringRes("Amount"),
amount = ZatoshiFixture.new()
),
FinancialInfoState(
title = stringRes("Fee"),
amount = ZatoshiFixture.new()
),
MessageState(
title = stringRes("Message"),
message = stringRes("Message")
)
),
primaryButton = ButtonState(
stringRes("Confirm with Keystone")
),
negativeButton = ButtonState(
stringRes("Cancel")
),
onBack = {},
)
)
}

View File

@ -28,12 +28,16 @@ import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.compose.BalanceState import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
import co.electriccoin.zcash.ui.common.model.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.model.ZashiAccount
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewKeystoneTransaction
import co.electriccoin.zcash.ui.screen.send.ext.Saver 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.AmountState
import co.electriccoin.zcash.ui.screen.send.model.MemoState import co.electriccoin.zcash.ui.screen.send.model.MemoState
@ -43,6 +47,7 @@ import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.view.Send import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import org.zecdev.zip321.ZIP321 import org.zecdev.zip321.ZIP321
import java.util.Locale import java.util.Locale
@ -54,6 +59,7 @@ internal fun WrapSend(
goBack: () -> Unit, goBack: () -> Unit,
goBalances: () -> Unit, goBalances: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit,
goPaymentRequest: (ZecSend, String) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit,
) { ) {
val activity = LocalActivity.current val activity = LocalActivity.current
@ -93,6 +99,7 @@ internal fun WrapSend(
hasCameraFeature = hasCameraFeature, hasCameraFeature = hasCameraFeature,
monetarySeparators = monetarySeparators, monetarySeparators = monetarySeparators,
exchangeRateState = exchangeRateState, exchangeRateState = exchangeRateState,
goReviewKeystoneTransaction = goReviewKeystoneTransaction
) )
} }
@ -107,6 +114,7 @@ internal fun WrapSend(
goBack: () -> Unit, goBack: () -> Unit,
goBalances: () -> Unit, goBalances: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit, goSendConfirmation: (ZecSend) -> Unit,
goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit,
goPaymentRequest: (ZecSend, String) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit,
hasCameraFeature: Boolean, hasCameraFeature: Boolean,
monetarySeparators: MonetarySeparators, monetarySeparators: MonetarySeparators,
@ -121,6 +129,8 @@ internal fun WrapSend(
val viewModel = koinViewModel<SendViewModel>() val viewModel = koinViewModel<SendViewModel>()
val getSelectedWalletAccount = koinInject<GetSelectedWalletAccountUseCase>()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.navigateCommand.collect { viewModel.navigateCommand.collect {
navController.navigate(it) navController.navigate(it)
@ -263,21 +273,49 @@ internal fun WrapSend(
isHideBalances = isHideBalances, isHideBalances = isHideBalances,
sendStage = sendStage, sendStage = sendStage,
onCreateZecSend = { newZecSend -> onCreateZecSend = { newZecSend ->
goReviewKeystoneTransaction(
ReviewKeystoneTransaction(
addressString = newZecSend.destination.address,
addressType = cash.z.ecc.sdk.model.AddressType.fromWalletAddress(newZecSend.destination),
amountLong = newZecSend.amount.value,
memoString = newZecSend.memo.value.takeIf { it.isNotEmpty() },
)
)
scope.launch { scope.launch {
spendingKey?.let { // goReviewKeystoneTransaction(
Twig.debug { "Getting send transaction proposal" } // ReviewKeystoneTransaction(
runCatching { // addressString = newZecSend.destination.address,
synchronizer.proposeSend(spendingKey.account, newZecSend) // addressType = cash.z.ecc.sdk.model.AddressType.fromWalletAddress(newZecSend.destination),
}.onSuccess { proposal -> // amountLong = newZecSend.amount.value,
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" } // memoString = newZecSend.memo.value,
val enrichedZecSend = newZecSend.copy(proposal = proposal) // )
setZecSend(enrichedZecSend) // )
goSendConfirmation(enrichedZecSend) // when (getSelectedWalletAccount()) {
}.onFailure { // is KeystoneAccount -> {
Twig.error(it) { "Transaction proposal failed" } // goReviewKeystoneTransaction(
setSendStage(SendStage.SendFailure(it.message ?: "")) // ReviewKeystoneTransaction(
} // addressString = newZecSend.destination.address,
} // addressType = cash.z.ecc.sdk.model.AddressType.fromWalletAddress(newZecSend.destination),
// amountLong = newZecSend.amount.value,
// memoString = newZecSend.memo.value,
// )
// )
// }
// is ZashiAccount -> spendingKey?.let {
// Twig.debug { "Getting send transaction proposal" }
// runCatching {
// synchronizer.proposeSend(spendingKey.account, newZecSend)
// }.onSuccess { proposal ->
// Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
// val enrichedZecSend = newZecSend.copy(proposal = proposal)
// setZecSend(enrichedZecSend)
// goSendConfirmation(enrichedZecSend)
// }.onFailure {
// Twig.error(it) { "Transaction proposal failed" }
// setSendStage(SendStage.SendFailure(it.message ?: ""))
// }
// }
// }
} }
}, },
onBack = onBackAction, onBack = onBackAction,