Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -592,10 +592,10 @@ private fun OnChainDetails(
stringResource(R.string.wallet__savings__title)
},
color = if (uiState.hardwareWalletId != null) Colors.Blue else Colors.Brand,
enabled = uiState.canSwitchBitcoinFundingSource && interactionsEnabled,
enabled = uiState.canSwitchConfirmFundingSource && interactionsEnabled,
isLoading = uiState.isFundingSourceLoading,
clickable = interactionsEnabled && !uiState.isRetainedHardwarePayment,
icon = R.drawable.ic_transfer.takeIf { uiState.canSwitchBitcoinFundingSource },
icon = R.drawable.ic_transfer.takeIf { uiState.canSwitchConfirmFundingSource },
onClick = { onEvent(SendEvent.PaymentMethodSwitch) },
modifier = Modifier.testTag("SendConfirmAssetButton")
)
Expand Down Expand Up @@ -739,9 +739,9 @@ private fun LightningDetails(
NumberPadActionButton(
text = stringResource(R.string.wallet__spending__title),
color = Colors.Purple,
enabled = uiState.canSwitchBitcoinFundingSource && interactionsEnabled,
enabled = uiState.canSwitchConfirmFundingSource && interactionsEnabled,
isLoading = uiState.isFundingSourceLoading,
icon = R.drawable.ic_transfer.takeIf { uiState.canSwitchBitcoinFundingSource },
icon = R.drawable.ic_transfer.takeIf { uiState.canSwitchConfirmFundingSource },
onClick = { onEvent(SendEvent.PaymentMethodSwitch) },
modifier = Modifier.testTag("SendConfirmAssetButton")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import to.bitkit.ui.scaffold.AppAlertDialog
import to.bitkit.ui.shared.modifiers.sheetHeight
import to.bitkit.ui.theme.Colors
import to.bitkit.viewmodels.AppViewModel
import to.bitkit.viewmodels.SendEvent
import to.bitkit.viewmodels.SendUiState
import kotlin.time.Duration.Companion.seconds

Expand All @@ -37,6 +38,7 @@ fun PaykitUsdtReview(
app: AppViewModel,
onBack: () -> Unit,
onDone: () -> Unit,
onEditAmount: () -> Unit = onBack,
viewModel: UsdtViewModel = hiltViewModel()
) {
val wallet by viewModel.wallet.collectAsStateWithLifecycle()
Expand Down Expand Up @@ -66,6 +68,11 @@ fun PaykitUsdtReview(
viewModel.quote(requireNotNull(uiState.usdtRecipient), uiState.paykitAmount, UsdtDestination.ARBITRUM)
}
}
LaunchedEffect(state.quote, state.error) {
if (state.quote == null && state.error == R.string.usdt__error_expired && uiState.isPaymentRequest) {
viewModel.requote(requireNotNull(uiState.usdtRecipient), uiState.paykitAmount, UsdtDestination.ARBITRUM)
Comment thread
ovitrif marked this conversation as resolved.
}
}
LaunchedEffect(state.submitted, lifecycle) {
if (state.submitted) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
Expand All @@ -81,7 +88,7 @@ fun PaykitUsdtReview(
confirmText = stringResource(R.string.wallet__send_yes),
dismissText = stringResource(R.string.common__cancel),
onConfirm = viewModel::acceptWarning,
onDismiss = viewModel::edit
onDismiss = viewModel::dismissWarning
)
}
val transfer = wallet.transfers.firstOrNull { it.id == state.quote?.id }
Expand All @@ -106,7 +113,9 @@ fun PaykitUsdtReview(
onRecipientChange = {},
onAmountChange = {},
onRecipientContinue = {},
onPageChange = { back() },
onPageChange = {
if (!state.busy && !state.authenticationRequired && !state.submitted) onEditAmount()
},
onBack = back,
onReview = {
viewModel.quote(
Expand All @@ -125,6 +134,8 @@ fun PaykitUsdtReview(
isPaymentRequest = uiState.isPaymentRequest,
paymentRequestNote = uiState.paymentRequestNote,
contact = uiState.contactPaymentProfile,
onSwitchAsset = { app.setSendEvent(SendEvent.PaymentMethodSwitch) }
.takeIf { uiState.canSwitchConfirmFundingSource },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
Expand Down Expand Up @@ -126,6 +127,7 @@ internal fun UsdtPaymentContent(
contact: PubkyProfile? = null,
onDestinationChange: ((UsdtDestination) -> Unit)? = null,
destinations: ImmutableList<UsdtDestination> = Env.usdtDestinations.toImmutableList(),
onSwitchAsset: (() -> Unit)? = null,
) {
if (page == UsdtPage.WALLET) return

Expand Down Expand Up @@ -160,6 +162,9 @@ internal fun UsdtPaymentContent(
}
}
val showSubmitted = pending || (completed && hasShownSubmitted)
// A payment request's amount is fixed, so its review waits for the quote on a confirm screen, not on Amount.
val preparingConfirmation = quote == null && page == UsdtPage.AMOUNT && isPaymentRequest && !amountEditable &&
state.error == null
Comment thread
ovitrif marked this conversation as resolved.
Comment thread
ovitrif marked this conversation as resolved.
if (showSuccess && completed && quote != null) {
NewTransactionSheetView(
details = NewTransactionSheetDetails(
Expand All @@ -184,7 +189,7 @@ internal fun UsdtPaymentContent(
showSubmitted && failed -> R.string.wallet__send_error_tx_failed
showSubmitted && state.error != null -> R.string.wallet__payment_request_expired
showSubmitted -> R.string.usdt__submitted
quote != null && isPaymentRequest -> R.string.wallet__payment_request
(quote != null || preparingConfirmation) && isPaymentRequest -> R.string.wallet__payment_request
quote != null -> R.string.wallet__send_review
page == UsdtPage.AMOUNT -> R.string.usdt__amount
else -> R.string.usdt__send_title
Expand Down Expand Up @@ -213,7 +218,9 @@ internal fun UsdtPaymentContent(
onConfirm,
completed,
onEditAmount = { onPageChange(UsdtPage.AMOUNT) },
onSwitchAsset = onSwitchAsset,
)
preparingConfirmation -> UsdtConfirmationLoading(amount, contact, paymentRequestNote)
page == UsdtPage.AMOUNT -> UsdtAmountContent(
amount,
destination,
Expand Down Expand Up @@ -540,6 +547,7 @@ private fun UsdtConfirmation(
onConfirm: () -> Unit,
completed: Boolean,
onEditAmount: () -> Unit,
onSwitchAsset: (() -> Unit)?,
) {
var showDetails by rememberSaveable(quote.id) { mutableStateOf(false) }
val swipeProgress = remember { mutableFloatStateOf(0f) }
Expand All @@ -558,6 +566,7 @@ private fun UsdtConfirmation(
note,
isPaymentRequest,
showDetails,
onSwitchAsset = onSwitchAsset?.takeUnless { state.busy || completed },
) {
if (!state.busy && !completed) onEditAmount()
}
Expand Down Expand Up @@ -595,6 +604,31 @@ private fun UsdtConfirmation(
}
}

@Composable
private fun UsdtConfirmationLoading(amount: String, contact: PubkyProfile?, note: String?) {
Column(modifier = Modifier.fillMaxSize().testTag("UsdtConfirmLoading")) {
UsdtAmountHeader(amount.ifEmpty { "0" })
VerticalSpacer(24.dp)
PaymentRequestSummary(
profile = contact,
note = note?.trim()?.takeIf { it.isNotEmpty() },
accentColor = Colors.Usdt,
)
Spacer(modifier = Modifier.weight(1f))
SwipeToConfirm(
text = stringResource(R.string.wallet__send_swipe),
color = Colors.Usdt,
onConfirm = {},
enabled = false,
loading = true,
confirmed = false,
progress = remember { mutableFloatStateOf(0f) },
textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp),
modifier = Modifier.fillMaxWidth(),
)
}
}

@Composable
private fun ColumnScope.UsdtReviewDetailsButton(showDetails: Boolean, onClick: () -> Unit) {
PrimaryButton(
Expand Down Expand Up @@ -642,6 +676,7 @@ private fun UsdtReviewHeader(
isPaymentRequest: Boolean,
showDetails: Boolean,
modifier: Modifier = Modifier,
onSwitchAsset: (() -> Unit)? = null,
onEditAmount: () -> Unit,
) {
Column(modifier = modifier) {
Expand All @@ -652,7 +687,7 @@ private fun UsdtReviewHeader(
)
VerticalSpacer(24.dp)
if (showDetails) {
UsdtQuoteDetails(quote, contact)
UsdtQuoteDetails(quote, contact, onSwitchAsset)
note?.let {
VerticalSpacer(16.dp)
PaymentRequestInvoiceNote(it)
Expand All @@ -665,16 +700,18 @@ private fun UsdtReviewHeader(
}

@Composable
private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) {
private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?, onSwitchAsset: (() -> Unit)?) {
var showFullAddress by rememberSaveable(quote.recipient) { mutableStateOf(false) }
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
UsdtDetailField(caption = stringResource(R.string.usdt__pay_from), modifier = Modifier.weight(1f)) {
NumberPadActionButton(
text = "USDT",
color = Colors.Usdt,
enabled = false,
enabled = onSwitchAsset != null,
icon = R.drawable.ic_transfer.takeIf { onSwitchAsset != null },
textStyle = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp),
onClick = {},
onClick = { onSwitchAsset?.invoke() },
modifier = Modifier.testTag("UsdtPayFrom"),
)
}
UsdtDetailCell(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,13 +246,29 @@ class UsdtViewModel @Inject constructor(
fun cancelAuthentication() = _state.update { it.copy(authenticationRequired = false) }
fun edit() = _state.update { UsdtSendState(refreshError = it.refreshError) }

/** Keeps the reviewed quote when a warning is declined; the warning shows again on the next swipe. */
fun dismissWarning() = _state.update { it.copy(warning = null) }

// One automatic requote per expired submission; a later expiry waits for the user's own retry.
private var requotedExpiredSubmission = false

/** Replaces an expired quote and keeps the review, telling the user to check the new fee and swipe again. */
fun requote(recipient: String, amount: String, destination: UsdtDestination) = viewModelScope.launch {
if (_state.value.error != R.string.usdt__error_expired || requotedExpiredSubmission) return@launch
requotedExpiredSubmission = true
_state.update { it.copy(error = null) }
quote(recipient, amount, destination).join()
_state.update { if (it.quote != null) it.copy(error = R.string.usdt__error_quote_refreshed) else it }
}

private data class PreparedQuote(val quote: UsdtQuote, val amount: String, val sender: String)

private fun send(quote: UsdtQuote) = viewModelScope.launch {
if (_state.value.quote?.id != quote.id) return@launch
if (_state.value.busy) return@launch
maximumQuote = null
preparedQuote = null
requotedExpiredSubmission = false
_state.update { it.copy(busy = true, error = null) }
runSuspendCatching {
val payment = sendPayment
Expand Down
17 changes: 14 additions & 3 deletions app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ internal fun NavController.navigateToCoinSelection() {
internal fun NavController.navigateToSendAmount(startDestination: SendRoute) {
if (popBackStack(SendRoute.Amount, inclusive = false)) return
navigateTo(SendRoute.Amount) {
if (startDestination == SendRoute.Confirm) {
popUpTo(SendRoute.Confirm) { inclusive = true }
if (startDestination == SendRoute.Confirm || startDestination == SendRoute.UsdtReview) {
popUpTo(startDestination) { inclusive = true }
}
}
}
Expand Down Expand Up @@ -269,6 +269,14 @@ fun SendSheet(
is SendEffect.NavigateToCoinSelection -> navController.navigateToCoinSelection()
is SendEffect.NavigateToConfirm -> navController.navigateTo(SendRoute.Confirm)
is SendEffect.NavigateToUsdtReview -> navController.navigateTo(SendRoute.UsdtReview)
is SendEffect.ShowPaykitConfirmation -> {
// Replace the other asset's confirmation, so Back never returns to its stale terms.
val replaced = when (it.route) {
SendRoute.Confirm -> SendRoute.UsdtReview
else -> SendRoute.Confirm
}
Comment thread
ovitrif marked this conversation as resolved.
navController.navigateTo(it.route) { popUpTo(replaced) { inclusive = true } }
}
Comment thread
ovitrif marked this conversation as resolved.
is SendEffect.NavigateToHardwareSign -> navController.navigateTo(SendRoute.HardwareSign)
is SendEffect.PopBack -> navController.popBackStack(it.route, inclusive = false)
is SendEffect.PaymentSuccess -> {
Expand Down Expand Up @@ -349,7 +357,10 @@ fun SendSheet(
PaykitUsdtReview(
uiState = uiState,
app = appViewModel,
onBack = { navController.popBackStack() },
onBack = {
if (!navController.popBackStack()) appViewModel.hideSheet()
},
onEditAmount = { navController.navigateToSendAmount(startDestination) },
onDone = { appViewModel.hideSheet() }
)
}
Expand Down
Loading
Loading