From c6257d84127880341304e8592a93e989fa815aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Honza=20Rychnovsk=C3=BD?= Date: Tue, 8 Oct 2024 09:46:42 +0200 Subject: [PATCH] [#1612] QR Code screen * Refactor Receive screen architecture - Added QrCodeScreen architecture and basic UI * QrCode Detail screen UI + logic * Improve share intent + Attach snackbar to the failed sharing attempt + Fix tests * Changelogs update * Ktlint warnings fix --- CHANGELOG.md | 3 + docs/whatsNew/WHATS_NEW_EN.md | 3 + .../zcash/ui/design/component/Button.kt | 2 + .../zcash/ui/design/component/ZashiBadge.kt | 30 +- .../zcash/ui/design/component/ZashiButton.kt | 53 +- .../ui/design/component/ZashiTopAppBar.kt | 13 +- .../common/drawable-night/ic_close_full.xml | 16 + .../res/ui/common/drawable/ic_close_full.xml | 16 + ui-lib/build.gradle.kts | 1 + .../zcash/ui/fixture/MockSynchronizer.kt | 7 + .../exportdata/util/FileShareUtilTest.kt | 2 + .../receive/view/ReceiveViewTestSetup.kt | 23 +- .../co/electriccoin/zcash/di/UseCaseModule.kt | 4 + .../electriccoin/zcash/di/ViewModelModule.kt | 4 + .../co/electriccoin/zcash/ui/Navigation.kt | 13 + .../common/usecase/CopyToClipboardUseCase.kt | 16 + .../ui/common/usecase/GetAddressesUseCase.kt | 10 + .../exportdata/AndroidExportPrivateData.kt | 1 + .../zcash/ui/screen/home/AndroidHome.kt | 2 +- .../zcash/ui/screen/qrcode/AndroidQrCode.kt | 61 +++ .../screen/qrcode/ext/WalletAddressesExt.kt | 11 + .../ui/screen/qrcode/model/QrCodeState.kt | 15 + .../util/AndroidQrCodeImageGenerator.kt | 2 +- .../util/JvmQrCodeGenerator.kt | 2 +- .../util/QrCodeGenerator.kt | 2 +- .../util/QrCodeImageGenerator.kt | 2 +- .../zcash/ui/screen/qrcode/view/QrCodeView.kt | 487 ++++++++++++++++++ .../qrcode/viewmodel/QrCodeViewModel.kt | 152 ++++++ .../zcash/ui/screen/receive/AndroidReceive.kt | 50 +- .../ui/screen/receive/ext/WalletAddressExt.kt | 12 + .../receive/model/ReceiveAddressType.kt | 11 + .../ui/screen/receive/model/ReceiveState.kt | 16 + .../ui/screen/receive/view/ReceiveView.kt | 161 +++--- .../receive/viewmodel/ReceiveViewModel.kt | 67 +++ .../zcash/ui/util/FileShareUtil.kt | 9 +- .../drawable-night/ic_alert_circle.xml | 10 + .../res/ui/qr_code/drawable-night/ic_copy.xml | 13 + .../ui/qr_code/drawable-night/ic_share.xml | 13 + .../qr_code/drawable-night/ic_solid_check.xml | 10 + .../drawable-night/logo_zec_fill_stroke.xml | 22 + .../ui/qr_code/drawable/ic_alert_circle.xml | 10 + .../main/res/ui/qr_code/drawable/ic_copy.xml | 13 + .../main/res/ui/qr_code/drawable/ic_share.xml | 13 + .../ui/qr_code/drawable/ic_solid_check.xml | 10 + .../qr_code/drawable/logo_zec_fill_stroke.xml | 22 + .../main/res/ui/qr_code/values/strings.xml | 28 +- 46 files changed, 1292 insertions(+), 151 deletions(-) create mode 100644 ui-design-lib/src/main/res/ui/common/drawable-night/ic_close_full.xml create mode 100644 ui-design-lib/src/main/res/ui/common/drawable/ic_close_full.xml create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetAddressesUseCase.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/AndroidQrCode.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/ext/WalletAddressesExt.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/model/QrCodeState.kt rename ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/{receive => qrcode}/util/AndroidQrCodeImageGenerator.kt (94%) rename ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/{receive => qrcode}/util/JvmQrCodeGenerator.kt (94%) rename ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/{receive => qrcode}/util/QrCodeGenerator.kt (86%) rename ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/{receive => qrcode}/util/QrCodeImageGenerator.kt (77%) create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/ext/WalletAddressExt.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveAddressType.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveState.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable-night/ic_alert_circle.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable-night/ic_copy.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable-night/ic_share.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable-night/ic_solid_check.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable-night/logo_zec_fill_stroke.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable/ic_alert_circle.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable/ic_copy.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable/ic_share.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable/ic_solid_check.xml create mode 100644 ui-lib/src/main/res/ui/qr_code/drawable/logo_zec_fill_stroke.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index e8a79b0b0..26398e6fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2 - Confirmation screen redesigned - History item redesigned +### Added +- New QR Code detail screen has been added + ## [1.2 (739)] - 2024-09-27 ### Changed diff --git a/docs/whatsNew/WHATS_NEW_EN.md b/docs/whatsNew/WHATS_NEW_EN.md index 01510c9d3..c4c4840fb 100644 --- a/docs/whatsNew/WHATS_NEW_EN.md +++ b/docs/whatsNew/WHATS_NEW_EN.md @@ -15,6 +15,9 @@ directly impact users rather than highlighting other key architectural updates.* - Confirmation screen redesigned - History item redesigned +### Added +- New QR Code detail screen has been added + ## [1.2 (739)] - 2024-09-27 ### Changed diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Button.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Button.kt index 330de8dc9..739c43bf7 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Button.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Button.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.TextStyle @@ -467,6 +468,7 @@ private enum class ButtonMode { Pressed, Idle } data class ButtonState( val text: StringResource, + val leadingIconVector: Painter? = null, val isEnabled: Boolean = true, val isLoading: Boolean = false, val onClick: () -> Unit = {}, diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBadge.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBadge.kt index 5ba93847f..75d2a9fd8 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBadge.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBadge.kt @@ -1,15 +1,22 @@ package co.electriccoin.zcash.ui.design.component import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens @@ -23,10 +30,12 @@ import co.electriccoin.zcash.ui.design.util.stringRes fun ZashiBadge( text: String, modifier: Modifier = Modifier, + leadingIconVector: Painter? = null, colors: ZashiBadgeColors = ZashiBadgeDefaults.successBadgeColors() ) { ZashiBadge( text = stringRes(text), + leadingIconVector = leadingIconVector, modifier = modifier, colors = colors ) @@ -36,6 +45,7 @@ fun ZashiBadge( fun ZashiBadge( text: StringResource, modifier: Modifier = Modifier, + leadingIconVector: Painter? = null, colors: ZashiBadgeColors = ZashiBadgeDefaults.successBadgeColors() ) { Surface( @@ -44,9 +54,20 @@ fun ZashiBadge( color = colors.container, border = BorderStroke(1.dp, colors.border), ) { - Box( + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp) ) { + if (leadingIconVector != null) { + Image( + painter = leadingIconVector, + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + } + Text( text = text.getValue(), style = ZcashTheme.extendedTypography.transactionItemStyles.contentMedium, @@ -82,5 +103,8 @@ object ZashiBadgeDefaults { @Composable private fun BadgePreview() = ZcashTheme { - ZashiBadge(text = stringRes("Badge")) + ZashiBadge( + text = stringRes("Badge"), + leadingIconVector = painterResource(id = android.R.drawable.ic_input_add), + ) } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt index 3f651825e..f47a9f442 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt @@ -1,10 +1,12 @@ package co.electriccoin.zcash.ui.design.component import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button @@ -14,6 +16,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import co.electriccoin.zcash.ui.design.R @@ -32,6 +36,7 @@ fun ZashiButton( ) { ZashiButton( text = state.text.getValue(), + leadingIcon = state.leadingIconVector, onClick = state.onClick, modifier = modifier, enabled = state.isEnabled, @@ -47,6 +52,7 @@ fun ZashiButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, + leadingIcon: Painter? = null, enabled: Boolean = true, isLoading: Boolean = false, colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(), @@ -54,6 +60,17 @@ fun ZashiButton( ) { val scope = object : ZashiButtonScope { + @Composable + override fun LeadingIcon() { + if (leadingIcon != null) { + Image( + painter = leadingIcon, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + } + @Composable override fun Text() { Text( @@ -92,6 +109,9 @@ fun ZashiButton( } interface ZashiButtonScope { + @Composable + fun LeadingIcon() + @Composable fun Text() @@ -102,6 +122,8 @@ interface ZashiButtonScope { object ZashiButtonDefaults { val content: @Composable RowScope.(ZashiButtonScope) -> Unit get() = { scope -> + scope.LeadingIcon() + Spacer(modifier = Modifier.width(6.dp)) scope.Text() Spacer(modifier = Modifier.width(6.dp)) scope.Loading() @@ -121,6 +143,20 @@ object ZashiButtonDefaults { borderColor = Color.Unspecified ) + @Composable + fun secondaryColors( + containerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBg, + contentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg, + disabledContainerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBgDisabled, + disabledContentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg, + ) = ZashiButtonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor, + borderColor = Color.Unspecified + ) + @Composable fun tertiaryColors( containerColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryBg, @@ -169,7 +205,6 @@ private fun ZashiButtonColors.toButtonColors() = disabledContentColor = disabledContentColor, ) -@Suppress("UnusedPrivateMember") @PreviewScreens @Composable private fun PrimaryPreview() = @@ -183,7 +218,20 @@ private fun PrimaryPreview() = } } -@Suppress("UnusedPrivateMember") +@PreviewScreens +@Composable +private fun PrimaryWithIconPreview() = + ZcashTheme { + BlankSurface { + ZashiButton( + modifier = Modifier.fillMaxWidth(), + text = "Primary", + leadingIcon = painterResource(id = android.R.drawable.ic_secure), + onClick = {}, + ) + } + } + @PreviewScreens @Composable private fun TertiaryPreview() = @@ -198,7 +246,6 @@ private fun TertiaryPreview() = } } -@Suppress("UnusedPrivateMember") @PreviewScreens @Composable private fun DestroyPreview() = diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTopAppBar.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTopAppBar.kt index c05ff43a0..ced27319a 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTopAppBar.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiTopAppBar.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.internal.SecondaryTypography import co.electriccoin.zcash.ui.design.theme.internal.TopAppBarColors @@ -11,7 +12,7 @@ import co.electriccoin.zcash.ui.design.theme.internal.TopAppBarColors @Composable @Suppress("LongParameterList") fun ZashiSmallTopAppBar( - title: String, + title: String?, subtitle: String?, modifier: Modifier = Modifier, showTitleLogo: Boolean = false, @@ -32,3 +33,13 @@ fun ZashiSmallTopAppBar( titleStyle = SecondaryTypography.headlineSmall.copy(fontWeight = FontWeight.SemiBold) ) } + +@PreviewScreens +@Composable +private fun ZashiSmallTopAppBarPreview() = + ZcashTheme { + ZashiSmallTopAppBar( + title = "Test Title", + subtitle = "Subtitle", + ) + } diff --git a/ui-design-lib/src/main/res/ui/common/drawable-night/ic_close_full.xml b/ui-design-lib/src/main/res/ui/common/drawable-night/ic_close_full.xml new file mode 100644 index 000000000..035d438f3 --- /dev/null +++ b/ui-design-lib/src/main/res/ui/common/drawable-night/ic_close_full.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui-design-lib/src/main/res/ui/common/drawable/ic_close_full.xml b/ui-design-lib/src/main/res/ui/common/drawable/ic_close_full.xml new file mode 100644 index 000000000..2b8663e76 --- /dev/null +++ b/ui-design-lib/src/main/res/ui/common/drawable/ic_close_full.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index d785e1c83..29b7f1b06 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -46,6 +46,7 @@ android { "src/main/res/ui/choose_server", "src/main/res/ui/new_wallet_recovery", "src/main/res/ui/onboarding", + "src/main/res/ui/qr_code", "src/main/res/ui/receive", "src/main/res/ui/restore", "src/main/res/ui/restore_success", diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/fixture/MockSynchronizer.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/fixture/MockSynchronizer.kt index e9162484d..c6995dce4 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/fixture/MockSynchronizer.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/fixture/MockSynchronizer.kt @@ -158,6 +158,13 @@ internal class MockSynchronizer : CloseableSynchronizer { error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} yet.") } + override suspend fun proposeFulfillingPaymentUri( + account: Account, + uri: String + ): Proposal { + error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} yet.") + } + override suspend fun quickRewind() { error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/util/FileShareUtilTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/util/FileShareUtilTest.kt index 0c4c5858c..b14bb83b2 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/util/FileShareUtilTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/util/FileShareUtilTest.kt @@ -28,6 +28,8 @@ class FileShareUtilTest { context = getAppContext(), dataFilePath = tempFilePath.pathString, fileType = FileShareUtil.ZASHI_INTERNAL_DATA_MIME_TYPE, + shareText = null, + sharePickerText = "Test Picker Title", versionInfo = VersionInfoFixture.new() ) assertEquals(intent.action, Intent.ACTION_VIEW) diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveViewTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveViewTestSetup.kt index bdda81b11..6762e180b 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveViewTestSetup.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveViewTestSetup.kt @@ -7,6 +7,8 @@ import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.fixture.VersionInfoFixture +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveState +import kotlinx.coroutines.runBlocking import java.util.concurrent.atomic.AtomicInteger class ReceiveViewTestSetup( @@ -31,17 +33,20 @@ class ReceiveViewTestSetup( composeTestRule.setContent { ZcashTheme { ZcashTheme { - Receive( - walletAddresses = walletAddresses, + ReceiveView( + state = + ReceiveState.Prepared( + walletAddresses = runBlocking { walletAddresses }, + isTestnet = versionInfo.isTestnet, + onAddressCopy = {}, + onQrCode = {}, + onSettings = { + onSettingsCount.getAndIncrement() + }, + onRequest = {}, + ), snackbarHostState = SnackbarHostState(), - onSettings = { - onSettingsCount.getAndIncrement() - }, - onAddrCopyToClipboard = {}, - onQrCode = {}, - onRequest = {}, topAppBarSubTitleState = TopAppBarSubTitleState.None, - versionInfo = versionInfo, ) } } 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 1c0a54417..b3ef4d000 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 @@ -1,6 +1,8 @@ package co.electriccoin.zcash.di +import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase +import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase import co.electriccoin.zcash.ui.common.usecase.GetContactUseCase import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase @@ -43,4 +45,6 @@ val useCaseModule = singleOf(::UpdateContactUseCase) singleOf(::DeleteContactUseCase) singleOf(::GetContactUseCase) + singleOf(::GetAddressesUseCase) + singleOf(::CopyToClipboardUseCase) } 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 b4a4e4b12..821c51772 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 @@ -11,6 +11,8 @@ 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.onboarding.viewmodel.OnboardingViewModel +import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel +import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel @@ -45,4 +47,6 @@ val viewModelModule = viewModelOf(::AddressBookViewModel) viewModelOf(::AddContactViewModel) viewModelOf(::UpdateContactViewModel) + viewModelOf(::ReceiveViewModel) + viewModelOf(::QrCodeViewModel) } 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 3a9c895cb..924240674 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 @@ -19,6 +19,7 @@ import androidx.navigation.navArgument import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.model.ZecSend import co.electriccoin.zcash.spackle.Twig +import co.electriccoin.zcash.ui.NavigationArgs.ADDRESS_TYPE import co.electriccoin.zcash.ui.NavigationArgs.UPDATE_CONTACT_ID import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT @@ -37,6 +38,7 @@ 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.NOT_ENOUGH_SPACE +import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE import co.electriccoin.zcash.ui.NavigationTargets.SCAN import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY import co.electriccoin.zcash.ui.NavigationTargets.SEND_CONFIRMATION @@ -67,6 +69,8 @@ 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.home.WrapHome +import co.electriccoin.zcash.ui.screen.qrcode.WrapQrCode +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery import co.electriccoin.zcash.ui.screen.send.ext.toSerializableAddress @@ -285,6 +289,13 @@ internal fun MainActivity.Navigation() { val contactId = backStackEntry.arguments?.getString(UPDATE_CONTACT_ID).orEmpty() WrapUpdateContact(contactId) } + composable( + route = "$QR_CODE/{$ADDRESS_TYPE}", + arguments = listOf(navArgument(ADDRESS_TYPE) { type = NavType.IntType }) + ) { backStackEntry -> + val addressType = backStackEntry.arguments?.getInt(ADDRESS_TYPE) ?: ReceiveAddressType.Unified.ordinal + WrapQrCode(addressType) + } } } @@ -464,6 +475,7 @@ object NavigationTargets { const val HOME = "home" const val CHOOSE_SERVER = "choose_server" const val NOT_ENOUGH_SPACE = "not_enough_space" + const val QR_CODE = "qr_code" const val SCAN = "scan" const val SEED_RECOVERY = "seed_recovery" const val SEND_CONFIRMATION = "send_confirmation" @@ -478,4 +490,5 @@ object NavigationTargets { object NavigationArgs { const val UPDATE_CONTACT_ID = "contactId" + const val ADDRESS_TYPE = "addressType" } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt new file mode 100644 index 000000000..27952d1de --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt @@ -0,0 +1,16 @@ +package co.electriccoin.zcash.ui.common.usecase + +import android.content.Context +import co.electriccoin.zcash.spackle.ClipboardManagerUtil + +class CopyToClipboardUseCase { + operator fun invoke( + context: Context, + tag: String, + value: String + ) = ClipboardManagerUtil.copyToClipboard( + context = context, + label = tag, + value = value + ) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetAddressesUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetAddressesUseCase.kt new file mode 100644 index 000000000..dedc2ea27 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetAddressesUseCase.kt @@ -0,0 +1,10 @@ +package co.electriccoin.zcash.ui.common.usecase + +import co.electriccoin.zcash.ui.common.repository.WalletRepository +import kotlinx.coroutines.flow.filterNotNull + +class GetAddressesUseCase( + private val walletRepository: WalletRepository +) { + operator fun invoke() = walletRepository.addresses.filterNotNull() +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt index 34f764a30..64974e6aa 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt @@ -107,6 +107,7 @@ fun shareData( network = ZcashNetwork.fromResources(context) ), fileType = FileShareUtil.ZASHI_INTERNAL_DATA_MIME_TYPE, + sharePickerText = context.getString(R.string.export_data_export_data_chooser_title), versionInfo = VersionInfo.new(context.applicationContext) ) runCatching { 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 eb41d41f8..d0c75e007 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 @@ -192,7 +192,7 @@ internal fun WrapHome( title = stringResource(id = R.string.home_tab_receive), testTag = HomeTag.TAB_RECEIVE, screenContent = { - WrapReceive(onSettings = goSettings) + WrapReceive() } ), TabItem( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/AndroidQrCode.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/AndroidQrCode.kt new file mode 100644 index 000000000..587634a2a --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/AndroidQrCode.kt @@ -0,0 +1,61 @@ +package co.electriccoin.zcash.ui.screen.qrcode + +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.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.electriccoin.zcash.di.koinActivityViewModel +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.screen.qrcode.model.QrCodeState +import co.electriccoin.zcash.ui.screen.qrcode.view.QrCodeView +import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +@Composable +internal fun WrapQrCode(addressType: Int) { + val context = LocalContext.current + val navController = LocalNavController.current + + val walletViewModel = koinActivityViewModel() + val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle() + + val qrCodeViewModel = koinViewModel { parametersOf(addressType) } + val qrCodeState by qrCodeViewModel.state.collectAsStateWithLifecycle() + + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(Unit) { + qrCodeViewModel.backNavigationCommand.collect { + navController.popBackStack() + } + } + LaunchedEffect(Unit) { + qrCodeViewModel.shareResultCommand.collect { sharedSuccessfully -> + if (!sharedSuccessfully) { + snackbarHostState.showSnackbar( + message = context.getString(R.string.qr_code_data_unable_to_share) + ) + } + } + } + + BackHandler { + when (qrCodeState) { + QrCodeState.Loading -> {} + is QrCodeState.Prepared -> (qrCodeState as QrCodeState.Prepared).onBack.invoke() + } + } + + QrCodeView( + state = qrCodeState, + topAppBarSubTitleState = walletState, + snackbarHostState = snackbarHostState + ) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/ext/WalletAddressesExt.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/ext/WalletAddressesExt.kt new file mode 100644 index 000000000..f39719c19 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/ext/WalletAddressesExt.kt @@ -0,0 +1,11 @@ +package co.electriccoin.zcash.ui.screen.qrcode.ext + +import cash.z.ecc.android.sdk.model.WalletAddresses +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType + +internal fun WalletAddresses.fromReceiveAddressType(receiveAddressType: ReceiveAddressType) = + when (receiveAddressType) { + ReceiveAddressType.Unified -> unified + ReceiveAddressType.Sapling -> sapling + ReceiveAddressType.Transparent -> transparent + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/model/QrCodeState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/model/QrCodeState.kt new file mode 100644 index 000000000..6280fdea7 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/model/QrCodeState.kt @@ -0,0 +1,15 @@ +package co.electriccoin.zcash.ui.screen.qrcode.model + +import androidx.compose.ui.graphics.ImageBitmap +import cash.z.ecc.android.sdk.model.WalletAddress + +internal sealed class QrCodeState { + data object Loading : QrCodeState() + + data class Prepared( + val walletAddress: WalletAddress, + val onAddressCopy: (String) -> Unit, + val onQrCodeShare: (ImageBitmap) -> Unit, + val onBack: () -> Unit, + ) : QrCodeState() +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/AndroidQrCodeImageGenerator.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/AndroidQrCodeImageGenerator.kt similarity index 94% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/AndroidQrCodeImageGenerator.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/AndroidQrCodeImageGenerator.kt index 312eceb8f..3deb19307 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/AndroidQrCodeImageGenerator.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/AndroidQrCodeImageGenerator.kt @@ -1,4 +1,4 @@ -package co.electriccoin.zcash.ui.screen.receive.util +package co.electriccoin.zcash.ui.screen.qrcode.util import android.graphics.Bitmap import android.graphics.Color diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/JvmQrCodeGenerator.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/JvmQrCodeGenerator.kt similarity index 94% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/JvmQrCodeGenerator.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/JvmQrCodeGenerator.kt index 0a99af9c4..d410e9ce6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/JvmQrCodeGenerator.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/JvmQrCodeGenerator.kt @@ -1,4 +1,4 @@ -package co.electriccoin.zcash.ui.screen.receive.util +package co.electriccoin.zcash.ui.screen.qrcode.util import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/QrCodeGenerator.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/QrCodeGenerator.kt similarity index 86% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/QrCodeGenerator.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/QrCodeGenerator.kt index b6e4d39d6..36b892898 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/QrCodeGenerator.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/QrCodeGenerator.kt @@ -1,4 +1,4 @@ -package co.electriccoin.zcash.ui.screen.receive.util +package co.electriccoin.zcash.ui.screen.qrcode.util interface QrCodeGenerator { /** diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/QrCodeImageGenerator.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/QrCodeImageGenerator.kt similarity index 77% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/QrCodeImageGenerator.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/QrCodeImageGenerator.kt index 292eeced1..0d26704f6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/util/QrCodeImageGenerator.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/util/QrCodeImageGenerator.kt @@ -1,4 +1,4 @@ -package co.electriccoin.zcash.ui.screen.receive.util +package co.electriccoin.zcash.ui.screen.qrcode.util import androidx.compose.ui.graphics.ImageBitmap diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt new file mode 100644 index 000000000..8a4712521 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt @@ -0,0 +1,487 @@ +@file:Suppress("TooManyFunctions") + +package co.electriccoin.zcash.ui.screen.qrcode.view + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.IconButton +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.remember +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.graphics.ImageBitmap +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import cash.z.ecc.android.sdk.fixture.WalletAddressFixture +import cash.z.ecc.android.sdk.model.WalletAddress +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState +import co.electriccoin.zcash.ui.design.component.BlankBgScaffold +import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator +import co.electriccoin.zcash.ui.design.component.ZashiBadge +import co.electriccoin.zcash.ui.design.component.ZashiBadgeColors +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.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.ZashiDimensionsInternal +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeState +import co.electriccoin.zcash.ui.screen.qrcode.util.AndroidQrCodeImageGenerator +import co.electriccoin.zcash.ui.screen.qrcode.util.JvmQrCodeGenerator +import kotlinx.coroutines.runBlocking +import kotlin.math.roundToInt + +@Composable +@PreviewScreens +private fun QrCodeLoadingPreview() = + ZcashTheme(forceDarkMode = true) { + QrCodeView( + state = QrCodeState.Loading, + snackbarHostState = SnackbarHostState(), + topAppBarSubTitleState = TopAppBarSubTitleState.None, + ) + } + +@Composable +@PreviewScreens +private fun QrCodePreview() = + ZcashTheme(forceDarkMode = false) { + QrCodeView( + state = + QrCodeState.Prepared( + walletAddress = runBlocking { WalletAddressFixture.unified() }, + onAddressCopy = {}, + onQrCodeShare = {}, + onBack = {}, + ), + snackbarHostState = SnackbarHostState(), + topAppBarSubTitleState = TopAppBarSubTitleState.None, + ) + } + +@Composable +internal fun QrCodeView( + state: QrCodeState, + snackbarHostState: SnackbarHostState, + topAppBarSubTitleState: TopAppBarSubTitleState, +) { + when (state) { + QrCodeState.Loading -> { + CircularScreenProgressIndicator() + } + is QrCodeState.Prepared -> { + val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt() + val qrCodeImage = + remember { + qrCodeForAddress( + address = state.walletAddress.address, + size = sizePixels + ) + } + + BlankBgScaffold( + topBar = { + QrCodeTopAppBar( + onBack = state.onBack, + subTitleState = topAppBarSubTitleState, + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + QrCodeBottomBar( + state = state, + qrCodeImage = qrCodeImage + ) + } + ) { paddingValues -> + QrCodeContents( + walletAddress = state.walletAddress, + onAddressCopy = state.onAddressCopy, + onQrCodeShare = state.onQrCodeShare, + modifier = + Modifier.padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ), + ) + } + } + } +} + +@Composable +private fun QrCodeTopAppBar( + onBack: () -> Unit, + subTitleState: TopAppBarSubTitleState, +) { + ZashiSmallTopAppBar( + subtitle = + when (subTitleState) { + TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) + TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) + TopAppBarSubTitleState.None -> null + }, + title = null, + navigationAction = { + IconButton( + onClick = onBack, + modifier = + Modifier + .padding(horizontal = ZcashTheme.dimens.spacingDefault) + // Making the size bigger by 3.dp so the rounded image corners are not stripped out + .size(43.dp), + ) { + Image( + painter = + painterResource( + id = co.electriccoin.zcash.ui.design.R.drawable.ic_close_full + ), + contentDescription = stringResource(id = R.string.qr_code_close_content_description), + modifier = + Modifier + .padding(all = 3.dp) + ) + } + }, + ) +} + +@Composable +private fun QrCodeBottomBar( + state: QrCodeState.Prepared, + qrCodeImage: ImageBitmap, +) { + ZashiBottomBar { + ZashiButton( + text = stringResource(id = R.string.qr_code_share_btn), + leadingIcon = painterResource(R.drawable.ic_share), + onClick = { state.onQrCodeShare(qrCodeImage) }, + modifier = + Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny)) + + ZashiButton( + text = stringResource(id = R.string.qr_code_copy_btn), + leadingIcon = painterResource(R.drawable.ic_copy), + onClick = { state.onAddressCopy(state.walletAddress.address) }, + colors = ZashiButtonDefaults.secondaryColors(), + modifier = + Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth() + ) + } +} + +@Composable +private fun QrCodeContents( + walletAddress: WalletAddress, + onAddressCopy: (String) -> Unit, + onQrCodeShare: (ImageBitmap) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular), + ) { + Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) + + when (walletAddress) { + // We use the same design for the Sapling address for the Testnet app variant + is WalletAddress.Unified, is WalletAddress.Sapling -> { + UnifiedQrCodePanel(walletAddress, onAddressCopy, onQrCodeShare) + } + is WalletAddress.Transparent -> { + TransparentQrCodePanel(walletAddress, onAddressCopy, onQrCodeShare) + } + else -> { + error("Unsupported address type: $walletAddress") + } + } + } +} + +@Composable +@Suppress("LongMethod") +fun UnifiedQrCodePanel( + walletAddress: WalletAddress, + onAddressCopy: (String) -> Unit, + onQrCodeShare: (ImageBitmap) -> Unit, + modifier: Modifier = Modifier +) { + var expandedAddress by rememberSaveable { mutableStateOf(false) } + + Column( + modifier = + modifier + .padding(vertical = ZcashTheme.dimens.spacingDefault), + horizontalAlignment = Alignment.CenterHorizontally + ) { + QrCode( + walletAddress = walletAddress, + onQrImageShare = onQrCodeShare, + modifier = + Modifier + .padding(horizontal = 24.dp), + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) + + ZashiBadge( + text = stringResource(id = R.string.qr_code_privacy_level_shielded), + leadingIconVector = painterResource(id = R.drawable.ic_solid_check), + colors = + ZashiBadgeColors( + border = ZashiColors.Utility.Purple.utilityPurple200, + text = ZashiColors.Utility.Purple.utilityPurple700, + container = ZashiColors.Utility.Purple.utilityPurple50, + ) + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) + + Text( + text = + when (walletAddress) { + is WalletAddress.Unified -> stringResource(id = R.string.qr_code_wallet_address_shielded) + is WalletAddress.Sapling -> stringResource(id = R.string.qr_code_wallet_address_sapling) + else -> error("Unsupported address type: $walletAddress") + }, + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.textXl, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) + + @OptIn(ExperimentalFoundationApi::class) + Text( + text = walletAddress.address, + color = ZashiColors.Text.textTertiary, + style = ZashiTypography.textSm, + textAlign = TextAlign.Center, + maxLines = + if (expandedAddress) { + Int.MAX_VALUE + } else { + 2 + }, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .animateContentSize() + .combinedClickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { expandedAddress = !expandedAddress }, + onLongClick = { onAddressCopy(walletAddress.address) } + ) + ) + } +} + +@Composable +@Suppress("LongMethod") +fun TransparentQrCodePanel( + walletAddress: WalletAddress, + onAddressCopy: (String) -> Unit, + onQrCodeShare: (ImageBitmap) -> Unit, + modifier: Modifier = Modifier +) { + var expandedAddress by rememberSaveable { mutableStateOf(false) } + + Column( + modifier = + modifier + .padding(vertical = ZcashTheme.dimens.spacingDefault), + horizontalAlignment = Alignment.CenterHorizontally + ) { + QrCode( + walletAddress = walletAddress, + onQrImageShare = onQrCodeShare, + modifier = + Modifier + .padding(horizontal = 24.dp), + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) + + ZashiBadge( + text = stringResource(id = R.string.qr_code_privacy_level_transparent), + leadingIconVector = painterResource(id = R.drawable.ic_alert_circle), + colors = + ZashiBadgeColors( + border = ZashiColors.Utility.WarningYellow.utilityOrange200, + text = ZashiColors.Utility.WarningYellow.utilityOrange700, + container = ZashiColors.Utility.WarningYellow.utilityOrange50, + ) + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) + + Text( + text = stringResource(id = R.string.qr_code_wallet_address_transparent), + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.textXl, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) + + @OptIn(ExperimentalFoundationApi::class) + Text( + text = walletAddress.address, + color = ZashiColors.Text.textTertiary, + style = ZashiTypography.textSm, + textAlign = TextAlign.Center, + maxLines = + if (expandedAddress) { + Int.MAX_VALUE + } else { + 2 + }, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .animateContentSize() + .combinedClickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { expandedAddress = !expandedAddress }, + onLongClick = { onAddressCopy(walletAddress.address) } + ) + ) + } +} + +@Composable +private fun ColumnScope.QrCode( + walletAddress: WalletAddress, + onQrImageShare: (ImageBitmap) -> Unit, + modifier: Modifier = Modifier +) { + val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt() + val qrCodeImage = + remember { + qrCodeForAddress( + address = walletAddress.address, + size = sizePixels + ) + } + + QrCode( + qrCodeImage = qrCodeImage, + onQrImageBitmapShare = onQrImageShare, + contentDescription = + stringResource( + when (walletAddress) { + is WalletAddress.Unified -> R.string.qr_code_unified_content_description + is WalletAddress.Sapling -> R.string.qr_code_sapling_content_description + is WalletAddress.Transparent -> R.string.qr_code_transparent_content_description + else -> error("Unsupported address type: $walletAddress") + } + ), + modifier = + modifier + .align(Alignment.CenterHorizontally) + .border( + border = + BorderStroke( + width = 1.dp, + color = ZashiColors.Surfaces.strokePrimary + ), + shape = RoundedCornerShape(ZashiDimensionsInternal.Radius.radius4xl) + ) + .padding(all = 12.dp) + ) +} + +private fun qrCodeForAddress( + address: String, + size: Int, +): ImageBitmap { + // In the future, use actual/expect to switch QR code generator implementations for multiplatform + + // Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform + // representation. This should have minimal performance impact since the QR code is relatively + // small and we only generate QR codes infrequently. + + val qrCodePixelArray = JvmQrCodeGenerator.generate(address, size) + + return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size) +} + +@Composable +private fun QrCode( + contentDescription: String, + qrCodeImage: ImageBitmap, + onQrImageBitmapShare: (ImageBitmap) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onQrImageBitmapShare(qrCodeImage) }, + ) + .then(modifier) + ) { + Image( + bitmap = qrCodeImage, + contentDescription = contentDescription, + ) + + Image( + painter = painterResource(id = R.drawable.logo_zec_fill_stroke), + contentDescription = contentDescription, + ) + } +} + +private val DEFAULT_QR_CODE_SIZE = 320.dp diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt new file mode 100644 index 000000000..cf86d089b --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt @@ -0,0 +1,152 @@ +package co.electriccoin.zcash.ui.screen.qrcode.viewmodel + +import android.app.Application +import android.content.Context +import android.graphics.Bitmap +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.spackle.Twig +import co.electriccoin.zcash.spackle.getInternalCacheDirSuspend +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.model.VersionInfo +import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider +import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase +import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase +import co.electriccoin.zcash.ui.screen.qrcode.ext.fromReceiveAddressType +import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeState +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType +import co.electriccoin.zcash.ui.util.FileShareUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + +class QrCodeViewModel( + private val addressTypeOrdinal: Int, + private val application: Application, + getAddresses: GetAddressesUseCase, + getVersionInfo: GetVersionInfoProvider, + private val copyToClipboard: CopyToClipboardUseCase, +) : ViewModel() { + private val versionInfo by lazy { getVersionInfo() } + + @OptIn(ExperimentalCoroutinesApi::class) + internal val state = + getAddresses().mapLatest { addresses -> + QrCodeState.Prepared( + walletAddress = addresses.fromReceiveAddressType(ReceiveAddressType.fromOrdinal(addressTypeOrdinal)), + onAddressCopy = { address -> onAddressCopyClick(address) }, + onQrCodeShare = { onQrCodeShareClick(it, versionInfo) }, + onBack = ::onBack, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = QrCodeState.Loading + ) + + val backNavigationCommand = MutableSharedFlow() + + val shareResultCommand = MutableSharedFlow() + + private fun onBack() = + viewModelScope.launch { + backNavigationCommand.emit(Unit) + } + + private fun onQrCodeShareClick( + bitmap: ImageBitmap, + versionInfo: VersionInfo + ) = viewModelScope.launch { + shareData( + context = application.applicationContext, + qrImageBitmap = bitmap.asAndroidBitmap(), + versionInfo = versionInfo + ).collect { shareResult -> + if (shareResult) { + Twig.info { "Sharing the address QR code was successful" } + shareResultCommand.emit(true) + } else { + Twig.info { "Sharing the address QR code failed" } + shareResultCommand.emit(false) + } + } + } + + private fun onAddressCopyClick(address: String) = + copyToClipboard( + context = application.applicationContext, + tag = application.getString(R.string.qr_code_clipboard_tag), + value = address + ) +} + +private const val CACHE_SUBDIR = "zcash_address_qr_images" // NON-NLS +private const val TEMP_FILE_NAME_PREFIX = "zcash_address_qr_" // NON-NLS +private const val TEMP_FILE_NAME_SUFFIX = ".png" // NON-NLS + +fun shareData( + context: Context, + qrImageBitmap: Bitmap, + versionInfo: VersionInfo +): Flow = + callbackFlow { + // Initialize cache directory + val cacheDir = context.getInternalCacheDirSuspend(CACHE_SUBDIR) + + // Save the bitmap to a temporary file in the cache directory + val bitmapFile = + withContext(Dispatchers.IO) { + File.createTempFile( + TEMP_FILE_NAME_PREFIX, + TEMP_FILE_NAME_SUFFIX, + cacheDir, + ).also { + it.storeBitmap(qrImageBitmap) + } + } + + // Example of the expected temporary file path: + // /data/user/0/co.electriccoin.zcash.debug/cache/zcash_address_qr_images/ + // zcash_address_qr_6455164324646067652.png + + val shareIntent = + FileShareUtil.newShareContentIntent( + context = context, + dataFilePath = bitmapFile.absolutePath, + fileType = FileShareUtil.ZASHI_QR_CODE_MIME_TYPE, + shareText = context.getString(R.string.qr_code_share_chooser_text), + sharePickerText = context.getString(R.string.qr_code_share_chooser_title), + versionInfo = versionInfo, + ) + runCatching { + context.startActivity(shareIntent) + trySend(true) + }.onFailure { + trySend(false) + } + awaitClose { + // No resources to release + } + } + +suspend fun File.storeBitmap(bitmap: Bitmap) = + withContext(Dispatchers.IO) { + outputStream().use { fOut -> + @Suppress("MagicNumber") + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut) + fOut.flush() + } + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt index 5cb682dfc..7faa8f4ef 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt @@ -2,51 +2,39 @@ package co.electriccoin.zcash.ui.screen.receive -import android.widget.Toast 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.lifecycle.compose.collectAsStateWithLifecycle import co.electriccoin.zcash.di.koinActivityViewModel -import co.electriccoin.zcash.spackle.ClipboardManagerUtil -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.LocalActivity -import co.electriccoin.zcash.ui.common.model.VersionInfo +import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel -import co.electriccoin.zcash.ui.screen.receive.view.Receive +import co.electriccoin.zcash.ui.screen.receive.view.ReceiveView +import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel @Composable -internal fun WrapReceive(onSettings: () -> Unit) { - val activity = LocalActivity.current +internal fun WrapReceive() { + val navController = LocalNavController.current val walletViewModel = koinActivityViewModel() - - val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value - val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value + val receiveViewModel = koinActivityViewModel() + val receiveState by receiveViewModel.state.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } - val versionInfo = VersionInfo.new(activity.applicationContext) - - Receive( - onAddrCopyToClipboard = { address -> - ClipboardManagerUtil.copyToClipboard( - activity.applicationContext, - activity.getString(R.string.receive_clipboard_tag), - address - ) - }, - onQrCode = { - Toast.makeText(activity, "Not implemented yet", Toast.LENGTH_SHORT).show() - }, - onRequest = { - Toast.makeText(activity, "Not implemented yet", Toast.LENGTH_SHORT).show() - }, - onSettings = onSettings, - snackbarHostState = snackbarHostState, + LaunchedEffect(Unit) { + receiveViewModel.navigationCommand.collect { + navController.navigate(it) + } + } + + ReceiveView( + state = receiveState, topAppBarSubTitleState = walletState, - versionInfo = versionInfo, - walletAddresses = walletAddresses, + snackbarHostState = snackbarHostState ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/ext/WalletAddressExt.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/ext/WalletAddressExt.kt new file mode 100644 index 000000000..62c23ed4d --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/ext/WalletAddressExt.kt @@ -0,0 +1,12 @@ +package co.electriccoin.zcash.ui.screen.receive.ext + +import cash.z.ecc.android.sdk.model.WalletAddress +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType + +internal fun WalletAddress.toReceiveAddressType() = + when (this) { + is WalletAddress.Unified -> ReceiveAddressType.Unified + is WalletAddress.Sapling -> ReceiveAddressType.Sapling + is WalletAddress.Transparent -> ReceiveAddressType.Transparent + else -> error("Unsupported address type") + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveAddressType.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveAddressType.kt new file mode 100644 index 000000000..a2815c9ba --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveAddressType.kt @@ -0,0 +1,11 @@ +package co.electriccoin.zcash.ui.screen.receive.model + +internal enum class ReceiveAddressType { + Unified, + Sapling, + Transparent; + + companion object { + fun fromOrdinal(ordinal: Int) = entries[ordinal] + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveState.kt new file mode 100644 index 000000000..fae9e7205 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/model/ReceiveState.kt @@ -0,0 +1,16 @@ +package co.electriccoin.zcash.ui.screen.receive.model + +import cash.z.ecc.android.sdk.model.WalletAddresses + +internal sealed class ReceiveState { + data object Loading : ReceiveState() + + data class Prepared( + val walletAddresses: WalletAddresses, + val onAddressCopy: (String) -> Unit, + val onQrCode: (ReceiveAddressType) -> Unit, + val onRequest: (ReceiveAddressType) -> Unit, + val onSettings: () -> Unit, + val isTestnet: Boolean, + ) : ReceiveState() +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt index 530b9c0dd..1bf4812b1 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt @@ -44,87 +44,84 @@ import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddresses import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState -import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.test.CommonTag import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar +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 import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensionsInternal import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography -import co.electriccoin.zcash.ui.fixture.VersionInfoFixture +import co.electriccoin.zcash.ui.screen.receive.ext.toReceiveAddressType +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveState import co.electriccoin.zcash.ui.screen.send.ext.abbreviated import kotlinx.coroutines.runBlocking -@Preview @Composable -private fun ReceivePreview() = - ZcashTheme(forceDarkMode = false) { - Receive( - walletAddresses = runBlocking { WalletAddressesFixture.new() }, +@PreviewScreens +private fun ReceiveLoadingPreview() = + ZcashTheme(forceDarkMode = true) { + ReceiveView( + state = ReceiveState.Loading, snackbarHostState = SnackbarHostState(), - onSettings = {}, - onAddrCopyToClipboard = {}, - onQrCode = {}, - onRequest = {}, - versionInfo = VersionInfoFixture.new(), topAppBarSubTitleState = TopAppBarSubTitleState.None, ) } @Preview @Composable -private fun ReceiveDarkPreview() = - ZcashTheme(forceDarkMode = true) { - Receive( - walletAddresses = runBlocking { WalletAddressesFixture.new() }, +private fun ReceivePreview() = + ZcashTheme(forceDarkMode = false) { + ReceiveView( + state = + ReceiveState.Prepared( + walletAddresses = runBlocking { WalletAddressesFixture.new() }, + isTestnet = false, + onAddressCopy = {}, + onQrCode = {}, + onSettings = {}, + onRequest = {}, + ), snackbarHostState = SnackbarHostState(), - onSettings = {}, - onAddrCopyToClipboard = {}, - onQrCode = {}, - onRequest = {}, - versionInfo = VersionInfoFixture.new(), topAppBarSubTitleState = TopAppBarSubTitleState.None, ) } -@Suppress("LongParameterList") @Composable -fun Receive( - walletAddresses: WalletAddresses?, +internal fun ReceiveView( + state: ReceiveState, snackbarHostState: SnackbarHostState, - onSettings: () -> Unit, - onAddrCopyToClipboard: (String) -> Unit, - onQrCode: (WalletAddress) -> Unit, - onRequest: (WalletAddress) -> Unit, topAppBarSubTitleState: TopAppBarSubTitleState, - versionInfo: VersionInfo, ) { - BlankBgScaffold( - topBar = { - ReceiveTopAppBar( - onSettings = onSettings, - subTitleState = topAppBarSubTitleState, - ) - }, - snackbarHost = { SnackbarHost(snackbarHostState) }, - ) { paddingValues -> - if (null == walletAddresses) { + when (state) { + ReceiveState.Loading -> { CircularScreenProgressIndicator() - } else { - ReceiveContents( - walletAddresses = walletAddresses, - onAddressCopyToClipboard = onAddrCopyToClipboard, - onQrCode = onQrCode, - onRequest = onRequest, - versionInfo = versionInfo, - modifier = - Modifier.padding( - top = paddingValues.calculateTopPadding() - // We intentionally do not set the rest paddings, those are set by the underlying composable - ), - ) + } + is ReceiveState.Prepared -> { + BlankBgScaffold( + topBar = { + ReceiveTopAppBar( + onSettings = state.onSettings, + subTitleState = topAppBarSubTitleState, + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { paddingValues -> + ReceiveContents( + walletAddresses = state.walletAddresses, + onAddressCopyToClipboard = state.onAddressCopy, + onQrCode = state.onQrCode, + onRequest = state.onRequest, + isTestnet = state.isTestnet, + modifier = + Modifier.padding( + top = paddingValues.calculateTopPadding() + // We intentionally do not set the rest paddings, those are set by the underlying composable + ), + ) + } } } } @@ -134,14 +131,14 @@ private fun ReceiveTopAppBar( onSettings: () -> Unit, subTitleState: TopAppBarSubTitleState, ) { - SmallTopAppBar( - subTitle = + ZashiSmallTopAppBar( + subtitle = when (subTitleState) { TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) TopAppBarSubTitleState.None -> null }, - titleText = stringResource(id = R.string.receive_title), + title = stringResource(id = R.string.receive_title), hamburgerMenuActions = { IconButton( onClick = onSettings, @@ -165,23 +162,17 @@ private fun ReceiveTopAppBar( ) } -private enum class AddressType { - Unified, - Sapling, - Transparent, -} - @Composable @Suppress("LongParameterList") private fun ReceiveContents( walletAddresses: WalletAddresses, onAddressCopyToClipboard: (String) -> Unit, - onQrCode: (WalletAddress) -> Unit, - onRequest: (WalletAddress) -> Unit, - versionInfo: VersionInfo, + onQrCode: (ReceiveAddressType) -> Unit, + onRequest: (ReceiveAddressType) -> Unit, + isTestnet: Boolean, modifier: Modifier = Modifier, ) { - var expandedAddressPanel by rememberSaveable { mutableStateOf(AddressType.Unified) } + var expandedAddressPanel by rememberSaveable { mutableStateOf(ReceiveAddressType.Unified) } Column( modifier = @@ -216,11 +207,11 @@ private fun ReceiveContents( onAddressCopyToClipboard = onAddressCopyToClipboard, onQrCode = onQrCode, onRequest = onRequest, - expanded = expandedAddressPanel == AddressType.Unified, - onExpand = { expandedAddressPanel = AddressType.Unified } + expanded = expandedAddressPanel == ReceiveAddressType.Unified, + onExpand = { expandedAddressPanel = ReceiveAddressType.Unified } ) - if (versionInfo.isTestnet) { + if (isTestnet) { Spacer(Modifier.height(ZcashTheme.dimens.spacingSmall)) SaplingAddressPanel( @@ -228,8 +219,8 @@ private fun ReceiveContents( onAddressCopyToClipboard = onAddressCopyToClipboard, onQrCode = onQrCode, onRequest = onRequest, - expanded = expandedAddressPanel == AddressType.Sapling, - onExpand = { expandedAddressPanel = AddressType.Sapling } + expanded = expandedAddressPanel == ReceiveAddressType.Sapling, + onExpand = { expandedAddressPanel = ReceiveAddressType.Sapling } ) } @@ -240,8 +231,8 @@ private fun ReceiveContents( onAddressCopyToClipboard = onAddressCopyToClipboard, onQrCode = onQrCode, onRequest = onRequest, - expanded = expandedAddressPanel == AddressType.Transparent, - onExpand = { expandedAddressPanel = AddressType.Transparent } + expanded = expandedAddressPanel == ReceiveAddressType.Transparent, + onExpand = { expandedAddressPanel = ReceiveAddressType.Transparent } ) } } @@ -251,8 +242,8 @@ private fun ReceiveContents( private fun UnifiedAddressPanel( walletAddress: WalletAddress, onAddressCopyToClipboard: (String) -> Unit, - onQrCode: (WalletAddress) -> Unit, - onRequest: (WalletAddress) -> Unit, + onQrCode: (ReceiveAddressType) -> Unit, + onRequest: (ReceiveAddressType) -> Unit, expanded: Boolean, onExpand: () -> Unit, modifier: Modifier = Modifier, @@ -328,7 +319,7 @@ private fun UnifiedAddressPanel( containerColor = ZashiColors.Utility.Purple.utilityPurple100, contentColor = ZashiColors.Utility.Purple.utilityPurple800, iconPainter = painterResource(id = R.drawable.ic_qr_code_shielded), - onClick = { onQrCode(walletAddress) }, + onClick = { onQrCode(walletAddress.toReceiveAddressType()) }, text = stringResource(id = R.string.receive_qr_code), modifier = Modifier.weight(1f) ) @@ -339,7 +330,7 @@ private fun UnifiedAddressPanel( containerColor = ZashiColors.Utility.Purple.utilityPurple100, contentColor = ZashiColors.Utility.Purple.utilityPurple800, iconPainter = painterResource(id = R.drawable.ic_request_shielded), - onClick = { onRequest(walletAddress) }, + onClick = { onRequest(walletAddress.toReceiveAddressType()) }, text = stringResource(id = R.string.receive_request), modifier = Modifier.weight(1f) ) @@ -353,8 +344,8 @@ private fun UnifiedAddressPanel( private fun SaplingAddressPanel( walletAddress: WalletAddress, onAddressCopyToClipboard: (String) -> Unit, - onQrCode: (WalletAddress) -> Unit, - onRequest: (WalletAddress) -> Unit, + onQrCode: (ReceiveAddressType) -> Unit, + onRequest: (ReceiveAddressType) -> Unit, expanded: Boolean, onExpand: () -> Unit, modifier: Modifier = Modifier, @@ -421,7 +412,7 @@ private fun SaplingAddressPanel( containerColor = ZashiColors.Surfaces.bgTertiary, contentColor = ZashiColors.Text.textPrimary, iconPainter = painterResource(id = R.drawable.ic_qr_code_other), - onClick = { onQrCode(walletAddress) }, + onClick = { onQrCode(walletAddress.toReceiveAddressType()) }, text = stringResource(id = R.string.receive_qr_code), modifier = Modifier.weight(1f) ) @@ -432,7 +423,7 @@ private fun SaplingAddressPanel( containerColor = ZashiColors.Surfaces.bgTertiary, contentColor = ZashiColors.Text.textPrimary, iconPainter = painterResource(id = R.drawable.ic_request_other), - onClick = { onRequest(walletAddress) }, + onClick = { onRequest(walletAddress.toReceiveAddressType()) }, text = stringResource(id = R.string.receive_request), modifier = Modifier.weight(1f) ) @@ -446,8 +437,8 @@ private fun SaplingAddressPanel( private fun TransparentAddressPanel( walletAddress: WalletAddress, onAddressCopyToClipboard: (String) -> Unit, - onQrCode: (WalletAddress) -> Unit, - onRequest: (WalletAddress) -> Unit, + onQrCode: (ReceiveAddressType) -> Unit, + onRequest: (ReceiveAddressType) -> Unit, expanded: Boolean, onExpand: () -> Unit, modifier: Modifier = Modifier, @@ -514,7 +505,7 @@ private fun TransparentAddressPanel( containerColor = ZashiColors.Surfaces.bgTertiary, contentColor = ZashiColors.Text.textPrimary, iconPainter = painterResource(id = R.drawable.ic_qr_code_other), - onClick = { onQrCode(walletAddress) }, + onClick = { onQrCode(walletAddress.toReceiveAddressType()) }, text = stringResource(id = R.string.receive_qr_code), modifier = Modifier.weight(1f) ) @@ -525,7 +516,7 @@ private fun TransparentAddressPanel( containerColor = ZashiColors.Surfaces.bgTertiary, contentColor = ZashiColors.Text.textPrimary, iconPainter = painterResource(id = R.drawable.ic_request_other), - onClick = { onRequest(walletAddress) }, + onClick = { onRequest(walletAddress.toReceiveAddressType()) }, text = stringResource(id = R.string.receive_request), modifier = Modifier.weight(1f) ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt new file mode 100644 index 000000000..184a68a83 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt @@ -0,0 +1,67 @@ +package co.electriccoin.zcash.ui.screen.receive.viewmodel + +import android.app.Application +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.ui.NavigationTargets +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider +import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase +import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType +import co.electriccoin.zcash.ui.screen.receive.model.ReceiveState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class ReceiveViewModel( + private val application: Application, + getVersionInfo: GetVersionInfoProvider, + getAddresses: GetAddressesUseCase, + private val copyToClipboard: CopyToClipboardUseCase, +) : ViewModel() { + @OptIn(ExperimentalCoroutinesApi::class) + internal val state = + getAddresses().mapLatest { addresses -> + ReceiveState.Prepared( + walletAddresses = addresses, + isTestnet = getVersionInfo().isTestnet, + onAddressCopy = { address -> + copyToClipboard( + context = application.applicationContext, + tag = application.getString(R.string.receive_clipboard_tag), + value = address + ) + }, + onQrCode = { addressType -> onQrCodeClick(addressType) }, + onRequest = { addressType -> onRequestClick(addressType) }, + onSettings = ::onSettingsClick, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = ReceiveState.Loading + ) + + val navigationCommand = MutableSharedFlow() + + @Suppress("UNUSED_PARAMETER") + private fun onRequestClick(addressType: ReceiveAddressType) = + Toast.makeText(application.applicationContext, "Not implemented yet", Toast.LENGTH_SHORT).show() + + private fun onQrCodeClick(addressType: ReceiveAddressType) = + viewModelScope.launch { + navigationCommand.emit("${NavigationTargets.QR_CODE}/${addressType.ordinal}") + } + + private fun onSettingsClick() = + viewModelScope.launch { + navigationCommand.emit(NavigationTargets.SETTINGS) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/util/FileShareUtil.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/util/FileShareUtil.kt index c7e74769d..752f42462 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/util/FileShareUtil.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/util/FileShareUtil.kt @@ -3,7 +3,6 @@ package co.electriccoin.zcash.ui.util import android.content.Context import android.content.Intent import androidx.core.content.FileProvider -import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.model.VersionInfo import java.io.File @@ -28,10 +27,13 @@ object FileShareUtil { * * @return Intent for launching an app for sharing */ + @Suppress("LongParameterList") internal fun newShareContentIntent( context: Context, dataFilePath: String, fileType: String, + shareText: String? = null, + sharePickerText: String, versionInfo: VersionInfo, ): Intent { val fileUri = @@ -45,13 +47,16 @@ object FileShareUtil { Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_STREAM, fileUri) + if (shareText != null) { + putExtra(Intent.EXTRA_TEXT, shareText) + } type = fileType } val shareDataIntent = Intent.createChooser( dataIntent, - context.getString(R.string.export_data_export_data_chooser_title) + sharePickerText ).apply { addFlags( SHARE_CONTENT_PERMISSION_FLAGS or diff --git a/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_alert_circle.xml b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_alert_circle.xml new file mode 100644 index 000000000..b8cedc536 --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_alert_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_copy.xml b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_copy.xml new file mode 100644 index 000000000..359683eb6 --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_copy.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_share.xml b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_share.xml new file mode 100644 index 000000000..9e271c732 --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_share.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_solid_check.xml b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_solid_check.xml new file mode 100644 index 000000000..df08a40ca --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable-night/ic_solid_check.xml @@ -0,0 +1,10 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable-night/logo_zec_fill_stroke.xml b/ui-lib/src/main/res/ui/qr_code/drawable-night/logo_zec_fill_stroke.xml new file mode 100644 index 000000000..49b66c1d7 --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable-night/logo_zec_fill_stroke.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable/ic_alert_circle.xml b/ui-lib/src/main/res/ui/qr_code/drawable/ic_alert_circle.xml new file mode 100644 index 000000000..d40bfce93 --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable/ic_alert_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable/ic_copy.xml b/ui-lib/src/main/res/ui/qr_code/drawable/ic_copy.xml new file mode 100644 index 000000000..296b671ad --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable/ic_copy.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable/ic_share.xml b/ui-lib/src/main/res/ui/qr_code/drawable/ic_share.xml new file mode 100644 index 000000000..aeb2313fd --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable/ic_share.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable/ic_solid_check.xml b/ui-lib/src/main/res/ui/qr_code/drawable/ic_solid_check.xml new file mode 100644 index 000000000..7d7dc57cf --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable/ic_solid_check.xml @@ -0,0 +1,10 @@ + + + diff --git a/ui-lib/src/main/res/ui/qr_code/drawable/logo_zec_fill_stroke.xml b/ui-lib/src/main/res/ui/qr_code/drawable/logo_zec_fill_stroke.xml new file mode 100644 index 000000000..eee50f2ec --- /dev/null +++ b/ui-lib/src/main/res/ui/qr_code/drawable/logo_zec_fill_stroke.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/ui-lib/src/main/res/ui/qr_code/values/strings.xml b/ui-lib/src/main/res/ui/qr_code/values/strings.xml index f6bfe806e..a97216171 100644 --- a/ui-lib/src/main/res/ui/qr_code/values/strings.xml +++ b/ui-lib/src/main/res/ui/qr_code/values/strings.xml @@ -1,15 +1,19 @@ - Receive - Unified Address QR code - Sapling Address QR code - Transparent Address QR code - Zcash Shielded Address - Zcash Sapling Address - Zcash Transparent Address - Copy - QR Code - Request - Zcash Wallet Address - Unable to find an application to share the QR code with. + Close + Unified Address QR code + Sapling Address QR code + Transparent Address QR code + Zcash Shielded Address + Zcash Sapling Address + Zcash Transparent Address + Maximum Privacy + Low Privacy + Share QR Code + Copy Address + Zcash Wallet Address + Unable to find an application for sharing the QR code. + Share internal Zashi data with: + Hi, scan this QR code to send me a ZEC payment! Download Link: + https://play.google.com/store/apps/details?id=co.electriccoin.zcash