[#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
This commit is contained in:
Honza Rychnovský 2024-10-08 09:46:42 +02:00 committed by GitHub
parent 1fedce1cff
commit c6257d8412
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1295 additions and 154 deletions

View File

@ -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

View File

@ -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

View File

@ -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 = {},

View File

@ -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),
)
}

View File

@ -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() =

View File

@ -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",
)
}

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,12C0,5.373 5.373,0 12,0H28C34.627,0 40,5.373 40,12V28C40,34.627 34.627,40 28,40H12C5.373,40 0,34.627 0,28V12Z"
android:fillColor="#343031"/>
<path
android:pathData="M26,14L14,26M14,14L26,26"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#D2D1D2"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,12C0,5.373 5.373,0 12,0H28C34.627,0 40,5.373 40,12V28C40,34.627 34.627,40 28,40H12C5.373,40 0,34.627 0,28V12Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M26,14L14,26M14,14L26,26"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#4D4941"
android:strokeLineCap="round"/>
</vector>

View File

@ -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",

View File

@ -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.")
}

View File

@ -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)

View File

@ -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,
)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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"
}

View File

@ -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
)
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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(

View File

@ -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<WalletViewModel>()
val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle()
val qrCodeViewModel = koinViewModel<QrCodeViewModel> { 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
)
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.receive.util
package co.electriccoin.zcash.ui.screen.qrcode.util
interface QrCodeGenerator {
/**

View File

@ -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

View File

@ -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

View File

@ -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<Unit>()
val shareResultCommand = MutableSharedFlow<Boolean>()
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<Boolean> =
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()
}
}

View File

@ -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<WalletViewModel>()
val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
val receiveViewModel = koinActivityViewModel<ReceiveViewModel>()
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
)
}

View File

@ -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")
}

View File

@ -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]
}
}

View File

@ -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()
}

View File

@ -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>(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)
)

View File

@ -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<String>()
@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)
}
}

View File

@ -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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="14dp"
android:viewportWidth="15"
android:viewportHeight="14">
<path
android:pathData="M7.5,0.583C3.956,0.583 1.083,3.456 1.083,7C1.083,10.543 3.956,13.416 7.5,13.416C11.044,13.416 13.917,10.543 13.917,7C13.917,3.456 11.044,0.583 7.5,0.583ZM8.083,4.666C8.083,4.344 7.822,4.083 7.5,4.083C7.178,4.083 6.917,4.344 6.917,4.666V7C6.917,7.322 7.178,7.583 7.5,7.583C7.822,7.583 8.083,7.322 8.083,7V4.666ZM7.5,8.75C7.178,8.75 6.917,9.011 6.917,9.333C6.917,9.655 7.178,9.916 7.5,9.916H7.506C7.828,9.916 8.089,9.655 8.089,9.333C8.089,9.011 7.828,8.75 7.506,8.75H7.5Z"
android:fillColor="#F7B27A"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="20dp"
android:viewportWidth="21"
android:viewportHeight="20">
<path
android:pathData="M4.667,12.5C3.89,12.5 3.502,12.5 3.196,12.373C2.787,12.204 2.463,11.88 2.294,11.472C2.167,11.165 2.167,10.777 2.167,10V4.334C2.167,3.4 2.167,2.934 2.348,2.577C2.508,2.263 2.763,2.008 3.077,1.849C3.433,1.667 3.9,1.667 4.833,1.667H10.5C11.277,1.667 11.665,1.667 11.971,1.794C12.38,1.963 12.704,2.287 12.873,2.696C13,3.002 13,3.39 13,4.167M10.667,18.334H16.167C17.1,18.334 17.567,18.334 17.923,18.152C18.237,17.992 18.492,17.737 18.652,17.424C18.833,17.067 18.833,16.6 18.833,15.667V10.167C18.833,9.234 18.833,8.767 18.652,8.41C18.492,8.097 18.237,7.842 17.923,7.682C17.567,7.5 17.1,7.5 16.167,7.5H10.667C9.733,7.5 9.267,7.5 8.91,7.682C8.596,7.842 8.342,8.097 8.182,8.41C8,8.767 8,9.234 8,10.167V15.667C8,16.6 8,17.067 8.182,17.424C8.342,17.737 8.596,17.992 8.91,18.152C9.267,18.334 9.733,18.334 10.667,18.334Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="20dp"
android:viewportWidth="21"
android:viewportHeight="20">
<path
android:pathData="M17.826,10.506C18.03,10.332 18.131,10.245 18.169,10.141C18.201,10.05 18.201,9.95 18.169,9.859C18.131,9.755 18.03,9.668 17.826,9.494L10.767,3.443C10.417,3.143 10.242,2.993 10.094,2.989C9.965,2.986 9.842,3.043 9.76,3.143C9.667,3.258 9.667,3.488 9.667,3.949V7.529C7.888,7.84 6.26,8.741 5.05,10.095C3.731,11.57 3.001,13.48 3,15.459V15.969C3.874,14.916 4.966,14.064 6.201,13.472C7.289,12.95 8.465,12.64 9.667,12.559V16.051C9.667,16.512 9.667,16.742 9.76,16.857C9.842,16.957 9.965,17.014 10.094,17.011C10.242,17.007 10.417,16.857 10.767,16.557L17.826,10.506Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:pathData="M6.833,0.635C6.944,0.619 7.056,0.619 7.167,0.635C7.294,0.653 7.413,0.698 7.507,0.734L7.532,0.743L10.736,1.945C11.097,2.079 11.413,2.198 11.657,2.412C11.87,2.6 12.034,2.837 12.135,3.103C12.251,3.406 12.25,3.744 12.25,4.129L12.25,7C12.25,8.649 11.353,10.024 10.384,11.033C9.408,12.05 8.291,12.768 7.701,13.113L7.677,13.127C7.569,13.19 7.43,13.272 7.245,13.312C7.093,13.344 6.907,13.344 6.755,13.312C6.57,13.272 6.43,13.19 6.323,13.127L6.299,13.113C5.708,12.768 4.592,12.05 3.616,11.033C2.647,10.024 1.75,8.649 1.75,7L1.75,4.129C1.749,3.744 1.749,3.406 1.864,3.103C1.965,2.837 2.13,2.6 2.343,2.412C2.587,2.198 2.903,2.079 3.263,1.945L6.467,0.743L6.493,0.734C6.587,0.698 6.706,0.653 6.833,0.635ZM9.454,5.663C9.682,5.435 9.682,5.065 9.454,4.838C9.226,4.61 8.857,4.61 8.629,4.838L6.417,7.05L5.662,6.296C5.435,6.068 5.065,6.068 4.837,6.296C4.61,6.524 4.61,6.893 4.837,7.121L6.004,8.288C6.232,8.515 6.601,8.515 6.829,8.288L9.454,5.663Z"
android:fillColor="#BDB4FE"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<group>
<clip-path
android:pathData="M32.5,8L32.5,8A24,24 0,0 1,56.5 32L56.5,32A24,24 0,0 1,32.5 56L32.5,56A24,24 0,0 1,8.5 32L8.5,32A24,24 0,0 1,32.5 8z"/>
<path
android:pathData="M32.5,8L32.5,8A24,24 0,0 1,56.5 32L56.5,32A24,24 0,0 1,32.5 56L32.5,56A24,24 0,0 1,8.5 32L8.5,32A24,24 0,0 1,32.5 8z"
android:fillColor="#231F20"/>
<path
android:pathData="M8.5,32C8.5,18.764 19.264,8 32.5,8C45.736,8 56.5,18.764 56.5,32C56.5,45.236 45.736,56 32.5,56C19.264,56 8.5,45.236 8.5,32ZM41.061,20.862V24.515L30.903,38.292H41.061V43.137H34.512V47.151H30.488V43.137H23.939V39.484L34.087,25.707H23.939V20.862H30.488V16.837H34.512V20.862H41.061Z"
android:fillColor="#E8E8E8"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M32.5,4L32.5,4A28,28 0,0 1,60.5 32L60.5,32A28,28 0,0 1,32.5 60L32.5,60A28,28 0,0 1,4.5 32L4.5,32A28,28 0,0 1,32.5 4z"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="14dp"
android:viewportWidth="15"
android:viewportHeight="14">
<path
android:pathData="M7.5,0.583C3.956,0.583 1.083,3.456 1.083,7C1.083,10.543 3.956,13.416 7.5,13.416C11.044,13.416 13.917,10.543 13.917,7C13.917,3.456 11.044,0.583 7.5,0.583ZM8.083,4.666C8.083,4.344 7.822,4.083 7.5,4.083C7.178,4.083 6.917,4.344 6.917,4.666V7C6.917,7.322 7.178,7.583 7.5,7.583C7.822,7.583 8.083,7.322 8.083,7V4.666ZM7.5,8.75C7.178,8.75 6.917,9.011 6.917,9.333C6.917,9.655 7.178,9.916 7.5,9.916H7.506C7.828,9.916 8.089,9.655 8.089,9.333C8.089,9.011 7.828,8.75 7.506,8.75H7.5Z"
android:fillColor="#B93815"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="20dp"
android:viewportWidth="21"
android:viewportHeight="20">
<path
android:pathData="M4.667,12.5C3.89,12.5 3.502,12.5 3.196,12.373C2.787,12.204 2.463,11.88 2.294,11.472C2.167,11.165 2.167,10.777 2.167,10V4.334C2.167,3.4 2.167,2.934 2.348,2.577C2.508,2.263 2.763,2.008 3.077,1.849C3.433,1.667 3.9,1.667 4.833,1.667H10.5C11.277,1.667 11.665,1.667 11.971,1.794C12.38,1.963 12.704,2.287 12.873,2.696C13,3.002 13,3.39 13,4.167M10.667,18.334H16.167C17.1,18.334 17.567,18.334 17.923,18.152C18.237,17.992 18.492,17.737 18.652,17.424C18.833,17.067 18.833,16.6 18.833,15.667V10.167C18.833,9.234 18.833,8.767 18.652,8.41C18.492,8.097 18.237,7.842 17.923,7.682C17.567,7.5 17.1,7.5 16.167,7.5H10.667C9.733,7.5 9.267,7.5 8.91,7.682C8.596,7.842 8.341,8.097 8.182,8.41C8,8.767 8,9.234 8,10.167V15.667C8,16.6 8,17.067 8.182,17.424C8.341,17.737 8.596,17.992 8.91,18.152C9.267,18.334 9.733,18.334 10.667,18.334Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="20dp"
android:viewportWidth="21"
android:viewportHeight="20">
<path
android:pathData="M17.826,10.506C18.03,10.332 18.131,10.245 18.169,10.141C18.201,10.05 18.201,9.95 18.169,9.859C18.131,9.755 18.03,9.668 17.826,9.494L10.767,3.443C10.417,3.143 10.242,2.993 10.094,2.989C9.965,2.986 9.842,3.043 9.76,3.143C9.667,3.258 9.667,3.488 9.667,3.949V7.529C7.888,7.84 6.26,8.741 5.05,10.095C3.731,11.57 3.001,13.48 3,15.459V15.969C3.874,14.916 4.966,14.064 6.201,13.472C7.289,12.95 8.465,12.64 9.667,12.559V16.051C9.667,16.512 9.667,16.742 9.76,16.857C9.842,16.957 9.965,17.014 10.094,17.011C10.242,17.007 10.417,16.857 10.767,16.557L17.826,10.506Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="14dp"
android:viewportWidth="12"
android:viewportHeight="14">
<path
android:pathData="M5.833,0.635C5.944,0.619 6.056,0.619 6.167,0.635C6.294,0.653 6.413,0.698 6.507,0.734L6.533,0.743L9.736,1.945C10.097,2.079 10.413,2.198 10.657,2.412C10.87,2.6 11.035,2.837 11.136,3.103C11.251,3.406 11.251,3.744 11.25,4.129L11.25,7C11.25,8.649 10.353,10.024 9.384,11.033C8.408,12.05 7.292,12.768 6.701,13.113L6.677,13.127C6.569,13.19 6.43,13.272 6.245,13.312C6.093,13.344 5.907,13.344 5.755,13.312C5.57,13.272 5.431,13.19 5.323,13.127L5.299,13.113C4.709,12.768 3.592,12.05 2.616,11.033C1.647,10.024 0.75,8.649 0.75,7L0.75,4.129C0.749,3.744 0.749,3.406 0.864,3.103C0.966,2.837 1.13,2.6 1.343,2.412C1.587,2.198 1.903,2.079 2.264,1.945L5.468,0.743L5.493,0.734C5.587,0.698 5.706,0.653 5.833,0.635ZM8.454,5.663C8.682,5.435 8.682,5.065 8.454,4.838C8.226,4.61 7.857,4.61 7.629,4.838L5.417,7.05L4.662,6.296C4.435,6.068 4.065,6.068 3.838,6.296C3.61,6.524 3.61,6.893 3.838,7.121L5.004,8.288C5.232,8.515 5.601,8.515 5.829,8.288L8.454,5.663Z"
android:fillColor="#5925DC"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<group>
<clip-path
android:pathData="M32.5,8L32.5,8A24,24 0,0 1,56.5 32L56.5,32A24,24 0,0 1,32.5 56L32.5,56A24,24 0,0 1,8.5 32L8.5,32A24,24 0,0 1,32.5 8z"/>
<path
android:pathData="M32.5,8L32.5,8A24,24 0,0 1,56.5 32L56.5,32A24,24 0,0 1,32.5 56L32.5,56A24,24 0,0 1,8.5 32L8.5,32A24,24 0,0 1,32.5 8z"
android:fillColor="#ffffff"/>
<path
android:pathData="M8.5,32C8.5,18.764 19.264,8 32.5,8C45.736,8 56.5,18.764 56.5,32C56.5,45.236 45.736,56 32.5,56C19.264,56 8.5,45.236 8.5,32ZM41.061,20.862V24.515L30.903,38.292H41.061V43.137H34.512V47.151H30.488V43.137H23.939V39.484L34.087,25.707H23.939V20.862H30.488V16.837H34.512V20.862H41.061Z"
android:fillColor="#231F20"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M32.5,4L32.5,4A28,28 0,0 1,60.5 32L60.5,32A28,28 0,0 1,32.5 60L32.5,60A28,28 0,0 1,4.5 32L4.5,32A28,28 0,0 1,32.5 4z"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
</vector>

View File

@ -1,15 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="receive_title">Receive</string>
<string name="receive_unified_content_description">Unified Address QR code</string>
<string name="receive_sapling_content_description">Sapling Address QR code</string>
<string name="receive_transparent_content_description">Transparent Address QR code</string>
<string name="receive_wallet_address_shielded">Zcash Shielded Address</string>
<string name="receive_wallet_address_sapling">Zcash Sapling Address</string>
<string name="receive_wallet_address_transparent">Zcash Transparent Address</string>
<string name="receive_copy">Copy</string>
<string name="receive_qr_code">QR Code</string>
<string name="receive_request">Request</string>
<string name="receive_clipboard_tag">Zcash Wallet Address</string>
<string name="qr_code_data_unable_to_share">Unable to find an application to share the QR code with.</string>
<string name="qr_code_close_content_description">Close</string>
<string name="qr_code_unified_content_description">Unified Address QR code</string>
<string name="qr_code_sapling_content_description">Sapling Address QR code</string>
<string name="qr_code_transparent_content_description">Transparent Address QR code</string>
<string name="qr_code_wallet_address_shielded">Zcash Shielded Address</string>
<string name="qr_code_wallet_address_sapling">Zcash Sapling Address</string>
<string name="qr_code_wallet_address_transparent">Zcash Transparent Address</string>
<string name="qr_code_privacy_level_shielded">Maximum Privacy</string>
<string name="qr_code_privacy_level_transparent">Low Privacy</string>
<string name="qr_code_share_btn">Share QR Code</string>
<string name="qr_code_copy_btn">Copy Address</string>
<string name="qr_code_clipboard_tag">Zcash Wallet Address</string>
<string name="qr_code_data_unable_to_share">Unable to find an application for sharing the QR code.</string>
<string name="qr_code_share_chooser_title">Share internal Zashi data with:</string>
<string name="qr_code_share_chooser_text">Hi, scan this QR code to send me a ZEC payment! Download Link:
https://play.google.com/store/apps/details?id=co.electriccoin.zcash</string>
</resources>