[#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:
parent
1fedce1cff
commit
c6257d8412
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {},
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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() =
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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",
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.screen.receive.util
|
||||
package co.electriccoin.zcash.ui.screen.qrcode.util
|
||||
|
||||
interface QrCodeGenerator {
|
||||
/**
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue