diff --git a/sdk-ext-lib/src/main/java/cash/z/ecc/sdk/model/AddressType.kt b/sdk-ext-lib/src/main/java/cash/z/ecc/sdk/model/AddressType.kt new file mode 100644 index 000000000..1626be514 --- /dev/null +++ b/sdk-ext-lib/src/main/java/cash/z/ecc/sdk/model/AddressType.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt index ce7397183..0753c8ac5 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt @@ -1,6 +1,7 @@ package co.electriccoin.zcash.ui.design.component import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -21,7 +22,7 @@ import co.electriccoin.zcash.ui.design.util.stringRes @Composable fun ZashiBottomBar( modifier: Modifier = Modifier, - content: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit, ) { Surface( shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp), diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt index ec7dc7c96..16b380afa 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt @@ -38,9 +38,9 @@ class SendViewIntegrationTest { goToQrScanner = {}, goBack = {}, goBalances = {}, - goSettings = {}, goPaymentRequest = { _, _ -> }, goSendConfirmation = {}, + goReviewKeystoneTransaction = {}, ) } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/model/SupportInfoTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/model/SupportFinancialInfoStateTest.kt similarity index 99% rename from ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/model/SupportInfoTest.kt rename to ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/model/SupportFinancialInfoStateTest.kt index ef623582a..f0c37fb3a 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/model/SupportInfoTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/model/SupportFinancialInfoStateTest.kt @@ -7,7 +7,7 @@ import org.junit.Test import kotlin.test.assertFalse import kotlin.test.assertTrue -class SupportInfoTest { +class SupportFinancialInfoStateTest { @Test fun filter_time() = runTest { diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt index 2f7d11f55..84adbefa6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt @@ -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.GetBackupPersistableWalletUseCase 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.GetSelectedEndpointUseCase +import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase import co.electriccoin.zcash.ui.common.usecase.GetSupportUseCase import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase @@ -100,4 +102,6 @@ val useCaseModule = factoryOf(::CreateKeystoneAccountUseCase) factoryOf(::DeriveKeystoneAccountUnifiedAddressUseCase) factoryOf(::DecodeUrToZashiAccountsUseCase) + factoryOf(::GetLoadedExchangeRateUseCase) + factoryOf(::GetSelectedWalletAccountUseCase) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt index be5c03395..4e2d1e350 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt @@ -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.restore.viewmodel.RestoreViewModel 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.viewmodel.ScanViewModel import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel @@ -113,4 +115,11 @@ val viewModelModule = decodeUrToZashiAccounts = get() ) } + viewModel { (args: ReviewKeystoneTransaction) -> + ReviewKeystoneTransactionViewModel( + args = args, + observeContactByAddress = get(), + getLoadedExchangeRate = get(), + ) + } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt index 3f65428d7..24a48f5ce 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt @@ -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.receive.model.ReceiveAddressType 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.WrapScanValidator import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystoneNavigationArgs @@ -394,9 +396,11 @@ internal fun MainActivity.Navigation() { composable(ConnectKeystoneArgs.PATH) { AndroidConnectKeystone() } - composable { backStackEntry -> - val args = backStackEntry.toRoute() - AndroidSelectKeystoneAccount(args) + composable { + AndroidSelectKeystoneAccount(it.toRoute()) + } + composable { + AndroidReviewKeystoneTransaction(it.toRoute()) } } } @@ -417,6 +421,9 @@ private fun MainActivity.NavigationHome( } navController.navigateJustOnce(SEND_CONFIRMATION) }, + goReviewKeystoneTransaction = { + navController.navigate(it) + }, goPaymentRequest = { zecSend, zip321Uri -> navController.currentBackStackEntry?.savedStateHandle?.let { handle -> fillInHandleForPaymentRequest(handle, zecSend, zip321Uri) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt index 7e6996f1b..4ff96ebb1 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt @@ -1,6 +1,8 @@ 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.model.Account import cash.z.ecc.android.sdk.model.PersistableWallet import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletAddress @@ -14,6 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -23,10 +26,12 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds interface AccountDataSource { @@ -73,7 +78,7 @@ class AccountDataSourceImpl( if (synchronizer == null || walletBalances == null || persistableWallet == null) { null } else { - synchronizer.getAccounts().mapIndexed { index, account -> + synchronizer.getAccountsSafe().mapIndexed { index, account -> val balance = walletBalances.getValue(account) val spendingKey = deriveSpendingKey(persistableWallet) @@ -91,11 +96,26 @@ class AccountDataSourceImpl( ) } } - }.stateIn( - scope = scope, - started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT, Duration.ZERO), - initialValue = null - ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT, Duration.ZERO), + initialValue = null + ) + + private suspend fun Synchronizer.getAccountsSafe(): List { + var accounts: List? = null + + while (accounts == null) { + try { + accounts = getAccounts() + } catch (_: Throwable) { + delay(1.seconds) + } + } + + return accounts + } private suspend fun deriveSpendingKey(persistableWallet: PersistableWallet): UnifiedSpendingKey? { // crashes currently diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetLoadedExchangeRateUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetLoadedExchangeRateUseCase.kt new file mode 100644 index 000000000..af7f46473 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetLoadedExchangeRateUseCase.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetSelectedWalletAccountUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetSelectedWalletAccountUseCase.kt new file mode 100644 index 000000000..0dffc1a1c --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetSelectedWalletAccountUseCase.kt @@ -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() +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt index fb9fecdb7..b92cc626a 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt @@ -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.view.Home 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.model.SendArguments import kotlinx.collections.immutable.persistentListOf @@ -43,6 +44,7 @@ internal fun WrapHome( goMultiTrxSubmissionFailure: () -> Unit, goScan: () -> Unit, goSendConfirmation: (ZecSend) -> Unit, + goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit, sendArguments: SendArguments ) { @@ -92,7 +94,8 @@ internal fun WrapHome( isShowingRestoreSuccess = isShowingRestoreSuccess, sendArguments = sendArguments, setShowingRestoreSuccess = setShowingRestoreSuccess, - walletSnapshot = walletSnapshot + walletSnapshot = walletSnapshot, + goReviewKeystoneTransaction = goReviewKeystoneTransaction, ) } @@ -102,6 +105,7 @@ internal fun WrapHome( goMultiTrxSubmissionFailure: () -> Unit, goScan: () -> Unit, goSendConfirmation: (ZecSend) -> Unit, + goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit, isKeepScreenOnWhileSyncing: Boolean?, isShowingRestoreSuccess: Boolean, @@ -180,7 +184,8 @@ internal fun WrapHome( }, goSendConfirmation = goSendConfirmation, goPaymentRequest = goPaymentRequest, - sendArguments = sendArguments + sendArguments = sendArguments, + goReviewKeystoneTransaction = goReviewKeystoneTransaction ) } ), diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/AndroidReviewKeystoneTransaction.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/AndroidReviewKeystoneTransaction.kt new file mode 100644 index 000000000..78ad4b415 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/AndroidReviewKeystoneTransaction.kt @@ -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 { parametersOf(args) } + val state by viewModel.state.collectAsStateWithLifecycle() + + BackHandler { + state?.onBack + } + + state?.let { + ReviewTransactionView(it) + } +} + diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewKeystoneTransaction.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewKeystoneTransaction.kt new file mode 100644 index 000000000..3a295bd95 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewKeystoneTransaction.kt @@ -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) } +} \ No newline at end of file diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewKeystoneTransactionViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewKeystoneTransactionViewModel.kt new file mode 100644 index 000000000..28ab9aeff --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewKeystoneTransactionViewModel.kt @@ -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") + } +} \ No newline at end of file diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewTransactionState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewTransactionState.kt new file mode 100644 index 000000000..5cf33bd66 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewTransactionState.kt @@ -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, + 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 diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewTransactionView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewTransactionView.kt new file mode 100644 index 000000000..07d47d851 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/reviewtransaction/ReviewTransactionView.kt @@ -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 = {}, + + ) + ) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt index 71b34d02c..697ca6d68 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt @@ -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.LocalActivity 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.ZashiAccount +import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase 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.reviewtransaction.ReviewKeystoneTransaction 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 @@ -43,6 +47,7 @@ import co.electriccoin.zcash.ui.screen.send.model.SendStage import co.electriccoin.zcash.ui.screen.send.view.Send import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject import org.zecdev.zip321.ZIP321 import java.util.Locale @@ -54,6 +59,7 @@ internal fun WrapSend( goBack: () -> Unit, goBalances: () -> Unit, goSendConfirmation: (ZecSend) -> Unit, + goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit, ) { val activity = LocalActivity.current @@ -93,6 +99,7 @@ internal fun WrapSend( hasCameraFeature = hasCameraFeature, monetarySeparators = monetarySeparators, exchangeRateState = exchangeRateState, + goReviewKeystoneTransaction = goReviewKeystoneTransaction ) } @@ -107,6 +114,7 @@ internal fun WrapSend( goBack: () -> Unit, goBalances: () -> Unit, goSendConfirmation: (ZecSend) -> Unit, + goReviewKeystoneTransaction: (ReviewKeystoneTransaction) -> Unit, goPaymentRequest: (ZecSend, String) -> Unit, hasCameraFeature: Boolean, monetarySeparators: MonetarySeparators, @@ -121,6 +129,8 @@ internal fun WrapSend( val viewModel = koinViewModel() + val getSelectedWalletAccount = koinInject() + LaunchedEffect(Unit) { viewModel.navigateCommand.collect { navController.navigate(it) @@ -263,21 +273,49 @@ internal fun WrapSend( isHideBalances = isHideBalances, sendStage = sendStage, 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 { - 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 ?: "")) - } - } + // goReviewKeystoneTransaction( + // ReviewKeystoneTransaction( + // addressString = newZecSend.destination.address, + // addressType = cash.z.ecc.sdk.model.AddressType.fromWalletAddress(newZecSend.destination), + // amountLong = newZecSend.amount.value, + // memoString = newZecSend.memo.value, + // ) + // ) + // when (getSelectedWalletAccount()) { + // is KeystoneAccount -> { + // goReviewKeystoneTransaction( + // 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,