diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8a79b0b..26398e6f 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 01510c9d..c4c4840f 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 330de8dc..739c43bf 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 5ba93847..75d2a9fd 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 3f651825..f47a9f44 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 c05ff43a..ced27319 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 00000000..035d438f
--- /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 00000000..2b8663e7
--- /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 d785e1c8..29b7f1b0 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 e9162484..c6995dce 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 0c4c5858..b14bb83b 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 bdda81b1..6762e180 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 1c0a5441..b3ef4d00 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 b4a4e4b1..821c5177 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 3a9c895c..92424067 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 00000000..27952d1d
--- /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 00000000..dedc2ea2
--- /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 34f764a3..64974e6a 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 eb41d41f..d0c75e00 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 00000000..587634a2
--- /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 00000000..f39719c1
--- /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 00000000..6280fdea
--- /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 312eceb8..3deb1930 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 0a99af9c..d410e9ce 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 b6e4d39d..36b89289 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 292eeced..0d26704f 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 00000000..8a471252
--- /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 00000000..cf86d089
--- /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 5cb682df..7faa8f4e 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)
+ LaunchedEffect(Unit) {
+ receiveViewModel.navigationCommand.collect {
+ navController.navigate(it)
+ }
+ }
- 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,
+ 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 00000000..62c23ed4
--- /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 00000000..a2815c9b
--- /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 00000000..fae9e720
--- /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 530b9c0d..1bf4812b 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
+@Composable
+@PreviewScreens
+private fun ReceiveLoadingPreview() =
+ ZcashTheme(forceDarkMode = true) {
+ ReceiveView(
+ state = ReceiveState.Loading,
+ snackbarHostState = SnackbarHostState(),
+ topAppBarSubTitleState = TopAppBarSubTitleState.None,
+ )
+ }
+
@Preview
@Composable
private fun ReceivePreview() =
ZcashTheme(forceDarkMode = false) {
- Receive(
- walletAddresses = runBlocking { WalletAddressesFixture.new() },
+ 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,
)
}
-@Preview
@Composable
-private fun ReceiveDarkPreview() =
- ZcashTheme(forceDarkMode = true) {
- Receive(
- walletAddresses = runBlocking { WalletAddressesFixture.new() },
- 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 00000000..184a68a8
--- /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 c7e74769..752f4246 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 00000000..b8cedc53
--- /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 00000000..359683eb
--- /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 00000000..9e271c73
--- /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 00000000..df08a40c
--- /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 00000000..49b66c1d
--- /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 00000000..d40bfce9
--- /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 00000000..296b671a
--- /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 00000000..aeb2313f
--- /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 00000000..7d7dc57c
--- /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 00000000..eee50f2e
--- /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 f6bfe806..a9721617 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