784 lines
28 KiB
Kotlin
784 lines
28 KiB
Kotlin
@file:Suppress("TooManyFunctions")
|
|
|
|
package co.electriccoin.zcash.ui.screen.send.view
|
|
|
|
import androidx.compose.animation.animateContentSize
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
import androidx.compose.foundation.ScrollState
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxHeight
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.text.KeyboardActions
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.mutableIntStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.focus.FocusDirection
|
|
import androidx.compose.ui.focus.FocusManager
|
|
import androidx.compose.ui.layout.onGloballyPositioned
|
|
import androidx.compose.ui.layout.positionInRoot
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.platform.LocalFocusManager
|
|
import androidx.compose.ui.platform.testTag
|
|
import androidx.compose.ui.res.painterResource
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.font.FontStyle
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.input.ImeAction
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.tooling.preview.Preview
|
|
import cash.z.ecc.android.sdk.model.Memo
|
|
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
|
import cash.z.ecc.android.sdk.model.ZecSend
|
|
import cash.z.ecc.android.sdk.model.ZecSendExt
|
|
import cash.z.ecc.android.sdk.model.toZecString
|
|
import cash.z.ecc.android.sdk.type.AddressType
|
|
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
|
import cash.z.ecc.sdk.type.ZcashCurrency
|
|
import co.electriccoin.zcash.spackle.Twig
|
|
import co.electriccoin.zcash.ui.R
|
|
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
|
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
|
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
|
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
|
import co.electriccoin.zcash.ui.common.model.canSpend
|
|
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
|
import co.electriccoin.zcash.ui.common.test.CommonTag
|
|
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
|
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
|
import co.electriccoin.zcash.ui.design.component.Body
|
|
import co.electriccoin.zcash.ui.design.component.BodySmall
|
|
import co.electriccoin.zcash.ui.design.component.FormTextField
|
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
|
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
|
import co.electriccoin.zcash.ui.design.component.Small
|
|
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
|
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
|
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
|
import co.electriccoin.zcash.ui.screen.send.SendTag
|
|
import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
|
import co.electriccoin.zcash.ui.screen.send.model.MemoState
|
|
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
|
|
import co.electriccoin.zcash.ui.screen.send.model.SendStage
|
|
import kotlinx.coroutines.launch
|
|
import java.util.Locale
|
|
|
|
@Composable
|
|
@Preview("SendForm")
|
|
private fun PreviewSendForm() {
|
|
ZcashTheme(forceDarkMode = false) {
|
|
GradientSurface {
|
|
Send(
|
|
sendStage = SendStage.Form,
|
|
onCreateZecSend = {},
|
|
focusManager = LocalFocusManager.current,
|
|
onBack = {},
|
|
onSettings = {},
|
|
onQrScannerOpen = {},
|
|
goBalances = {},
|
|
hasCameraFeature = true,
|
|
recipientAddressState = RecipientAddressState("invalid_address", AddressType.Invalid()),
|
|
onRecipientAddressChange = {},
|
|
setAmountState = {},
|
|
amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()),
|
|
setMemoState = {},
|
|
memoState = MemoState.new("Test message"),
|
|
walletRestoringState = WalletRestoringState.NONE,
|
|
walletSnapshot = WalletSnapshotFixture.new(),
|
|
balanceState = BalanceStateFixture.new()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO [#1260]: Cover Send screens UI with tests
|
|
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
|
|
|
@Suppress("LongParameterList")
|
|
@Composable
|
|
fun Send(
|
|
balanceState: BalanceState,
|
|
sendStage: SendStage,
|
|
onCreateZecSend: (ZecSend) -> Unit,
|
|
focusManager: FocusManager,
|
|
onBack: () -> Unit,
|
|
onSettings: () -> Unit,
|
|
onQrScannerOpen: () -> Unit,
|
|
goBalances: () -> Unit,
|
|
hasCameraFeature: Boolean,
|
|
recipientAddressState: RecipientAddressState,
|
|
onRecipientAddressChange: (String) -> Unit,
|
|
setAmountState: (AmountState) -> Unit,
|
|
amountState: AmountState,
|
|
setMemoState: (MemoState) -> Unit,
|
|
memoState: MemoState,
|
|
walletRestoringState: WalletRestoringState,
|
|
walletSnapshot: WalletSnapshot,
|
|
) {
|
|
Scaffold(topBar = {
|
|
SendTopAppBar(
|
|
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
|
|
onSettings = onSettings
|
|
)
|
|
}) { paddingValues ->
|
|
SendMainContent(
|
|
balanceState = balanceState,
|
|
walletSnapshot = walletSnapshot,
|
|
onBack = onBack,
|
|
focusManager = focusManager,
|
|
sendStage = sendStage,
|
|
onCreateZecSend = onCreateZecSend,
|
|
recipientAddressState = recipientAddressState,
|
|
onRecipientAddressChange = onRecipientAddressChange,
|
|
amountState = amountState,
|
|
setAmountState = setAmountState,
|
|
memoState = memoState,
|
|
setMemoState = setMemoState,
|
|
onQrScannerOpen = onQrScannerOpen,
|
|
goBalances = goBalances,
|
|
hasCameraFeature = hasCameraFeature,
|
|
modifier =
|
|
Modifier
|
|
.padding(
|
|
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
|
|
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingHuge,
|
|
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
|
|
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun SendTopAppBar(
|
|
onSettings: () -> Unit,
|
|
showRestoring: Boolean
|
|
) {
|
|
SmallTopAppBar(
|
|
restoringLabel =
|
|
if (showRestoring) {
|
|
stringResource(id = R.string.restoring_wallet_label)
|
|
} else {
|
|
null
|
|
},
|
|
titleText = stringResource(id = R.string.send_stage_send_title),
|
|
hamburgerMenuActions = {
|
|
IconButton(
|
|
onClick = onSettings,
|
|
modifier = Modifier.testTag(CommonTag.SETTINGS_TOP_BAR_BUTTON)
|
|
) {
|
|
Icon(
|
|
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon),
|
|
contentDescription = stringResource(id = R.string.settings_menu_content_description)
|
|
)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
@Suppress("LongParameterList")
|
|
@Composable
|
|
private fun SendMainContent(
|
|
balanceState: BalanceState,
|
|
walletSnapshot: WalletSnapshot,
|
|
focusManager: FocusManager,
|
|
onBack: () -> Unit,
|
|
goBalances: () -> Unit,
|
|
onCreateZecSend: (ZecSend) -> Unit,
|
|
sendStage: SendStage,
|
|
onQrScannerOpen: () -> Unit,
|
|
recipientAddressState: RecipientAddressState,
|
|
onRecipientAddressChange: (String) -> Unit,
|
|
hasCameraFeature: Boolean,
|
|
amountState: AmountState,
|
|
setAmountState: (AmountState) -> Unit,
|
|
memoState: MemoState,
|
|
setMemoState: (MemoState) -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
// For now, we merge [SendStage.Form] and [SendStage.Proposing] into one stage. We could eventually display a
|
|
// loader if calling the Proposal API takes longer than expected
|
|
|
|
SendForm(
|
|
balanceState = balanceState,
|
|
walletSnapshot = walletSnapshot,
|
|
recipientAddressState = recipientAddressState,
|
|
onRecipientAddressChange = onRecipientAddressChange,
|
|
amountState = amountState,
|
|
setAmountState = setAmountState,
|
|
memoState = memoState,
|
|
setMemoState = setMemoState,
|
|
onCreateZecSend = onCreateZecSend,
|
|
focusManager = focusManager,
|
|
onQrScannerOpen = onQrScannerOpen,
|
|
goBalances = goBalances,
|
|
hasCameraFeature = hasCameraFeature,
|
|
modifier = modifier
|
|
)
|
|
|
|
if (sendStage is SendStage.SendFailure) {
|
|
SendFailure(
|
|
reason = sendStage.error,
|
|
onDone = onBack
|
|
)
|
|
}
|
|
}
|
|
|
|
const val DEFAULT_LESS_THAN_FEE = 100_000L
|
|
|
|
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
|
|
// TODO [#217]: https://github.com/Electric-Coin-Company/zashi-android/issues/217
|
|
|
|
// TODO [#1257]: Send.Form TextFields not persisted on a configuration change when the underlying ViewPager is on the
|
|
// Balances page
|
|
// TODO [#1257]: https://github.com/Electric-Coin-Company/zashi-android/issues/1257
|
|
@Suppress("LongMethod", "LongParameterList")
|
|
@Composable
|
|
private fun SendForm(
|
|
balanceState: BalanceState,
|
|
walletSnapshot: WalletSnapshot,
|
|
focusManager: FocusManager,
|
|
recipientAddressState: RecipientAddressState,
|
|
onRecipientAddressChange: (String) -> Unit,
|
|
amountState: AmountState,
|
|
setAmountState: (AmountState) -> Unit,
|
|
memoState: MemoState,
|
|
setMemoState: (MemoState) -> Unit,
|
|
onCreateZecSend: (ZecSend) -> Unit,
|
|
onQrScannerOpen: () -> Unit,
|
|
goBalances: () -> Unit,
|
|
hasCameraFeature: Boolean,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
// TODO [#1171]: Remove default MonetarySeparators locale
|
|
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
|
val monetarySeparators = MonetarySeparators.current(Locale.US)
|
|
|
|
val scrollState = rememberScrollState()
|
|
|
|
val (scrollToFeePixels, setScrollToFeePixels) = rememberSaveable { mutableIntStateOf(0) }
|
|
|
|
Column(
|
|
modifier =
|
|
Modifier
|
|
.fillMaxHeight()
|
|
.verticalScroll(scrollState)
|
|
.then(modifier),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
|
|
|
BalanceWidget(
|
|
balanceState = balanceState,
|
|
isReferenceToBalances = true,
|
|
onReferenceClick = goBalances
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
|
|
|
|
// TODO [#1256]: Consider Send.Form TextFields scrolling
|
|
// TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256
|
|
|
|
SendFormAddressTextField(
|
|
focusManager = focusManager,
|
|
hasCameraFeature = hasCameraFeature,
|
|
onQrScannerOpen = onQrScannerOpen,
|
|
recipientAddressState = recipientAddressState,
|
|
setRecipientAddress = onRecipientAddressChange
|
|
)
|
|
|
|
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
|
|
|
|
SendFormAmountTextField(
|
|
amountSate = amountState,
|
|
focusManager = focusManager,
|
|
imeAction =
|
|
if (recipientAddressState.type == AddressType.Transparent) {
|
|
ImeAction.Done
|
|
} else {
|
|
ImeAction.Next
|
|
},
|
|
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
|
|
monetarySeparators = monetarySeparators,
|
|
setAmountState = setAmountState,
|
|
walletSnapshot = walletSnapshot,
|
|
)
|
|
|
|
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
|
|
|
|
SendFormMemoTextField(
|
|
memoState = memoState,
|
|
setMemoState = setMemoState,
|
|
focusManager = focusManager,
|
|
isMemoFieldAvailable = (
|
|
recipientAddressState.address.isEmpty() ||
|
|
recipientAddressState.type is AddressType.Invalid ||
|
|
(
|
|
recipientAddressState.type is AddressType.Valid &&
|
|
recipientAddressState.type !is AddressType.Transparent
|
|
)
|
|
),
|
|
scrollState = scrollState,
|
|
scrollTo = scrollToFeePixels
|
|
)
|
|
|
|
Spacer(
|
|
modifier =
|
|
Modifier
|
|
.fillMaxHeight()
|
|
.weight(MINIMAL_WEIGHT)
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
|
|
|
SendButton(
|
|
amountState = amountState,
|
|
memoState = memoState,
|
|
monetarySeparators = monetarySeparators,
|
|
onCreateZecSend = onCreateZecSend,
|
|
recipientAddressState = recipientAddressState,
|
|
walletSnapshot = walletSnapshot,
|
|
setScrollToFeePixels = setScrollToFeePixels
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
@Suppress("LongParameterList")
|
|
fun SendButton(
|
|
amountState: AmountState,
|
|
memoState: MemoState,
|
|
monetarySeparators: MonetarySeparators,
|
|
onCreateZecSend: (ZecSend) -> Unit,
|
|
recipientAddressState: RecipientAddressState,
|
|
setScrollToFeePixels: (Int) -> Unit,
|
|
walletSnapshot: WalletSnapshot,
|
|
) {
|
|
val context = LocalContext.current
|
|
|
|
// Common conditions continuously checked for validity
|
|
val sendButtonEnabled =
|
|
recipientAddressState.type !is AddressType.Invalid &&
|
|
recipientAddressState.address.isNotEmpty() &&
|
|
amountState is AmountState.Valid &&
|
|
amountState.value.isNotBlank() &&
|
|
walletSnapshot.canSpend(amountState.zatoshi) &&
|
|
// A valid memo is necessary only for non-transparent recipient
|
|
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
|
|
|
|
Column(
|
|
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
PrimaryButton(
|
|
onClick = {
|
|
// SDK side validations
|
|
val zecSendValidation =
|
|
ZecSendExt.new(
|
|
context = context,
|
|
destinationString = recipientAddressState.address,
|
|
zecString = amountState.value,
|
|
// Take memo for a valid non-transparent receiver only
|
|
memoString =
|
|
if (recipientAddressState.type == AddressType.Transparent) {
|
|
""
|
|
} else {
|
|
memoState.text
|
|
},
|
|
monetarySeparators = monetarySeparators
|
|
)
|
|
|
|
when (zecSendValidation) {
|
|
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
|
|
is ZecSendExt.ZecSendValidation.Invalid -> {
|
|
// We do not expect this validation to fail, so logging is enough here
|
|
// An error popup could be reasonable here as well
|
|
Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" }
|
|
}
|
|
}
|
|
},
|
|
text = stringResource(id = R.string.send_create),
|
|
enabled = sendButtonEnabled,
|
|
modifier =
|
|
Modifier
|
|
.testTag(SendTag.SEND_FORM_BUTTON)
|
|
.fillMaxWidth()
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
|
|
|
BodySmall(
|
|
text =
|
|
stringResource(
|
|
id = R.string.send_fee,
|
|
// TODO [#1047]: Representing Zatoshi amount
|
|
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
|
|
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
|
|
),
|
|
textFontWeight = FontWeight.SemiBold,
|
|
modifier =
|
|
Modifier.onGloballyPositioned {
|
|
setScrollToFeePixels(it.positionInRoot().y.toInt())
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Suppress("LongMethod")
|
|
@Composable
|
|
fun SendFormAddressTextField(
|
|
focusManager: FocusManager,
|
|
hasCameraFeature: Boolean,
|
|
onQrScannerOpen: () -> Unit,
|
|
recipientAddressState: RecipientAddressState,
|
|
setRecipientAddress: (String) -> Unit,
|
|
) {
|
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
|
|
|
Column(
|
|
modifier =
|
|
Modifier
|
|
// Animate error show/hide
|
|
.animateContentSize()
|
|
// Scroll TextField above ime keyboard
|
|
.bringIntoViewRequester(bringIntoViewRequester)
|
|
) {
|
|
Small(text = stringResource(id = R.string.send_address_label))
|
|
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
|
|
|
val recipientAddressValue = recipientAddressState.address
|
|
val recipientAddressError =
|
|
if (
|
|
recipientAddressValue.isNotEmpty() &&
|
|
recipientAddressState.type is AddressType.Invalid
|
|
) {
|
|
stringResource(id = R.string.send_address_invalid)
|
|
} else {
|
|
null
|
|
}
|
|
|
|
FormTextField(
|
|
value = recipientAddressValue,
|
|
onValueChange = {
|
|
setRecipientAddress(it)
|
|
},
|
|
modifier =
|
|
Modifier
|
|
.fillMaxWidth(),
|
|
error = recipientAddressError,
|
|
placeholder = {
|
|
Text(
|
|
text = stringResource(id = R.string.send_address_hint),
|
|
style = ZcashTheme.extendedTypography.textFieldHint,
|
|
color = ZcashTheme.colors.textFieldHint
|
|
)
|
|
},
|
|
trailingIcon =
|
|
if (hasCameraFeature) {
|
|
{
|
|
IconButton(
|
|
onClick = onQrScannerOpen,
|
|
content = {
|
|
Icon(
|
|
painter = painterResource(id = R.drawable.qr_code_icon),
|
|
contentDescription = stringResource(R.string.send_scan_content_description)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
} else {
|
|
null
|
|
},
|
|
keyboardOptions =
|
|
KeyboardOptions(
|
|
keyboardType = KeyboardType.Text,
|
|
imeAction = ImeAction.Next
|
|
),
|
|
keyboardActions =
|
|
KeyboardActions(
|
|
onNext = {
|
|
focusManager.moveFocus(FocusDirection.Down)
|
|
}
|
|
),
|
|
bringIntoViewRequester = bringIntoViewRequester,
|
|
)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Suppress("LongParameterList", "LongMethod")
|
|
@Composable
|
|
fun SendFormAmountTextField(
|
|
amountSate: AmountState,
|
|
focusManager: FocusManager,
|
|
imeAction: ImeAction,
|
|
isTransparentRecipient: Boolean,
|
|
monetarySeparators: MonetarySeparators,
|
|
setAmountState: (AmountState) -> Unit,
|
|
walletSnapshot: WalletSnapshot,
|
|
) {
|
|
val context = LocalContext.current
|
|
|
|
val zcashCurrency = ZcashCurrency.getLocalizedName(context)
|
|
|
|
val amountError =
|
|
when (amountSate) {
|
|
is AmountState.Invalid -> {
|
|
if (amountSate.value.isEmpty()) {
|
|
null
|
|
} else {
|
|
stringResource(id = R.string.send_amount_invalid)
|
|
}
|
|
}
|
|
is AmountState.Valid -> {
|
|
if (walletSnapshot.spendableBalance() < amountSate.zatoshi) {
|
|
stringResource(id = R.string.send_amount_insufficient_balance)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
}
|
|
|
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
|
|
|
Column(
|
|
modifier =
|
|
Modifier
|
|
// Animate error show/hide
|
|
.animateContentSize()
|
|
// Scroll TextField above ime keyboard
|
|
.bringIntoViewRequester(bringIntoViewRequester)
|
|
) {
|
|
Small(text = stringResource(id = R.string.send_amount_label))
|
|
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
|
|
|
FormTextField(
|
|
value = amountSate.value,
|
|
onValueChange = { newValue ->
|
|
setAmountState(
|
|
AmountState.new(
|
|
context = context,
|
|
value = newValue,
|
|
monetarySeparators = monetarySeparators,
|
|
isTransparentRecipient = isTransparentRecipient
|
|
)
|
|
)
|
|
},
|
|
modifier = Modifier.fillMaxWidth(),
|
|
error = amountError,
|
|
placeholder = {
|
|
Text(
|
|
text =
|
|
stringResource(
|
|
id = R.string.send_amount_hint,
|
|
zcashCurrency
|
|
),
|
|
style = ZcashTheme.extendedTypography.textFieldHint,
|
|
color = ZcashTheme.colors.textFieldHint
|
|
)
|
|
},
|
|
keyboardOptions =
|
|
KeyboardOptions(
|
|
keyboardType = KeyboardType.Number,
|
|
imeAction = imeAction
|
|
),
|
|
keyboardActions =
|
|
KeyboardActions(
|
|
onDone = {
|
|
focusManager.clearFocus(true)
|
|
},
|
|
onNext = {
|
|
focusManager.moveFocus(FocusDirection.Down)
|
|
}
|
|
),
|
|
bringIntoViewRequester = bringIntoViewRequester,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TODO [#1259]: Send.Form screen Memo field stroke bubble style
|
|
// TODO [#1259]: https://github.com/Electric-Coin-Company/zashi-android/issues/1259
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Suppress("LongMethod", "LongParameterList")
|
|
@Composable
|
|
fun SendFormMemoTextField(
|
|
focusManager: FocusManager,
|
|
isMemoFieldAvailable: Boolean,
|
|
memoState: MemoState,
|
|
setMemoState: (MemoState) -> Unit,
|
|
scrollState: ScrollState,
|
|
scrollTo: Int
|
|
) {
|
|
val scope = rememberCoroutineScope()
|
|
|
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
|
|
|
Column(
|
|
modifier =
|
|
Modifier
|
|
// Animate error show/hide
|
|
.animateContentSize()
|
|
// Scroll TextField above ime keyboard
|
|
.bringIntoViewRequester(bringIntoViewRequester)
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(
|
|
painter = painterResource(id = R.drawable.send_paper_plane),
|
|
contentDescription = null,
|
|
tint =
|
|
if (isMemoFieldAvailable) {
|
|
ZcashTheme.colors.textCommon
|
|
} else {
|
|
ZcashTheme.colors.textDisabled
|
|
}
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
|
|
|
|
Small(
|
|
text = stringResource(id = R.string.send_memo_label),
|
|
color =
|
|
if (isMemoFieldAvailable) {
|
|
ZcashTheme.colors.textCommon
|
|
} else {
|
|
ZcashTheme.colors.textDisabled
|
|
}
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
|
|
|
FormTextField(
|
|
enabled = isMemoFieldAvailable,
|
|
value =
|
|
if (isMemoFieldAvailable) {
|
|
memoState.text
|
|
} else {
|
|
""
|
|
},
|
|
onValueChange = {
|
|
setMemoState(MemoState.new(it))
|
|
},
|
|
bringIntoViewRequester = bringIntoViewRequester,
|
|
keyboardOptions =
|
|
KeyboardOptions(
|
|
keyboardType = KeyboardType.Text,
|
|
imeAction = ImeAction.Done
|
|
),
|
|
keyboardActions =
|
|
KeyboardActions(
|
|
onDone = {
|
|
focusManager.clearFocus(true)
|
|
// Scroll down to make sure the Send button is visible on small screens
|
|
if (scrollTo > 0) {
|
|
scope.launch {
|
|
scrollState.animateScrollTo(scrollTo)
|
|
}
|
|
}
|
|
}
|
|
),
|
|
placeholder = {
|
|
Text(
|
|
text = stringResource(id = R.string.send_memo_hint),
|
|
style = ZcashTheme.extendedTypography.textFieldHint,
|
|
color = ZcashTheme.colors.textFieldHint
|
|
)
|
|
},
|
|
modifier = Modifier.fillMaxWidth(),
|
|
minHeight = ZcashTheme.dimens.textFieldMemoPanelDefaultHeight,
|
|
)
|
|
|
|
if (isMemoFieldAvailable) {
|
|
Body(
|
|
text =
|
|
stringResource(
|
|
id = R.string.send_memo_bytes_counter,
|
|
Memo.MAX_MEMO_LENGTH_BYTES - memoState.byteSize,
|
|
Memo.MAX_MEMO_LENGTH_BYTES
|
|
),
|
|
textFontWeight = FontWeight.Bold,
|
|
color =
|
|
if (memoState is MemoState.Correct) {
|
|
ZcashTheme.colors.textFieldHint
|
|
} else {
|
|
ZcashTheme.colors.textFieldError
|
|
},
|
|
textAlign = TextAlign.End,
|
|
modifier =
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = ZcashTheme.dimens.spacingTiny)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
@Preview("SendFailure")
|
|
private fun PreviewSendFailure() {
|
|
ZcashTheme(forceDarkMode = false) {
|
|
GradientSurface {
|
|
SendFailure(
|
|
onDone = {},
|
|
reason = "Insufficient balance"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun SendFailure(
|
|
onDone: () -> Unit,
|
|
reason: String,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
// TODO [#1276]: Once we ensure that the reason contains a localized message, we can leverage it for the UI prompt
|
|
// TODO [#1276]: Consider adding support for a specific exception in AppAlertDialog
|
|
// TODO [#1276]: https://github.com/Electric-Coin-Company/zashi-android/issues/1276
|
|
|
|
AppAlertDialog(
|
|
title = stringResource(id = R.string.send_dialog_error_title),
|
|
text = {
|
|
Column(
|
|
Modifier.verticalScroll(rememberScrollState())
|
|
) {
|
|
Text(text = stringResource(id = R.string.send_dialog_error_text))
|
|
|
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
|
|
|
Text(
|
|
text = reason,
|
|
fontStyle = FontStyle.Italic
|
|
)
|
|
}
|
|
},
|
|
confirmButtonText = stringResource(id = R.string.send_dialog_error_btn),
|
|
onConfirmButtonClick = onDone,
|
|
modifier = modifier
|
|
)
|
|
}
|