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 @@ -316,7 +316,7 @@ fun ReceiveQrScreen(
Column(
modifier = modifier.fillMaxSize().background(
Colors.Gray7
).navigationBarsPadding().padding(horizontal = 16.dp)
).navigationBarsPadding()
) {
UsdtDepositReceiveScreen(
viewModel = hiltViewModel(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package to.bitkit.ui.screens.wallets.usdt

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -31,6 +35,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.synonym.bitkitcore.UsdtDepositAddress
import com.synonym.bitkitcore.UsdtDepositDetail
import com.synonym.bitkitcore.UsdtDepositNetwork
import com.synonym.bitkitcore.usdtFormatAmount
Expand Down Expand Up @@ -58,6 +63,7 @@ import to.bitkit.ui.screens.wallets.receive.ReceiveTab
import to.bitkit.ui.shared.modifiers.swipeToChangeTab
import to.bitkit.ui.theme.AppTextStyles
import to.bitkit.ui.theme.Colors
import to.bitkit.ui.utils.Transitions
import java.text.DecimalFormatSymbols
import java.text.NumberFormat
import java.util.Locale
Expand Down Expand Up @@ -102,93 +108,111 @@ internal fun ColumnScope.UsdtDepositReceiveScreen(
LaunchedEffect(state.address) { if (state.address != null) page = ReceivePage.FEES }
UsdtRefreshEffect(viewModel, receiving = true, onReceived = { !state.busy })
BackHandler(onBack = back)
SheetTopBar(
titleText = stringResource(
when (page) {
ReceivePage.ADDRESS -> R.string.usdt__receive_title
ReceivePage.NETWORKS -> R.string.usdt__receive_network_title
ReceivePage.AMOUNT -> R.string.usdt__receive_amount_title
ReceivePage.FEES -> R.string.usdt__receive_fees_title
}
),
onBack = back.takeIf { page != ReceivePage.ADDRESS },
action = if (page == ReceivePage.ADDRESS && onContacts != null) {
{
IconButton(onClick = onContacts) {
Icon(
painterResource(R.drawable.ic_users),
stringResource(R.string.wallet__payment_request_choose_recipient)
)
}
AnimatedContent(
Comment thread
ovitrif marked this conversation as resolved.
targetState = ShownPage(page, state, if (page == ReceivePage.AMOUNT) draftAmount else amount),
contentKey = { it.page },
transitionSpec = {
if (targetState.page > initialState.page) {
Transitions.defaultEnter togetherWith Transitions.defaultExit
} else {
ContentTransform(Transitions.defaultPopEnter, Transitions.defaultPopExit, targetContentZIndex = -1f)
}
} else {
null
},
)
VerticalSpacer(
if (page == ReceivePage.NETWORKS || page == ReceivePage.AMOUNT) 8.dp else 16.dp
)
if (page == ReceivePage.ADDRESS) {
CustomTabRowWithSpacing(
tabs = tabs,
currentTabIndex = tabs.indexOf(ReceiveTab.USDT),
labelOverrides = tabLabels,
onTabChange = onSelectTab
)
VerticalSpacer(16.dp)
}
walletUi.error?.let { BodyS(stringResource(it), color = Colors.Brand) }
val tabIndex = tabs.indexOf(ReceiveTab.USDT)
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.then(
if (page == ReceivePage.ADDRESS) {
Modifier.swipeToChangeTab(tabIndex, tabs.size, { onSelectTab(tabs[it]) }, wrapAround = true)
label = "UsdtReceivePage",
modifier = Modifier.weight(1f)
) { shown ->
val target = shown.page
val isCurrent = { target == page }
Column(modifier = Modifier.fillMaxSize().background(Colors.Gray7).padding(horizontal = 16.dp)) {
SheetTopBar(
titleText = stringResource(
when (target) {
ReceivePage.ADDRESS -> R.string.usdt__receive_title
ReceivePage.NETWORKS -> R.string.usdt__receive_network_title
ReceivePage.AMOUNT -> R.string.usdt__receive_amount_title
ReceivePage.FEES -> R.string.usdt__receive_fees_title
}
),
onBack = whenCurrent(isCurrent, back).takeIf { target != ReceivePage.ADDRESS },
action = if (target == ReceivePage.ADDRESS && onContacts != null) {
{
IconButton(onClick = onContacts) {
Icon(
painterResource(R.drawable.ic_users),
stringResource(R.string.wallet__payment_request_choose_recipient)
)
}
}
} else {
Modifier
}
null
},
)
) {
UsdtDepositReceiveContent(
page = page,
onPage = { page = it },
wallet = wallet,
receiveError = walletUi.refreshError,
state = state,
amount = if (page == ReceivePage.AMOUNT) draftAmount else amount,
onNetwork = {
if (state.network != it) {
amount = ""
draftAmount = ""
deposits.selectNetwork(it)
}
page = if (it == null || state.address != null && state.network == it) {
ReceivePage.ADDRESS
} else {
ReceivePage.AMOUNT
}
},
onAmount = {
if (draftAmount != it) deposits.clearError()
draftAmount = it
},
onCreate = {
if (state.network == null) {
amount = draftAmount
page = ReceivePage.ADDRESS
} else {
deposits.prepare(draftAmount)
}
},
onHistory = { app?.navigateToUsdtDeposits() },
onRefresh = {
draftAmount = if (state.network == null) amount else draftAmount
deposits.selectNetwork(state.network)
page = ReceivePage.AMOUNT
},
)
VerticalSpacer(
if (target == ReceivePage.NETWORKS || target == ReceivePage.AMOUNT) 8.dp else 16.dp
)
if (target == ReceivePage.ADDRESS) {
CustomTabRowWithSpacing(
tabs = tabs,
currentTabIndex = tabs.indexOf(ReceiveTab.USDT),
labelOverrides = tabLabels,
onTabChange = onSelectTab
)
VerticalSpacer(16.dp)
}
walletUi.error?.let { BodyS(stringResource(it), color = Colors.Brand) }
val tabIndex = tabs.indexOf(ReceiveTab.USDT)
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.then(
if (target == ReceivePage.ADDRESS) {
Modifier.swipeToChangeTab(tabIndex, tabs.size, { onSelectTab(tabs[it]) }, wrapAround = true)
} else {
Modifier
}
)
) {
UsdtDepositReceiveContent(
page = target,
onPage = whenCurrentOf(isCurrent) { page = it },
wallet = wallet,
receiveError = walletUi.refreshError,
state = shown.state,
amount = shown.amount,
onNetwork = whenCurrentOf(isCurrent) {
if (state.network != it) {
amount = ""
draftAmount = ""
deposits.selectNetwork(it)
}
page = if (it == null || state.address != null && state.network == it) {
ReceivePage.ADDRESS
} else {
ReceivePage.AMOUNT
}
},
onAmount = whenCurrentOf(isCurrent) {
if (draftAmount != it) deposits.clearError()
draftAmount = it
},
onCreate = whenCurrent(isCurrent) {
if (state.network == null) {
amount = draftAmount
page = ReceivePage.ADDRESS
} else {
deposits.prepare(draftAmount)
}
},
onHistory = { app?.navigateToUsdtDeposits() },
onRefresh = whenCurrent(isCurrent) {
draftAmount = if (state.network == null) amount else draftAmount
deposits.selectNetwork(state.network)
page = ReceivePage.AMOUNT
},
)
}
}
}
}

Expand Down Expand Up @@ -217,24 +241,8 @@ internal fun ColumnScope.UsdtDepositReceiveContent(
onAmount,
onCreate
)
ReceivePage.FEES -> state.address?.let { address ->
UsdtAmountHeader(usdtFormatAmount(address.amount), displayAmount = usdtOverviewAmount(address.amount))
VerticalSpacer(24.dp)
BodyM(stringResource(R.string.usdt__deposit_fee_note), color = Colors.White64, letterSpacing = 0.sp)
VerticalSpacer(24.dp)
EstimateRow(
stringResource(R.string.usdt__receive_fee),
if (address.amount >= address.estimatedReceived) {
usdtOverviewAmount(address.amount - address.estimatedReceived)
} else {
"—"
}
)
EstimateRow(stringResource(R.string.usdt__receive_estimate), usdtOverviewAmount(address.estimatedReceived))
FillHeight()
PrimaryButton(stringResource(R.string.common__continue), onClick = {
onPage(ReceivePage.ADDRESS)
}, textStyle = UsdtDetailStyles.Body, modifier = Modifier.testTag("UsdtReceiveFeesContinue"))
ReceivePage.FEES -> {
FeeEstimate(state.address, onContinue = { onPage(ReceivePage.ADDRESS) })
}
ReceivePage.ADDRESS -> {
val atomic = runCatching { PaykitAmount.parseInput(PaykitAsset.USDT, amount).atomic }.getOrNull()
Expand All @@ -253,8 +261,41 @@ internal fun ColumnScope.UsdtDepositReceiveContent(
}
}

@Composable
private fun ColumnScope.FeeEstimate(address: UsdtDepositAddress?, onContinue: () -> Unit) {
if (address == null) return
UsdtAmountHeader(usdtFormatAmount(address.amount), displayAmount = usdtOverviewAmount(address.amount))
VerticalSpacer(24.dp)
BodyM(stringResource(R.string.usdt__deposit_fee_note), color = Colors.White64, letterSpacing = 0.sp)
VerticalSpacer(24.dp)
EstimateRow(
stringResource(R.string.usdt__receive_fee),
if (address.amount >= address.estimatedReceived) {
usdtOverviewAmount(address.amount - address.estimatedReceived)
} else {
"—"
}
)
EstimateRow(stringResource(R.string.usdt__receive_estimate), usdtOverviewAmount(address.estimatedReceived))
FillHeight()
PrimaryButton(
stringResource(R.string.common__continue),
onClick = onContinue,
textStyle = UsdtDetailStyles.Body,
modifier = Modifier.testTag("UsdtReceiveFeesContinue")
)
}

internal enum class ReceivePage { ADDRESS, NETWORKS, AMOUNT, FEES }

private fun whenCurrent(isCurrent: () -> Boolean, block: () -> Unit): () -> Unit = { if (isCurrent()) block() }

private fun <T> whenCurrentOf(isCurrent: () -> Boolean, block: (T) -> Unit): (T) -> Unit = {
if (isCurrent()) block(it)
}

private data class ShownPage(val page: ReceivePage, val state: DepositUiState, val amount: String)

@Composable
private fun ColumnScope.NetworkSelector(
state: DepositUiState,
Expand Down
59 changes: 32 additions & 27 deletions app/src/main/java/to/bitkit/ui/utils/Transitions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,38 +33,43 @@ object Transitions {
val slideInVertically = slideInVertically(animationSpec = tween(), initialOffsetY = { it })
val slideOutVertically = slideOutVertically(animationSpec = tween(), targetOffsetY = { it })

val defaultEnterTrans: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?) = {
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
}
/** Slide in from the right when a screen is pushed. */
val defaultEnter: EnterTransition = slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(300, easing = FastOutSlowInEasing)
)

val defaultExitTrans: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?) = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeOut(
animationSpec = tween(300, easing = FastOutSlowInEasing),
targetAlpha = 0.8f
)
}
/** Slide partly out to the left and dim when a screen is covered. */
val defaultExit: ExitTransition = slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeOut(
animationSpec = tween(300, easing = FastOutSlowInEasing),
targetAlpha = 0.8f
)

/** Slide back in from the left when the screen above is popped. */
val defaultPopEnter: EnterTransition = slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeIn(
animationSpec = tween(300, easing = FastOutSlowInEasing),
initialAlpha = 0.8f
)

/** Slide out to the right when a screen is popped. */
val defaultPopExit: ExitTransition = slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(300, easing = FastOutSlowInEasing)
)

val defaultEnterTrans: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?) = { defaultEnter }
val defaultExitTrans: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?) = { defaultExit }
val defaultPopEnterTrans: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?) = {
slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeIn(
animationSpec = tween(300, easing = FastOutSlowInEasing),
initialAlpha = 0.8f
)
defaultPopEnter
}

val defaultPopExitTrans: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?) = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
defaultPopExit
}
}

Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1586.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Receiving USDT now slides between the address, network, amount and fee screens, like the Bitcoin receive flow.
1 change: 1 addition & 0 deletions journeys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691):
| `coin-selection/manual-wallet-switch.xml` | same file, journey name and actions; iOS can open the picker on the wallet switch, Android on the next swipe. The iOS picker has no accessibility identifiers, so assert its visible "Coin Selection" title. |
| `payment-requests/requested-resolution-failure.xml` | same file and journey name; both platforms keep the preparing confirmation open across retries and stop automatic retries when it closes. Native progress and request-row identifiers differ. Android's subscription-reminder preparation has no intermediate Send sheet; see `paykit-clock-changes.md`. |
| `payment-requests/request-summary.xml` | same file and journey name; Android additionally checks the open Sent receipt's live lifecycle subtitle after payment. iOS keeps a static note/date receipt without that subtitle, so those checks are not ported. |
| `usdt-receive-amount-and-units.xml` | same file and journey name; the Edit slide step (Amount slides in from the right, Back slides it out over the QR) waits on synonymdev/bitkit-ios#1018, since iOS still swaps the USDT receive pages without a transition. Port the step with that fix. |
| `usdt-payment-restoration.xml` | same file, journey name and actions; Android restores an unpaid amount draft after process death, while iOS may return to the wallet. A submitted payment must return to the wallet without reopening the paid draft on either platform. |
| `payment-requests/success-dismissal.xml` | same file, journey name and actions; both platforms dismiss successful and unpaid requests without a send error or duplicate payment. |
| `payment-requests/minimum-onchain-amount.xml` | shared — minimum amount feedback, preparation cleanup, and manual retry use the same journey and `PaymentRequestAmountTooLowToast` identifier on both platforms. |
Expand Down
1 change: 1 addition & 0 deletions journeys/usdt-receive-amount-and-units.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<action>Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow.</action>
<action>Open USDT from Home. Verify the USDT balance, teal Tether icon and existing activity rows.</action>
<action>Open Receive. Verify Receive Dollars, the USDT tab and Network: Arbitrum One on the QR screen.</action>
<action>Tap Edit. Verify the Amount screen slides in from the right, as Edit Invoice does on the Savings tab. Tap Back and verify it slides out to the right over the QR.</action>
Comment thread
ovitrif marked this conversation as resolved.
<action>Tap Edit, enter 5 and tap Back. Verify the QR has no uint256 amount.</action>
<action>Tap Edit. Enter 3.50. Verify the dollar amount and BTC equivalent are displayed, there is no input-unit toggle, the keypad follows the divider, and Continue stays at the bottom without overlap.</action>
<action>Continue to the QR. Verify it requests the Arbitrum USDT token, the wallet address and uint256=3500000, never Bitcoin.</action>
Expand Down
Loading