From 8bcefb07d591576d67efb45a6e16c156a9e6b2f2 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 06:20:05 -0500 Subject: [PATCH 01/11] feat: refine usdt payment and earn screens --- .../CreatePaymentRequestScreenTest.kt | 20 ++- .../CreateSubscriptionScreenTest.kt | 2 +- .../wallets/usdt/UsdtPaymentContentTest.kt | 32 ++-- .../to/bitkit/models/PaykitRequestPricing.kt | 7 +- .../repositories/PaykitPaymentRequestRepo.kt | 12 +- .../ui/components/NumberPadActionButton.kt | 22 +++ .../ui/components/PaykitAcceptedAssets.kt | 52 ------ .../ui/components/PaykitAcceptedMethods.kt | 81 +++++++++ .../CreatePaymentRequestScreen.kt | 13 +- .../screens/profile/PubkyAuthApprovalSheet.kt | 11 +- .../subscriptions/CreateSubscriptionScreen.kt | 38 +++-- .../wallets/usdt/UsdtDepositReceiveScreen.kt | 16 +- .../screens/wallets/usdt/UsdtSendContent.kt | 157 +++++++----------- .../wallets/usdt/UsdtSupportActions.kt | 19 ++- .../main/res/drawable/ic_arrow_down_right.xml | 10 ++ .../main/res/drawable/ic_arrow_up_right.xml | 10 ++ app/src/main/res/values/strings.xml | 9 +- .../bitkit/models/PaykitRequestPricingTest.kt | 24 +++ ...aykitPaymentRequestRepoSubscriptionTest.kt | 4 +- changelog.d/next/1494.changed.md | 1 + docs/screens-map.md | 2 +- docs/usdt-qa.md | 13 +- journeys/paykit-quoted-usdt.xml | 10 +- .../pubky-auth/share-receiving-details.xml | 8 +- journeys/subscriptions/create-and-propose.xml | 2 +- journeys/usdt-bridge-details.xml | 4 +- journeys/usdt-bridge-support.xml | 6 +- journeys/usdt-send-review-and-details.xml | 6 +- 28 files changed, 339 insertions(+), 252 deletions(-) delete mode 100644 app/src/main/java/to/bitkit/ui/components/PaykitAcceptedAssets.kt create mode 100644 app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt create mode 100644 app/src/main/res/drawable/ic_arrow_down_right.xml create mode 100644 app/src/main/res/drawable/ic_arrow_up_right.xml create mode 100644 changelog.d/next/1494.changed.md diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt index e21a51899b..f064376050 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreenTest.kt @@ -2,6 +2,7 @@ package to.bitkit.ui.screens.paymentrequests +import android.os.SystemClock import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -74,7 +75,7 @@ class CreatePaymentRequestScreenTest { } @Test - fun acceptedAssetsGroupsBitcoinRailsAndRequiresASelection() { + fun acceptedMethodsCanBeRemovedIndependentlyAndRequireASelection() { var submitted: PaykitPaymentRequestDraft? = null composeTestRule.setContent { AppThemeSurface { @@ -93,18 +94,27 @@ class CreatePaymentRequestScreenTest { ) } } - composeTestRule.onNodeWithTag("PaymentRequestAccept-bitcoin") + composeTestRule.onNodeWithTag("PaymentRequestAccept-savings") .performScrollTo().assertIsSelected().performClick() + composeTestRule.onNodeWithTag("PaymentRequestAccept-spending").assertIsSelected() + composeTestRule.onNodeWithTag("PaymentRequestSend").performClick() + assertEquals( + listOf(MethodId.Bolt11.rawValue, MethodId.UsdtArbitrum.rawValue), + submitted?.acceptedPaymentEndpointIdentifiers + ) + composeTestRule.onNodeWithTag("PaymentRequestAccept-spending").performClick().assertIsNotSelected() composeTestRule.onNodeWithTag("PaymentRequestAccept-usdt").assertIsSelected() + SystemClock.sleep(600) composeTestRule.onNodeWithTag("PaymentRequestSend").performClick() assertEquals(listOf(MethodId.UsdtArbitrum.rawValue), submitted?.acceptedPaymentEndpointIdentifiers) composeTestRule.onNodeWithTag("PaymentRequestAccept-usdt").performClick().assertIsNotSelected() composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsNotEnabled() - composeTestRule.onNodeWithTag("PaymentRequestAccept-bitcoin").performClick().assertIsSelected() + composeTestRule.onNodeWithTag("PaymentRequestAccept-savings").performClick().assertIsSelected() + SystemClock.sleep(600) composeTestRule.onNodeWithTag("PaymentRequestSend").assertIsEnabled().performClick() assertEquals( - setOf(MethodId.P2wpkh.rawValue, MethodId.Bolt11.rawValue), - submitted?.acceptedPaymentEndpointIdentifiers?.toSet() + listOf(MethodId.P2wpkh.rawValue), + submitted?.acceptedPaymentEndpointIdentifiers ) } diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt index d20e3d5fef..995c09ff10 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt @@ -72,7 +72,7 @@ class CreateSubscriptionScreenTest { selectedIconUri = null, isLoadingIcon = isLoadingIcon, onAmountClick = {}, - onClickAsset = {}, + onClickMethod = {}, onNameChange = { name = it }, onDescriptionChange = {}, onFrequencyChange = { frequency = it }, diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt index 76a2da4cd5..eb667a1fe9 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt @@ -1,5 +1,6 @@ package to.bitkit.ui.screens.wallets.usdt +import android.os.SystemClock import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable @@ -25,7 +26,6 @@ import to.bitkit.repositories.UsdtWalletState import to.bitkit.test.annotations.ComposeUi import to.bitkit.ui.theme.AppThemeSurface import java.util.Locale -import kotlin.test.assertTrue @HiltAndroidTest @ComposeUi @@ -60,29 +60,27 @@ class UsdtPaymentContentTest { } @Test - fun bridgeMaximumDebitRemainsVisibleWithoutScrolling() { + fun bridgeAmountsAndNetworksAreInExpandedDetails() { composeTestRule.setContent { Content(UsdtSendState(quote = quote), UsdtPage.AMOUNT) } val total = composeTestRule.onNodeWithTag("UsdtMaximumTotal") val swipe = composeTestRule.onNodeWithTag("UsdtConfirm") - composeTestRule.onNodeWithText("YOU SEND").assertIsDisplayed() - composeTestRule.onNodeWithTag("UsdtRecipientAmount").assertIsDisplayed() - composeTestRule.onNodeWithText("≈ 2.99 USDT").assertIsDisplayed() - total.assertIsDisplayed() - composeTestRule.onNodeWithText("4.47 USDT").assertIsDisplayed() + composeTestRule.onNodeWithText("YOU SEND").assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtRecipientAmount").assertDoesNotExist() + total.assertDoesNotExist() swipe.assertIsDisplayed() - assertTrue(total.fetchSemanticsNode().boundsInRoot.bottom <= swipe.fetchSemanticsNode().boundsInRoot.top) composeTestRule.onNodeWithTag("UsdtReviewDetails").performScrollTo().performClick() - composeTestRule.onNodeWithTag("UsdtRecipientAmount").assertIsDisplayed() - total.assertIsDisplayed() - swipe.assertIsDisplayed() - val cost = composeTestRule.onNodeWithText("0.01 USDT") - cost.performScrollTo().assertIsDisplayed() - assertTrue(cost.fetchSemanticsNode().boundsInRoot.bottom <= total.fetchSemanticsNode().boundsInRoot.top) - composeTestRule.onNodeWithText( - "Delivery is estimated. Bridge costs are deducted from the amount; the network fee is additional." - ).performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithText("Arbitrum One").performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithTag("UsdtRecipientAmount").performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithText("±2.99").assertIsDisplayed() total.assertIsDisplayed() + composeTestRule.onNodeWithText("4.47").assertIsDisplayed() + composeTestRule.onNodeWithText("Polygon").performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithText("All fees are paid in USDT. Final fee may be lower.").assertIsDisplayed() swipe.assertIsDisplayed() + SystemClock.sleep(600) + composeTestRule.onNodeWithTag("UsdtReviewDetails").performScrollTo().performClick() + total.assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtRecipientAmount").assertDoesNotExist() } @Test diff --git a/app/src/main/java/to/bitkit/models/PaykitRequestPricing.kt b/app/src/main/java/to/bitkit/models/PaykitRequestPricing.kt index 1d493cc6b3..a0b2d1146f 100644 --- a/app/src/main/java/to/bitkit/models/PaykitRequestPricing.kt +++ b/app/src/main/java/to/bitkit/models/PaykitRequestPricing.kt @@ -22,7 +22,12 @@ data class PaykitRequestPricing( val quotes: List = emptyList(), ) { companion object { - fun subscriptionEndpoints(requested: PaykitAsset, available: List): List = available.filter { + fun subscriptionEndpoints( + requested: PaykitAsset, + available: List, + selected: List? = null, + ): List = available.filter { + if (selected != null && it !in selected) return@filter false val method = MethodId.fromRawValue(it) ?: return@filter false when (requested) { PaykitAsset.BTC -> method != MethodId.UsdtArbitrum diff --git a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt index 45c123caf9..744f2df348 100644 --- a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt @@ -263,6 +263,7 @@ data class PaykitPaymentRequestCreation( val wasPublishedToActiveState: Boolean, ) +@Suppress("LongParameterList") class PaykitSubscriptionDraft( val amount: PaykitAmount, val name: String, @@ -270,6 +271,7 @@ class PaykitSubscriptionDraft( val frequency: PaykitRecurrenceUnit, val expiresAt: Instant, val iconBytes: ByteArray? = null, + val acceptedPaymentEndpointIdentifiers: List? = null, ) data class PaykitSubscriptionCreation( @@ -827,7 +829,7 @@ class PaykitPaymentRequestRepo @Inject constructor( val description = draft.description.trim() validateSubscriptionDraft(draft, name, validationDate) - val endpoints = subscriptionProposalEndpoints(draft.amount.asset, target, savedPublicKeys, expectedIdentity) + val endpoints = subscriptionProposalEndpoints(draft, target, savedPublicKeys, expectedIdentity) val reservedIconUri = PaykitSubscriptionProposal.reservedIconUri.takeIf { draft.iconBytes != null } val preflight = buildSubscriptionProposal(draft, name, description, reservedIconUri, endpoints, clock.now()) PaykitSubscriptionProposal.validate(preflight) @@ -878,7 +880,7 @@ class PaykitPaymentRequestRepo @Inject constructor( } private suspend fun subscriptionProposalEndpoints( - asset: PaykitAsset, + draft: PaykitSubscriptionDraft, target: PaykitPaymentRequestTarget, savedPublicKeys: List, expectedIdentity: String, @@ -891,7 +893,11 @@ class PaykitPaymentRequestRepo @Inject constructor( } .orEmpty() val settings = settingsStore.data.first() - val endpoints = PaykitRequestPricing.subscriptionEndpoints(asset, acceptedPaymentEndpointIdentifiers(settings)) + val endpoints = PaykitRequestPricing.subscriptionEndpoints( + draft.amount.asset, + acceptedPaymentEndpointIdentifiers(settings), + draft.acceptedPaymentEndpointIdentifiers, + ) if (target !in targets || !settings.sharesPrivatePaykitEndpoints || endpoints.isEmpty()) { throw PaykitPaymentRequestError.RequestUnavailable } diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt index e4d31ccd5b..c0f6ebfa10 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt @@ -18,11 +18,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.primaryButtonStyle +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -37,6 +39,8 @@ fun NumberPadActionButton( clickable: Boolean = true, outlined: Boolean = false, @DrawableRes icon: Int? = null, + @DrawableRes trailingIcon: Int? = null, + textStyle: TextStyle = AppTextStyles.CaptionM, ) { val contentPadding = PaddingValues(horizontal = 8.dp, vertical = 5.dp) val height = 28.dp @@ -78,7 +82,16 @@ fun NumberPadActionButton( Caption13Up( text = text, color = color, + style = textStyle, ) + if (trailingIcon != null) { + Icon( + painter = painterResource(trailingIcon), + contentDescription = null, + tint = color, + modifier = Modifier.size(16.dp) + ) + } } } else { Row( @@ -104,7 +117,16 @@ fun NumberPadActionButton( Caption13Up( text = text, color = color, + style = textStyle, ) + if (trailingIcon != null) { + Icon( + painter = painterResource(trailingIcon), + contentDescription = null, + tint = color, + modifier = Modifier.size(16.dp) + ) + } } } } diff --git a/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedAssets.kt b/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedAssets.kt deleted file mode 100644 index f71cda0468..0000000000 --- a/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedAssets.kt +++ /dev/null @@ -1,52 +0,0 @@ -package to.bitkit.ui.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.selected -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import kotlinx.collections.immutable.ImmutableList -import to.bitkit.R -import to.bitkit.models.PaykitAsset -import to.bitkit.repositories.MethodId -import to.bitkit.ui.theme.Colors - -@Composable -fun PaykitAcceptedAssets( - title: String, - availableMethods: ImmutableList, - selectedMethods: ImmutableList, - testTagPrefix: String, - onClickAsset: (PaykitAsset) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - Caption13Up(text = title, color = Colors.White64) - VerticalSpacer(8.dp) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - listOf(PaykitAsset.BTC, PaykitAsset.USD).forEach { asset -> - val isUsdt = asset == PaykitAsset.USD - val endpoints = availableMethods.filter { rawValue -> - MethodId.fromRawValue(rawValue)?.let { (it == MethodId.UsdtArbitrum) == isUsdt } == true - } - if (endpoints.isNotEmpty()) { - val selected = endpoints.any { it in selectedMethods } - NumberPadActionButton( - text = if (isUsdt) "USDT" else stringResource(R.string.settings__general__unit_bitcoin), - color = if (!selected) Colors.White32 else if (isUsdt) Colors.Usdt else Colors.Brand, - outlined = true, - onClick = { onClickAsset(asset) }, - modifier = Modifier - .semantics { this.selected = selected } - .testTag("${testTagPrefix}Accept-${if (isUsdt) "usdt" else "bitcoin"}") - ) - } - } - } - } -} diff --git a/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt b/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt new file mode 100644 index 0000000000..55c1057e86 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt @@ -0,0 +1,81 @@ +package to.bitkit.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.collections.immutable.ImmutableList +import to.bitkit.R +import to.bitkit.models.PaykitAsset +import to.bitkit.repositories.MethodId +import to.bitkit.ui.theme.AppTextStyles +import to.bitkit.ui.theme.Colors + +@Composable +fun PaykitAcceptedMethods( + title: String, + availableMethods: ImmutableList, + selectedMethods: ImmutableList, + testTagPrefix: String, + onClickMethod: (PaykitPaymentOption) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Caption13Up(text = title, color = Colors.White64) + VerticalSpacer(8.dp) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + PaykitPaymentOption.entries.forEach { option -> + val endpoints = option.endpoints(availableMethods) + if (endpoints.isNotEmpty()) { + val selected = endpoints.any { it in selectedMethods } + NumberPadActionButton( + text = when (option) { + PaykitPaymentOption.Savings -> stringResource(R.string.lightning__savings) + PaykitPaymentOption.Spending -> stringResource(R.string.lightning__spending) + PaykitPaymentOption.Usdt -> "USDT" + }, + color = if (!selected) { + Colors.White32 + } else { + when (option) { + PaykitPaymentOption.Savings -> Colors.Brand + PaykitPaymentOption.Spending -> Colors.Purple + PaykitPaymentOption.Usdt -> Colors.Usdt + } + }, + outlined = true, + textStyle = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + trailingIcon = R.drawable.ic_x.takeIf { selected }, + onClick = { onClickMethod(option) }, + modifier = Modifier + .semantics { this.selected = selected } + .testTag("${testTagPrefix}Accept-${option.id}") + ) + } + } + } + } +} + +enum class PaykitPaymentOption(val id: String, val asset: PaykitAsset) { + Savings("savings", PaykitAsset.BTC), + Spending("spending", PaykitAsset.BTC), + Usdt("usdt", PaykitAsset.USD), + ; + + fun endpoints(available: List): List = available.filter { + val method = MethodId.fromRawValue(it) ?: return@filter false + when (this) { + Savings -> method.isOnchain + Spending -> method == MethodId.Bolt11 || method == MethodId.Lnurl + Usdt -> method == MethodId.UsdtArbitrum + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index c1856520f9..8318e6f7e9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -52,7 +52,6 @@ import to.bitkit.models.PaykitAmountConversion import to.bitkit.models.PaykitAsset import to.bitkit.models.PubkyProfile import to.bitkit.models.PubkyPublicKeyFormat -import to.bitkit.repositories.MethodId import to.bitkit.repositories.PaykitPaymentRequest import to.bitkit.repositories.PaykitPaymentRequestDeliveryStatus import to.bitkit.repositories.PaykitPaymentRequestDirection @@ -74,7 +73,7 @@ import to.bitkit.ui.components.NumberPadActionButton import to.bitkit.ui.components.NumberPadAmountText import to.bitkit.ui.components.NumberPadTextField import to.bitkit.ui.components.NumberPadType -import to.bitkit.ui.components.PaykitAcceptedAssets +import to.bitkit.ui.components.PaykitAcceptedMethods import to.bitkit.ui.components.PaykitAmountCell import to.bitkit.ui.components.PaykitAmountDisplay import to.bitkit.ui.components.PrimaryButton @@ -349,7 +348,7 @@ internal fun PaymentRequestDetailsContent( var acceptedMethods by remember(initialDraft.acceptedPaymentEndpointIdentifiers) { mutableStateOf(initialDraft.acceptedPaymentEndpointIdentifiers) } - val selectedMethods = acceptedMethods ?: availableMethods + val selectedMethods = availableMethods.filter { acceptedMethods?.contains(it) != false } var note by remember(initialDraft.note) { mutableStateOf(initialDraft.note) } var expiration by remember(initialDraft.expiresAt) { mutableStateOf(PaymentRequestExpiration.from(initialDraft.expiresAt, Clock.System.now())) @@ -449,15 +448,13 @@ internal fun PaymentRequestDetailsContent( } } VerticalSpacer(32.dp) - PaykitAcceptedAssets( + PaykitAcceptedMethods( title = stringResource(R.string.wallet__payment_request_accepted_methods), availableMethods = availableMethods, selectedMethods = selectedMethods.toImmutableList(), testTagPrefix = "PaymentRequest", - onClickAsset = { asset -> - val endpoints = availableMethods.filter { - (it == MethodId.UsdtArbitrum.rawValue) == (asset == PaykitAsset.USD) - } + onClickMethod = { option -> + val endpoints = option.endpoints(availableMethods) acceptedMethods = if (endpoints.any { it in selectedMethods }) { selectedMethods - endpoints.toSet() } else { diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt index a60fd76819..488fdf5ac3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt @@ -314,7 +314,9 @@ private fun ColumnScope.WatchOnlyConsentContent( VerticalSpacer(32.dp) Display( text = stringResource(R.string.profile__auth_approval_watch_only_intro_title) - .withAccent(accentColor = Colors.Blue), + .withAccent( + accentColor = if (uiState.bitkitClaim?.sharesBitcoin == true) Colors.Blue else Colors.Usdt + ), ) VerticalSpacer(8.dp) BodyM( @@ -326,10 +328,11 @@ private fun ColumnScope.WatchOnlyConsentContent( } ), color = Colors.White64, + letterSpacing = 0.sp, ) } } - if (uiState.showsUsdtDetails) { + if (uiState.showsUsdtDetails && (!uiState.usdtRequired || uiState.usdtAddress == null)) { VerticalSpacer(32.dp) UsdtSharingChoice(uiState, onShareUsdt, onRetryUsdt) } @@ -382,10 +385,6 @@ private fun UsdtSharingChoice( ) } } - if (uiState.shareUsdt || uiState.usdtRequired) { - if (!uiState.usdtRequired) VerticalSpacer(8.dp) - BodyS(text = stringResource(R.string.profile__auth_approval_usdt_privacy), color = Colors.White64) - } if (uiState.usdtAddress == null) { VerticalSpacer(8.dp) BodyS( diff --git a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt index 0d2bb88539..c859bbeeb7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt @@ -83,8 +83,9 @@ import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display -import to.bitkit.ui.components.PaykitAcceptedAssets +import to.bitkit.ui.components.PaykitAcceptedMethods import to.bitkit.ui.components.PaykitAmountDisplay +import to.bitkit.ui.components.PaykitPaymentOption import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.PubkyContactRow import to.bitkit.ui.components.TextInput @@ -124,6 +125,8 @@ fun CreateSubscriptionSheet( var step by remember { mutableStateOf(SubscriptionCreationStep.Details) } var amount by remember { mutableStateOf(PaykitAmount(PaykitAsset.BTC, 0uL)) } val amountConversion = remember { PaykitAmountConversion() } + var restrictedMethods by remember { mutableStateOf?>(null) } + val acceptedMethods = PaykitRequestPricing.subscriptionEndpoints(amount.asset, receivingMethods, restrictedMethods) var name by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } var frequency by remember { mutableStateOf(PaykitRecurrenceUnit.Month) } @@ -138,32 +141,39 @@ fun CreateSubscriptionSheet( SubscriptionCreationStep.Details -> CreateSubscriptionDetails( amount = amount, availableMethods = receivingMethods.toImmutableList(), - acceptedMethods = PaykitRequestPricing.subscriptionEndpoints( - amount.asset, - receivingMethods, - ).toImmutableList(), + acceptedMethods = acceptedMethods.toImmutableList(), name = name, description = description, frequency = frequency, selectedIconUri = selectedIconUri, isLoadingIcon = isLoadingIcon, onAmountClick = { step = SubscriptionCreationStep.Amount }, - onClickAsset = { asset -> - if (asset != amount.asset) { + onClickMethod = { option -> + val endpoints = option.endpoints(receivingMethods) + if (option.asset != amount.asset) { runCatching { amountConversion.convert( amount, - asset, + option.asset, currencies.paykitRate, Clock.System.now().toEpochMilliseconds() ) - }.onSuccess { amount = it }.onFailure { + }.onSuccess { + amount = it + restrictedMethods = endpoints + }.onFailure { appViewModel.toast( type = Toast.ToastType.ERROR, title = context.getString(R.string.common__error), description = context.getString(R.string.wallet__payment_request_rate_unavailable), ) } + } else { + restrictedMethods = if (endpoints.any { it in acceptedMethods }) { + acceptedMethods - endpoints.toSet() + } else { + acceptedMethods + endpoints.filter { it !in acceptedMethods } + } } }, onNameChange = { name = it.take(256) }, @@ -205,6 +215,7 @@ fun CreateSubscriptionSheet( contact = null, onBack = { step = SubscriptionCreationStep.Details }, onContinue = { + if (amount.asset != it.amount.asset) restrictedMethods = null amount = it.amount step = SubscriptionCreationStep.Details }, @@ -231,6 +242,7 @@ fun CreateSubscriptionSheet( frequency = frequency, expiresAt = SubscriptionClockOffset.subscriptionNow() + expiration.duration, iconBytes = iconBytes, + acceptedPaymentEndpointIdentifiers = acceptedMethods, ), target = target, ) { @@ -264,7 +276,7 @@ internal fun CreateSubscriptionDetails( selectedIconUri: Uri?, isLoadingIcon: Boolean, onAmountClick: () -> Unit, - onClickAsset: (PaykitAsset) -> Unit, + onClickMethod: (PaykitPaymentOption) -> Unit, onNameChange: (String) -> Unit, onDescriptionChange: (String) -> Unit, onFrequencyChange: (PaykitRecurrenceUnit) -> Unit, @@ -322,12 +334,12 @@ internal fun CreateSubscriptionDetails( VerticalSpacer(8.dp) SubscriptionFrequencyPicker(frequency, onFrequencyChange) VerticalSpacer(24.dp) - PaykitAcceptedAssets( + PaykitAcceptedMethods( title = stringResource(R.string.subscriptions__accepted_payments), availableMethods = availableMethods, selectedMethods = acceptedMethods, testTagPrefix = "Subscription", - onClickAsset = onClickAsset, + onClickMethod = onClickMethod, modifier = Modifier.testTag("SubscriptionAcceptedMethods") ) VerticalSpacer(24.dp) @@ -636,7 +648,7 @@ private fun CreateSubscriptionDetailsPreview() { selectedIconUri = null, isLoadingIcon = false, onAmountClick = {}, - onClickAsset = {}, + onClickMethod = {}, onNameChange = {}, onDescriptionChange = {}, onFrequencyChange = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt index e6a2a62ffb..70ebc9aa9b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt @@ -42,7 +42,6 @@ import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SendCell -import to.bitkit.ui.components.TertiaryButton import to.bitkit.ui.components.UsdtAmountHeader import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.usdtOverviewAmount @@ -251,15 +250,16 @@ private fun ColumnScope.NetworkSelector( modifier = Modifier.padding(vertical = 16.dp) ) UsdtCoinIllustration(modifier = Modifier.align(Alignment.CenterHorizontally)) - if (Env.usdtDepositsUrl != null) { - TertiaryButton( - stringResource(R.string.usdt__deposit_history), - onClick = onHistory, - modifier = Modifier.testTag("UsdtDepositHistory") - ) - } state.error?.let { BodyS(stringResource(it), color = Colors.White64) } } + if (Env.usdtDepositsUrl != null) { + VerticalSpacer(16.dp) + PrimaryButton( + stringResource(R.string.usdt__deposit_history), + onClick = onHistory, + modifier = Modifier.testTag("UsdtDepositHistory") + ) + } } @Composable diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt index 6b84b4b4b3..82dce1207d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -35,14 +34,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -67,7 +65,6 @@ import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize -import to.bitkit.ui.components.Caption import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.FillHeight @@ -94,6 +91,7 @@ import to.bitkit.ui.screens.wallets.send.HourglassAnimation import to.bitkit.ui.shared.UiConstants import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.sheets.NewTransactionSheetView +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.NumberPadInputHandler @@ -513,13 +511,10 @@ private fun UsdtConfirmation( var showDetails by rememberSaveable(quote.id) { mutableStateOf(false) } val swipeProgress = remember { mutableFloatStateOf(0f) } val note = paymentRequestNote?.trim()?.takeIf { it.isNotEmpty() } - var headerHeight by remember { mutableIntStateOf(0) } - val density = LocalDensity.current val scrollState = rememberScrollState() LaunchedEffect(showDetails) { scrollState.scrollTo(0) } Column(modifier = Modifier.fillMaxSize()) { BoxWithConstraints(modifier = Modifier.weight(1f)) { - val availableHeight = maxHeight Column( verticalArrangement = if (showDetails) Arrangement.Top else Arrangement.SpaceBetween, modifier = Modifier.verticalScroll(scrollState).heightIn(min = maxHeight) @@ -530,7 +525,6 @@ private fun UsdtConfirmation( note, isPaymentRequest, showDetails, - modifier = Modifier.onSizeChanged { headerHeight = it.height }, ) { if (!state.busy && !completed) onEditAmount() } @@ -539,31 +533,17 @@ private fun UsdtConfirmation( PaymentReviewIllustration( swipeProgress = { swipeProgress.floatValue }, isUsdt = true, - modifier = Modifier.height( - if (quote.destination == UsdtDestination.ARBITRUM) { - 256.dp - } else { - (availableHeight - with(density) { headerHeight.toDp() }).coerceIn(0.dp, 256.dp) - } - ) + modifier = Modifier.height(256.dp) ) - } else { - UsdtBridgeCost(quote) - if (quote.destination != UsdtDestination.ARBITRUM) { - VerticalSpacer(16.dp) - UsdtFeeNote(quote) - } } state.error?.let { BodyS(stringResource(it), color = Colors.Brand, modifier = Modifier.testTag("UsdtError")) } - if (quote.destination == UsdtDestination.ARBITRUM) { - UsdtReviewDetailsButton(showDetails) { showDetails = !showDetails } - } + UsdtReviewDetailsButton(showDetails) { showDetails = !showDetails } } } } - UsdtReviewFooter(quote, showDetails) { showDetails = !showDetails } + if (showDetails) UsdtFeeNote() SwipeToConfirm( text = stringResource(R.string.wallet__send_swipe), color = Colors.Usdt, @@ -577,16 +557,6 @@ private fun UsdtConfirmation( } } -@Composable -private fun ColumnScope.UsdtReviewFooter(quote: UsdtQuote, showDetails: Boolean, onToggleDetails: () -> Unit) { - if (quote.destination != UsdtDestination.ARBITRUM) { - UsdtReviewDetailsButton(showDetails, onToggleDetails) - } else if (showDetails) { - UsdtFeeNote(quote) - } - UsdtMaximumTotal(quote) -} - @Composable private fun ColumnScope.UsdtReviewDetailsButton(showDetails: Boolean, onClick: () -> Unit) { PrimaryButton( @@ -615,28 +585,12 @@ private fun ColumnScope.UsdtReviewDetailsButton(showDetails: Boolean, onClick: ( } @Composable -private fun UsdtMaximumTotal(quote: UsdtQuote) { - if (quote.destination == UsdtDestination.ARBITRUM) return - VerticalSpacer(16.dp) - SendCell( - caption = stringResource(R.string.usdt__maximum_total), - modifier = Modifier.testTag("UsdtMaximumTotal") - ) { BodySSB(usdtOverviewAmount(quote.amount + quote.maximumFee) + " USDT") } - VerticalSpacer(16.dp) -} - -@Composable -private fun UsdtFeeNote(quote: UsdtQuote) { - Caption( - stringResource( - if (quote.bridgeProvider == UsdtBridgeProvider.ORCHESTRA) { - R.string.usdt__bridge_estimate_note - } else { - R.string.usdt__fee_note - } - ), +private fun UsdtFeeNote() { + BodyS( + stringResource(R.string.usdt__fee_note), color = Colors.White64, textAlign = TextAlign.Center, + letterSpacing = 0.sp, modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp) ) } @@ -652,26 +606,12 @@ private fun UsdtReviewHeader( onEditAmount: () -> Unit, ) { Column(modifier = modifier) { - if (quote.destination != UsdtDestination.ARBITRUM) { - Caption13Up(stringResource(R.string.usdt__you_send), color = Colors.White64) - VerticalSpacer(8.dp) - } UsdtAmountHeader( usdtFormatAmount(quote.amount), displayAmount = usdtOverviewAmount(quote.amount), onClick = onEditAmount ) - if (quote.destination != UsdtDestination.ARBITRUM) { - VerticalSpacer(16.dp) - SendCell( - caption = stringResource(R.string.usdt__recipient_gets), - modifier = Modifier.testTag("UsdtRecipientAmount") - ) { - val estimate = if (quote.bridgeProvider == UsdtBridgeProvider.ORCHESTRA) "≈ " else "" - BodySSB(estimate + usdtOverviewAmount(quote.receivedAmount) + " USDT") - } - } - VerticalSpacer(32.dp) + VerticalSpacer(24.dp) if (showDetails) { UsdtQuoteDetails(quote, contact) note?.let { @@ -685,40 +625,30 @@ private fun UsdtReviewHeader( } } -@Composable -private fun UsdtBridgeCost(quote: UsdtQuote) { - if (quote.bridgeProvider == UsdtBridgeProvider.ORCHESTRA) { - VerticalSpacer(16.dp) - SendCell(caption = stringResource(R.string.usdt__bridge_deducted_fee)) { - BodySSB(usdtOverviewAmount(quote.amount - minOf(quote.amount, quote.receivedAmount)) + " USDT") - } - } -} - @Composable private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - SendCell(caption = stringResource(R.string.usdt__pay_from), modifier = Modifier.weight(1f)) { - NumberPadActionButton(text = "USDT", color = Colors.Usdt, enabled = false, onClick = {}) + UsdtDetailField(caption = stringResource(R.string.usdt__pay_from), modifier = Modifier.weight(1f)) { + NumberPadActionButton( + text = "USDT", + color = Colors.Usdt, + enabled = false, + textStyle = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + onClick = {}, + ) } UsdtDetailCell( stringResource(R.string.usdt__network), - quote.destination.label, + UsdtDestination.ARBITRUM.label, R.drawable.usdt_network, - modifier = Modifier.weight(1f), - minValueHeight = 28.dp + minValueHeight = 28.dp, + modifier = Modifier.weight(1f) ) } VerticalSpacer(16.dp) Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { UsdtDetailCell( - stringResource( - if (quote.destination == UsdtDestination.ARBITRUM) { - R.string.wallet__activity_payment - } else { - R.string.usdt__you_send - } - ), + stringResource(R.string.wallet__activity_payment), usdtOverviewAmount(quote.amount), R.drawable.ic_user, modifier = Modifier.weight(1f) @@ -731,6 +661,24 @@ private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) { ) } VerticalSpacer(16.dp) + if (quote.destination != UsdtDestination.ARBITRUM) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + val estimate = if (quote.bridgeProvider == UsdtBridgeProvider.ORCHESTRA) "±" else "" + UsdtDetailCell( + stringResource(R.string.usdt__recipient_gets), + estimate + usdtOverviewAmount(quote.receivedAmount), + R.drawable.ic_arrow_down_right, + modifier = Modifier.weight(1f).testTag("UsdtRecipientAmount") + ) + UsdtDetailCell( + stringResource(R.string.usdt__maximum_total), + usdtOverviewAmount(quote.amount + quote.maximumFee), + R.drawable.ic_arrow_up_right, + modifier = Modifier.weight(1f).testTag("UsdtMaximumTotal") + ) + } + VerticalSpacer(16.dp) + } if (contact != null) { SendCell(caption = stringResource(R.string.wallet__payment_request_contact)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -741,13 +689,24 @@ private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) { } VerticalSpacer(16.dp) } - SendCell( - caption = stringResource(R.string.wallet__activity_address) - ) { SelectionContainer { BodySSB(quote.recipient) } } - quote.bridgeProvider?.let { provider -> - VerticalSpacer(16.dp) - SendCell(caption = stringResource(R.string.usdt__bridge_provider)) { - BodySSB(if (provider == UsdtBridgeProvider.ORCHESTRA) "Orchestra" else "USDT0") + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + UsdtDetailField(caption = stringResource(R.string.wallet__activity_address), modifier = Modifier.weight(1f)) { + SelectionContainer { + BodySSB( + quote.recipient, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + style = UsdtDetailStyles.Value.copy(letterSpacing = 0.sp) + ) + } + } + if (quote.destination != UsdtDestination.ARBITRUM) { + UsdtDetailCell( + stringResource(R.string.usdt__recipient_network), + quote.destination.label, + R.drawable.usdt_network, + modifier = Modifier.weight(1f) + ) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSupportActions.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSupportActions.kt index e7adc60cc9..63bbf2e28d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSupportActions.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSupportActions.kt @@ -1,7 +1,7 @@ package to.bitkit.ui.screens.wallets.usdt import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -14,27 +14,32 @@ import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.Toast import to.bitkit.ui.appViewModel -import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.ButtonSize +import to.bitkit.ui.components.PrimaryButton @Composable internal fun UsdtSupportActions(details: String, enabled: Boolean = true) { val app = appViewModel ?: return val context = LocalContext.current - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - SecondaryButton( + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + PrimaryButton( text = stringResource(R.string.wallet__send_error_support), onClick = { app.navigateToReportIssue(details) }, enabled = enabled, - modifier = Modifier.testTag("UsdtContactSupport") + size = ButtonSize.Small, + textStyle = UsdtDetailStyles.SmallButton, + modifier = Modifier.weight(1f).testTag("UsdtContactSupport") ) - SecondaryButton( + PrimaryButton( text = stringResource(R.string.usdt__copy_details), onClick = { context.setClipboardText(details) app.toast(type = Toast.ToastType.SUCCESS, title = context.getString(R.string.common__copied)) }, enabled = enabled, - modifier = Modifier.testTag("UsdtCopyDetails") + size = ButtonSize.Small, + textStyle = UsdtDetailStyles.SmallButton, + modifier = Modifier.weight(1f).testTag("UsdtCopyDetails") ) } } diff --git a/app/src/main/res/drawable/ic_arrow_down_right.xml b/app/src/main/res/drawable/ic_arrow_down_right.xml new file mode 100644 index 0000000000..dbe0c7d84e --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down_right.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_arrow_up_right.xml b/app/src/main/res/drawable/ic_arrow_up_right.xml new file mode 100644 index 0000000000..079f69cde0 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_right.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70da55eedf..462bac27ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -673,9 +673,8 @@ Authorize Make sure you trust the service, browser, and device before authorizing with your pubky. USDT is turned off. - Share your existing USDT address on Arbitrum One to receive earnings directly in Bitkit. The service cannot spend your funds. + To earn, you need to share your USDT address with Paykit. It can view sales activity, but cannot spend funds. Loading USDT address… - This is your existing USDT account. The service can see its balance and transaction history, but cannot spend your funds. Your USDT address is unavailable. Please try again. %1$s server Approve @@ -1222,8 +1221,6 @@ Amount Balance Bridge needs attention - Estimated bridge cost (included) - Delivery is estimated. Bridge costs are deducted from the amount; the network fee is additional. Bridge Bridge reference USDT was returned to your wallet. See details for the refunded amount. @@ -1308,7 +1305,7 @@ <accent>DOLLARS?</accent>\nYOUR CALL New: USDT Maximum fee - Maximum total debit + Max total debit Network Select the network used by the recipient. Sending to an address on the wrong network can lose funds. Pay from @@ -1329,6 +1326,7 @@ Receiving network Receiving USDT Recipient gets + Recipient network Refund transaction Replaced by another payment Send @@ -1345,7 +1343,6 @@ To address To network {network} Track cross-network delivery - You send Activity Address All Activity diff --git a/app/src/test/java/to/bitkit/models/PaykitRequestPricingTest.kt b/app/src/test/java/to/bitkit/models/PaykitRequestPricingTest.kt index eee91c482c..e133e5389f 100644 --- a/app/src/test/java/to/bitkit/models/PaykitRequestPricingTest.kt +++ b/app/src/test/java/to/bitkit/models/PaykitRequestPricingTest.kt @@ -20,6 +20,30 @@ import kotlin.time.Instant class PaykitRequestPricingTest { private val now = Instant.fromEpochSeconds(1_800_000_000) + @Test fun `subscription methods respect selection and availability`() { + val savings = MethodId.P2wpkh.rawValue + val spending = MethodId.Bolt11.rawValue + val usdt = MethodId.UsdtArbitrum.rawValue + val available = listOf(savings, spending, usdt) + assertEquals( + listOf(spending), + PaykitRequestPricing.subscriptionEndpoints(PaykitAsset.BTC, available, listOf(spending, usdt)) + ) + assertEquals( + listOf(savings), + PaykitRequestPricing.subscriptionEndpoints(PaykitAsset.BTC, available, listOf(savings)) + ) + assertEquals(listOf(usdt), PaykitRequestPricing.subscriptionEndpoints(PaykitAsset.USD, available, available)) + assertTrue(PaykitRequestPricing.subscriptionEndpoints(PaykitAsset.BTC, available, emptyList()).isEmpty()) + assertTrue( + PaykitRequestPricing.subscriptionEndpoints( + PaykitAsset.USD, + listOf(savings, spending), + listOf(usdt) + ).isEmpty() + ) + } + @Test fun `fixed rates determine exact amounts without market rates`() { val pricing = PaykitRequestPricing( PaymentConversion.Fixed( diff --git a/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoSubscriptionTest.kt b/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoSubscriptionTest.kt index 45596aeecf..80cb0de9f1 100644 --- a/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoSubscriptionTest.kt +++ b/app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoSubscriptionTest.kt @@ -457,6 +457,7 @@ class PaykitPaymentRequestRepoSubscriptionTest : BaseUnitTest(StandardTestDispat description = " Thank you ", frequency = PaykitRecurrenceUnit.Month, expiresAt = expiresAt, + acceptedPaymentEndpointIdentifiers = listOf(MethodId.P2wpkh.rawValue), ), target = target, savedPublicKeys = listOf(COUNTERPARTY), @@ -480,8 +481,7 @@ class PaykitPaymentRequestRepoSubscriptionTest : BaseUnitTest(StandardTestDispat } with(captured.firstValue) { assertEquals("0.001", amountValue) - assertFalse(MethodId.UsdtArbitrum.rawValue in acceptedPaymentEndpointIdentifiers) - assertTrue(acceptedPaymentEndpointIdentifiers.isNotEmpty()) + assertEquals(listOf(MethodId.P2wpkh.rawValue), acceptedPaymentEndpointIdentifiers) assertEquals(null, conversion) assertEquals(expiresAt.toString(), proposalExpiresAt) assertEquals(1u, recurrence?.every) diff --git a/changelog.d/next/1494.changed.md b/changelog.d/next/1494.changed.md new file mode 100644 index 0000000000..386803068a --- /dev/null +++ b/changelog.d/next/1494.changed.md @@ -0,0 +1 @@ +Simplify Earn setup and USDT payment details, improve cross-network deposit access and support actions, and let payment requests and subscriptions choose individual receiving methods. diff --git a/docs/screens-map.md b/docs/screens-map.md index 31112fae9a..4cf62cf17d 100644 --- a/docs/screens-map.md +++ b/docs/screens-map.md @@ -225,7 +225,7 @@ Frame names are stable across handoff iterations; node ids are not, so the map l | Android | Figma | | - | - | -| UsdtDepositReceiveScreen.kt | Receive USDT › HW QR / Recipient / Specify Amount (populated) / Fee To Add Liquidity | +| UsdtDepositReceiveScreen.kt | Receive USDT › [Network selection and Cross-Network Deposits](https://www.figma.com/design/ltqvnKiejWj0JQiqtDf2JJ/Bitkit-Wallet?node-id=49893-663424) / HW QR / Recipient / Specify Amount (populated) / Fee To Add Liquidity | | UsdtDepositsScreen.kt | Activity › Transaction Cross Network USDT / Transaction Receiving USDT Tech details | | UsdtWalletScreen.kt | Savings, Spending, Hardware, USDT Balances › Wallet USDT (funds & activity) / Wallet USDT (empty) | diff --git a/docs/usdt-qa.md b/docs/usdt-qa.md index c26535e5ce..d706f7ee5a 100644 --- a/docs/usdt-qa.md +++ b/docs/usdt-qa.md @@ -12,8 +12,8 @@ A funded Arbitrum wallet uses the real gateway with USDT0 and Orchestra enabled. - [ ] Open USDT from the home balance, then Send. Verify UsdtNetwork offers Arbitrum and the enabled provider destinations. - [ ] Choose Base, open manual entry and enter an owned EVM address. Enter an affordable USDT amount and continue to review. -- [ ] Verify the review displays Base, the selected Orchestra provider in details, estimated destination receipt, included bridge cost, maximum source fee and maximum total debit. All amounts use USDT. -- [ ] Close without paying. Start a send to Polygon and obtain a quote to an owned address. Verify the selected provider is shown and the same amount and fee fields remain clear. +- [ ] Verify the basic review shows the amount, illustration, Show Details and Swipe To Pay. Open Details and verify Pay from / Network, Payment / Fee, Recipient gets / Max total debit and Address / Recipient network. Network is Arbitrum One and Recipient network is Base. The received estimate follows the quote, with an approximation marker for Orchestra; Max total debit equals the payment plus maximum fee. All amounts use USDT. +- [ ] Close without paying. Start a send to Polygon and obtain a quote to an owned address. Verify the same detail rows show the selected destination and quoted amounts. The fee note says all fees are paid in USDT and the final fee may be lower. Hide Details and verify the basic review remains free of technical rows. - [ ] Close without paying. Select Solana and verify an EVM address is rejected, then select Tron and verify an EVM address is rejected. - [ ] Close Send and verify the home balance has not changed and no outgoing payment was created. @@ -24,7 +24,7 @@ USDT is configured against the real gateway; both app and service enable Polygon - [ ] Open the home screen and verify the USDT balance, then open USDT and Send. - [ ] Choose Polygon in UsdtNetwork and open manual address entry. Verify an invalid address cannot advance. Enter an Arbitrum-specific payment URI and verify the selected network changes to Arbitrum and uses the URI recipient and amount. - [ ] Change to Polygon from amount entry. Verify the valid EVM recipient and entered amount are retained. Change to Solana and verify a compatible recipient is required; entering a plain Solana address preserves the amount, while a supported payment URI with an explicit amount replaces it. -- [ ] Enter the owned destination address and a small USDT amount. Verify the review shows Polygon, the full recipient, selected provider, estimated received amount, included Orchestra routing cost where applicable, additional maximum source fee and maximum total debit in USDT. +- [ ] Enter the owned destination address and a small USDT amount. Open review Details and verify source Network is Arbitrum One, Recipient network is Polygon, and the recipient can be read or copied in full despite its shortened display. Verify Payment, maximum Fee, quoted Recipient gets and Max total debit in USDT. Hide Details to return to the basic confirmation before paying. - [ ] Authorize one payment. Verify it appears in activity as awaiting cross-network delivery rather than claiming delivery from source-chain execution alone. - [ ] Restart the app and verify the same transfer continues tracking without sending a second payment. - [ ] Verify destination delivery independently from its transaction receipt, token, recipient, amount and balance. Confirm activity details update and the Arbitrum wallet retains zero ETH. @@ -38,7 +38,7 @@ Requires a wallet with USDT enabled and a matching backend with Orchestra creden - [ ] Open the network selector. Verify only configured supported sources are offered: Ethereum, Tron, Solana, Polygon, Base and BNB Smart Chain. - [ ] Select each enabled source network, enter an amount within its live limits, and get its deposit address. Verify the selected network, reusable address, estimated received amount and deduction are shown. Verify the QR and Copy action use the same address. - [ ] Return to Arbitrum One and verify its original wallet address remains unchanged. -- [ ] Open cross-network deposit activity. Verify empty history and Load more work without showing a failure. +- [ ] From Receive → Network, verify the full-width primary Cross-Network Deposits button below the list and artwork. Open it before any receipt exists and verify empty history and Load more work without showing a failure. - [ ] Enter an amount below the route minimum and verify the limit error appears without a deposit address. - [ ] Edit the amount. Verify the old limit error clears immediately and the keypad and button remain fully visible. Get an address with an amount within the route limits. - [ ] Reopen the app and verify the reusable address and deposit history are recovered. Inspect any existing deposit details without claiming a new settlement. @@ -88,7 +88,10 @@ Reference: [USDT designs](https://www.figma.com/design/ltqvnKiejWj0JQiqtDf2JJ/Bi - [ ] With an authorized incoming transfer or an explicitly identified fixture, verify teal receive confetti, Details and OK; Details opens that transfer. - [ ] With an authorized send or an explicitly identified fixture, check the swipe after warning and PIN prompts: loading and the completed checkmark stay at the right end, the swipe label is hidden, and confirmation keeps its side spacing until the success screen appears. - [ ] Verify bridge review, estimated fees and deposit history round displayed amounts to cents without changing the exact payment, QR or support-export values. Check a nonzero sub-cent value displays <0.01. -- [ ] Open a payment-request review and verify From and For icons use the selected payment method’s color: orange for Savings, purple for Spending and teal for USDT. +- [ ] In delayed, failed, refunding, refunded and support-needed bridge details, verify Contact Support and Copy Details are small 40-point buttons in one row, matching Request refund / Hide details. Copy Details retains the full references; Contact Support opens the prefilled report without submitting it. +- [ ] With all three receiving methods enabled and available, open request creation and verify separate Savings, Spending and USDT chips, with an X on selected chips. Remove Spending, retain Savings and USDT, and verify the received request offers only the selected methods. Empty selection disables sending. On subscription creation, verify Savings and Spending may be selected together, while choosing USDT deselects both; selecting either Bitcoin rail deselects USDT. Verify the resulting proposal carries only selected, currently available endpoints. +- [ ] Review the Earn introduction for combined Bitcoin/USDT and USDT-only authorization. Combined uses the blue heading and optional USDT switch; USDT-only uses the teal heading and its USDT-address description with no switch. Neither adds an address panel or balance/history explanation. Keep permission review, trust warning, loading/retry feedback and disabled-USDT behavior from `journeys/pubky-auth/share-receiving-details.xml`. +- [ ] Open a payment-request review and verify From and For icons use the selected payment method’s color: orange for Savings, purple for Spending and teal for USDT. Keep this request summary on the basic confirmation. ## Paykit quoted reviews and history diff --git a/journeys/paykit-quoted-usdt.xml b/journeys/paykit-quoted-usdt.xml index bd97b2a6fa..1836b62cf1 100644 --- a/journeys/paykit-quoted-usdt.xml +++ b/journeys/paykit-quoted-usdt.xml @@ -2,7 +2,7 @@ Two linked Bitkit wallets with Paykit and USDT enabled, sharing enabled by their owners, and a fresh BTC/USD rate. These request-creation and amount-screen checks need no funds and stop before payment review. Funded Arbitrum review, paid history, controlled rates and proof verification are manual tests in Docs/USDT-QA.md; a Bitcoin regtest balance does not fund USDT. Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. - Open a linked contact and create a USD payment request. Verify BTC and USD are the denomination choices and enabled receiving methods start selected. Verify Accept Payment In appears below expiry, with compact outlined Bitcoin and USDT chips. Bitcoin groups all available Savings and Spending methods; no separate rail chips appear here. + Open a linked contact and create a USD payment request. Verify BTC and USD are the denomination choices and enabled receiving methods start selected. Verify Accept Payment In appears below expiry, with separate compact outlined Savings, Spending and USDT chips for available methods. Savings is orange, Spending is purple and USDT is teal; selected chips have a trailing X. Remove one available method and verify the other selections remain unchanged; tap it again to restore it. On a comma-decimal device locale, enter 150,50 USD while creating a payment request. Verify the keypad, entered amount, request review and recipient row all show exactly 150,50 USD. Go back and verify the amount and denomination are preserved. On iOS with Classic Bitcoin display, repeat with a BTC amount and verify the keypad and amount use a dot and returning from review preserves the amount. Return to the request amount editor, select USD and replace the amount with 10. Switch to BTC and back to USD four times without editing. Verify the USD amount is still exactly 10 and each return to BTC shows the same satoshi amount. @@ -12,10 +12,10 @@ On the payer using the same comma-decimal locale, verify the incoming request card and details show 150,50 USD. Open the request and verify Amount represents exactly 150.50 USDT with USDT selected as the payment method, the amount cannot be edited, and no Bitcoin funding source is offered. Close without continuing to review. Reopen the unpaid USDT request and wait for its Amount screen. Tap Back and verify the send sheet closes without showing a Bitcoin confirmation or a Swipe To Pay control. Reopen the same request and verify its amount and USDT payment method are preserved. Close without paying. Create a USD request accepting Bitcoin and USDT whose quoted USDT amount is greater than the payer's USDT balance. On the payer, open the request and verify Bitcoin starts selected; USDT remains available for manual selection. Close without continuing. Repeat with a request whose quoted USDT amount is covered by that balance and verify USDT starts selected. This initial choice covers the requested amount only; fees are checked when preparing payment. - Create a BTC request accepting Bitcoin and USDT. On the payer's Amount screen, switch between available Savings, Spending and USDT sources. Verify the displayed amount preserves the request's quoted value and cannot be edited. Close without continuing. - Create a USD subscription for 150,50 on a comma-decimal locale. Verify the amount review shows 150,50 USD and editing preserves that amount. Verify Accepted Payments appears below frequency, with USDT selected and Bitcoin unselected. Verify the description and icon controls are side by side. Propose it to the linked contact. On the payer, verify the subscription card and details show 150,50 USD, then review its recurrence and USDT payment method. Verify the subscription swipe uses USDT teal, then close before subscribing. - Create a BTC subscription for 12,345 sats and verify Bitcoin is selected. Tap SubscriptionAccept-usdt; verify the amount converts to USD and only USDT is selected. Tap SubscriptionAccept-bitcoin; verify the amount returns to exactly 12,345 sats and only Bitcoin is selected, covering the enabled Savings and Spending methods. - Repeat the subscription USDT and Bitcoin switches four times without editing. Verify every return to Bitcoin shows exactly 12,345 sats. Edit the amount to 10 USD, repeat the switches and verify every return to USDT shows exactly 10 USD. Close without proposing. + Create a BTC request accepting Savings and USDT. If Spending is available, deselect it before sending. On the payer's Amount screen, verify Savings and USDT are available but Spending is not. Switch between the accepted sources and verify the displayed amount preserves the request's quoted value and cannot be edited. Close without continuing. + Create a USD subscription for 150,50 on a comma-decimal locale. Verify the amount review shows 150,50 USD and editing preserves that amount. Verify Accepted Payments appears below frequency, with USDT selected and any available Savings and Spending chips unselected. Verify the description and icon controls are side by side. Propose it to the linked contact. On the payer, verify the subscription card and details show 150,50 USD, then review its recurrence and USDT-only payment method. Verify the subscription swipe uses USDT teal, then close before subscribing. + Create a BTC subscription for 12,345 sats and verify available Savings and Spending chips start selected and USDT is unselected. Deselect each selected Bitcoin chip and verify Choose Recipient is disabled when none remain. Select Savings (id "SubscriptionAccept-savings"), then USDT (id "SubscriptionAccept-usdt"); verify the amount converts to USD and only USDT is selected. Select Savings again; verify the amount returns to exactly 12,345 sats and only Savings is selected. If Spending is available, select it (id "SubscriptionAccept-spending") and verify both Bitcoin rails can be selected together, with USDT still unselected. + Repeat the subscription USDT and Savings switches four times without editing. Verify every return to Bitcoin shows exactly 12,345 sats and USDT is never selected with a Bitcoin rail. Edit the amount to 10 USD, repeat the switches and verify every return to USDT shows exactly 10 USD. Select Savings only and propose to the linked contact. On the payer, verify the proposal offers Savings but not Spending or USDT, then close before subscribing. Open a linked USDT-only contact that has published a private payment list. Verify the amount screen offers only USDT, enter 0.01, verify the USDT balance is labeled AVAILABLE rather than AVAILABLE (SPENDING), then close without continuing to review. diff --git a/journeys/pubky-auth/share-receiving-details.xml b/journeys/pubky-auth/share-receiving-details.xml index 12d4f4e2c6..62936a76bb 100644 --- a/journeys/pubky-auth/share-receiving-details.xml +++ b/journeys/pubky-auth/share-receiving-details.xml @@ -2,14 +2,14 @@ Use an onboarded empty wallet with a Pubky identity and configured USDT. Enable USDT in Settings for the first checks. Use a local test auth request; cancel before Authorize so no receiving details or session are exported. Open the existing watch-only setup fixture with x-bitkit-claim=usdt-address-v1 - Verify the Earn Money introduction describes sharing the existing USDT address on Arbitrum One, with the balance/history disclosure and no optional sharing switch. Tap Approve + Verify the Earn introduction has a teal EARN MONEY heading and says: "To earn, you need to share your USDT address with Paykit. It can view sales activity, but cannot spend funds." Verify no optional sharing switch, address panel or additional balance/history explanation appears. Tap Approve Verify the authorization screen shows the requested permissions and trust warning without repeating the receiving-details controls Cancel and verify no Bitcoin account was created - Repeat with x-bitkit-claim=watch-only-account-v1.usdt-address-v1; verify the Earn Money introduction describes the Bitcoin watch-only account and shows the blue "Also share my USDT address" switch (id "PubkyAuthShareUsdt"). Turn sharing off and verify its privacy disclosure disappears. Tap Approve, then Back; verify the choice remains off. Cancel + Repeat with x-bitkit-claim=watch-only-account-v1.usdt-address-v1; verify the Earn introduction has a blue EARN MONEY heading, describes the Bitcoin watch-only account and shows the blue "Also share my USDT address" switch (id "PubkyAuthShareUsdt") without an address panel or additional balance/history explanation. Turn sharing off. Tap Approve, then Back; verify the choice remains off. Cancel Repeat with x-bitkit-claim=watch-only-account-v1; verify only Bitcoin is requested, then cancel With USDT enabled but its address temporarily unavailable, open a combined Bitcoin and USDT request. Verify sharing is off and Approve remains available; turn USDT sharing on and verify Approve is disabled until Retry loads the address Open the setup fixture with x-bitkit-claim=paykit-access-v1.watch-only-account-v1.usdt-address-v1 - Verify the introduction shows the optional USDT sharing switch and selected-address privacy disclosure. Continue to authorization; verify the named requester, exact requested permission paths and access, and Paykit Details explanation are shown without a second sharing switch or address panel + Verify the introduction shows the optional USDT sharing switch without an address panel or additional balance/history explanation. Continue to authorization; verify the named requester, exact requested permission paths and access, and Paykit Details explanation are shown without a second sharing switch or address panel Verify Before You Continue explains trusting the service, browser, and device. Verify the compact horizontal profile card (id "PubkyAuthProfile") shows the correct identity and name, with Cancel and Authorize reachable without overlap. Go back to the introduction and turn off PubkyAuthShareUsdt. Continue to authorization; verify Authorize remains enabled, then cancel Open the auth fixture with only x-bitkit-claim=paykit-access-v1; verify Paykit permissions and Details are shown without an empty Payment Details to Share section, then cancel. @@ -17,7 +17,7 @@ With USDT still off, repeat with x-bitkit-claim=paykit-access-v1.watch-only-account-v1.usdt-address-v1. Verify Bitcoin and Paykit permissions remain available without any USDT content or enable prompt, then cancel. With USDT still off, open x-bitkit-claim=paykit-access-v1.usdt-address-v1. Verify Paykit permissions appear without a receiving-details introduction or USDT content. Go back and verify the request closes without showing a Bitcoin introduction. With USDT still off, open x-bitkit-claim=usdt-address-v1. Verify the request is unavailable and authorization cannot be submitted; no enable prompt or successful setup is shown. Cancel and verify Settings still shows USDT off. - Enable USDT in Settings and reopen the combined request. Verify the optional USDT switch and selected-address privacy disclosure return on the introduction. Turn sharing off and verify Bitcoin approval remains available, then cancel. + Enable USDT in Settings and reopen the combined request. Verify the optional USDT switch returns on the introduction without an address panel or additional balance/history explanation. Turn sharing off and verify Bitcoin approval remains available, then cancel. With an owned wallet restored before its funded USDT balance has loaded, open a combined Bitcoin and USDT authorization. Let the balance refresh finish while the sheet remains open. Verify the optional USDT sharing choice appears off, its address finishes loading and Bitcoin approval remains available. Turn sharing on, then off, and verify it stays off. Cancel before Authorize. Repeat the delayed funded-balance refresh with a USDT-only authorization. Verify receiving-details consent becomes available once the address loads; continue to the permission review and cancel before Authorize. diff --git a/journeys/subscriptions/create-and-propose.xml b/journeys/subscriptions/create-and-propose.xml index b990d4e94c..5d8b93a6e4 100644 --- a/journeys/subscriptions/create-and-propose.xml +++ b/journeys/subscriptions/create-and-propose.xml @@ -14,7 +14,7 @@ Verify the create sheet (id "CreateSubscription") appears with Choose Recipient disabled Tap the amount pencil (id "SubscriptionEditAmount") and verify USD is not offered while USDT is disabled Tap the Bitcoin/fiat unit control (id "PaymentRequestAmountUnit") twice and verify both input units remain available for the BTC denomination. Select Bitcoin units, enter "5000" on the keypad, and tap Continue (id "PaymentRequestAmountContinue") - Verify the amount reads 5,000 sats and the accepted methods show only enabled Savings or Spending options, with no USDT + Verify the amount reads 5,000 sats and the accepted methods show separate selected Savings and Spending chips for enabled rails, each with a trailing X, and no USDT. If both rails are available, remove one and verify the other remains selected. Remove all selected rails and verify Choose Recipient stays disabled; restore the available Bitcoin rails before continuing. Type "Journey Sub" into the subscription name field (id "SubscriptionName") Enter 600 ASCII letters in the description field (id "SubscriptionDescription") Verify the frequency selector has "Monthly" selected (id "Tab-month") diff --git a/journeys/usdt-bridge-details.xml b/journeys/usdt-bridge-details.xml index 07c9d93c63..2b6000e323 100644 --- a/journeys/usdt-bridge-details.xml +++ b/journeys/usdt-bridge-details.xml @@ -11,9 +11,9 @@ While Cross-Network Deposits is open, inject a confirmed incoming USDT transfer through the isolated fixture. Verify no received sheet interrupts deposit details or refund review. Return to the USDT wallet and verify the received celebration appears once after any active sheet is dismissed. Open the outgoing delivering USDT activity. Verify the Delivering USDT title, outgoing amount, teal up arrow, yellow Delivering status and actual receiving network. Tap Details (id "UsdtActivityDetails"). Verify the source transaction, sender and recipient addresses, sender and recipient networks, Orchestra provider and bridge reference. Verify Open Block Explorer (id "UsdtActivityExplorer") and Hide details remain reachable. - For a delayed, failed, refunding, refunded or support-needed bridge fixture, verify Contact Support (id "UsdtContactSupport") and Copy Details (id "UsdtCopyDetails") remain reachable from its details without submitting either action. + For a delayed, failed, refunding, refunded or support-needed bridge fixture, verify Contact Support (id "UsdtContactSupport") and Copy Details (id "UsdtCopyDetails") appear side by side as small 40-point buttons, matching the Request refund / Hide details row. Verify both remain reachable and their labels are not clipped, without submitting either action. Return to the USDT activity list. Verify Pending uses a yellow hourglass and 16% yellow circle, Failed uses a red x-circle and 16% red circle, and Transferring uses the teal transfer arrows. Sent and Received remain teal. Open USDT Receive and inject one confirmed incoming USDT transfer through the isolated fixture. Verify Receive is replaced by one received celebration. Dismiss it and verify the same transfer is not celebrated again; a different subsequent transfer must still be shown. - Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") remains reachable before any Arbitrum receipt exists. + Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") is a full-width primary 56-point button below the network list and artwork, and remains reachable before any Arbitrum receipt exists. diff --git a/journeys/usdt-bridge-support.xml b/journeys/usdt-bridge-support.xml index 7d090b692b..ceea138b3f 100644 --- a/journeys/usdt-bridge-support.xml +++ b/journeys/usdt-bridge-support.xml @@ -3,14 +3,14 @@ Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. Open USDT from Home, then open a cross-network transfer in Activity. - Tap Details (id "UsdtActivityDetails"), then verify the source transaction is displayed. For an Orchestra transfer, verify the bridge reference is also displayed. + Tap Details (id "UsdtActivityDetails"), then verify the source transaction is displayed. For an Orchestra transfer, verify the bridge reference is also displayed. Verify Contact Support and Copy Details use the small 40-point buttons side by side on one row, with neither label clipped. Tap Copy Details (id "UsdtCopyDetails"). Verify the clipboard contains the bridge, networks, status and available transaction references. Tap Contact Support (id "UsdtContactSupport"). Verify the report form opens with those details prefilled and remains unsent. Return without submitting. - Open USDT Receive, then Network, scroll to Cross-Network Deposits (id "UsdtDepositHistory") and open it. Verify rows show the source network and provider status, then open a deposit. + Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") is a full-width primary 56-point button below the network list and artwork. Open it, verify rows show the source network and provider status, then open a deposit. For a deposit whose conversion order is funded, confirming, swapping, bridging or delivering without an error, verify Pending is shown. Refresh after completion and verify Confirmed. An active order with an explicit error, or an unknown status, must show Needs attention. For a refunded deposit, verify the detail shows Refunded, even when an earlier error code is retained. Verify the transaction is labeled Refund transaction in the detail. Verify no pending-refund notice remains after refreshing the completed refund. For a refund in progress, verify Refunding is shown for the provider's pending acknowledgment. A refund requiring operator intervention must still show Needs attention. - Tap Copy Details (id "UsdtCopyDetails"). Verify the clipboard includes the deposit reference, source network and source transaction, plus the current status and any destination or refund transaction. + Verify Contact Support and Copy Details use the same small side-by-side buttons in deposit details. Tap Copy Details (id "UsdtCopyDetails"). Verify the clipboard includes the deposit reference, source network and source transaction, plus the current status and any destination or refund transaction. Tap Contact Support (id "UsdtContactSupport"). Verify the report form opens with the deposit details prefilled. Return without submitting or requesting a refund. diff --git a/journeys/usdt-send-review-and-details.xml b/journeys/usdt-send-review-and-details.xml index 2209f76bba..efdaa3dd17 100644 --- a/journeys/usdt-send-review-and-details.xml +++ b/journeys/usdt-send-review-and-details.xml @@ -6,9 +6,9 @@ Open the network pill and verify the full USDT Network picker, Tether rows and selected teal check. Select another EVM network. Verify the amount and valid recipient remain. Change to Solana with an EVM recipient and verify recipient entry is required. Enter an owned plain Solana address and verify the previous amount remains. Return to Arbitrum, enter the owned EVM address and verify the amount still remains. From recipient entry with another network selected, paste an explicit Arbitrum USDT URI. Verify the selected network becomes Arbitrum and the link amount is retained. A plain 0x address preserves the selected network. Tap MAX. Verify it fills a positive amount below the balance and remains on Amount. Continue and verify the payment plus maximum fee fits within the balance. Back out without swiping. - Enter a smaller amount and continue. Verify the collapsed review shows the amount, coin illustration, Show Details and teal Swipe To Pay. Show Details reveals Pay from, Network, Payment, Fee and Address, with the fee note in the scrollable details. Displayed amounts round to cents; nonzero values below one cent show <0.01. Hide Details restores the illustration. Cancel without paying. - Prepare an ordinary amount and note the payment, fee and maximum total in Details. Go back to Amount and immediately continue without editing; verify the same review values return. Go back, change the amount and continue; verify the payment reflects the new amount. Go back, wait until the quote expires (at most two minutes) and continue; verify a new quote is obtained or an error leaves the amount editable, rather than displaying expired fees. Cancel without swiping. - Prepare a cross-network payment and continue. Verify You send labels the entered amount and Recipient gets appears directly beneath it, with an approximation marker for Orchestra. Verify the recipient amount follows the quote rather than assuming it equals the sent amount. Without scrolling, verify Show Details (id "UsdtReviewDetails"), Maximum total debit (id "UsdtMaximumTotal") and its value are fully visible above Swipe To Pay. Show Details and verify Hide Details remains visible above the maximum, Payment is labelled You send, and the fee note can be read by scrolling without overlapping the pinned total. For Orchestra, also verify the deducted bridge cost is fully readable when scrolled into view. Cancel without paying. + Enter a smaller amount and continue. Verify the collapsed review shows the amount, coin illustration, Show Details and teal Swipe To Pay. Show Details reveals Pay from, Network, Payment, Fee and Address. Verify Network is Arbitrum One and the fee note says all fees are paid in USDT and the final fee may be lower. Displayed amounts round to cents; nonzero values below one cent show <0.01. Hide Details restores the illustration. Cancel without paying. + Prepare an ordinary amount and note the payment and fee in Details. Go back to Amount and immediately continue without editing; verify the same review values return. Go back, change the amount and continue; verify the payment reflects the new amount. Go back, wait until the quote expires (at most two minutes) and continue; verify a new quote is obtained or an error leaves the amount editable, rather than displaying expired fees. Cancel without swiping. + Prepare a cross-network payment and continue. Verify the basic confirmation shows the amount, coin illustration, Show Details and teal Swipe To Pay without Recipient gets or Max total debit rows. Tap Show Details (id "UsdtReviewDetails"). Verify the two-column rows are Pay from / Network, Payment / Fee, Recipient gets / Max total debit and Address / Recipient network. Network must be Arbitrum One; Recipient network must be the chosen destination. Verify Recipient gets (id "UsdtRecipientAmount") follows the quote, with an approximation marker for Orchestra, and Max total debit (id "UsdtMaximumTotal") equals the payment plus maximum fee. Verify the recipient address can be read or copied in full despite its shortened display. Hide Details restores the basic confirmation. Cancel without paying. Using the local quote fixture, prepare a 5 USDT bridge with 2 USDT received and a small network fee. Try to proceed and verify the fee-over-half-value warning appears before payment authentication. Cancel without paying. Open an existing sent USDT activity. Verify the heading is Sent USDT, the signed amount includes the actual fee, and summary rows show Status/Network, Date/Time and Payment/Fee with teal icons. Open Details and verify transaction ID, known sender, recipient and source/destination networks; Open Block Explorer stays at the bottom. Open an existing received activity. Verify Received USDT and a positive amount. Verify Details shows the verified sender when available and does not invent one for older unknown records. Check pending, failed and transferring rows use their matching icons, and transferring shows its destination. From 93da6f0fa8ccb1e1d9c8412d1fcbb670136111e2 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 06:55:36 -0500 Subject: [PATCH 02/11] fix: align payment controls with figma --- .../components/PaykitAcceptedMethodsTest.kt | 75 +++++++++++++++++++ .../CreateSubscriptionScreenTest.kt | 19 ++++- .../wallets/usdt/UsdtPaymentContentTest.kt | 23 ++++++ .../java/to/bitkit/ui/components/Money.kt | 3 + .../ui/components/NumberPadActionButton.kt | 6 +- .../ui/components/PaykitAcceptedMethods.kt | 23 +++--- .../bitkit/ui/components/WalletBalanceView.kt | 13 +++- .../CreatePaymentRequestScreen.kt | 19 ++++- .../screens/profile/PubkyAuthApprovalSheet.kt | 1 + .../subscriptions/CreateSubscriptionScreen.kt | 22 +++++- .../components/CustomTabRowWithSpacing.kt | 13 +++- .../screens/wallets/send/SendAmountScreen.kt | 20 ++--- .../screens/wallets/usdt/UsdtSendContent.kt | 10 ++- app/src/main/res/drawable/ic_plus_small.xml | 14 ++++ docs/screens-map.md | 6 +- journeys/paykit-quoted-usdt.xml | 2 +- journeys/subscriptions/create-and-propose.xml | 2 +- journeys/usdt-send-review-and-details.xml | 4 +- 18 files changed, 228 insertions(+), 47 deletions(-) create mode 100644 app/src/androidTest/java/to/bitkit/ui/components/PaykitAcceptedMethodsTest.kt create mode 100644 app/src/main/res/drawable/ic_plus_small.xml diff --git a/app/src/androidTest/java/to/bitkit/ui/components/PaykitAcceptedMethodsTest.kt b/app/src/androidTest/java/to/bitkit/ui/components/PaykitAcceptedMethodsTest.kt new file mode 100644 index 0000000000..81e6b7e69b --- /dev/null +++ b/app/src/androidTest/java/to/bitkit/ui/components/PaykitAcceptedMethodsTest.kt @@ -0,0 +1,75 @@ +package to.bitkit.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.persistentListOf +import org.junit.Rule +import org.junit.Test +import to.bitkit.repositories.MethodId +import to.bitkit.test.annotations.ComposeUi +import to.bitkit.ui.theme.AppThemeSurface +import java.util.Locale +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@ComposeUi +class PaykitAcceptedMethodsTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun largeLocalizedMethodsWrapAndRemainReachable() { + val clicked = mutableListOf() + val methods = persistentListOf( + MethodId.P2wpkh.rawValue, + MethodId.Bolt11.rawValue, + MethodId.UsdtArbitrum.rawValue, + ) + composeTestRule.setContent { + val configuration = Configuration(LocalConfiguration.current).apply { setLocale(Locale.GERMANY) } + val localizedContext = LocalContext.current.createConfigurationContext(configuration) + CompositionLocalProvider( + LocalContext provides localizedContext, + LocalConfiguration provides configuration, + LocalDensity provides Density(LocalDensity.current.density, fontScale = 1.5f), + ) { + AppThemeSurface { + Box(modifier = Modifier.width(280.dp).testTag("MethodsBounds")) { + PaykitAcceptedMethods( + title = "USDT", + availableMethods = methods, + selectedMethods = methods, + testTagPrefix = "PaymentRequest", + onClickMethod = { clicked += it }, + ) + } + } + } + } + + val bounds = composeTestRule.onNodeWithTag("MethodsBounds").fetchSemanticsNode().boundsInRoot + val chips = PaykitPaymentOption.entries.map { option -> + composeTestRule.onNodeWithTag("PaymentRequestAccept-${option.id}").also { + it.assertIsDisplayed().performClick() + }.fetchSemanticsNode().boundsInRoot + } + chips.forEach { chip -> + assertTrue(chip.left >= bounds.left && chip.right <= bounds.right) + } + assertTrue(chips.last().top > chips.first().top) + assertEquals(PaykitPaymentOption.entries.toList(), clicked) + } +} diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt index 995c09ff10..2cd3cd9087 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreenTest.kt @@ -40,6 +40,7 @@ import to.bitkit.repositories.PaykitSubscriptionMetadata import to.bitkit.repositories.PaykitSubscriptionRecurrence import to.bitkit.repositories.PaykitSubscriptionRole import to.bitkit.test.annotations.ComposeUi +import to.bitkit.ui.components.PaykitPaymentOption import to.bitkit.ui.screens.paymentrequests.PaymentRequestExpiration import to.bitkit.ui.theme.AppThemeSurface import kotlin.test.assertEquals @@ -55,15 +56,17 @@ class CreateSubscriptionScreenTest { @Test fun detailsRequiresAmountNameAndCompletedIconLoad() { var amount by mutableStateOf(0uL) + var asset by mutableStateOf(PaykitAsset.BTC) var name by mutableStateOf("") var frequency by mutableStateOf(PaykitRecurrenceUnit.Month) var isLoadingIcon by mutableStateOf(false) var choseRecipient = false + var clickedMethod: PaykitPaymentOption? = null var acceptedMethods by mutableStateOf(persistentListOf(MethodId.P2wpkh.rawValue)) composeTestRule.setContent { AppThemeSurface { CreateSubscriptionDetails( - amount = PaykitAmount(PaykitAsset.BTC, amount), + amount = PaykitAmount(asset, amount), availableMethods = persistentListOf(MethodId.P2wpkh.rawValue, MethodId.UsdtArbitrum.rawValue), acceptedMethods = acceptedMethods, name = name, @@ -72,7 +75,7 @@ class CreateSubscriptionScreenTest { selectedIconUri = null, isLoadingIcon = isLoadingIcon, onAmountClick = {}, - onClickMethod = {}, + onClickMethod = { clickedMethod = it }, onNameChange = { name = it }, onDescriptionChange = {}, onFrequencyChange = { frequency = it }, @@ -97,9 +100,19 @@ class CreateSubscriptionScreenTest { composeTestRule.onNodeWithTag("Tab-${option.name.lowercase()}").performClick() assertEquals(option, frequency) } + composeTestRule.onNodeWithTag("SubscriptionAccept-savings").performClick() + assertEquals(PaykitPaymentOption.Savings, clickedMethod) composeTestRule.runOnIdle { acceptedMethods = persistentListOf() } composeTestRule.onNodeWithTag("SubscriptionChooseRecipient").assertIsNotEnabled() - composeTestRule.runOnIdle { acceptedMethods = persistentListOf(MethodId.P2wpkh.rawValue) } + composeTestRule.onNodeWithTag("SubscriptionAccept-savings").assertIsNotSelected() + composeTestRule.onNodeWithTag("SubscriptionAccept-usdt").performClick() + assertEquals(PaykitPaymentOption.Usdt, clickedMethod) + composeTestRule.runOnIdle { + asset = PaykitAsset.USD + acceptedMethods = persistentListOf(MethodId.UsdtArbitrum.rawValue) + } + composeTestRule.onNodeWithTag("SubscriptionAccept-usdt").assertIsSelected() + composeTestRule.onNodeWithTag("SubscriptionChooseRecipient").assertIsEnabled() composeTestRule.runOnIdle { isLoadingIcon = true } composeTestRule.onNodeWithTag("SubscriptionChooseRecipient").assertIsNotEnabled() composeTestRule.runOnIdle { isLoadingIcon = false } diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt index eb667a1fe9..be8697b620 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt @@ -6,12 +6,15 @@ import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performSemanticsAction +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.UsdtBridgeProvider import com.synonym.bitkitcore.UsdtDestination @@ -26,6 +29,8 @@ import to.bitkit.repositories.UsdtWalletState import to.bitkit.test.annotations.ComposeUi import to.bitkit.ui.theme.AppThemeSurface import java.util.Locale +import kotlin.test.assertFalse +import kotlin.test.assertTrue @HiltAndroidTest @ComposeUi @@ -83,6 +88,24 @@ class UsdtPaymentContentTest { composeTestRule.onNodeWithTag("UsdtRecipientAmount").assertDoesNotExist() } + @Test + fun recipientAddressCanBeFullyReviewed() { + composeTestRule.setContent { Content(UsdtSendState(quote = quote), UsdtPage.AMOUNT) } + composeTestRule.onNodeWithTag("UsdtReviewDetails").performScrollTo().performClick() + val address = composeTestRule.onNodeWithTag("UsdtReviewAddress") + address.performScrollTo().assertIsDisplayed() + val layouts = mutableListOf() + address.performSemanticsAction(SemanticsActions.GetTextLayoutResult) { it(layouts) } + assertTrue(layouts.single().isLineEllipsized(0)) + address.performClick() + composeTestRule.waitForIdle() + layouts.clear() + address.performSemanticsAction(SemanticsActions.GetTextLayoutResult) { it(layouts) } + assertTrue(layouts.single().lineCount > 1) + assertFalse(layouts.single().hasVisualOverflow) + composeTestRule.onNodeWithTag("UsdtConfirm").assertIsDisplayed() + } + @Test fun closingPaymentDoesNotOpenRecipientEntry() { val page = mutableStateOf(UsdtPage.AMOUNT) diff --git a/app/src/main/java/to/bitkit/ui/components/Money.kt b/app/src/main/java/to/bitkit/ui/components/Money.kt index 0558d9c7ce..9f72bae8d1 100644 --- a/app/src/main/java/to/bitkit/ui/components/Money.kt +++ b/app/src/main/java/to/bitkit/ui/components/Money.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import to.bitkit.models.BITCOIN_SYMBOL import to.bitkit.models.PrimaryDisplay @@ -128,11 +129,13 @@ fun MoneyMSB( color: Color = MaterialTheme.colorScheme.primary, accent: Color = Colors.White64, showSymbol: Boolean = unit == PrimaryDisplay.FIAT, + letterSpacing: TextUnit = TextUnit.Unspecified, ) { rememberMoneyText(sats = sats, unit = unit, showSymbol = showSymbol)?.let { text -> BodyMSB( text = text.withAccent(accentColor = accent), color = color, + letterSpacing = letterSpacing, modifier = modifier.testTag("MoneyText") ) } diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt index c0f6ebfa10..1073575317 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPadActionButton.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -51,7 +51,7 @@ fun NumberPadActionButton( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier - .requiredHeight(height) + .heightIn(min = height) .then( if (outlined) { Modifier.border(width = 1.dp, color = color, shape = buttonShape) @@ -98,7 +98,7 @@ fun NumberPadActionButton( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier - .requiredHeight(height) + .heightIn(min = height) .border( width = 1.dp, color = color, diff --git a/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt b/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt index 55c1057e86..2496d3f99e 100644 --- a/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt +++ b/app/src/main/java/to/bitkit/ui/components/PaykitAcceptedMethods.kt @@ -2,9 +2,10 @@ package to.bitkit.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.FlowRow import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.selected @@ -30,7 +31,10 @@ fun PaykitAcceptedMethods( Column(modifier = modifier) { Caption13Up(text = title, color = Colors.White64) VerticalSpacer(8.dp) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { PaykitPaymentOption.entries.forEach { option -> val endpoints = option.endpoints(availableMethods) if (endpoints.isNotEmpty()) { @@ -41,20 +45,17 @@ fun PaykitAcceptedMethods( PaykitPaymentOption.Spending -> stringResource(R.string.lightning__spending) PaykitPaymentOption.Usdt -> "USDT" }, - color = if (!selected) { - Colors.White32 - } else { - when (option) { - PaykitPaymentOption.Savings -> Colors.Brand - PaykitPaymentOption.Spending -> Colors.Purple - PaykitPaymentOption.Usdt -> Colors.Usdt - } + color = when (option) { + PaykitPaymentOption.Savings -> Colors.Brand + PaykitPaymentOption.Spending -> Colors.Purple + PaykitPaymentOption.Usdt -> Colors.Usdt }, outlined = true, textStyle = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), - trailingIcon = R.drawable.ic_x.takeIf { selected }, + trailingIcon = if (selected) R.drawable.ic_x else R.drawable.ic_plus_small, onClick = { onClickMethod(option) }, modifier = Modifier + .alpha(if (selected) 1f else 0.5f) .semantics { this.selected = selected } .testTag("${testTagPrefix}Accept-${option.id}") ) diff --git a/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt b/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt index 6adde6541c..ce2c6f8ce7 100644 --- a/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt +++ b/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -35,6 +37,7 @@ import to.bitkit.ui.currencyViewModel import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.UiConstants import to.bitkit.ui.shared.animations.BalanceAnimations +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -160,11 +163,19 @@ fun RowScope.WalletBalanceContent( icon: @Composable () -> Unit, modifier: Modifier = Modifier, titleTrailing: @Composable RowScope.() -> Unit = {}, + titleStyle: TextStyle = AppTextStyles.CaptionM, amount: @Composable () -> Unit, ) { Column(modifier = Modifier.weight(1f).then(modifier)) { Row(verticalAlignment = Alignment.CenterVertically) { - Text13Up(text = title, color = Colors.White64, maxLines = 1, modifier = Modifier.weight(1f, fill = false)) + Caption13Up( + text = title, + color = Colors.White64, + style = titleStyle, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false) + ) titleTrailing() } VerticalSpacer(8.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 8318e6f7e9..1f99aee971 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -86,6 +87,7 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.NumberPadInputHandler @@ -392,7 +394,11 @@ internal fun PaymentRequestDetailsContent( ) } VerticalSpacer(32.dp) - Caption13Up(text = stringResource(R.string.wallet__payment_request_note), color = Colors.White64) + Caption13Up( + text = stringResource(R.string.wallet__payment_request_note), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) VerticalSpacer(8.dp) TextInput( value = note, @@ -415,11 +421,12 @@ internal fun PaymentRequestDetailsContent( ) { PubkyContactAvatar(profile = contact, size = 40.dp) Column(modifier = Modifier.padding(start = 16.dp).weight(1f)) { - BodyMSB(text = contact.name, maxLines = 1) - BodyS( + BodyMSB(text = contact.name, maxLines = 1, letterSpacing = 0.sp) + CaptionB( text = note.ifBlank { stringResource(R.string.wallet__payment_request) }, color = Colors.White64, maxLines = 1, + letterSpacing = 0.sp, ) } PaykitAmountCell(initialDraft.amount) @@ -438,7 +445,11 @@ internal fun PaymentRequestDetailsContent( .testTag("PaymentRequestExpiry${option.name}"), ) { VerticalSpacer(8.dp) - CaptionB(text = option.title(), color = if (isSelected) Colors.White else Colors.White50) + CaptionB( + text = option.title(), + color = if (isSelected) Colors.White else Colors.White50, + letterSpacing = 0.sp, + ) VerticalSpacer(8.dp) HorizontalDivider( thickness = 2.dp, diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt index 488fdf5ac3..79fe726fc7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt @@ -371,6 +371,7 @@ private fun UsdtSharingChoice( BodyM( text = stringResource(R.string.profile__auth_approval_share_usdt_optional), color = Colors.White64, + letterSpacing = 0.sp, modifier = Modifier.weight(1f) ) Switch( diff --git a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt index c859bbeeb7..85e086468f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage @@ -100,6 +101,7 @@ import to.bitkit.ui.screens.wallets.activity.components.TabItem import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -343,7 +345,11 @@ internal fun CreateSubscriptionDetails( modifier = Modifier.testTag("SubscriptionAcceptedMethods") ) VerticalSpacer(24.dp) - Caption13Up(text = stringResource(R.string.subscriptions__name), color = Colors.White64) + Caption13Up( + text = stringResource(R.string.subscriptions__name), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) VerticalSpacer(8.dp) TextInput( value = name, @@ -355,7 +361,11 @@ internal fun CreateSubscriptionDetails( VerticalSpacer(24.dp) Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Column(modifier = Modifier.weight(1f)) { - Caption13Up(text = stringResource(R.string.subscriptions__description), color = Colors.White64) + Caption13Up( + text = stringResource(R.string.subscriptions__description), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) VerticalSpacer(8.dp) TextInput( value = description, @@ -367,7 +377,11 @@ internal fun CreateSubscriptionDetails( ) } Column { - Caption13Up(text = stringResource(R.string.subscriptions__custom_icon), color = Colors.White64) + Caption13Up( + text = stringResource(R.string.subscriptions__custom_icon), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) VerticalSpacer(8.dp) val iconDescription = stringResource(R.string.subscriptions__custom_icon_description) Box( @@ -416,6 +430,8 @@ private fun SubscriptionFrequencyPicker( currentTabIndex = creationFrequencies.indexOfFirst { it.unit == selected }, onTabChange = { onSelected(it.unit) }, selectedColor = Colors.White, + labelStyle = AppTextStyles.CaptionB.copy(letterSpacing = 0.sp), + labelMinHeight = 18.dp, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index 7c7284920f..b79f26aeb0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -22,14 +22,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import to.bitkit.ui.components.CaptionB import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.Colors @Composable @@ -42,6 +45,8 @@ fun CustomTabRowWithSpacing( /** Labels that replace a tab's own text, for tabs whose name depends on runtime data. */ labelOverrides: ImmutableMap = persistentMapOf(), badgeCount: (T) -> Int? = { null }, + labelStyle: TextStyle = AppTextStyles.CaptionB, + labelMinHeight: Dp = TabBadgeSize, ) { Column(modifier = modifier) { Row( @@ -59,6 +64,8 @@ fun CustomTabRowWithSpacing( text = labelOverrides[tab] ?: tab.uiText, color = if (isSelected) Colors.White else Colors.White50, badgeCount = badgeCount(tab)?.takeIf { it > 0 }, + style = labelStyle, + minHeight = labelMinHeight, modifier = Modifier .fillMaxWidth() .clickableAlpha { onTabChange(tab) } @@ -100,10 +107,12 @@ private fun TabLabel( color: Color, badgeCount: Int?, modifier: Modifier = Modifier, + style: TextStyle = AppTextStyles.CaptionB, + minHeight: Dp = TabBadgeSize, ) { Layout( contents = listOf( - { CaptionB(text, maxLines = 1, overflow = TextOverflow.Ellipsis, color = color) }, + { CaptionB(text, maxLines = 1, overflow = TextOverflow.Ellipsis, color = color, style = style) }, { badgeCount?.let { count -> Box( @@ -125,7 +134,7 @@ private fun TabLabel( constraints.copy(minWidth = 0, maxWidth = (constraints.maxWidth - 2 * reserved).coerceAtLeast(0)) ) val width = constraints.maxWidth - val height = maxOf(label.height, TabBadgeSize.roundToPx()) + val height = maxOf(label.height, minHeight.roundToPx(), badge?.height ?: 0) layout(width, height) { val labelX = (width - label.width) / 2 diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index dda7333e32..2669b9857e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -35,9 +35,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview 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.LnurlPayData @@ -85,6 +87,7 @@ import to.bitkit.ui.shared.UiConstants import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.NumberPadInputHandler @@ -511,6 +514,10 @@ private fun FundingSourceOption(option: SendFundingOption, state: SendUiState, o WalletBalanceContent( title = title, icon = { FundingSourceIcon(source) }, + titleStyle = AppTextStyles.CaptionM.copy( + letterSpacing = 1.sp, + lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None), + ), titleTrailing = { option.transportType?.let { transport -> HorizontalSpacer(8.dp) @@ -556,7 +563,8 @@ private fun FundingSourceBalance(option: SendFundingOption) { val source = option.source if (source == SendFundingSource.Usdt) { BodyMSB( - if (hidden) UiConstants.HIDE_BALANCE_SHORT else usdt?.balance?.let { usdtOverviewAmount(it) } ?: "—" + if (hidden) UiConstants.HIDE_BALANCE_SHORT else usdt?.balance?.let { usdtOverviewAmount(it) } ?: "—", + letterSpacing = 0.sp, ) } else { MoneyMSB( @@ -567,15 +575,7 @@ private fun FundingSourceBalance(option: SendFundingOption) { }, unit = PrimaryDisplay.BITCOIN, showSymbol = false, - ) - } - if (source == SendFundingSource.Spending) { - HorizontalSpacer(4.dp) - Icon( - painterResource(R.drawable.ic_transfer), - contentDescription = null, - tint = Colors.White64, - modifier = Modifier.size(16.dp) + letterSpacing = 0.sp, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt index 82dce1207d..9694b05fb5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt @@ -627,6 +627,7 @@ private fun UsdtReviewHeader( @Composable private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) { + 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( @@ -694,9 +695,12 @@ private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) { SelectionContainer { BodySSB( quote.recipient, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - style = UsdtDetailStyles.Value.copy(letterSpacing = 0.sp) + maxLines = if (showFullAddress) Int.MAX_VALUE else 1, + overflow = if (showFullAddress) TextOverflow.Clip else TextOverflow.MiddleEllipsis, + style = UsdtDetailStyles.Value.copy(letterSpacing = 0.sp), + modifier = Modifier + .clickable { showFullAddress = !showFullAddress } + .testTag("UsdtReviewAddress") ) } } diff --git a/app/src/main/res/drawable/ic_plus_small.xml b/app/src/main/res/drawable/ic_plus_small.xml new file mode 100644 index 0000000000..627d96dc33 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_small.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/docs/screens-map.md b/docs/screens-map.md index 4cf62cf17d..eee81e5181 100644 --- a/docs/screens-map.md +++ b/docs/screens-map.md @@ -70,7 +70,7 @@ Frame names are stable across handoff iterations; node ids are not, so the map l | Android | Figma | | - | - | -| CreatePaymentRequestScreen.kt | Payment Request (from Receive > Contacts OR Contact profile OR Payment Requests) › Request payment enter amount / Payment Request (from Edit invoice) › [Request payment details](https://www.figma.com/design/ltqvnKiejWj0JQiqtDf2JJ/Bitkit-Wallet?node-id=49115-131077) | +| CreatePaymentRequestScreen.kt | Payment Request (from Receive > Contacts OR Contact profile OR Payment Requests) › Request payment enter amount / Payment Request (from Edit invoice) › Request payment details | | IncomingPaymentRequestDetailsScreen.kt | Subscriptions › Payment Request Details | | PaymentRequestsScreen.kt | Payment Requests › Payment Requests | @@ -121,7 +121,7 @@ Frame names are stable across handoff iterations; node ids are not, so the map l | Android | Figma | | - | - | -| CreateSubscriptionScreen.kt | Subscriptions › [Create Subscription](https://www.figma.com/design/ltqvnKiejWj0JQiqtDf2JJ/Bitkit-Wallet?node-id=49027-623659) / Choose Subscription Recipient / Sent Subscription Proposal | +| CreateSubscriptionScreen.kt | Subscriptions › Create Subscription / Choose Subscription Recipient / Sent Subscription Proposal | | SubscriptionsScreen.kt | Subscriptions › Subscriptions Intro / Subscriptions overview | ## ui/screens/transfer @@ -225,7 +225,7 @@ Frame names are stable across handoff iterations; node ids are not, so the map l | Android | Figma | | - | - | -| UsdtDepositReceiveScreen.kt | Receive USDT › [Network selection and Cross-Network Deposits](https://www.figma.com/design/ltqvnKiejWj0JQiqtDf2JJ/Bitkit-Wallet?node-id=49893-663424) / HW QR / Recipient / Specify Amount (populated) / Fee To Add Liquidity | +| UsdtDepositReceiveScreen.kt | Receive USDT › Network selection and Cross-Network Deposits / HW QR / Recipient / Specify Amount (populated) / Fee To Add Liquidity | | UsdtDepositsScreen.kt | Activity › Transaction Cross Network USDT / Transaction Receiving USDT Tech details | | UsdtWalletScreen.kt | Savings, Spending, Hardware, USDT Balances › Wallet USDT (funds & activity) / Wallet USDT (empty) | diff --git a/journeys/paykit-quoted-usdt.xml b/journeys/paykit-quoted-usdt.xml index 1836b62cf1..dc561d2f42 100644 --- a/journeys/paykit-quoted-usdt.xml +++ b/journeys/paykit-quoted-usdt.xml @@ -2,7 +2,7 @@ Two linked Bitkit wallets with Paykit and USDT enabled, sharing enabled by their owners, and a fresh BTC/USD rate. These request-creation and amount-screen checks need no funds and stop before payment review. Funded Arbitrum review, paid history, controlled rates and proof verification are manual tests in Docs/USDT-QA.md; a Bitcoin regtest balance does not fund USDT. Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. - Open a linked contact and create a USD payment request. Verify BTC and USD are the denomination choices and enabled receiving methods start selected. Verify Accept Payment In appears below expiry, with separate compact outlined Savings, Spending and USDT chips for available methods. Savings is orange, Spending is purple and USDT is teal; selected chips have a trailing X. Remove one available method and verify the other selections remain unchanged; tap it again to restore it. + Open a linked contact and create a USD payment request. Verify BTC and USD are the denomination choices and enabled receiving methods start selected. Verify Accept Payment In appears below expiry, with separate compact outlined Savings, Spending and USDT chips for available methods. Savings is orange, Spending is purple and USDT is teal; selected chips have a trailing X. Unselected chips retain their asset color at 50% opacity and show a trailing +. Remove one available method and verify the other selections remain unchanged; tap it again to restore it. With long translated method names on a narrow screen, verify the chips wrap onto another row without clipping their labels. On a comma-decimal device locale, enter 150,50 USD while creating a payment request. Verify the keypad, entered amount, request review and recipient row all show exactly 150,50 USD. Go back and verify the amount and denomination are preserved. On iOS with Classic Bitcoin display, repeat with a BTC amount and verify the keypad and amount use a dot and returning from review preserves the amount. Return to the request amount editor, select USD and replace the amount with 10. Switch to BTC and back to USD four times without editing. Verify the USD amount is still exactly 10 and each return to BTC shows the same satoshi amount. diff --git a/journeys/subscriptions/create-and-propose.xml b/journeys/subscriptions/create-and-propose.xml index 5d8b93a6e4..865f17e073 100644 --- a/journeys/subscriptions/create-and-propose.xml +++ b/journeys/subscriptions/create-and-propose.xml @@ -14,7 +14,7 @@ Verify the create sheet (id "CreateSubscription") appears with Choose Recipient disabled Tap the amount pencil (id "SubscriptionEditAmount") and verify USD is not offered while USDT is disabled Tap the Bitcoin/fiat unit control (id "PaymentRequestAmountUnit") twice and verify both input units remain available for the BTC denomination. Select Bitcoin units, enter "5000" on the keypad, and tap Continue (id "PaymentRequestAmountContinue") - Verify the amount reads 5,000 sats and the accepted methods show separate selected Savings and Spending chips for enabled rails, each with a trailing X, and no USDT. If both rails are available, remove one and verify the other remains selected. Remove all selected rails and verify Choose Recipient stays disabled; restore the available Bitcoin rails before continuing. + Verify the amount reads 5,000 sats and the accepted methods show separate selected Savings and Spending chips for enabled rails, each with a trailing X, and no USDT. A removed method retains its asset color at 50% opacity with a trailing +; tap it to restore it. If both rails are available, remove one and verify the other remains selected. Remove all selected rails and verify Choose Recipient stays disabled; restore the available Bitcoin rails before continuing. Type "Journey Sub" into the subscription name field (id "SubscriptionName") Enter 600 ASCII letters in the description field (id "SubscriptionDescription") Verify the frequency selector has "Monthly" selected (id "Tab-month") diff --git a/journeys/usdt-send-review-and-details.xml b/journeys/usdt-send-review-and-details.xml index efdaa3dd17..b669f920b6 100644 --- a/journeys/usdt-send-review-and-details.xml +++ b/journeys/usdt-send-review-and-details.xml @@ -8,10 +8,10 @@ Tap MAX. Verify it fills a positive amount below the balance and remains on Amount. Continue and verify the payment plus maximum fee fits within the balance. Back out without swiping. Enter a smaller amount and continue. Verify the collapsed review shows the amount, coin illustration, Show Details and teal Swipe To Pay. Show Details reveals Pay from, Network, Payment, Fee and Address. Verify Network is Arbitrum One and the fee note says all fees are paid in USDT and the final fee may be lower. Displayed amounts round to cents; nonzero values below one cent show <0.01. Hide Details restores the illustration. Cancel without paying. Prepare an ordinary amount and note the payment and fee in Details. Go back to Amount and immediately continue without editing; verify the same review values return. Go back, change the amount and continue; verify the payment reflects the new amount. Go back, wait until the quote expires (at most two minutes) and continue; verify a new quote is obtained or an error leaves the amount editable, rather than displaying expired fees. Cancel without swiping. - Prepare a cross-network payment and continue. Verify the basic confirmation shows the amount, coin illustration, Show Details and teal Swipe To Pay without Recipient gets or Max total debit rows. Tap Show Details (id "UsdtReviewDetails"). Verify the two-column rows are Pay from / Network, Payment / Fee, Recipient gets / Max total debit and Address / Recipient network. Network must be Arbitrum One; Recipient network must be the chosen destination. Verify Recipient gets (id "UsdtRecipientAmount") follows the quote, with an approximation marker for Orchestra, and Max total debit (id "UsdtMaximumTotal") equals the payment plus maximum fee. Verify the recipient address can be read or copied in full despite its shortened display. Hide Details restores the basic confirmation. Cancel without paying. + Prepare a cross-network payment and continue. Verify the basic confirmation shows the amount, coin illustration, Show Details and teal Swipe To Pay without Recipient gets or Max total debit rows. Tap Show Details (id "UsdtReviewDetails"). Verify the two-column rows are Pay from / Network, Payment / Fee, Recipient gets / Max total debit and Address / Recipient network. Network must be Arbitrum One; Recipient network must be the chosen destination. Verify Recipient gets (id "UsdtRecipientAmount") follows the quote, with an approximation marker for Orchestra, and Max total debit (id "UsdtMaximumTotal") equals the payment plus maximum fee. Tap the shortened address (id "UsdtReviewAddress") and verify the full value is shown and can be selected and copied. Tap again to restore the compact row. Hide Details restores the basic confirmation. Cancel without paying. Using the local quote fixture, prepare a 5 USDT bridge with 2 USDT received and a small network fee. Try to proceed and verify the fee-over-half-value warning appears before payment authentication. Cancel without paying. Open an existing sent USDT activity. Verify the heading is Sent USDT, the signed amount includes the actual fee, and summary rows show Status/Network, Date/Time and Payment/Fee with teal icons. Open Details and verify transaction ID, known sender, recipient and source/destination networks; Open Block Explorer stays at the bottom. Open an existing received activity. Verify Received USDT and a positive amount. Verify Details shows the verified sender when available and does not invent one for older unknown records. Check pending, failed and transferring rows use their matching icons, and transferring shows its destination. - Prepare a contact payment that accepts multiple payment methods. Open the payment-source button and verify the balance menu shows Savings, Spending, any paired hardware wallet and USDT. Unsupported methods are disabled. Select a supported method and verify its label, available balance and payable amount update. Cancel without paying. + Prepare a contact payment that accepts multiple payment methods. Open the payment-source button and verify the balance menu shows Savings, Spending, any paired hardware wallet and USDT. Unsupported methods are disabled and shown at 30% opacity. Verify the menu has 24px outer padding, 24px asset icons, 13/18 uppercase labels and 17/22 balances, with no extra transfer icon next to Spending. Select a supported method and verify its label, available balance and payable amount update. Cancel without paying. From 2057b382303a0c2ee608815c12324d242ea688b2 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 07:34:23 -0500 Subject: [PATCH 03/11] fix: match usdt screen typography --- .../java/to/bitkit/ui/components/Button.kt | 3 +- .../to/bitkit/ui/components/SwipeToConfirm.kt | 4 ++ .../main/java/to/bitkit/ui/components/Text.kt | 3 +- .../java/to/bitkit/ui/scaffold/SheetTopBar.kt | 4 ++ .../CreatePaymentRequestScreen.kt | 1 + .../screens/profile/PubkyAuthApprovalSheet.kt | 20 +++++++++- .../subscriptions/CreateSubscriptionScreen.kt | 1 + .../wallets/usdt/UsdtDepositReceiveScreen.kt | 2 + .../screens/wallets/usdt/UsdtSendContent.kt | 37 +++++++++++++------ journeys/usdt-payment-links-and-scans.xml | 3 +- 10 files changed, 61 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/Button.kt b/app/src/main/java/to/bitkit/ui/components/Button.kt index 4488d8c666..696f95906e 100644 --- a/app/src/main/java/to/bitkit/ui/components/Button.kt +++ b/app/src/main/java/to/bitkit/ui/components/Button.kt @@ -190,6 +190,7 @@ fun SecondaryButton( fullWidth: Boolean = true, hazeState: HazeState? = null, testTag: String? = null, + textStyle: TextStyle? = null, ) { val contentPadding = PaddingValues(horizontal = size.secondaryHorizontalPadding.takeIf { text != null } ?: 0.dp) val border = size.secondaryBorder(enabled) @@ -256,7 +257,7 @@ fun SecondaryButton( text?.let { Text( text = text, - style = size.textStyle(), + style = textStyle ?: size.textStyle(), maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt index a693d8740f..d6bb1c327f 100644 --- a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt +++ b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -40,6 +41,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset @@ -71,6 +73,7 @@ fun SwipeToConfirm( loading: Boolean = false, confirmed: Boolean = false, progress: MutableFloatState? = null, + textStyle: TextStyle = LocalTextStyle.current, ) { val scope = rememberCoroutineScope() val trailColor = remember(color) { color.copy(alpha = 0.24f) } @@ -136,6 +139,7 @@ fun SwipeToConfirm( // Text Text( text = text, + style = textStyle, fontWeight = FontWeight.SemiBold, modifier = Modifier .align(Alignment.Center) diff --git a/app/src/main/java/to/bitkit/ui/components/Text.kt b/app/src/main/java/to/bitkit/ui/components/Text.kt index 52856df36f..71f2976133 100644 --- a/app/src/main/java/to/bitkit/ui/components/Text.kt +++ b/app/src/main/java/to/bitkit/ui/components/Text.kt @@ -164,10 +164,11 @@ fun Subtitle( textAlign: TextAlign = TextAlign.Start, maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = if (maxLines == 1) TextOverflow.Ellipsis else TextOverflow.Clip, + style: TextStyle = AppTextStyles.Subtitle, ) { Text( text = text, - style = AppTextStyles.Subtitle.merge( + style = style.merge( color = color, textAlign = textAlign, ), diff --git a/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt index 71442da66a..0a65dbc530 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt @@ -13,10 +13,12 @@ import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.ui.components.Subtitle +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface /** For pixel perfection from FIGMA, use the back arrow + 14 padding as starting point for added space under this. */ @@ -25,6 +27,7 @@ fun SheetTopBar( titleText: String?, modifier: Modifier = Modifier, action: (@Composable () -> Unit)? = null, + titleStyle: TextStyle = AppTextStyles.Subtitle, onBack: (() -> Unit)? = null, ) { Box( @@ -37,6 +40,7 @@ fun SheetTopBar( text = titleText, textAlign = TextAlign.Center, maxLines = 1, + style = titleStyle, modifier = Modifier .fillMaxWidth() .padding(horizontal = LocalMinimumInteractiveComponentSize.current) diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 1f99aee971..56fc24662b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -477,6 +477,7 @@ internal fun PaymentRequestDetailsContent( } PrimaryButton( text = stringResource(R.string.wallet__payment_request_send_request), + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), enabled = !isCreating && selectedMethods.isNotEmpty(), isLoading = isCreating, onClick = { onSend(updatedDraft(trimNote = true)) }, diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt index 79fe726fc7..ca0dbbb85d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt @@ -59,6 +59,7 @@ import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.HorizontalSpacer @@ -72,6 +73,7 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.theme.AppSwitchDefaults +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.rememberBiometricAuthSupported @@ -241,7 +243,15 @@ private fun Content( .navigationBarsPadding() .padding(horizontal = 16.dp) ) { - SheetTopBar(titleText = headerTitle, onBack = onBack) + SheetTopBar( + titleText = headerTitle, + onBack = onBack, + titleStyle = if (approvalState == ApprovalState.WatchOnlyConsent) { + AppTextStyles.Subtitle.copy(letterSpacing = 0.sp, lineHeight = 22.sp) + } else { + AppTextStyles.Subtitle + }, + ) when (approvalState) { ApprovalState.Loading -> LoadingContent() @@ -341,12 +351,14 @@ private fun ColumnScope.WatchOnlyConsentContent( SecondaryButton( text = stringResource(R.string.common__cancel), onClick = onCancel, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.weight(1f).testTag("PubkyAuthWatchOnlyCancel") ) PrimaryButton( text = stringResource(R.string.profile__auth_approval_watch_only_intro_approve), onClick = onApprove, enabled = uiState.canAuthorize, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.weight(1f).testTag("PubkyAuthWatchOnlyApprove") ) } @@ -434,12 +446,14 @@ private fun ColumnScope.AuthorizeContent( SecondaryButton( text = stringResource(R.string.common__cancel), onClick = onCancel, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.weight(1f), ) PrimaryButton( text = stringResource(R.string.profile__auth_approval_authorize), onClick = onAuthorize, enabled = uiState.canAuthorize, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier .weight(1f) .testTag("PubkyAuthAuthorize") @@ -586,6 +600,7 @@ private fun ColumnScope.SuccessContent( PrimaryButton( text = stringResource(R.string.profile__auth_approval_ok), onClick = onDismiss, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.testTag("PubkyAuthOK") ) VerticalSpacer(16.dp) @@ -714,10 +729,11 @@ private fun ProfileCard(profile: PubkyProfile) { } Column(modifier = Modifier.weight(1f)) { - Text13Up( + Caption13Up( text = profile.authDisplayPublicKey, color = Colors.White64, maxLines = 1, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), ) BodyMSB( text = profile.name, diff --git a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt index 85e086468f..e774aabbec 100644 --- a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt @@ -412,6 +412,7 @@ internal fun CreateSubscriptionDetails( } PrimaryButton( text = stringResource(R.string.subscriptions__choose_recipient), + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), onClick = onChooseRecipient, enabled = amount.atomic > 0uL && name.isNotBlank() && !isLoadingIcon && acceptedMethods.isNotEmpty(), modifier = Modifier.testTag("SubscriptionChooseRecipient") diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt index 70ebc9aa9b..dbf3a43afe 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt @@ -48,6 +48,7 @@ import to.bitkit.ui.components.usdtOverviewAmount import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing import to.bitkit.ui.screens.wallets.receive.ReceiveTab +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.Colors import java.text.DecimalFormatSymbols import java.text.NumberFormat @@ -257,6 +258,7 @@ private fun ColumnScope.NetworkSelector( PrimaryButton( stringResource(R.string.usdt__deposit_history), onClick = onHistory, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.testTag("UsdtDepositHistory") ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt index 9694b05fb5..1d46a58de0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt @@ -95,7 +95,6 @@ import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.NumberPadInputHandler -import to.bitkit.ui.utils.withAccent import kotlin.time.Duration.Companion.seconds @Suppress("CyclomaticComplexMethod") @@ -434,26 +433,36 @@ private fun UsdtAmountContent( modifier = Modifier.fillMaxWidth(), ) { Column(modifier = Modifier.padding(end = 8.dp).align(Alignment.CenterVertically)) { - Caption13Up(stringResource(R.string.wallet__send_available), color = Colors.White64) + Caption13Up( + stringResource(R.string.wallet__send_available), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) VerticalSpacer(4.dp) - BodySSB( - ( - if (hideBalance) { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + if (!hideBalance && wallet.balance != null) BodySSB("$", color = Colors.White64) + BodySSB( + text = if (hideBalance) { UiConstants.HIDE_BALANCE_SHORT } else { - wallet.balance?.let { - "$ " + usdtOverviewAmount(it) - } ?: "—" - } - ).withAccent(accentColor = Colors.White64) - ) + wallet.balance?.let { usdtOverviewAmount(it) } ?: "—" + }, + style = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), + ) + } } Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.align(Alignment.CenterVertically), ) { - NumberPadActionButton(text = "USDT", color = Colors.Usdt, enabled = false, onClick = {}) + NumberPadActionButton( + text = "USDT", + color = Colors.Usdt, + enabled = false, + textStyle = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + onClick = {}, + ) if (onMaximum != null && amountEditable) { NumberPadActionButton( text = stringResource(R.string.common__max), @@ -461,6 +470,7 @@ private fun UsdtAmountContent( onClick = onMaximum, enabled = !state.busy, isLoading = state.preparingMaximum, + textStyle = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), modifier = Modifier.testTag("UsdtMax") ) } @@ -492,6 +502,7 @@ private fun UsdtAmountContent( onClick = onReview, enabled = amount.isNotEmpty() && !state.busy, isLoading = state.busy && !state.preparingMaximum, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.testTag("UsdtReview") ) } @@ -552,6 +563,7 @@ private fun UsdtConfirmation( loading = state.busy && !completed, confirmed = state.busy || state.authenticationRequired || completed, progress = swipeProgress, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.fillMaxWidth().testTag("UsdtConfirm") ) } @@ -565,6 +577,7 @@ private fun ColumnScope.UsdtReviewDetailsButton(showDetails: Boolean, onClick: ( ), size = ButtonSize.Small, onClick = onClick, + textStyle = UsdtDetailStyles.SmallButton, fullWidth = false, color = Colors.White06, enableGradient = false, diff --git a/journeys/usdt-payment-links-and-scans.xml b/journeys/usdt-payment-links-and-scans.xml index 85c2151c5d..7162345e20 100644 --- a/journeys/usdt-payment-links-and-scans.xml +++ b/journeys/usdt-payment-links-and-scans.xml @@ -10,12 +10,13 @@ Open USDT from Home, open a USDT link, then open a second link with another owned recipient and amount. Verify the first amount and recipient remain. Cancel without paying. Cancel the send. Open the same link without uint256. Verify the amount screen opens with no amount and accepts a new amount. Cancel the send. From the main scanner, paste or import a QR containing the same amount-bearing URI. Verify it opens the USDT send flow with the same recipient and amount. + Cancel the send. Scan another owned wallet's bare 0x USDT address from the main scanner, then repeat through ordinary Send paste and Enter Manually. Verify each opens USDT with the same recipient, an empty amount, Arbitrum One selected and the network chooser available. In hardware-wallet manual entry, verify the same address shows the Bitcoin-address-required warning and Continue stays disabled. Cancel without paying. Cancel the send. From the ordinary Send options, paste the URI. Verify it opens the USDT send flow. Cancel, choose Enter Manually, enter the same URI and continue. Verify the USDT recipient and exact amount are preserved. Repeat with a wrong-chain URI and in hardware-wallet Send; verify neither opens a USDT payment. In hardware-wallet manual entry, verify the Bitcoin-address-required warning appears and Continue stays disabled. Replace the URI with a valid Bitcoin address and verify Continue becomes available. Cancel, then open Send again from the USDT wallet and verify the previous recipient and amount are cleared. Open or scan variants with the wrong chain, wrong token, duplicate amount and malformed recipient. Verify an error and no payable review. Check an ordinary Bitcoin or Lightning invoice still opens its existing send flow. On iOS, open a USDT link while Lightning is stopped but the wallet is ready. Verify it routes without starting Lightning. Open two different USDT links while the main scanner sheet is dismissing; verify an older delayed link cannot replace the review that opened first. Enter this wallet’s own Arbitrum address in USDT Send, enter an amount and continue. Verify the error explicitly says you cannot send USDT to your own address, and no payment is submitted. Repeat with MAX. On Android, enter this wallet’s own bare Arbitrum address through Home Send → Enter Manually and verify the self-send message appears while Continue stays disabled. Enter the entire available USDT balance and continue. Verify the fee-collection error explains that the transaction fee could not be paid in USDT and suggests a smaller amount. Reduce the amount, reach review and close without signing. With an empty wallet and no pending transfers, turn USDT off in Settings. Copy an owned bare USDT address, return to the app, and verify no automatic payment or enable prompt. Repeat with an amount-bearing USDT payment URI. Copy a valid Bitcoin address and a Lightning invoice in turn; verify their normal clipboard prompts still appear and cancel without paying. - With USDT off, explicitly open the same USDT URI and verify the designed Enable USDT modal appears. Decline and verify a quiet return with no stuck spinner. Scan its QR, enable USDT, and verify the original recipient and exact amount resume. Cancel without paying. + With USDT off, explicitly open the same USDT URI and verify the designed Enable USDT modal appears. Decline and verify a quiet return with no stuck spinner. Scan its QR, enable USDT, and verify the original recipient and exact amount resume. Cancel without paying. Turn USDT off again and repeat the explicit scan, decline and enable checks with the owned bare 0x address; verify the recipient resumes with an empty amount. From 86e370ccbf7235d7240770dbb9934d4133bd469e Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 08:43:02 -0500 Subject: [PATCH 04/11] fix: refine usdt layouts and payment entry --- .../PaymentRequestAmountLocaleTest.kt | 83 ++++++++++++++++++ .../wallets/usdt/UsdtBridgeDetailsTest.kt | 37 +++++++- .../wallets/usdt/UsdtPaymentContentTest.kt | 87 +++++++++++++++---- .../CreatePaymentRequestScreen.kt | 7 +- .../screens/profile/PubkyAuthApprovalSheet.kt | 16 ++-- .../screens/wallets/send/SendAmountScreen.kt | 66 +++++++++----- .../wallets/usdt/UsdtActivityDetail.kt | 13 ++- .../wallets/usdt/UsdtDepositReceiveScreen.kt | 12 +-- .../wallets/usdt/UsdtDepositsScreen.kt | 14 ++- .../wallets/usdt/UsdtReceiveAmountContent.kt | 4 +- .../wallets/usdt/UsdtReceiveContent.kt | 2 + .../screens/wallets/usdt/UsdtSendContent.kt | 2 +- .../screens/wallets/usdt/UsdtWalletScreen.kt | 6 +- .../bitkit/ui/sheets/NewTransactionSheet.kt | 50 ++++++++--- .../to/bitkit/ui/sheets/UsdtIntroSheet.kt | 18 +++- app/src/main/res/values/strings.xml | 2 + changelog.d/next/1494.changed.md | 2 +- journeys/paykit-quoted-usdt.xml | 2 +- .../pubky-auth/share-receiving-details.xml | 1 + journeys/usdt-bridge-support.xml | 1 + journeys/usdt-opt-in.xml | 1 + journeys/usdt-send-review-and-details.xml | 1 + 22 files changed, 342 insertions(+), 85 deletions(-) create mode 100644 app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt new file mode 100644 index 0000000000..5a54a6b2a3 --- /dev/null +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestAmountLocaleTest.kt @@ -0,0 +1,83 @@ +@file:OptIn(ExperimentalTime::class) + +package to.bitkit.ui.screens.paymentrequests + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import kotlinx.collections.immutable.persistentListOf +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import to.bitkit.models.PaykitAmount +import to.bitkit.models.PaykitAsset +import to.bitkit.repositories.AmountInputHandler +import to.bitkit.repositories.PaykitPaymentRequestDraft +import to.bitkit.test.annotations.ComposeUi +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.viewmodels.AmountInputViewModel +import java.util.Locale +import kotlin.test.assertEquals +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@ComposeUi +class PaymentRequestAmountLocaleTest { + private val originalLocale = Locale.getDefault() + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun setup() { + Locale.setDefault(Locale.GERMANY) + } + + @After + fun teardown() { + Locale.setDefault(originalLocale) + } + + @Test + fun commaDecimalEntryPreservesExactCentsAndTrailingZeros() { + var submitted: PaykitPaymentRequestDraft? = null + val amountInput = AmountInputViewModel(AmountInputHandler.stub()) + composeTestRule.setContent { + AppThemeSurface { + CompositionLocalProvider(LocalInspectionMode provides true) { + PaymentRequestAmountContent( + amountInputViewModel = amountInput, + initialDraft = PaykitPaymentRequestDraft( + amount = PaykitAmount(PaykitAsset.USD, 0uL), + note = "", + expiresAt = Instant.fromEpochSeconds(2_000_000_000), + ), + contact = null, + onBack = {}, + onContinue = { submitted = it }, + allowedAssets = persistentListOf(PaykitAsset.USD), + ) + } + } + } + + composeTestRule.onNodeWithText(",", useUnmergedTree = true).assertIsDisplayed() + listOf("N1", "N5", "N0", "NDecimal").forEach { + composeTestRule.onNodeWithTag(it).performClick() + } + composeTestRule.onNodeWithText("150,", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestAmountContinue").assertIsEnabled() + composeTestRule.onNodeWithTag("N5").performClick() + composeTestRule.onNodeWithTag("N0").performClick() + composeTestRule.onNodeWithText("150,50", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestAmountContinue").performClick() + + assertEquals(PaykitAmount(PaykitAsset.USD, 15_050uL), submitted?.amount) + } +} diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtBridgeDetailsTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtBridgeDetailsTest.kt index ef1094302a..a6e2634938 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtBridgeDetailsTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtBridgeDetailsTest.kt @@ -8,7 +8,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.test.assertHeightIsAtLeast import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsNotEnabled @@ -19,6 +21,8 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp import androidx.test.platform.app.InstrumentationRegistry import com.synonym.bitkitcore.UsdtDeposit import com.synonym.bitkitcore.UsdtDepositDetail @@ -152,7 +156,7 @@ class UsdtBridgeDetailsTest { composeTestRule.waitForIdle() composeTestRule.onNodeWithText("USDT address on BSC").performScrollTo().assertIsDisplayed() composeTestRule.onNodeWithTag("UsdtDepositRefund").assertIsNotEnabled() - assertHeight("UsdtRefundAddress", 52f) + assertHeight("UsdtRefundAddress", 56f) screenshot("receiving-details") val refundField = composeTestRule.onNodeWithTag("UsdtRefundAddress") refundField.performTextInput(address.take(20)) @@ -165,17 +169,19 @@ class UsdtBridgeDetailsTest { composeTestRule.onNodeWithTag("UsdtDepositRefund").performScrollTo().performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Confirm Refund").assertIsDisplayed() - composeTestRule.onNodeWithText("50,00 USDT", substring = true).assertIsDisplayed() + composeTestRule.onNodeWithText("50,000001 USDT", substring = true).assertIsDisplayed() composeTestRule.onNodeWithText("Refund address:\n" + address, substring = true).assertIsDisplayed() composeTestRule.onNodeWithText("BNB Smart Chain (BSC)", substring = true).assertIsDisplayed() screenshot("refund") composeTestRule.onNodeWithTag("UsdtRefundCancel").performClick() composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag("UsdtDepositDetails").performClick() + waitForClickDebounce() + composeTestRule.onNodeWithTag("UsdtDepositDetails").performScrollTo().performClick() composeTestRule.waitForIdle() + screenshot("receiving-summary-after-refund") composeTestRule.onNodeWithText("Needs attention").assertIsDisplayed() waitForClickDebounce() - composeTestRule.onNodeWithTag("UsdtDepositDetails").performClick() + composeTestRule.onNodeWithTag("UsdtDepositDetails").performScrollTo().performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText(address).performScrollTo().assertIsDisplayed() composeTestRule.onNodeWithTag("UsdtDepositHistory").performClick() @@ -198,6 +204,29 @@ class UsdtBridgeDetailsTest { } } + @Test + fun refundAddressExpandsForLargeText() { + val enteredAddress = mutableStateOf("") + composeTestRule.setContent { + CompositionLocalProvider(LocalDensity provides Density(LocalDensity.current.density, fontScale = 2f)) { + Frame { + UsdtDepositsContent( + state = DepositUiState(detail = UsdtDepositDetail(deposit, null)), + address = enteredAddress.value, hideBalance = false, + onBack = {}, onAddress = { enteredAddress.value = it }, + onDetail = {}, onHistory = {}, onMore = {}, onRefresh = {}, onRefund = {}, + ) + } + } + } + composeTestRule.onNodeWithTag("UsdtDepositDetails").performScrollTo().performClick() + val field = composeTestRule.onNodeWithTag("UsdtRefundAddress") + field.performScrollTo().performTextInput(address) + field.assertTextContains(address) + screenshot("refund-large-font") + field.assertHeightIsAtLeast(64.dp) + } + @Composable private fun Frame(content: @Composable () -> Unit) { AppThemeSurface { diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt index be8697b620..43abe29a24 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt @@ -4,8 +4,10 @@ import android.os.SystemClock import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule @@ -19,8 +21,11 @@ import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.UsdtBridgeProvider import com.synonym.bitkitcore.UsdtDestination import com.synonym.bitkitcore.UsdtQuote +import com.synonym.bitkitcore.UsdtTransfer +import com.synonym.bitkitcore.UsdtTransferStatus import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.collections.immutable.persistentListOf import org.junit.After import org.junit.Before import org.junit.Rule @@ -121,27 +126,71 @@ class UsdtPaymentContentTest { composeTestRule.onNodeWithTag("UsdtPaste").assertDoesNotExist() } + @Test + fun directPaymentShowsCompletionBeforeSuccess() { + assertCompletionTransition(UsdtDestination.ARBITRUM, UsdtTransferStatus.PENDING) + } + + @Test + fun bridgeShowsPendingUntilDeliveryThenCompletion() { + assertCompletionTransition(UsdtDestination.POLYGON, UsdtTransferStatus.BRIDGING) + } + + private fun assertCompletionTransition(destination: UsdtDestination, pendingStatus: UsdtTransferStatus) { + val payment = quote.copy(destination = destination) + val transfer = UsdtTransfer( + id = payment.id, sender = null, txHash = null, userOperationHash = null, + bridgeGuid = null, orchestra = null, recipient = payment.recipient, + destination = destination, amount = payment.amount, receivedAmount = payment.receivedAmount, + fee = null, isIncoming = false, status = pendingStatus, timestamp = 1u, + ) + val wallet = mutableStateOf(UsdtWalletState(transfers = persistentListOf(transfer))) + composeTestRule.mainClock.autoAdvance = false + composeTestRule.setContent { + Content(UsdtSendState(quote = payment, submitted = true), UsdtPage.AMOUNT, wallet = wallet.value) + } + composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.onNodeWithTag("UsdtConfirm").assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtSendSuccess").assertDoesNotExist() + composeTestRule.runOnUiThread { + wallet.value = wallet.value.copy(transfers = persistentListOf(transfer.copy(status = UsdtTransferStatus.CONFIRMED))) + } + composeTestRule.mainClock.advanceTimeBy(500) + composeTestRule.onNodeWithTag("UsdtConfirm").assertIsDisplayed() + composeTestRule.onNodeWithTag("UsdtSendSuccess").assertDoesNotExist() + composeTestRule.mainClock.advanceTimeBy(700) + composeTestRule.onNodeWithTag("UsdtConfirm").assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtSendSuccess").assertIsDisplayed() + } + @Composable - private fun Content(state: UsdtSendState, page: UsdtPage, onDetails: () -> Unit = {}) { + private fun Content( + state: UsdtSendState, + page: UsdtPage, + wallet: UsdtWalletState = UsdtWalletState(), + onDetails: () -> Unit = {}, + ) { AppThemeSurface { - Box(modifier = Modifier.size(width = 360.dp, height = 600.dp)) { - UsdtPaymentContent( - wallet = UsdtWalletState(), - state = state, - page = page, - recipient = quote.recipient, - amount = "3", - destination = quote.destination, - onRecipientChange = {}, - onAmountChange = {}, - onRecipientContinue = {}, - onPageChange = {}, - onBack = {}, - onReview = {}, - onConfirm = {}, - onDone = {}, - onDetails = onDetails, - ) + CompositionLocalProvider(LocalInspectionMode provides true) { + Box(modifier = Modifier.size(width = 360.dp, height = 600.dp)) { + UsdtPaymentContent( + wallet = wallet, + state = state, + page = page, + recipient = quote.recipient, + amount = "3", + destination = state.quote?.destination ?: quote.destination, + onRecipientChange = {}, + onAmountChange = {}, + onRecipientContinue = {}, + onPageChange = {}, + onBack = {}, + onReview = {}, + onConfirm = {}, + onDone = {}, + onDetails = onDetails, + ) + } } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt index 56fc24662b..eec6be7dbc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/CreatePaymentRequestScreen.kt @@ -94,6 +94,8 @@ import to.bitkit.ui.utils.NumberPadInputHandler import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.AppViewModel +import java.text.DecimalFormatSymbols +import java.util.Locale import kotlin.math.abs import kotlin.time.Clock import kotlin.time.Duration @@ -148,6 +150,8 @@ internal fun PaymentRequestAmountContent( ) { val currencies = LocalCurrencies.current val amountState by amountInputViewModel.uiState.collectAsStateWithLifecycle() + val locale = Locale.getDefault() + val decimalSeparator = remember(locale) { DecimalFormatSymbols.getInstance(locale).decimalSeparator } var asset by remember(initialDraft.amount.asset) { mutableStateOf(initialDraft.amount.asset) } var dollars by remember(initialDraft.amount) { @@ -200,7 +204,7 @@ internal fun PaymentRequestAmountContent( ) } else { NumberPadAmountText( - value = dollars.ifEmpty { "0" }, + value = dollars.ifEmpty { "0" }.replace('.', decimalSeparator), symbol = "$", modifier = Modifier.testTag("PaymentRequestAmountField") ) @@ -257,6 +261,7 @@ internal fun PaymentRequestAmountContent( }, type = NumberPadType.DECIMAL, availableHeight = availableHeight, + decimalSeparator = decimalSeparator.toString(), modifier = Modifier.testTag("PaymentRequestNumberPad") ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt index ca0dbbb85d..50dfb19caf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt @@ -331,10 +331,10 @@ private fun ColumnScope.WatchOnlyConsentContent( VerticalSpacer(8.dp) BodyM( text = stringResource( - if (uiState.bitkitClaim?.sharesBitcoin == true) { - R.string.profile__auth_approval_watch_only_intro_description - } else { - R.string.profile__auth_approval_usdt_intro_description + when { + uiState.bitkitClaim?.sharesBitcoin == true -> R.string.profile__auth_approval_watch_only_intro_description + uiState.usdtRequired -> R.string.profile__auth_approval_usdt_intro_description + else -> R.string.profile__auth_approval_usdt_intro_optional } ), color = Colors.White64, @@ -381,7 +381,13 @@ private fun UsdtSharingChoice( .testTag("PubkyAuthShareUsdt") ) { BodyM( - text = stringResource(R.string.profile__auth_approval_share_usdt_optional), + text = stringResource( + if (uiState.bitkitClaim?.sharesBitcoin == true) { + R.string.profile__auth_approval_share_usdt_optional + } else { + R.string.profile__auth_approval_share_usdt + } + ), color = Colors.White64, letterSpacing = 0.sp, modifier = Modifier.weight(1f) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 2669b9857e..625e5bfa00 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -63,6 +63,7 @@ import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.BottomSheetPreview +import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth import to.bitkit.ui.components.HorizontalSpacer @@ -316,7 +317,7 @@ private fun SendAmountNodeRunning( ) } - FillHeight(min = 12.dp) + if (uiState.paykitUsesUsdt) VerticalSpacer(32.dp) else FillHeight(min = 12.dp) if (uiState.paykitRateUnavailable) { BodyS( stringResource(R.string.wallet__payment_request_rate_unavailable), @@ -344,11 +345,20 @@ private fun SendAmountNodeRunning( ) { onClickMax(availableAmount) } .testTag("AvailableAmount") ) { - Text13Up( - text = stringResource(textAvailable), - color = Colors.White64, - modifier = Modifier.testTag("available_balance") - ) + if (uiState.paykitUsesUsdt) { + Caption13Up( + text = stringResource(textAvailable), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + modifier = Modifier.testTag("available_balance") + ) + } else { + Text13Up( + text = stringResource(textAvailable), + color = Colors.White64, + modifier = Modifier.testTag("available_balance") + ) + } VerticalSpacer(4.dp) if (uiState.paykitUsesUsdt) { val balance = if (hideBalance) { @@ -356,7 +366,10 @@ private fun SendAmountNodeRunning( } else { usdtWallet?.balance?.let { "$ " + usdtOverviewAmount(it) } ?: "—" } - BodySSB(balance.withAccent(accentColor = Colors.White64)) + BodySSB( + balance.withAccent(accentColor = Colors.White64), + style = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), + ) } else { MoneySSB(sats = availableAmount, showSymbol = true) } @@ -385,8 +398,8 @@ private fun SendAmountNodeRunning( .testTag("SendAmountMax") ) } - HorizontalSpacer(8.dp) if (!uiState.paykitUsesUsdt) { + HorizontalSpacer(8.dp) UnitButton( onClick = { amountInputViewModel.switchUnit(currencies) }, modifier = Modifier @@ -396,20 +409,25 @@ private fun SendAmountNodeRunning( } } - HorizontalDivider(modifier = Modifier.padding(top = 12.dp)) + HorizontalDivider(modifier = Modifier.padding(top = if (uiState.paykitUsesUsdt) 16.dp else 12.dp)) if (uiState.paykitUsesUsdt) { - NumberPad( - type = NumberPadType.DECIMAL, - availableHeight = maxHeight, - enabled = uiState.isAmountEditable, - onPress = { - app?.onUsdtAmountChange( - NumberPadInputHandler.handleInput(it, uiState.paykitAmount, maxLength = 21, maxDecimals = 6) - ) - }, - onDeleteLongPress = { app?.onUsdtAmountChange("") } - ) + BoxWithConstraints( + contentAlignment = Alignment.Center, + modifier = Modifier.weight(1f).fillMaxWidth() + ) { + NumberPad( + type = NumberPadType.DECIMAL, + availableHeight = this.maxHeight, + enabled = uiState.isAmountEditable, + onPress = { + app?.onUsdtAmountChange( + NumberPadInputHandler.handleInput(it, uiState.paykitAmount, maxLength = 21, maxDecimals = 6) + ) + }, + onDeleteLongPress = { app?.onUsdtAmountChange("") } + ) + } } else { NumberPad( enabled = uiState.isAmountEditable, @@ -421,12 +439,13 @@ private fun SendAmountNodeRunning( ) } - if (uiState.paykitUsesUsdt) VerticalSpacer(24.dp) + if (uiState.paykitUsesUsdt) VerticalSpacer(16.dp) PrimaryButton( text = stringResource(R.string.common__continue), enabled = uiState.isAmountInputValid && !uiState.isFundingSourceLoading, isLoading = uiState.isLoading, onClick = onContinue, + textStyle = if (uiState.paykitUsesUsdt) AppTextStyles.BodySSB.copy(letterSpacing = 0.sp) else null, modifier = Modifier.testTag("ContinueAmount") ) @@ -458,6 +477,11 @@ private fun PaymentMethodButton( else -> stringResource(R.string.wallet__spending__title) }, color = paymentMethodColor(uiState), + textStyle = if (uiState.paykitUsesUsdt) { + AppTextStyles.CaptionM.copy(letterSpacing = 1.sp) + } else { + AppTextStyles.CaptionM + }, icon = if (uiState.canSwitchFundingSource) R.drawable.ic_transfer else null, onClick = { expanded = true }, enabled = uiState.fundingOptions.isNotEmpty(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt index 4ff616ed18..37c42653fd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -112,6 +113,8 @@ internal fun UsdtActivityDetail( val app = appViewModel val canOpenDeposits = transfer.isIncoming && Env.usdtDepositsUrl != null var showDetails by rememberSaveable(transfer.id) { mutableStateOf(false) } + val scrollState = rememberScrollState() + LaunchedEffect(transfer.id, showDetails) { scrollState.scrollTo(0) } Column(modifier = Modifier.fillMaxSize()) { AppTopBar( titleText = stringResource( @@ -125,7 +128,7 @@ internal fun UsdtActivityDetail( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .weight(1f) - .verticalScroll(rememberScrollState()) + .verticalScroll(scrollState) .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) ) { UsdtActivityAmountHeader( @@ -308,7 +311,13 @@ private fun UsdtActivitySummary(transfer: UsdtTransfer, hideBalance: Boolean) { modifier = Modifier.weight(1f).testTag("UsdtActivityStatus") ) UsdtDetailCell( - stringResource(R.string.usdt__receiving_network), + stringResource( + if (transfer.status == UsdtTransferStatus.CONFIRMED) { + R.string.usdt__network + } else { + R.string.usdt__receiving_network + } + ), transfer.destination.label, R.drawable.usdt_network, modifier = Modifier.weight(1f) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt index dbf3a43afe..ec3e36737b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt @@ -37,8 +37,8 @@ import to.bitkit.models.PaykitAsset import to.bitkit.repositories.UsdtWalletState import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SendCell @@ -197,7 +197,7 @@ private fun ColumnScope.Content( 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) + BodyM(stringResource(R.string.usdt__deposit_fee_note), color = Colors.White64, letterSpacing = 0.sp) VerticalSpacer(24.dp) EstimateRow( stringResource(R.string.usdt__receive_fee), @@ -211,7 +211,7 @@ private fun ColumnScope.Content( FillHeight() PrimaryButton(stringResource(R.string.common__continue), onClick = { onPage(ReceivePage.ADDRESS) - }, modifier = Modifier.testTag("UsdtReceiveFeesContinue")) + }, textStyle = UsdtDetailStyles.Body, modifier = Modifier.testTag("UsdtReceiveFeesContinue")) } ReceivePage.ADDRESS -> { val atomic = runCatching { PaykitAmount.parseInput(PaykitAsset.USDT, amount).atomic }.getOrNull() @@ -266,10 +266,10 @@ private fun ColumnScope.NetworkSelector( @Composable private fun EstimateRow(title: String, value: String) { - SendCell(caption = title, dividerSpacing = 24.dp) { + SendCell(caption = title, captionStyle = UsdtDetailStyles.Caption, dividerSpacing = 24.dp) { Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - BodyMSB("± $", color = Colors.White64) - BodyMSB(value) + BodySSB("± $", color = Colors.White64, style = UsdtDetailStyles.Value) + BodySSB(value, style = UsdtDetailStyles.Body) } } VerticalSpacer(24.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositsScreen.kt index ef261d12cc..2b9fb9db1d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositsScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -26,6 +25,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -217,6 +217,9 @@ internal fun UsdtDepositsContent( ) { val detail = state.detail var showDetails by rememberSaveable(detail?.deposit?.id) { mutableStateOf(false) } + val scrollState = rememberScrollState() + val focusManager = LocalFocusManager.current + LaunchedEffect(detail?.deposit?.id, showDetails) { scrollState.scrollTo(0) } Column(modifier = Modifier.fillMaxSize().imePadding()) { AppTopBar( titleText = stringResource( @@ -233,7 +236,7 @@ internal fun UsdtDepositsContent( ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + modifier = Modifier.fillMaxSize().verticalScroll(scrollState) .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 16.dp) ) { if (detail == null) { @@ -260,7 +263,10 @@ internal fun UsdtDepositsContent( busy = state.busy, address = address, onRefund = onRefund, - onToggleDetails = { showDetails = !showDetails }, + onToggleDetails = { + focusManager.clearFocus() + showDetails = !showDetails + }, ) if (showDetails) { if (detail.canRefund && detail.deposit.network.depositNetwork() == UsdtDepositNetwork.TRON) { @@ -466,7 +472,7 @@ private fun UsdtDepositTechnicalDetails( textStyle = UsdtDetailStyles.Body, placeholderColor = Colors.White.copy(alpha = 0.5f), placeholder = stringResource(R.string.usdt__deposit_refund_address).replace("{network}", network), - modifier = Modifier.fillMaxWidth().height(52.dp).testTag("UsdtRefundAddress") + modifier = Modifier.fillMaxWidth().testTag("UsdtRefundAddress") ) } else { UsdtDepositSummary(detail) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt index 2ae43b1e07..2ae1e189b1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveAmountContent.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.synonym.bitkitcore.UsdtException import to.bitkit.R import to.bitkit.models.PaykitAmount @@ -40,7 +41,7 @@ internal fun ColumnScope.UsdtReceiveAmountContent( Column(verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier.heightIn(min = 146.dp)) { UsdtAmountHeader(amount.ifEmpty { "0" }) depositAmountMessage(amount, limits, error)?.let { message -> - BodyS(message, color = Colors.Brand, modifier = Modifier.padding(vertical = 16.dp)) + BodyS(message, color = Colors.Brand, letterSpacing = 0.sp, modifier = Modifier.padding(vertical = 16.dp)) } } HorizontalDivider() @@ -72,6 +73,7 @@ internal fun ColumnScope.UsdtReceiveAmountContent( onClick = onContinue, enabled = !busy && validAmount, isLoading = busy, + textStyle = UsdtDetailStyles.Body, modifier = Modifier.testTag("UsdtDepositCreate") ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveContent.kt index b7c62408be..dcff736537 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtReceiveContent.kt @@ -90,6 +90,7 @@ internal fun ColumnScope.UsdtReceiveContent( text = networkTitle, onClick = onNetwork, size = ButtonSize.Small, + textStyle = UsdtDetailStyles.SmallButton, icon = { Icon( painterResource(R.drawable.usdt_network), @@ -116,6 +117,7 @@ internal fun ColumnScope.UsdtReceiveContent( null }, onClick = { showDetails = !showDetails }, + textStyle = UsdtDetailStyles.Body, modifier = Modifier.testTag("UsdtReceiveDetails") ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt index 1d46a58de0..b0e8980ab8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt @@ -151,7 +151,7 @@ internal fun UsdtPaymentContent( showSuccess = true } } - val showSubmitted = state.submitted && !state.busy + val showSubmitted = state.submitted && !state.busy && !completed if (showSuccess && completed && quote != null) { NewTransactionSheetView( details = NewTransactionSheetDetails( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt index 39fa41d13e..36c03b12be 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtWalletScreen.kt @@ -91,11 +91,12 @@ fun RowScope.UsdtWalletBalance(onClick: () -> Unit, viewModel: UsdtViewModel = h val settings by viewModel.settings.collectAsStateWithLifecycle() WalletBalanceContent( title = "USDT", + titleStyle = UsdtDetailStyles.Caption, icon = { Image( painterResource(R.drawable.tether_circle), contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.padding(end = 4.dp).size(24.dp) ) }, modifier = Modifier.clickableAlpha(onClick = onClick).padding(vertical = 4.dp) @@ -108,7 +109,8 @@ fun RowScope.UsdtWalletBalance(onClick: () -> Unit, viewModel: UsdtViewModel = h wallet.balance?.let { usdtOverviewAmount(it) } ?: "—" - } + }, + style = UsdtDetailStyles.HistoryTitle ) } } diff --git a/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt index 15516ecbab..2779847022 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.airbnb.lottie.LottieProperty import com.airbnb.lottie.compose.LottieAnimation @@ -55,6 +56,7 @@ import to.bitkit.ui.components.usdtOverviewAmount import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.localizedRandom @@ -156,20 +158,7 @@ fun NewTransactionSheetView( .testTag("transaction_content_column") .padding(horizontal = 16.dp), ) { - val titleText = stringResource( - when { - details.usdtAmount != null -> if (details.direction == NewTransactionSheetDirection.SENT) { - R.string.usdt__payment_sent - } else { - R.string.usdt__payment_received - } - details.direction == NewTransactionSheetDirection.SENT -> R.string.wallet__send_sent - details.type == NewTransactionSheetType.LIGHTNING -> R.string.wallet__instant_payment_received - else -> R.string.wallet__payment_received - } - ) - - SheetTopBar(titleText) + TransactionHeader(details) Spacer(modifier = Modifier.height(if (details.usdtAmount != null) 8.dp else 24.dp)) @@ -200,6 +189,31 @@ fun NewTransactionSheetView( } } +@Composable +private fun TransactionHeader(details: NewTransactionSheetDetails) { + val titleText = stringResource( + when { + details.usdtAmount != null -> if (details.direction == NewTransactionSheetDirection.SENT) { + R.string.usdt__payment_sent + } else { + R.string.usdt__payment_received + } + details.direction == NewTransactionSheetDirection.SENT -> R.string.wallet__send_sent + details.type == NewTransactionSheetType.LIGHTNING -> R.string.wallet__instant_payment_received + else -> R.string.wallet__payment_received + } + ) + + SheetTopBar( + titleText, + titleStyle = if (details.usdtAmount != null && details.direction == NewTransactionSheetDirection.RECEIVED) { + AppTextStyles.Subtitle.copy(letterSpacing = 0.sp, lineHeight = 22.sp) + } else { + AppTextStyles.Subtitle + } + ) +} + private val ConfettiShade = Color(0xFF8E8E93) @Composable @@ -247,6 +261,11 @@ private fun TransactionButtons( onCloseClick: () -> Unit, onDetailClick: () -> Unit ) { + val buttonStyle = if (details.usdtAmount != null) { + AppTextStyles.BodySSB.copy(letterSpacing = 0.sp) + } else { + null + } if (details.direction == NewTransactionSheetDirection.SENT || details.usdtTransferId != null) { Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -257,6 +276,7 @@ private fun TransactionButtons( SecondaryButton( text = stringResource(R.string.wallet__send_details), onClick = onDetailClick, + textStyle = buttonStyle, enabled = !details.isLoadingDetails && ( details.activityId != null || details.paymentHashOrTxId != null || details.usdtTransferId != null ), @@ -271,6 +291,7 @@ private fun TransactionButtons( stringResource(R.string.common__close) }, onClick = onCloseClick, + textStyle = buttonStyle, modifier = Modifier .weight(1f) .testTag("Close") @@ -280,6 +301,7 @@ private fun TransactionButtons( PrimaryButton( text = localizedRandom(R.string.common__ok_random), onClick = onCloseClick, + textStyle = buttonStyle, modifier = Modifier.testTag("ReceivedTransactionButton") ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/UsdtIntroSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/UsdtIntroSheet.kt index fb9c2f8ee5..51a5b966fa 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/UsdtIntroSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/UsdtIntroSheet.kt @@ -3,7 +3,7 @@ package to.bitkit.ui.sheets import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -11,6 +11,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -26,6 +27,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.usdt.UsdtCoinIllustration import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -35,7 +37,10 @@ fun UsdtIntroSheet(onDecline: () -> Unit, onEnable: () -> Unit, modifier: Modifi Column( modifier = modifier.sheetHeight().background(Colors.Gray7).navigationBarsPadding().testTag("UsdtIntro") ) { - SheetTopBar(stringResource(R.string.usdt__intro_title)) + SheetTopBar( + stringResource(R.string.usdt__intro_title), + titleStyle = AppTextStyles.Subtitle.copy(letterSpacing = 0.sp, lineHeight = 22.sp), + ) Column( verticalArrangement = Arrangement.Bottom, modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()).padding(horizontal = 16.dp) @@ -54,16 +59,23 @@ fun UsdtIntroSheet(onDecline: () -> Unit, onEnable: () -> Unit, modifier: Modifi ) } VerticalSpacer(32.dp) - Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.padding(horizontal = 16.dp)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = if (LocalDensity.current.fontScale > 1.3f) 1 else 2, + modifier = Modifier.padding(horizontal = 16.dp) + ) { SecondaryButton( stringResource(R.string.usdt__intro_decline), onClick = onDecline, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), testTag = "UsdtIntroDecline", modifier = Modifier.weight(1f) ) PrimaryButton( stringResource(R.string.usdt__enable), onClick = onEnable, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), modifier = Modifier.weight(1f).testTag("UsdtIntroEnable") ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 462bac27ac..fb93e82778 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -663,6 +663,7 @@ <accent>%1$s</accent> is requesting permission to access and edit your <accent>%2$s</accent> data. Unknown service " and " + Share my USDT address Also share my USDT address Also, you shared a watch-only Bitcoin account with Paykit. Also, you shared a watch-only Bitcoin account and USDT address with Paykit. @@ -674,6 +675,7 @@ Make sure you trust the service, browser, and device before authorizing with your pubky. USDT is turned off. To earn, you need to share your USDT address with Paykit. It can view sales activity, but cannot spend funds. + You can share your USDT address with Paykit to receive earnings. It can view sales activity, but cannot spend funds. Loading USDT address… Your USDT address is unavailable. Please try again. %1$s server diff --git a/changelog.d/next/1494.changed.md b/changelog.d/next/1494.changed.md index 386803068a..0529636b1e 100644 --- a/changelog.d/next/1494.changed.md +++ b/changelog.d/next/1494.changed.md @@ -1 +1 @@ -Simplify Earn setup and USDT payment details, improve cross-network deposit access and support actions, and let payment requests and subscriptions choose individual receiving methods. +Refine Earn and USDT payment screens, improve receive fees and activity details, keep refund inputs readable with large text, and let requests and subscriptions choose individual receiving methods. diff --git a/journeys/paykit-quoted-usdt.xml b/journeys/paykit-quoted-usdt.xml index dc561d2f42..cb4c453351 100644 --- a/journeys/paykit-quoted-usdt.xml +++ b/journeys/paykit-quoted-usdt.xml @@ -16,6 +16,6 @@ Create a USD subscription for 150,50 on a comma-decimal locale. Verify the amount review shows 150,50 USD and editing preserves that amount. Verify Accepted Payments appears below frequency, with USDT selected and any available Savings and Spending chips unselected. Verify the description and icon controls are side by side. Propose it to the linked contact. On the payer, verify the subscription card and details show 150,50 USD, then review its recurrence and USDT-only payment method. Verify the subscription swipe uses USDT teal, then close before subscribing. Create a BTC subscription for 12,345 sats and verify available Savings and Spending chips start selected and USDT is unselected. Deselect each selected Bitcoin chip and verify Choose Recipient is disabled when none remain. Select Savings (id "SubscriptionAccept-savings"), then USDT (id "SubscriptionAccept-usdt"); verify the amount converts to USD and only USDT is selected. Select Savings again; verify the amount returns to exactly 12,345 sats and only Savings is selected. If Spending is available, select it (id "SubscriptionAccept-spending") and verify both Bitcoin rails can be selected together, with USDT still unselected. Repeat the subscription USDT and Savings switches four times without editing. Verify every return to Bitcoin shows exactly 12,345 sats and USDT is never selected with a Bitcoin rail. Edit the amount to 10 USD, repeat the switches and verify every return to USDT shows exactly 10 USD. Select Savings only and propose to the linked contact. On the payer, verify the proposal offers Savings but not Spending or USDT, then close before subscribing. - Open a linked USDT-only contact that has published a private payment list. Verify the amount screen offers only USDT, enter 0.01, verify the USDT balance is labeled AVAILABLE rather than AVAILABLE (SPENDING), then close without continuing to review. + Open a linked USDT-only contact that has published a private payment list. Verify the amount screen offers only USDT, enter 0.01, and verify the balance is labeled AVAILABLE rather than AVAILABLE (SPENDING). Compare with direct USDT Send: the amount, balance, USDT chip and Continue use the same typography; the balance stays close below the amount and the keypad uses the remaining space without clipping Continue. Close without continuing to review. diff --git a/journeys/pubky-auth/share-receiving-details.xml b/journeys/pubky-auth/share-receiving-details.xml index 62936a76bb..dce45f880a 100644 --- a/journeys/pubky-auth/share-receiving-details.xml +++ b/journeys/pubky-auth/share-receiving-details.xml @@ -7,6 +7,7 @@ Cancel and verify no Bitcoin account was created Repeat with x-bitkit-claim=watch-only-account-v1.usdt-address-v1; verify the Earn introduction has a blue EARN MONEY heading, describes the Bitcoin watch-only account and shows the blue "Also share my USDT address" switch (id "PubkyAuthShareUsdt") without an address panel or additional balance/history explanation. Turn sharing off. Tap Approve, then Back; verify the choice remains off. Cancel Repeat with x-bitkit-claim=watch-only-account-v1; verify only Bitcoin is requested, then cancel + With USDT enabled, open x-bitkit-claim=paykit-access-v1.usdt-address-v1. Verify the teal Earn introduction says "You can share your USDT address with Paykit to receive earnings" and the optional switch says "Share my USDT address", without "Also" or mandatory-sharing copy. Turn sharing off, continue to authorization and go back; verify the choice remains off. Cancel before Authorize. With USDT enabled but its address temporarily unavailable, open a combined Bitcoin and USDT request. Verify sharing is off and Approve remains available; turn USDT sharing on and verify Approve is disabled until Retry loads the address Open the setup fixture with x-bitkit-claim=paykit-access-v1.watch-only-account-v1.usdt-address-v1 Verify the introduction shows the optional USDT sharing switch without an address panel or additional balance/history explanation. Continue to authorization; verify the named requester, exact requested permission paths and access, and Paykit Details explanation are shown without a second sharing switch or address panel diff --git a/journeys/usdt-bridge-support.xml b/journeys/usdt-bridge-support.xml index ceea138b3f..959fd59a4a 100644 --- a/journeys/usdt-bridge-support.xml +++ b/journeys/usdt-bridge-support.xml @@ -8,6 +8,7 @@ Tap Contact Support (id "UsdtContactSupport"). Verify the report form opens with those details prefilled and remains unsent. Return without submitting. Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") is a full-width primary 56-point button below the network list and artwork. Open it, verify rows show the source network and provider status, then open a deposit. For a deposit whose conversion order is funded, confirming, swapping, bridging or delivering without an error, verify Pending is shown. Refresh after completion and verify Confirmed. An active order with an explicit error, or an unknown status, must show Needs attention. + For a deposit eligible for a refund, open Details and enter an owned address on the source network. With large system text enabled, verify the address field grows so the text is not clipped. Open and cancel the refund confirmation without requesting a refund. Scroll to Hide Details and tap it. Verify the refund field and keyboard close and the summary is readable. Reopen Details and verify the entered address remains available. For a refunded deposit, verify the detail shows Refunded, even when an earlier error code is retained. Verify the transaction is labeled Refund transaction in the detail. Verify no pending-refund notice remains after refreshing the completed refund. For a refund in progress, verify Refunding is shown for the provider's pending acknowledgment. A refund requiring operator intervention must still show Needs attention. Verify Contact Support and Copy Details use the same small side-by-side buttons in deposit details. Tap Copy Details (id "UsdtCopyDetails"). Verify the clipboard includes the deposit reference, source network and source transaction, plus the current status and any destination or refund transaction. diff --git a/journeys/usdt-opt-in.xml b/journeys/usdt-opt-in.xml index c4bea8ddb9..14bb0d12f0 100644 --- a/journeys/usdt-opt-in.xml +++ b/journeys/usdt-opt-in.xml @@ -2,6 +2,7 @@ Use a fresh onboarded wallet with USDT services configured. The wallet has no USDT balance or pending transfers. Use owned payment links and existing requests; no payment is submitted. The explicit-link enable prompt is a swipe-dismissable sheet on iOS; Android uses an alert with Cancel and Enable actions and blocks outside/Back dismissal. On Home, verify the New: USDT sheet (id "UsdtIntro") shows the Tether coin, DOLLARS? YOUR CALL heading, No Thanks and Enable USDT. + With large system text enabled, verify No Thanks and Enable USDT remain fully readable and tappable, stacking vertically when needed. Scroll the introduction to read the complete description, then restore the normal text size before continuing. Swipe the intro sheet down. Verify Home shows Savings and Spending without a USDT tile. Open Receive and verify there is no USDT tab, then close it. Terminate and relaunch the app. Verify the USDT intro returns. Tap No Thanks (id "UsdtIntroDecline"), relaunch again, and verify the intro does not return. While USDT remains off on this fresh wallet, use the local gateway request log to verify Home and wallet backups do not request the wallet's USDT balance or history. Bitcoin and Lightning continue normally. diff --git a/journeys/usdt-send-review-and-details.xml b/journeys/usdt-send-review-and-details.xml index b669f920b6..68bfb8e3aa 100644 --- a/journeys/usdt-send-review-and-details.xml +++ b/journeys/usdt-send-review-and-details.xml @@ -10,6 +10,7 @@ Prepare an ordinary amount and note the payment and fee in Details. Go back to Amount and immediately continue without editing; verify the same review values return. Go back, change the amount and continue; verify the payment reflects the new amount. Go back, wait until the quote expires (at most two minutes) and continue; verify a new quote is obtained or an error leaves the amount editable, rather than displaying expired fees. Cancel without swiping. Prepare a cross-network payment and continue. Verify the basic confirmation shows the amount, coin illustration, Show Details and teal Swipe To Pay without Recipient gets or Max total debit rows. Tap Show Details (id "UsdtReviewDetails"). Verify the two-column rows are Pay from / Network, Payment / Fee, Recipient gets / Max total debit and Address / Recipient network. Network must be Arbitrum One; Recipient network must be the chosen destination. Verify Recipient gets (id "UsdtRecipientAmount") follows the quote, with an approximation marker for Orchestra, and Max total debit (id "UsdtMaximumTotal") equals the payment plus maximum fee. Tap the shortened address (id "UsdtReviewAddress") and verify the full value is shown and can be selected and copied. Tap again to restore the compact row. Hide Details restores the basic confirmation. Cancel without paying. Using the local quote fixture, prepare a 5 USDT bridge with 2 USDT received and a small network fee. Try to proceed and verify the fee-over-half-value warning appears before payment authentication. Cancel without paying. + Using the local payment fixture, show a submitted direct payment as Pending, then change it to Confirmed. Verify completion feedback and the success screen are shown. Repeat with a cross-network payment: Bridging remains submitted until delivery is Confirmed, then shows successful completion without presenting it as still pending. Do not submit a real payment for this check. Open an existing sent USDT activity. Verify the heading is Sent USDT, the signed amount includes the actual fee, and summary rows show Status/Network, Date/Time and Payment/Fee with teal icons. Open Details and verify transaction ID, known sender, recipient and source/destination networks; Open Block Explorer stays at the bottom. Open an existing received activity. Verify Received USDT and a positive amount. Verify Details shows the verified sender when available and does not invent one for older unknown records. Check pending, failed and transferring rows use their matching icons, and transferring shows its destination. Prepare a contact payment that accepts multiple payment methods. Open the payment-source button and verify the balance menu shows Savings, Spending, any paired hardware wallet and USDT. Unsupported methods are disabled and shown at 30% opacity. Verify the menu has 24px outer padding, 24px asset icons, 13/18 uppercase labels and 17/22 balances, with no extra transfer icon next to Spending. Select a supported method and verify its label, available balance and payable amount update. Cancel without paying. From 1c37a7e7e07eff297bf01c3d03a39f2b80432967 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 09:11:11 -0500 Subject: [PATCH 05/11] fix: align expanded usdt address details --- .../main/java/to/bitkit/ui/components/SendCell.kt | 2 ++ .../ui/screens/profile/PubkyAuthApprovalSheet.kt | 4 +++- .../ui/screens/wallets/send/SendAmountScreen.kt | 7 ++++++- .../ui/screens/wallets/usdt/UsdtActivityDetail.kt | 3 ++- .../ui/screens/wallets/usdt/UsdtDetailStyles.kt | 2 ++ .../ui/screens/wallets/usdt/UsdtSendContent.kt | 13 ++++++++++--- changelog.d/next/1494.changed.md | 2 +- docs/screens-map.md | 2 +- journeys/usdt-send-review-and-details.xml | 2 +- 9 files changed, 28 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SendCell.kt b/app/src/main/java/to/bitkit/ui/components/SendCell.kt index e7f55ef349..66db668cb3 100644 --- a/app/src/main/java/to/bitkit/ui/components/SendCell.kt +++ b/app/src/main/java/to/bitkit/ui/components/SendCell.kt @@ -20,12 +20,14 @@ fun SendCell( modifier: Modifier = Modifier, dividerSpacing: Dp = 16.dp, captionStyle: TextStyle = AppTextStyles.CaptionM, + alignDividerToBottom: Boolean = false, content: @Composable () -> Unit, ) { Column(modifier = modifier.fillMaxWidth()) { Caption13Up(text = caption, color = Colors.White64, style = captionStyle) VerticalSpacer(8.dp) content() + if (alignDividerToBottom) FillHeight() VerticalSpacer(dividerSpacing) HorizontalDivider(modifier = Modifier.fillMaxWidth()) } diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt index 50dfb19caf..4dce36fa08 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt @@ -332,7 +332,9 @@ private fun ColumnScope.WatchOnlyConsentContent( BodyM( text = stringResource( when { - uiState.bitkitClaim?.sharesBitcoin == true -> R.string.profile__auth_approval_watch_only_intro_description + uiState.bitkitClaim?.sharesBitcoin == true -> { + R.string.profile__auth_approval_watch_only_intro_description + } uiState.usdtRequired -> R.string.profile__auth_approval_usdt_intro_description else -> R.string.profile__auth_approval_usdt_intro_optional } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 625e5bfa00..bcf052b425 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -422,7 +422,12 @@ private fun SendAmountNodeRunning( enabled = uiState.isAmountEditable, onPress = { app?.onUsdtAmountChange( - NumberPadInputHandler.handleInput(it, uiState.paykitAmount, maxLength = 21, maxDecimals = 6) + NumberPadInputHandler.handleInput( + it, + uiState.paykitAmount, + maxLength = 21, + maxDecimals = 6 + ) ) }, onDeleteLongPress = { app?.onUsdtAmountChange("") } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt index 37c42653fd..43c00dca47 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtActivityDetail.kt @@ -364,8 +364,9 @@ internal fun UsdtDetailCell( iconColor: Color = Colors.Usdt, minValueHeight: Dp = 0.dp, textStyle: TextStyle = UsdtDetailStyles.Value, + alignDividerToBottom: Boolean = false, ) { - UsdtDetailField(caption = title, modifier = modifier) { + UsdtDetailField(caption = title, alignDividerToBottom = alignDividerToBottom, modifier = modifier) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDetailStyles.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDetailStyles.kt index a91dd9a84f..6b6d282c1a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDetailStyles.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDetailStyles.kt @@ -28,12 +28,14 @@ internal object UsdtDetailStyles { internal fun UsdtDetailField( caption: String, modifier: Modifier = Modifier, + alignDividerToBottom: Boolean = false, content: @Composable () -> Unit, ) { SendCell( caption = caption, captionStyle = UsdtDetailStyles.Caption, dividerSpacing = 15.dp, + alignDividerToBottom = alignDividerToBottom, content = content, modifier = modifier ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt index b0e8980ab8..e0470b3c58 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt @@ -9,7 +9,9 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -703,8 +705,12 @@ private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) { } VerticalSpacer(16.dp) } - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - UsdtDetailField(caption = stringResource(R.string.wallet__activity_address), modifier = Modifier.weight(1f)) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min)) { + UsdtDetailField( + caption = stringResource(R.string.wallet__activity_address), + alignDividerToBottom = true, + modifier = Modifier.weight(1f).fillMaxHeight() + ) { SelectionContainer { BodySSB( quote.recipient, @@ -722,7 +728,8 @@ private fun UsdtQuoteDetails(quote: UsdtQuote, contact: PubkyProfile?) { stringResource(R.string.usdt__recipient_network), quote.destination.label, R.drawable.usdt_network, - modifier = Modifier.weight(1f) + alignDividerToBottom = true, + modifier = Modifier.weight(1f).fillMaxHeight() ) } } diff --git a/changelog.d/next/1494.changed.md b/changelog.d/next/1494.changed.md index 0529636b1e..b6397169c7 100644 --- a/changelog.d/next/1494.changed.md +++ b/changelog.d/next/1494.changed.md @@ -1 +1 @@ -Refine Earn and USDT payment screens, improve receive fees and activity details, keep refund inputs readable with large text, and let requests and subscriptions choose individual receiving methods. +Refine Earn and USDT payment layouts, improve large-text controls and localized amount entry, and let requests and subscriptions choose individual receiving methods. diff --git a/docs/screens-map.md b/docs/screens-map.md index eee81e5181..4ababf187e 100644 --- a/docs/screens-map.md +++ b/docs/screens-map.md @@ -225,7 +225,7 @@ Frame names are stable across handoff iterations; node ids are not, so the map l | Android | Figma | | - | - | -| UsdtDepositReceiveScreen.kt | Receive USDT › Network selection and Cross-Network Deposits / HW QR / Recipient / Specify Amount (populated) / Fee To Add Liquidity | +| UsdtDepositReceiveScreen.kt | Receive USDT › HW QR / Recipient / Specify Amount (populated) / Fee To Add Liquidity | | UsdtDepositsScreen.kt | Activity › Transaction Cross Network USDT / Transaction Receiving USDT Tech details | | UsdtWalletScreen.kt | Savings, Spending, Hardware, USDT Balances › Wallet USDT (funds & activity) / Wallet USDT (empty) | diff --git a/journeys/usdt-send-review-and-details.xml b/journeys/usdt-send-review-and-details.xml index 68bfb8e3aa..64186accf5 100644 --- a/journeys/usdt-send-review-and-details.xml +++ b/journeys/usdt-send-review-and-details.xml @@ -8,7 +8,7 @@ Tap MAX. Verify it fills a positive amount below the balance and remains on Amount. Continue and verify the payment plus maximum fee fits within the balance. Back out without swiping. Enter a smaller amount and continue. Verify the collapsed review shows the amount, coin illustration, Show Details and teal Swipe To Pay. Show Details reveals Pay from, Network, Payment, Fee and Address. Verify Network is Arbitrum One and the fee note says all fees are paid in USDT and the final fee may be lower. Displayed amounts round to cents; nonzero values below one cent show <0.01. Hide Details restores the illustration. Cancel without paying. Prepare an ordinary amount and note the payment and fee in Details. Go back to Amount and immediately continue without editing; verify the same review values return. Go back, change the amount and continue; verify the payment reflects the new amount. Go back, wait until the quote expires (at most two minutes) and continue; verify a new quote is obtained or an error leaves the amount editable, rather than displaying expired fees. Cancel without swiping. - Prepare a cross-network payment and continue. Verify the basic confirmation shows the amount, coin illustration, Show Details and teal Swipe To Pay without Recipient gets or Max total debit rows. Tap Show Details (id "UsdtReviewDetails"). Verify the two-column rows are Pay from / Network, Payment / Fee, Recipient gets / Max total debit and Address / Recipient network. Network must be Arbitrum One; Recipient network must be the chosen destination. Verify Recipient gets (id "UsdtRecipientAmount") follows the quote, with an approximation marker for Orchestra, and Max total debit (id "UsdtMaximumTotal") equals the payment plus maximum fee. Tap the shortened address (id "UsdtReviewAddress") and verify the full value is shown and can be selected and copied. Tap again to restore the compact row. Hide Details restores the basic confirmation. Cancel without paying. + Prepare a cross-network payment and continue. Verify the basic confirmation shows the amount, coin illustration, Show Details and teal Swipe To Pay without Recipient gets or Max total debit rows. Tap Show Details (id "UsdtReviewDetails"). Verify the two-column rows are Pay from / Network, Payment / Fee, Recipient gets / Max total debit and Address / Recipient network. Network must be Arbitrum One; Recipient network must be the chosen destination. Verify Recipient gets (id "UsdtRecipientAmount") follows the quote, with an approximation marker for Orchestra, and Max total debit (id "UsdtMaximumTotal") equals the payment plus maximum fee. Tap the shortened address (id "UsdtReviewAddress") and verify the full value is shown and can be selected and copied. Verify the Address and Recipient network dividers stay aligned below the expanded address. Tap again to restore the compact row. Hide Details restores the basic confirmation. Cancel without paying. Using the local quote fixture, prepare a 5 USDT bridge with 2 USDT received and a small network fee. Try to proceed and verify the fee-over-half-value warning appears before payment authentication. Cancel without paying. Using the local payment fixture, show a submitted direct payment as Pending, then change it to Confirmed. Verify completion feedback and the success screen are shown. Repeat with a cross-network payment: Bridging remains submitted until delivery is Confirmed, then shows successful completion without presenting it as still pending. Do not submit a real payment for this check. Open an existing sent USDT activity. Verify the heading is Sent USDT, the signed amount includes the actual fee, and summary rows show Status/Network, Date/Time and Payment/Fee with teal icons. Open Details and verify transaction ID, known sender, recipient and source/destination networks; Open Block Explorer stays at the bottom. From 9c75d43ecea344d015fe8fc23feac83183107267 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 10:51:15 -0500 Subject: [PATCH 06/11] fix: preserve submitted usdt payment transitions --- .../wallets/usdt/UsdtPaymentContentTest.kt | 73 ++++++++++++++++--- .../screens/wallets/usdt/UsdtSendContent.kt | 11 ++- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt index 43abe29a24..5118ac38af 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/usdt/UsdtPaymentContentTest.kt @@ -127,42 +127,97 @@ class UsdtPaymentContentTest { } @Test - fun directPaymentShowsCompletionBeforeSuccess() { + fun pendingDirectPaymentStaysSubmittedUntilSuccess() { assertCompletionTransition(UsdtDestination.ARBITRUM, UsdtTransferStatus.PENDING) } @Test - fun bridgeShowsPendingUntilDeliveryThenCompletion() { + fun bridgeStaysSubmittedUntilDeliverySuccess() { assertCompletionTransition(UsdtDestination.POLYGON, UsdtTransferStatus.BRIDGING) } + @Test + fun immediateConfirmationShowsCompletionBeforeSuccess() { + val payment = quote.copy(destination = UsdtDestination.ARBITRUM) + val state = mutableStateOf(UsdtSendState(quote = payment, submitted = true, busy = true)) + val wallet = UsdtWalletState(transfers = persistentListOf(transfer(payment, UsdtTransferStatus.CONFIRMED))) + composeTestRule.mainClock.autoAdvance = false + composeTestRule.setContent { Content(state.value, UsdtPage.AMOUNT, wallet = wallet) } + composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.onNodeWithTag("UsdtConfirm").assertIsDisplayed() + composeTestRule.runOnUiThread { state.value = state.value.copy(busy = false) } + composeTestRule.mainClock.advanceTimeBy(500) + composeTestRule.onNodeWithTag("UsdtConfirm").assertIsDisplayed() + composeTestRule.onNodeWithTag("NavigationBack").assertDoesNotExist() + composeTestRule.onNodeWithText("Payment submitted").assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtSendSuccess").assertDoesNotExist() + composeTestRule.mainClock.advanceTimeBy(700) + composeTestRule.onNodeWithTag("UsdtConfirm").assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtSendSuccess").assertIsDisplayed() + } + + @Test + fun returningToConfirmedPaymentDoesNotReopenReview() { + val showDetails = mutableStateOf(false) + val payment = transfer(quote, UsdtTransferStatus.BRIDGING) + val wallet = mutableStateOf(UsdtWalletState(transfers = persistentListOf(payment))) + composeTestRule.mainClock.autoAdvance = false + composeTestRule.setContent { + if (!showDetails.value) { + Content(UsdtSendState(quote = quote, submitted = true), UsdtPage.AMOUNT, wallet = wallet.value) { + showDetails.value = true + } + } + } + composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.onNodeWithText("Details").performClick() + composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.onNodeWithText("Payment submitted").assertDoesNotExist() + composeTestRule.runOnUiThread { + wallet.value = wallet.value.copy(transfers = persistentListOf(payment.copy(status = UsdtTransferStatus.CONFIRMED))) + showDetails.value = false + } + composeTestRule.mainClock.advanceTimeBy(500) + composeTestRule.onNodeWithTag("UsdtConfirm").assertDoesNotExist() + composeTestRule.onNodeWithTag("NavigationBack").assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtSendSuccess").assertDoesNotExist() + composeTestRule.mainClock.advanceTimeBy(700) + composeTestRule.onNodeWithTag("UsdtSendSuccess").assertIsDisplayed() + } + private fun assertCompletionTransition(destination: UsdtDestination, pendingStatus: UsdtTransferStatus) { val payment = quote.copy(destination = destination) - val transfer = UsdtTransfer( - id = payment.id, sender = null, txHash = null, userOperationHash = null, - bridgeGuid = null, orchestra = null, recipient = payment.recipient, - destination = destination, amount = payment.amount, receivedAmount = payment.receivedAmount, - fee = null, isIncoming = false, status = pendingStatus, timestamp = 1u, - ) + val transfer = transfer(payment, pendingStatus) val wallet = mutableStateOf(UsdtWalletState(transfers = persistentListOf(transfer))) composeTestRule.mainClock.autoAdvance = false composeTestRule.setContent { Content(UsdtSendState(quote = payment, submitted = true), UsdtPage.AMOUNT, wallet = wallet.value) } composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.onNodeWithText("Payment submitted").assertIsDisplayed() + composeTestRule.onNodeWithTag("NavigationBack").assertDoesNotExist() composeTestRule.onNodeWithTag("UsdtConfirm").assertDoesNotExist() composeTestRule.onNodeWithTag("UsdtSendSuccess").assertDoesNotExist() composeTestRule.runOnUiThread { wallet.value = wallet.value.copy(transfers = persistentListOf(transfer.copy(status = UsdtTransferStatus.CONFIRMED))) } composeTestRule.mainClock.advanceTimeBy(500) - composeTestRule.onNodeWithTag("UsdtConfirm").assertIsDisplayed() + composeTestRule.onNodeWithText("Payment submitted").assertIsDisplayed() + composeTestRule.onNodeWithTag("NavigationBack").assertDoesNotExist() + composeTestRule.onNodeWithTag("UsdtConfirm").assertDoesNotExist() composeTestRule.onNodeWithTag("UsdtSendSuccess").assertDoesNotExist() composeTestRule.mainClock.advanceTimeBy(700) composeTestRule.onNodeWithTag("UsdtConfirm").assertDoesNotExist() composeTestRule.onNodeWithTag("UsdtSendSuccess").assertIsDisplayed() } + private fun transfer(payment: UsdtQuote, status: UsdtTransferStatus) = UsdtTransfer( + id = payment.id, sender = null, txHash = null, userOperationHash = null, + bridgeGuid = null, orchestra = null, recipient = payment.recipient, + destination = payment.destination, amount = payment.amount, receivedAmount = payment.receivedAmount, + fee = null, isIncoming = false, status = status, timestamp = 1u, + ) + @Composable private fun Content( state: UsdtSendState, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt index e0470b3c58..2f9b3f6a2e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtSendContent.kt @@ -146,14 +146,19 @@ internal fun UsdtPaymentContent( val quote = state.quote val status = wallet.transfers.firstOrNull { it.id == quote?.id }?.status val completed = state.submitted && !state.busy && status == UsdtTransferStatus.CONFIRMED + val pending = state.submitted && !state.busy && !completed + var hasShownSubmitted by rememberSaveable(quote?.id) { mutableStateOf(state.submitted && !state.busy) } var showSuccess by rememberSaveable(quote?.id) { mutableStateOf(false) } - LaunchedEffect(completed) { + LaunchedEffect(quote?.id, pending) { + if (pending) hasShownSubmitted = true + } + LaunchedEffect(quote?.id, completed) { if (completed) { delay(1.seconds) showSuccess = true } } - val showSubmitted = state.submitted && !state.busy && !completed + val showSubmitted = pending || (completed && hasShownSubmitted) if (showSuccess && completed && quote != null) { NewTransactionSheetView( details = NewTransactionSheetDetails( @@ -184,7 +189,7 @@ internal fun UsdtPaymentContent( else -> R.string.usdt__send_title } Column(modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)) { - SheetTopBar(stringResource(title), onBack = onBack.takeUnless { showSubmitted || state.busy }, action = { + SheetTopBar(stringResource(title), onBack = onBack.takeUnless { state.submitted || state.busy }, action = { if (contact != null && !showSubmitted) { PubkyContactAvatar(profile = contact, size = 32.dp) } }) VerticalSpacer(if (quote != null || page == UsdtPage.AMOUNT) 8.dp else 16.dp) From 76e1803c05ce68fc77a69390cf7cf9a4763a962b Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 11:12:16 -0500 Subject: [PATCH 07/11] fix: handle unavailable scanner cameras --- .../ui/screens/scanner/QrScanningScreen.kt | 23 +++++++++++++++---- changelog.d/next/1494.changed.md | 2 +- docs/usdt-qa.md | 9 ++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 127c86e455..4eb3047dff 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -53,6 +53,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScanning @@ -63,6 +64,7 @@ import kotlinx.coroutines.withContext import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.getClipboardText +import to.bitkit.ext.runSuspendCatching import to.bitkit.ext.startActivityAppSettings import to.bitkit.models.QrCodePayload import to.bitkit.models.Toast @@ -195,12 +197,23 @@ fun QrScanningScreen( } var camera by remember { mutableStateOf(null) } - LaunchedEffect(lensFacing) { - val cameraProvider = withContext(Dispatchers.IO) { - ProcessCameraProvider.getInstance(context).get() + LaunchedEffect(lensFacing, cameraPermissionState.status) { + if (!cameraPermissionState.status.isGranted) return@LaunchedEffect + runSuspendCatching { + val cameraProvider = withContext(Dispatchers.IO) { + ProcessCameraProvider.getInstance(context).get() + } + camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) + preview.surfaceProvider = previewView.surfaceProvider + }.onFailure { + Logger.error("Failed to initialize camera", it, context = TAG) + app.toast( + type = Toast.ToastType.ERROR, + title = context.getString(R.string.other__qr_error_header), + description = context.getString(R.string.other__camera_init_error) + .replace("{message}", it.message.orEmpty()) + ) } - camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) - preview.surfaceProvider = previewView.surfaceProvider } DisposableEffect(Unit) { onDispose { diff --git a/changelog.d/next/1494.changed.md b/changelog.d/next/1494.changed.md index b6397169c7..28d65ad1ac 100644 --- a/changelog.d/next/1494.changed.md +++ b/changelog.d/next/1494.changed.md @@ -1 +1 @@ -Refine Earn and USDT payment layouts, improve large-text controls and localized amount entry, and let requests and subscriptions choose individual receiving methods. +Refine Earn and USDT payment layouts, improve large-text controls and localized amount entry, and let requests and subscriptions choose individual receiving methods. Keep manual entry and paste available when a camera cannot start. diff --git a/docs/usdt-qa.md b/docs/usdt-qa.md index d706f7ee5a..e89f307b06 100644 --- a/docs/usdt-qa.md +++ b/docs/usdt-qa.md @@ -6,6 +6,15 @@ The drivable wallet navigation and request/subscription UI paths are in `journey Enable USDT through its introduction or Settings before starting payment checks. The opt-in, disable safeguards and disabled-link paths are covered by `journeys/usdt-opt-in.xml`. +## Android camera availability + +Use a disposable unfunded wallet with USDT enabled. The no-camera case requires an emulator with both cameras disabled; this hardware configuration is outside the standard journey environment. + +- [ ] With camera permission denied, open USDT Send and verify Paste and Enter Manually remain available. +- [ ] Grant camera permission on a device with no available camera. Verify a camera error is shown without closing the app. Enter Manually must still open recipient entry, and Paste must still process a supported address. +- [ ] Leave and reopen Send. Verify camera initialization failure does not crash or prevent navigation. +- [ ] Repeat with a working camera. Grant permission while the scanner is open, verify the preview starts, and scan an owned USDT address without submitting a payment. + ## usdt bridge quotes A funded Arbitrum wallet uses the real gateway with USDT0 and Orchestra enabled. This journey obtains quotes only; do not swipe to pay or create any funded order. From c46da492ad9a99fa5e7c67c75022bc53e2fedb2d Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 12:54:42 -0500 Subject: [PATCH 08/11] fix: float deposit history above network selection --- .../wallets/usdt/UsdtDepositReceiveScreen.kt | 61 +++++++++++-------- changelog.d/next/1494.changed.md | 2 +- journeys/usdt-bridge-details.xml | 2 +- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt index ec3e36737b..d648463e1f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtDepositReceiveScreen.kt @@ -2,9 +2,11 @@ package to.bitkit.ui.screens.wallets.usdt import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -19,6 +21,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -236,31 +240,40 @@ private fun ColumnScope.NetworkSelector( onNetwork: (UsdtDepositNetwork?) -> Unit, onHistory: () -> Unit, ) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()) - ) { - UsdtNetworkRow("Arbitrum One", state.network == null, onClick = { onNetwork(null) }) - state.networks.forEach { item -> - UsdtNetworkRow(item.label, state.network == item, onClick = { onNetwork(item) }) + val density = LocalDensity.current + var footerHeight by remember { mutableStateOf(0.dp) } + Box(modifier = Modifier.weight(1f)) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = footerHeight) + ) { + UsdtNetworkRow("Arbitrum One", state.network == null, onClick = { onNetwork(null) }) + state.networks.forEach { item -> + UsdtNetworkRow(item.label, state.network == item, onClick = { onNetwork(item) }) + } + BodyS( + stringResource(R.string.usdt__receive_network_note), + color = Colors.White64, + letterSpacing = 0.sp, + modifier = Modifier.padding(vertical = 16.dp) + ) + UsdtCoinIllustration(modifier = Modifier.align(Alignment.CenterHorizontally)) + state.error?.let { BodyS(stringResource(it), color = Colors.White64) } + } + if (Env.usdtDepositsUrl != null) { + Column( + modifier = Modifier.align(Alignment.BottomCenter) + .onSizeChanged { footerHeight = with(density) { it.height.toDp() } } + ) { + VerticalSpacer(16.dp) + PrimaryButton( + stringResource(R.string.usdt__deposit_history), + onClick = onHistory, + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), + modifier = Modifier.testTag("UsdtDepositHistory") + ) + } } - BodyS( - stringResource(R.string.usdt__receive_network_note), - color = Colors.White64, - letterSpacing = 0.sp, - modifier = Modifier.padding(vertical = 16.dp) - ) - UsdtCoinIllustration(modifier = Modifier.align(Alignment.CenterHorizontally)) - state.error?.let { BodyS(stringResource(it), color = Colors.White64) } - } - if (Env.usdtDepositsUrl != null) { - VerticalSpacer(16.dp) - PrimaryButton( - stringResource(R.string.usdt__deposit_history), - onClick = onHistory, - textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), - modifier = Modifier.testTag("UsdtDepositHistory") - ) } } diff --git a/changelog.d/next/1494.changed.md b/changelog.d/next/1494.changed.md index 28d65ad1ac..e23df58765 100644 --- a/changelog.d/next/1494.changed.md +++ b/changelog.d/next/1494.changed.md @@ -1 +1 @@ -Refine Earn and USDT payment layouts, improve large-text controls and localized amount entry, and let requests and subscriptions choose individual receiving methods. Keep manual entry and paste available when a camera cannot start. +Refine Earn and USDT payment layouts, improve large-text controls and localized amount entry, let requests and subscriptions choose individual receiving methods, and keep manual entry and paste available when a camera cannot start. diff --git a/journeys/usdt-bridge-details.xml b/journeys/usdt-bridge-details.xml index 2b6000e323..991ac50aae 100644 --- a/journeys/usdt-bridge-details.xml +++ b/journeys/usdt-bridge-details.xml @@ -14,6 +14,6 @@ For a delayed, failed, refunding, refunded or support-needed bridge fixture, verify Contact Support (id "UsdtContactSupport") and Copy Details (id "UsdtCopyDetails") appear side by side as small 40-point buttons, matching the Request refund / Hide details row. Verify both remain reachable and their labels are not clipped, without submitting either action. Return to the USDT activity list. Verify Pending uses a yellow hourglass and 16% yellow circle, Failed uses a red x-circle and 16% red circle, and Transferring uses the teal transfer arrows. Sent and Received remain teal. Open USDT Receive and inject one confirmed incoming USDT transfer through the isolated fixture. Verify Receive is replaced by one received celebration. Dismiss it and verify the same transfer is not celebrated again; a different subsequent transfer must still be shown. - Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") is a full-width primary 56-point button below the network list and artwork, and remains reachable before any Arbitrum receipt exists. + Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") is a full-width primary button pinned over the bottom of the network list. Scroll the list and verify the rows and artwork pass behind the button while it stays fixed. Scroll to the end and verify the last content can be brought above the button. Repeat with large text, verify the button label is not clipped, then tap it and verify deposit history opens before any Arbitrum receipt exists. From f43cb252faed2f9888ab3ec196515097df2e8ce8 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 13:22:08 -0500 Subject: [PATCH 09/11] fix: float the subscription recipient action --- .../subscriptions/CreateSubscriptionScreen.kt | 219 ++++++++++-------- journeys/subscriptions/create-and-propose.xml | 1 + 2 files changed, 121 insertions(+), 99 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt index e774aabbec..ce9b918664 100644 --- a/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/subscriptions/CreateSubscriptionScreen.kt @@ -15,10 +15,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -42,7 +45,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -300,6 +305,9 @@ internal fun CreateSubscriptionDetails( } } + val density = LocalDensity.current + val isKeyboardVisible = WindowInsets.ime.getBottom(density) > 0 + var footerHeight by remember { mutableStateOf(0.dp) } Column( modifier = modifier .fillMaxSize() @@ -310,114 +318,127 @@ internal fun CreateSubscriptionDetails( .testTag("CreateSubscription") ) { SheetTopBar(titleText = stringResource(R.string.subscriptions__create_subscription)) - LazyColumn(modifier = Modifier.weight(1f)) { - item { - VerticalSpacer(8.dp) - Caption13Up(text = stringResource(R.string.wallet__payment_request_amount), color = Colors.White64) - VerticalSpacer(16.dp) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.clickableAlpha(onClick = onAmountClick).testTag("SubscriptionEditAmount") - ) { - PaykitAmountDisplay( - amount = amount, - modifier = Modifier.weight(1f, fill = false).width(IntrinsicSize.Max) - ) - Icon( - painter = painterResource(R.drawable.ic_pencil_simple), - contentDescription = stringResource(R.string.common__edit), - tint = Colors.White, - modifier = Modifier.size(24.dp) - ) - } - VerticalSpacer(24.dp) - Caption13Up(text = stringResource(R.string.subscriptions__frequency), color = Colors.White64) - VerticalSpacer(8.dp) - SubscriptionFrequencyPicker(frequency, onFrequencyChange) - VerticalSpacer(24.dp) - PaykitAcceptedMethods( - title = stringResource(R.string.subscriptions__accepted_payments), - availableMethods = availableMethods, - selectedMethods = acceptedMethods, - testTagPrefix = "Subscription", - onClickMethod = onClickMethod, - modifier = Modifier.testTag("SubscriptionAcceptedMethods") - ) - VerticalSpacer(24.dp) - Caption13Up( - text = stringResource(R.string.subscriptions__name), - color = Colors.White64, - style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), - ) - VerticalSpacer(8.dp) - TextInput( - value = name, - onValueChange = onNameChange, - placeholder = stringResource(R.string.subscriptions__name_placeholder), - singleLine = true, - modifier = Modifier.fillMaxWidth().testTag("SubscriptionName") - ) - VerticalSpacer(24.dp) - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Column(modifier = Modifier.weight(1f)) { - Caption13Up( - text = stringResource(R.string.subscriptions__description), - color = Colors.White64, - style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + Box(modifier = Modifier.weight(1f)) { + LazyColumn( + contentPadding = PaddingValues(bottom = if (isKeyboardVisible) 0.dp else footerHeight), + modifier = Modifier.fillMaxSize().padding(bottom = if (isKeyboardVisible) footerHeight else 0.dp) + ) { + item { + VerticalSpacer(8.dp) + Caption13Up(text = stringResource(R.string.wallet__payment_request_amount), color = Colors.White64) + VerticalSpacer(16.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .clickableAlpha(onClick = onAmountClick) + .testTag("SubscriptionEditAmount") + ) { + PaykitAmountDisplay( + amount = amount, + modifier = Modifier.weight(1f, fill = false).width(IntrinsicSize.Max) ) - VerticalSpacer(8.dp) - TextInput( - value = description, - onValueChange = onDescriptionChange, - placeholder = stringResource(R.string.subscriptions__description_placeholder), - minLines = 4, - maxLines = 4, - modifier = Modifier.fillMaxWidth().testTag("SubscriptionDescription") + Icon( + painter = painterResource(R.drawable.ic_pencil_simple), + contentDescription = stringResource(R.string.common__edit), + tint = Colors.White, + modifier = Modifier.size(24.dp) ) } - Column { - Caption13Up( - text = stringResource(R.string.subscriptions__custom_icon), - color = Colors.White64, - style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), - ) - VerticalSpacer(8.dp) - val iconDescription = stringResource(R.string.subscriptions__custom_icon_description) - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(72.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Colors.White10) - .clickable(enabled = !isLoadingIcon, onClick = launchPhotoPicker) - .semantics { contentDescription = iconDescription } - .testTag("SubscriptionIconPicker") - ) { - if (selectedIconUri == null) { - SubscriptionDefaultIcon(size = 40.dp) - } else { - AsyncImage( - model = selectedIconUri, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(40.dp).clip(RoundedCornerShape(8.dp)) - ) + VerticalSpacer(24.dp) + Caption13Up(text = stringResource(R.string.subscriptions__frequency), color = Colors.White64) + VerticalSpacer(8.dp) + SubscriptionFrequencyPicker(frequency, onFrequencyChange) + VerticalSpacer(24.dp) + PaykitAcceptedMethods( + title = stringResource(R.string.subscriptions__accepted_payments), + availableMethods = availableMethods, + selectedMethods = acceptedMethods, + testTagPrefix = "Subscription", + onClickMethod = onClickMethod, + modifier = Modifier.testTag("SubscriptionAcceptedMethods") + ) + VerticalSpacer(24.dp) + Caption13Up( + text = stringResource(R.string.subscriptions__name), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) + VerticalSpacer(8.dp) + TextInput( + value = name, + onValueChange = onNameChange, + placeholder = stringResource(R.string.subscriptions__name_placeholder), + singleLine = true, + modifier = Modifier.fillMaxWidth().testTag("SubscriptionName") + ) + VerticalSpacer(24.dp) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + Column(modifier = Modifier.weight(1f)) { + Caption13Up( + text = stringResource(R.string.subscriptions__description), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) + VerticalSpacer(8.dp) + TextInput( + value = description, + onValueChange = onDescriptionChange, + placeholder = stringResource(R.string.subscriptions__description_placeholder), + minLines = 4, + maxLines = 4, + modifier = Modifier.fillMaxWidth().testTag("SubscriptionDescription") + ) + } + Column { + Caption13Up( + text = stringResource(R.string.subscriptions__custom_icon), + color = Colors.White64, + style = AppTextStyles.CaptionM.copy(letterSpacing = 1.sp), + ) + VerticalSpacer(8.dp) + val iconDescription = stringResource(R.string.subscriptions__custom_icon_description) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(72.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Colors.White10) + .clickable(enabled = !isLoadingIcon, onClick = launchPhotoPicker) + .semantics { contentDescription = iconDescription } + .testTag("SubscriptionIconPicker") + ) { + if (selectedIconUri == null) { + SubscriptionDefaultIcon(size = 40.dp) + } else { + AsyncImage( + model = selectedIconUri, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(40.dp).clip(RoundedCornerShape(8.dp)) + ) + } } } } + VerticalSpacer(24.dp) } - VerticalSpacer(24.dp) + } + Column( + modifier = Modifier.align(Alignment.BottomCenter) + .onSizeChanged { footerHeight = with(density) { it.height.toDp() } } + ) { + PrimaryButton( + text = stringResource(R.string.subscriptions__choose_recipient), + textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), + onClick = onChooseRecipient, + enabled = amount.atomic > 0uL && name.isNotBlank() && + !isLoadingIcon && acceptedMethods.isNotEmpty(), + modifier = Modifier.testTag("SubscriptionChooseRecipient") + ) + VerticalSpacer(16.dp) } } - PrimaryButton( - text = stringResource(R.string.subscriptions__choose_recipient), - textStyle = AppTextStyles.BodySSB.copy(letterSpacing = 0.sp), - onClick = onChooseRecipient, - enabled = amount.atomic > 0uL && name.isNotBlank() && !isLoadingIcon && acceptedMethods.isNotEmpty(), - modifier = Modifier.testTag("SubscriptionChooseRecipient") - ) - VerticalSpacer(16.dp) } } diff --git a/journeys/subscriptions/create-and-propose.xml b/journeys/subscriptions/create-and-propose.xml index 865f17e073..33758fcf14 100644 --- a/journeys/subscriptions/create-and-propose.xml +++ b/journeys/subscriptions/create-and-propose.xml @@ -12,6 +12,7 @@ Verify the Subscriptions screen (id "SubscriptionsScreen") appears with the Overview tab (id "Tab-overview") selected Tap Create (id "SubscriptionCreate") Verify the create sheet (id "CreateSubscription") appears with Choose Recipient disabled + With the keyboard closed, scroll the form and verify Choose Recipient (id "SubscriptionChooseRecipient") stays pinned while the fields scroll behind it. Scroll to the end and verify Description and Custom icon clear the button. Repeat with large text. Focus Name and Description and verify the focused field and Choose Recipient remain reachable above the keyboard, then dismiss the keyboard. Tap the amount pencil (id "SubscriptionEditAmount") and verify USD is not offered while USDT is disabled Tap the Bitcoin/fiat unit control (id "PaymentRequestAmountUnit") twice and verify both input units remain available for the BTC denomination. Select Bitcoin units, enter "5000" on the keypad, and tap Continue (id "PaymentRequestAmountContinue") Verify the amount reads 5,000 sats and the accepted methods show separate selected Savings and Spending chips for enabled rails, each with a trailing X, and no USDT. A removed method retains its asset color at 50% opacity with a trailing +; tap it to restore it. If both rails are available, remove one and verify the other remains selected. Remove all selected rails and verify Choose Recipient stays disabled; restore the available Bitcoin rails before continuing. From 05453b253a4df040b7e2595e4855baefa7137198 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 14:47:05 -0500 Subject: [PATCH 10/11] docs: align usdt journeys with current messages --- journeys/usdt-bridge-support.xml | 2 +- journeys/usdt-payment-links-and-scans.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/journeys/usdt-bridge-support.xml b/journeys/usdt-bridge-support.xml index 959fd59a4a..3c6e39cf5f 100644 --- a/journeys/usdt-bridge-support.xml +++ b/journeys/usdt-bridge-support.xml @@ -10,7 +10,7 @@ For a deposit whose conversion order is funded, confirming, swapping, bridging or delivering without an error, verify Pending is shown. Refresh after completion and verify Confirmed. An active order with an explicit error, or an unknown status, must show Needs attention. For a deposit eligible for a refund, open Details and enter an owned address on the source network. With large system text enabled, verify the address field grows so the text is not clipped. Open and cancel the refund confirmation without requesting a refund. Scroll to Hide Details and tap it. Verify the refund field and keyboard close and the summary is readable. Reopen Details and verify the entered address remains available. For a refunded deposit, verify the detail shows Refunded, even when an earlier error code is retained. Verify the transaction is labeled Refund transaction in the detail. Verify no pending-refund notice remains after refreshing the completed refund. - For a refund in progress, verify Refunding is shown for the provider's pending acknowledgment. A refund requiring operator intervention must still show Needs attention. + For a refund in progress, verify "Refund in progress" is shown for the provider's pending acknowledgment. A refund requiring operator intervention must still show Needs attention. Verify Contact Support and Copy Details use the same small side-by-side buttons in deposit details. Tap Copy Details (id "UsdtCopyDetails"). Verify the clipboard includes the deposit reference, source network and source transaction, plus the current status and any destination or refund transaction. Tap Contact Support (id "UsdtContactSupport"). Verify the report form opens with the deposit details prefilled. Return without submitting or requesting a refund. diff --git a/journeys/usdt-payment-links-and-scans.xml b/journeys/usdt-payment-links-and-scans.xml index 7162345e20..fdbde82a46 100644 --- a/journeys/usdt-payment-links-and-scans.xml +++ b/journeys/usdt-payment-links-and-scans.xml @@ -15,7 +15,7 @@ Open or scan variants with the wrong chain, wrong token, duplicate amount and malformed recipient. Verify an error and no payable review. Check an ordinary Bitcoin or Lightning invoice still opens its existing send flow. On iOS, open a USDT link while Lightning is stopped but the wallet is ready. Verify it routes without starting Lightning. Open two different USDT links while the main scanner sheet is dismissing; verify an older delayed link cannot replace the review that opened first. Enter this wallet’s own Arbitrum address in USDT Send, enter an amount and continue. Verify the error explicitly says you cannot send USDT to your own address, and no payment is submitted. Repeat with MAX. On Android, enter this wallet’s own bare Arbitrum address through Home Send → Enter Manually and verify the self-send message appears while Continue stays disabled. - Enter the entire available USDT balance and continue. Verify the fee-collection error explains that the transaction fee could not be paid in USDT and suggests a smaller amount. Reduce the amount, reach review and close without signing. + Enter another owned wallet’s Arbitrum address, manually enter the entire available USDT balance and continue. Verify the error says "Not enough USDT to cover the amount and fees." Reduce the amount, reach review and close without signing. With an empty wallet and no pending transfers, turn USDT off in Settings. Copy an owned bare USDT address, return to the app, and verify no automatic payment or enable prompt. Repeat with an amount-bearing USDT payment URI. Copy a valid Bitcoin address and a Lightning invoice in turn; verify their normal clipboard prompts still appear and cancel without paying. With USDT off, explicitly open the same USDT URI and verify the designed Enable USDT modal appears. Decline and verify a quiet return with no stuck spinner. Scan its QR, enable USDT, and verify the original recipient and exact amount resume. Cancel without paying. Turn USDT off again and repeat the explicit scan, decline and enable checks with the owned bare 0x address; verify the recipient resumes with an empty amount. From ff127b06d959a803f1ffa66bb9c00561aa9cdce5 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 15 Oct 2026 15:17:56 -0500 Subject: [PATCH 11/11] docs: align deposit history layout checks --- docs/usdt-qa.md | 2 +- journeys/usdt-bridge-support.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usdt-qa.md b/docs/usdt-qa.md index e89f307b06..fa731cb7a4 100644 --- a/docs/usdt-qa.md +++ b/docs/usdt-qa.md @@ -47,7 +47,7 @@ Requires a wallet with USDT enabled and a matching backend with Orchestra creden - [ ] Open the network selector. Verify only configured supported sources are offered: Ethereum, Tron, Solana, Polygon, Base and BNB Smart Chain. - [ ] Select each enabled source network, enter an amount within its live limits, and get its deposit address. Verify the selected network, reusable address, estimated received amount and deduction are shown. Verify the QR and Copy action use the same address. - [ ] Return to Arbitrum One and verify its original wallet address remains unchanged. -- [ ] From Receive → Network, verify the full-width primary Cross-Network Deposits button below the list and artwork. Open it before any receipt exists and verify empty history and Load more work without showing a failure. +- [ ] From Receive → Network, verify the full-width primary Cross-Network Deposits button stays pinned over the bottom of the network list while the rows and artwork scroll behind it. Scroll to the end and verify the last content clears the button. Repeat with large text and verify the label is not clipped. Open it before any receipt exists and verify empty history and Load more work without showing a failure. - [ ] Enter an amount below the route minimum and verify the limit error appears without a deposit address. - [ ] Edit the amount. Verify the old limit error clears immediately and the keypad and button remain fully visible. Get an address with an amount within the route limits. - [ ] Reopen the app and verify the reusable address and deposit history are recovered. Inspect any existing deposit details without claiming a new settlement. diff --git a/journeys/usdt-bridge-support.xml b/journeys/usdt-bridge-support.xml index 3c6e39cf5f..dcc416b16b 100644 --- a/journeys/usdt-bridge-support.xml +++ b/journeys/usdt-bridge-support.xml @@ -6,7 +6,7 @@ Tap Details (id "UsdtActivityDetails"), then verify the source transaction is displayed. For an Orchestra transfer, verify the bridge reference is also displayed. Verify Contact Support and Copy Details use the small 40-point buttons side by side on one row, with neither label clipped. Tap Copy Details (id "UsdtCopyDetails"). Verify the clipboard contains the bridge, networks, status and available transaction references. Tap Contact Support (id "UsdtContactSupport"). Verify the report form opens with those details prefilled and remains unsent. Return without submitting. - Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") is a full-width primary 56-point button below the network list and artwork. Open it, verify rows show the source network and provider status, then open a deposit. + Open USDT Receive, then Network. Verify Cross-Network Deposits (id "UsdtDepositHistory") is a full-width primary button pinned over the bottom of the network list. Scroll the list and verify the rows and artwork pass behind the button while it stays fixed. Scroll to the end and verify the last content can be brought above the button. Repeat with large text and verify the button label is not clipped. Open it, verify rows show the source network and provider status, then open a deposit. For a deposit whose conversion order is funded, confirming, swapping, bridging or delivering without an error, verify Pending is shown. Refresh after completion and verify Confirmed. An active order with an explicit error, or an unknown status, must show Needs attention. For a deposit eligible for a refund, open Details and enter an owned address on the source network. With large system text enabled, verify the address field grows so the text is not clipped. Open and cancel the refund confirmation without requesting a refund. Scroll to Hide Details and tap it. Verify the refund field and keyboard close and the summary is readable. Reopen Details and verify the entered address remains available. For a refunded deposit, verify the detail shows Refunded, even when an earlier error code is retained. Verify the transaction is labeled Refund transaction in the detail. Verify no pending-refund notice remains after refreshing the completed refund.