From 6de185e009f0f26021c348cf0140199148ec6c82 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 05:51:29 -0600 Subject: [PATCH 1/5] fix: unify usdt balance and amount presentation --- Bitkit/Components/PaykitAmountText.swift | 2 +- Bitkit/Managers/UsdtWalletManager.swift | 2 +- Bitkit/Models/Currency.swift | 11 ++++--- Bitkit/Views/Home/HomeWalletView.swift | 2 +- .../CreatePaymentRequestView.swift | 3 +- .../Views/Wallets/Send/SendAmountView.swift | 1 - .../Views/Wallets/UsdtReceiveAmountView.swift | 2 +- Bitkit/Views/Wallets/UsdtSendView.swift | 1 - Bitkit/Views/Wallets/UsdtWalletScreen.swift | 24 ++++---------- BitkitTests/PaykitAmountTests.swift | 23 ++++++------- BitkitTests/UsdtDisplayTests.swift | 30 ++++++++--------- BitkitTests/UsdtWalletManagerTests.swift | 6 ++-- BitkitUITests/UsdtWalletUITests.swift | 32 ++++++++++++++++--- changelog.d/next/1077.fixed.md | 1 + journeys/README.md | 2 +- journeys/usdt-number-format.xml | 12 +++++++ journeys/usdt-wallet-navigation.xml | 3 +- 17 files changed, 87 insertions(+), 70 deletions(-) create mode 100644 changelog.d/next/1077.fixed.md create mode 100644 journeys/usdt-number-format.xml diff --git a/Bitkit/Components/PaykitAmountText.swift b/Bitkit/Components/PaykitAmountText.swift index d901b4d98..fe826b988 100644 --- a/Bitkit/Components/PaykitAmountText.swift +++ b/Bitkit/Components/PaykitAmountText.swift @@ -29,5 +29,5 @@ struct PaykitAmountText: View { } func usdDisplayAmount(_ cents: UInt64, locale: Locale = .current) -> String { - (Decimal(cents) / 100).formatted(.number.precision(.fractionLength(2)).locale(locale)) + formatFiatAmount(Decimal(cents) / 100, locale: locale) } diff --git a/Bitkit/Managers/UsdtWalletManager.swift b/Bitkit/Managers/UsdtWalletManager.swift index 05a5e5454..ae66da8d8 100644 --- a/Bitkit/Managers/UsdtWalletManager.swift +++ b/Bitkit/Managers/UsdtWalletManager.swift @@ -558,7 +558,7 @@ extension String { func formattedUsdCents(locale: Locale = .current) -> String { guard let value = UInt64(self) else { return "—" } - return "$" + (Decimal(value) / 100).formatted(.number.locale(locale).precision(.fractionLength(2))) + return "$" + usdDisplayAmount(value, locale: locale) } } diff --git a/Bitkit/Models/Currency.swift b/Bitkit/Models/Currency.swift index 0844bb22a..0cc6f02eb 100644 --- a/Bitkit/Models/Currency.swift +++ b/Bitkit/Models/Currency.swift @@ -88,15 +88,18 @@ func isSuffixSymbolCurrency(_ currencyCode: String) -> Bool { suffixSymbolCurrencies.contains(currencyCode) } -/// Formats a fiat amount the way the in-app currency display does: 2 fraction digits, +/// Formats a fiat amount the way the in-app currency display does: at least 2 fraction digits, /// "." decimal / "," grouping. Single source of truth shared by the app and the widget /// extension so both produce identical strings. -func formatFiatAmount(_ value: Decimal) -> String { +func formatFiatAmount(_ value: Decimal, maximumFractionDigits: Int = 2, + roundingMode: NumberFormatter.RoundingMode = .halfEven, locale: Locale = .current) -> String +{ let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.minimumFractionDigits = 2 - formatter.maximumFractionDigits = 2 - formatter.locale = Locale.current + formatter.maximumFractionDigits = maximumFractionDigits + formatter.roundingMode = roundingMode + formatter.locale = locale formatter.decimalSeparator = "." formatter.groupingSeparator = "," return formatter.string(from: value as NSDecimalNumber) ?? "0.00" diff --git a/Bitkit/Views/Home/HomeWalletView.swift b/Bitkit/Views/Home/HomeWalletView.swift index 36b1160b2..34f1270e9 100644 --- a/Bitkit/Views/Home/HomeWalletView.swift +++ b/Bitkit/Views/Home/HomeWalletView.swift @@ -82,7 +82,7 @@ struct HomeWalletView: View { } icon: { Image("tether-circle").resizable().frame(width: 24, height: 24) } amount: { - BodyMSBText(settings.hideBalance ? " • • • • •" : usdt.balance.map { usdtOverviewAmount($0) } ?? "—", + BodyMSBText(settings.hideBalance ? " • • • • •" : "$ " + usdtOverviewAmount(usdt.balance ?? 0), kerning: 0, lineHeight: 22) .lineLimit(1).minimumScaleFactor(0.7) } diff --git a/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift b/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift index 40e122f65..4a70b8317 100644 --- a/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift +++ b/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift @@ -331,7 +331,7 @@ struct PaymentRequestAmountView: View { ) if asset == .usd { - NumberPadAmountText(value: usdtInputAmount(dollarAmount.isEmpty ? "0" : dollarAmount), symbol: "$") + NumberPadAmountText(value: dollarAmount.isEmpty ? "0" : dollarAmount, symbol: "$") .accessibilityIdentifier("\(testIdentifierPrefix)AmountField") } else { NumberPadTextField(viewModel: amountViewModel, showEditButton: false, isFocused: true, @@ -377,7 +377,6 @@ struct PaymentRequestAmountView: View { } NumberPad( type: asset == .usd ? .decimal : amountViewModel.getNumberPadType(currency: currency), - decimalSeparator: asset == .usd ? Locale.current.decimalSeparator ?? "." : ".", errorKey: amountViewModel.errorKey ) { key in entryConversion.reset() diff --git a/Bitkit/Views/Wallets/Send/SendAmountView.swift b/Bitkit/Views/Wallets/Send/SendAmountView.swift index b49c9288a..ddcb2df25 100644 --- a/Bitkit/Views/Wallets/Send/SendAmountView.swift +++ b/Bitkit/Views/Wallets/Send/SendAmountView.swift @@ -239,7 +239,6 @@ struct SendAmountView: View { NumberPad( type: app.paykitUsesUsdt ? .decimal : amountViewModel.getNumberPadType(currency: currency), - decimalSeparator: app.paykitUsesUsdt ? Locale.current.decimalSeparator ?? "." : ".", errorKey: amountViewModel.errorKey, isDisabled: !canEditAmount ) { key in diff --git a/Bitkit/Views/Wallets/UsdtReceiveAmountView.swift b/Bitkit/Views/Wallets/UsdtReceiveAmountView.swift index ad2413cc7..be193ef1f 100644 --- a/Bitkit/Views/Wallets/UsdtReceiveAmountView.swift +++ b/Bitkit/Views/Wallets/UsdtReceiveAmountView.swift @@ -26,7 +26,7 @@ struct UsdtReceiveAmountView: View { .fixedSize(horizontal: false, vertical: true) Divider() Spacer(minLength: 34) - NumberPad(type: .decimal, decimalSeparator: Locale.current.decimalSeparator ?? ".", isDisabled: busy, + NumberPad(type: .decimal, isDisabled: busy, onDeleteLongPress: { amount = "" }) { key in amount = NumberPadInputHandler.handleInput(key: key, current: amount, maxLength: 21, maxDecimals: 6) diff --git a/Bitkit/Views/Wallets/UsdtSendView.swift b/Bitkit/Views/Wallets/UsdtSendView.swift index dd21dbb6a..b0bb3b92c 100644 --- a/Bitkit/Views/Wallets/UsdtSendView.swift +++ b/Bitkit/Views/Wallets/UsdtSendView.swift @@ -359,7 +359,6 @@ struct UsdtSendView: View { CustomDivider() NumberPad( type: .decimal, - decimalSeparator: Locale.current.decimalSeparator ?? ".", isDisabled: busy || embedded, onDeleteLongPress: { amount = ""; maximumQuote = nil; isMaximumAmount = false; error = nil }, onPress: enterAmount diff --git a/Bitkit/Views/Wallets/UsdtWalletScreen.swift b/Bitkit/Views/Wallets/UsdtWalletScreen.swift index 29395a1f3..067eee96f 100644 --- a/Bitkit/Views/Wallets/UsdtWalletScreen.swift +++ b/Bitkit/Views/Wallets/UsdtWalletScreen.swift @@ -27,15 +27,12 @@ struct UsdtWalletScreen: View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 32) { UsdtAmountHeader( - amount: usdt.balance.map { usdtFormatAmount(amount: $0) } ?? "—", - displayAmount: usdt.balance.map { usdtOverviewAmount($0) }, hideBalance: settings.hideBalance + amount: usdtFormatAmount(amount: usdt.balance ?? 0), + displayAmount: usdtOverviewAmount(usdt.balance ?? 0), hideBalance: settings.hideBalance ) .balanceVisibilityToggle() .accessibilityElement(children: .contain) .accessibilityIdentifier("UsdtBalance") - if let error = usdt.errorMessage { - BodySText(error, textColor: .brandAccent) - } activityList } } @@ -115,9 +112,6 @@ struct UsdtWalletScreen: View { .buttonStyle(.plain) .accessibilityIdentifier("UsdtActivity-\(index)") } - if !usdt.historyComplete { - BodySText(t("usdt__history_sync"), textColor: .textSecondary) - } } } } @@ -149,7 +143,7 @@ struct UsdtAmountHeader: View { CaptionMText("USDT", textColor: .textSecondary, lineHeight: 18) } DisplayText( - "\(prefix)$ " + (hideBalance ? " • • • • •" : displayAmount ?? usdtInputAmount(amount)), + "\(prefix)$ " + (hideBalance ? " • • • • •" : displayAmount ?? amount), accentColor: .textSecondary, accentFont: Fonts.extraBold ) .lineLimit(1).minimumScaleFactor(0.5) @@ -200,18 +194,12 @@ func usdtDisplaySats(amount: String, rate: PaykitExchangeRate?, now: Date = Date } func usdtOverviewAmount(_ amount: UInt64, locale: Locale = .current) -> String { - let format = Decimal.FormatStyle.number.precision(.fractionLength(2)) - .rounded(rule: .toNearestOrAwayFromZero).locale(locale) if amount > 0, amount < 10000 { - return "<" + (Decimal(1) / 100).formatted(format) + return "<" + formatFiatAmount(Decimal(1) / 100, locale: locale) } - return (Decimal(amount) / 1_000_000).formatted(format) + return formatFiatAmount(Decimal(amount) / 1_000_000, roundingMode: .halfUp, locale: locale) } func usdtExactAmount(_ amount: UInt64, locale: Locale = .current) -> String { - (Decimal(amount) / 1_000_000).formatted(.number.precision(.fractionLength(2 ... 6)).locale(locale)) -} - -func usdtInputAmount(_ amount: String, locale: Locale = .current) -> String { - amount.replacingOccurrences(of: ".", with: locale.decimalSeparator ?? ".") + formatFiatAmount(Decimal(amount) / 1_000_000, maximumFractionDigits: 6, locale: locale) } diff --git a/BitkitTests/PaykitAmountTests.swift b/BitkitTests/PaykitAmountTests.swift index f5f1cfede..a8fa402be 100644 --- a/BitkitTests/PaykitAmountTests.swift +++ b/BitkitTests/PaykitAmountTests.swift @@ -18,25 +18,20 @@ final class PaykitAmountTests: XCTestCase { let amount = try PaykitAmount(asset: asset, value: value) XCTAssertEqual(amount.inputValue, expected) XCTAssertEqual(try PaykitAmount(asset: asset, value: amount.inputValue), amount) - if asset == .usdt { - XCTAssertEqual(usdtInputAmount(amount.inputValue, locale: Locale(identifier: "en_US")), expected) - XCTAssertEqual( - usdtInputAmount(amount.inputValue, locale: Locale(identifier: "de_DE")), - expected.replacingOccurrences(of: ".", with: ",") - ) - } } XCTAssertEqual(try PaykitAmount(asset: .usdt, value: "150.50").value, "150.5") } - func testUsdDisplayPreservesCentsAndUsesLocaleGrouping() { - for (cents, us, de): (UInt64, String, String) in [ - (0, "0.00", "0,00"), (1, "0.01", "0,01"), (100, "1.00", "1,00"), - (15050, "150.50", "150,50"), (123_456, "1,234.56", "1.234,56"), - (.max, "184,467,440,737,095,516.15", "184.467.440.737.095.516,15"), + func testUsdDisplayPreservesCentsAndMatchesFiatSeparatorsAcrossRegions() { + for (cents, expected): (UInt64, String) in [ + (0, "0.00"), (1, "0.01"), (100, "1.00"), (15050, "150.50"), (123_456, "1,234.56"), + (.max, "184,467,440,737,095,516.15"), ] { - XCTAssertEqual(usdDisplayAmount(cents, locale: Locale(identifier: "en_US")), us, "cents: \(cents)") - XCTAssertEqual(usdDisplayAmount(cents, locale: Locale(identifier: "de_DE")), de, "cents: \(cents)") + for identifier in ["en_US", "de_DE", "nl_NL", "fr_FR"] { + let locale = Locale(identifier: identifier) + XCTAssertEqual(usdDisplayAmount(cents, locale: locale), expected, identifier) + XCTAssertEqual(usdDisplayAmount(cents, locale: locale), formatFiatAmount(Decimal(cents) / 100, locale: locale)) + } } } diff --git a/BitkitTests/UsdtDisplayTests.swift b/BitkitTests/UsdtDisplayTests.swift index b49a51856..cb8bc7e73 100644 --- a/BitkitTests/UsdtDisplayTests.swift +++ b/BitkitTests/UsdtDisplayTests.swift @@ -3,10 +3,16 @@ import Foundation import XCTest final class UsdtDisplayTests: XCTestCase { - func testExactAmountsPreserveTokenPrecisionAndLocaleSeparators() { - XCTAssertEqual(usdtExactAmount(4_996_000, locale: Locale(identifier: "en_US")), "4.996") - XCTAssertEqual(usdtExactAmount(1_234_567, locale: Locale(identifier: "de_DE")), "1,234567") - XCTAssertEqual(usdtExactAmount(3_500_000, locale: Locale(identifier: "de_DE")), "3,50") + func testExactAmountsPreserveTokenPrecisionAcrossRegions() { + for identifier in ["en_US", "de_DE", "nl_NL", "fr_FR"] { + let locale = Locale(identifier: identifier) + for (amount, expected): (UInt64, String) in [ + (0, "0.00"), (1, "0.000001"), (4_996_000, "4.996"), (1_234_567, "1.234567"), + (3_500_000, "3.50"), (.max, "18,446,744,073,709.551615"), + ] { + XCTAssertEqual(usdtExactAmount(amount, locale: locale), expected, identifier) + } + } } func testBitcoinEquivalentRequiresFreshRateAndFitsDisplayBounds() { @@ -20,24 +26,14 @@ final class UsdtDisplayTests: XCTestCase { XCTAssertNil(usdtDisplaySats(amount: "999999999999999999999999", rate: rate, now: now)) } - func testAmountInputUsesLocaleSeparatorWithoutRoundingTypedPrecision() { - for amount in ["0", "0.", "3.25", "0.000001", "1.230000", "1234567.890000"] { - XCTAssertEqual(usdtInputAmount(amount, locale: Locale(identifier: "en_US")), amount) - XCTAssertEqual(usdtInputAmount(amount, locale: Locale(identifier: "de_DE")), amount.replacingOccurrences(of: ".", with: ",")) - XCTAssertEqual(usdtInputAmount(amount, locale: Locale(identifier: "fr_FR")), amount.replacingOccurrences(of: ".", with: ",")) - XCTAssertEqual(usdtInputAmount(amount, locale: Locale(identifier: "en_BR")), amount.replacingOccurrences(of: ".", with: ",")) - } - } - func testOverviewAmountsRoundToCentsWithoutHidingSmallBalances() { - let locale = Locale(identifier: "en_US") for (amount, expected): (UInt64, String) in [ (0, "0.00"), (1, "<0.01"), (9999, "<0.01"), (10000, "0.01"), (10_000_000, "10.00"), (123_456_789, "123.46"), (1_025_000, "1.03"), (1_234_567_890, "1,234.57"), ] { - XCTAssertEqual(usdtOverviewAmount(amount, locale: locale), expected, "amount: \(amount)") + for identifier in ["en_US", "de_DE", "nl_NL", "fr_FR"] { + XCTAssertEqual(usdtOverviewAmount(amount, locale: Locale(identifier: identifier)), expected, identifier) + } } - XCTAssertEqual(usdtOverviewAmount(1, locale: Locale(identifier: "de_DE")), "<0,01") - XCTAssertEqual(usdtOverviewAmount(1_234_567_890, locale: Locale(identifier: "de_DE")), "1.234,57") } } diff --git a/BitkitTests/UsdtWalletManagerTests.swift b/BitkitTests/UsdtWalletManagerTests.swift index 959791bac..f047c8e7e 100644 --- a/BitkitTests/UsdtWalletManagerTests.swift +++ b/BitkitTests/UsdtWalletManagerTests.swift @@ -115,10 +115,10 @@ final class UsdtWalletManagerTests: XCTestCase { XCTAssertEqual(attempts, 2) } - func testUsdCentsUseTheLocalesDecimalAndGroupingSeparators() { + func testUsdCentsUseTheFiatDecimalAndGroupingSeparators() { XCTAssertEqual("1000000".formattedUsdCents(locale: Locale(identifier: "en_US")), "$10,000.00") - XCTAssertEqual("1000000".formattedUsdCents(locale: Locale(identifier: "de_DE")), "$10.000,00") - XCTAssertEqual("5".formattedUsdCents(locale: Locale(identifier: "de_DE")), "$0,05") + XCTAssertEqual("1000000".formattedUsdCents(locale: Locale(identifier: "de_DE")), "$10,000.00") + XCTAssertEqual("5".formattedUsdCents(locale: Locale(identifier: "de_DE")), "$0.05") XCTAssertEqual("invalid".formattedUsdCents(locale: Locale(identifier: "en_US")), "—") } diff --git a/BitkitUITests/UsdtWalletUITests.swift b/BitkitUITests/UsdtWalletUITests.swift index 786e4d046..74db3c297 100644 --- a/BitkitUITests/UsdtWalletUITests.swift +++ b/BitkitUITests/UsdtWalletUITests.swift @@ -2,26 +2,44 @@ import XCTest final class UsdtWalletUITests: XCTestCase { @MainActor - func testOrdinaryManualEntryRoutesBareAddressToUsdt() throws { + func testEmptyWalletAndManualEntryUseConsistentAmountsInCommaRegion() throws { continueAfterFailure = false let app = XCUIApplication() - app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] - app.launchEnvironment["USDT_RPC_URL"] = "http://127.0.0.1:18545" - app.launchEnvironment["USDT_BUNDLER_URL"] = "http://127.0.0.1:18546" + app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "de_DE"] + app.launchEnvironment["USDT_RPC_URL"] = "http://127.0.0.1:1" + app.launchEnvironment["USDT_BUNDLER_URL"] = "http://127.0.0.1:1" app.launchEnvironment["USDT_BRIDGE_NETWORKS"] = "polygon" app.launch() if app.buttons["Continue"].waitForExistence(timeout: 3) { app.buttons["Continue"].tap() } + var createdWallet = false if app.buttons["SkipIntro"].waitForExistence(timeout: 3) { app.buttons["SkipIntro"].tap() XCTAssertTrue(app.buttons["NewWallet"].waitForExistence(timeout: 5)) app.buttons["NewWallet"].tap() + createdWallet = true } XCTAssertTrue(app.otherElements["TotalBalance-primary"].waitForExistence(timeout: 90)) if app.buttons["UsdtIntroEnable"].waitForExistence(timeout: 10) { app.buttons["UsdtIntroEnable"].tap() } + let homeTile = app.buttons["UsdtWallet"] + XCTAssertTrue(homeTile.waitForExistence(timeout: 10), app.debugDescription) + if createdWallet { + XCTAssertTrue(homeTile.label.contains("$ 0.00"), homeTile.label) + } + capture(app, "USDT empty Home balance in German region") + homeTile.tap() + XCTAssertTrue(app.buttons["UsdtReceive"].waitForExistence(timeout: 5)) + let balance = app.otherElements["UsdtBalance"] + if createdWallet { + XCTAssertTrue(balance.staticTexts["$ 0.00"].exists, app.debugDescription) + } + XCTAssertFalse(app.staticTexts["Could not update USDT. Check your connection and try again."].exists) + XCTAssertFalse(app.staticTexts["Older activity is still syncing. Your balance is up to date when the connection is available."].exists) + capture(app, "USDT empty wallet in German region") + try XCTUnwrap(app.buttons.matching(identifier: "NavigationBack").allElementsBoundByIndex.first(where: \.isHittable)).tap() app.buttons["Send"].tap() XCTAssertTrue(app.buttons["RecipientManual"].waitForExistence(timeout: 5)) app.buttons["RecipientManual"].tap() @@ -40,6 +58,12 @@ final class UsdtWalletUITests: XCTestCase { XCTAssertTrue(app.buttons["UsdtReview"].waitForExistence(timeout: 10), app.debugDescription) XCTAssertTrue(app.staticTexts["$ 0"].exists) XCTAssertFalse(app.buttons["UsdtReview"].isEnabled) + XCTAssertTrue(app.buttons["NDecimal"].label.contains(".")) + app.buttons["N1"].tap() + app.buttons["NDecimal"].tap() + app.buttons["N5"].tap() + XCTAssertTrue(app.staticTexts["$ 1.5"].exists, app.debugDescription) + capture(app, "USDT dot decimal entry in German region") let network = app.buttons["UsdtNetwork"] XCTAssertTrue(network.label.contains("Arbitrum One")) XCTAssertTrue(network.isEnabled) diff --git a/changelog.d/next/1077.fixed.md b/changelog.d/next/1077.fixed.md new file mode 100644 index 000000000..a5ed912ba --- /dev/null +++ b/changelog.d/next/1077.fixed.md @@ -0,0 +1 @@ +Show USDT balances quietly from zero with consistent decimal formatting and a currency symbol on Home. diff --git a/journeys/README.md b/journeys/README.md index 84ec3912b..5c9c4b960 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -189,7 +189,7 @@ journey PR, which is what made this file conflict on every merge. | Capability | Provided by | | --- | --- | -| USDT wallet navigation without funding | an onboarded build with USDT configured; provider errors may remain visible while offline — [USDT QA](../Docs/USDT-QA.md) | +| USDT wallet navigation without funding | an onboarded build with USDT configured; wallet syncing remains silent while offline; payment-action errors remain available — [USDT QA](../Docs/USDT-QA.md) | | Controlled USDT quotes, provider history, funded submission and recovery | an explicitly configured local USDT gateway/RPC/bundler fixture with owned test accounts and funds; absent from the standard journey environment. Report these steps as blocked unless that setup and transfer authorization are supplied — [USDT QA](../Docs/USDT-QA.md) | | On-chain funds and blocks on regtest | `../bitkit-android/lsp` deposit and mine, borrowed from the sibling Android checkout until #694 lands an iOS copy — [Backend preconditions](#backend-preconditions) | | Lightning channels, CJIT orders and quoted maxima | the LSP the `E2E_BUILD` app targets, plus its node as an external LN peer — [Backend preconditions](#backend-preconditions), [amount-limits](amount-limits/README.md) | diff --git a/journeys/usdt-number-format.xml b/journeys/usdt-number-format.xml new file mode 100644 index 000000000..489e74ce4 --- /dev/null +++ b/journeys/usdt-number-format.xml @@ -0,0 +1,12 @@ + + Use an onboarded unfunded test wallet with USDT enabled. Set the system region to Germany or the Netherlands, keep the app language English, and restart the app. Use the same region on both platforms. No payment is submitted; a configured offline USDT provider is sufficient for empty balances and amount entry. + + Set the wallet display unit to dollars. On Home, verify Savings, Spending and USDT all use a dot decimal separator, and the USDT tile shows $ 0.00 for an empty wallet. + Open USDT. Verify the balance shows $ 0.00, no refresh-error or history-sync copy appears underneath, and Send and Receive remain available. + Return Home and hide balances. Verify the USDT tile is masked. Open USDT and verify its balance is masked. Restore visible balances. + Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the displayed request is 3.50 with a dot. + Open Show Details and verify the exact amount remains 3.50. Return and edit the amount to 0.000001. Verify all six decimals remain visible while editing, the QR payment URI encodes one atomic unit (uint256=1), and the receive overview shows <0.01 rather than 0.00. + Return Home. Open Send, Enter Manually, and enter 0x1111111111111111111111111111111111111111. Continue to the USDT amount screen. Verify its decimal key is a dot and entering 1.25 displays $ 1.25. Close without submitting. + Repeat the empty balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. + + diff --git a/journeys/usdt-wallet-navigation.xml b/journeys/usdt-wallet-navigation.xml index a742c3cb9..e61965445 100644 --- a/journeys/usdt-wallet-navigation.xml +++ b/journeys/usdt-wallet-navigation.xml @@ -1,9 +1,10 @@ - Use an onboarded wallet in an E2E build with USDT RPC and bundler URLs configured. No chain funding or live provider is required. A network error may be visible when services are offline; no payment is submitted. + Use an onboarded wallet in an E2E build with USDT RPC and bundler URLs configured. No chain funding or live provider is required. Wallet syncing stays silent when services are offline; no payment is submitted. Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. On Home, verify the Savings/Spending and USDT balance dividers align vertically, including any hardware wallet rows. With an odd number of paired hardware wallets, verify USDT fills the last row’s empty cell; with an even number, it starts the next row. Verify USDT balances and activity amounts show at most two decimals; a nonzero amount below one cent shows <0.01. Open USDT from Home and verify the wallet screen uses the same balance, action buttons and activity layout as the Bitcoin wallet, with its Tether coin illustration anchored at the upper right. + With an empty wallet, verify Home and the USDT wallet show $ 0.00, including before the first successful refresh. Verify no red refresh error or grey history-sync copy appears below the balance while the provider is offline. Open Receive. Verify the network is Arbitrum One. Open details, verify the address starts with 0x, and verify Details Copy contains only the address. Return to the QR and verify Copy and tapping the QR both copy only the same address, while the QR still contains the full USDT payment URI including the network and token. On Android, system Back from Details returns to the USDT QR; Back from the QR closes Receive without switching to Savings. Close Receive, open Send and manual entry. Enter an invalid Bitcoin address and verify the address error prevents continuing. Replace it with an owned valid EVM address. Continue to Amount, enter six decimal places and try a seventh; verify the amount remains limited to six. Close before requesting a quote. From 54f1f23e68f85e9360791321387223e1e8e3294e Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 05:59:10 -0600 Subject: [PATCH 2/5] chore: remove unused usdt sync translations --- Bitkit/Resources/Localization/ar.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/ca.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/cs.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/de.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/el.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/en.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/es-419.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/es.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/fr.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/it.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/nl.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/pl.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/pt.lproj/Localizable.strings | 1 - Bitkit/Resources/Localization/ru.lproj/Localizable.strings | 1 - 15 files changed, 15 deletions(-) diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 0bcf440cd..29dfe5c1f 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -1235,7 +1235,6 @@ "usdt__failed" = "فشل"; "usdt__fee_note" = "تُدفع جميع الرسوم بـ USDT. قد تكون الرسوم النهائية أقل."; "usdt__hide_details" = "إخفاء التفاصيل"; -"usdt__history_sync" = "لا تزال مزامنة النشاط الأقدم جارية. رصيدك محدَّث عند توفر الاتصال."; "usdt__network_warning" = "اختر الشبكة التي يستخدمها المستلم. قد يؤدي الإرسال إلى عنوان على شبكة خاطئة إلى فقدان الأموال."; "usdt__pending" = "معلق"; "usdt__received" = "مستلم"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 640207c62..afba5c2ad 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -1665,7 +1665,6 @@ "usdt__failed" = "Ha fallat"; "usdt__fee_note" = "Totes les tarifes es paguen en USDT. La tarifa final pot ser inferior."; "usdt__hide_details" = "Amagar detalls"; -"usdt__history_sync" = "L'activitat anterior encara s'està sincronitzant. El teu saldo està al dia quan hi ha connexió."; "usdt__network_warning" = "Selecciona la xarxa que utilitza el destinatari. Si envies a una adreça d'una xarxa incorrecta, pots perdre els fons."; "usdt__pending" = "Pendent"; "usdt__received" = "Rebut"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index b28cb1392..64b9901f2 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -1805,7 +1805,6 @@ "usdt__failed" = "Selhání"; "usdt__fee_note" = "Všechny poplatky se platí v USDT. Konečný poplatek může být nižší."; "usdt__hide_details" = "Skrýt podrobnosti"; -"usdt__history_sync" = "Starší aktivita se stále synchronizuje. Když je připojení k dispozici, je váš zůstatek aktuální."; "usdt__network_warning" = "Vyberte síť, kterou používá příjemce. Odesláním na adresu v nesprávné síti můžete přijít o prostředky."; "usdt__pending" = "Čekající"; "usdt__received" = "Přijato"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index e082793b2..7193a9114 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -1802,7 +1802,6 @@ "usdt__failed" = "Fehlgeschlagen"; "usdt__fee_note" = "Alle Gebühren werden in USDT bezahlt. Die endgültige Gebühr kann niedriger sein."; "usdt__hide_details" = "Details ausblenden"; -"usdt__history_sync" = "Ältere Aktivitäten werden noch synchronisiert. Dein Guthaben ist aktuell, sobald eine Verbindung besteht."; "usdt__network_warning" = "Wähle das Netzwerk, das der Empfänger verwendet. Beim Senden an eine Adresse im falschen Netzwerk kann Guthaben verloren gehen."; "usdt__pending" = "Ausstehend"; "usdt__received" = "Empfangen"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index a417f60da..e9e0891df 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -1536,7 +1536,6 @@ "usdt__failed" = "Απέτυχε"; "usdt__fee_note" = "Όλα τα τέλη πληρώνονται σε USDT. Το τελικό τέλος μπορεί να είναι χαμηλότερο."; "usdt__hide_details" = "Απόκρυψη λεπτομερειών"; -"usdt__history_sync" = "Η παλαιότερη δραστηριότητα συγχρονίζεται ακόμα. Το υπόλοιπό σου είναι ενημερωμένο όταν υπάρχει σύνδεση."; "usdt__network_warning" = "Επίλεξε το δίκτυο που χρησιμοποιεί ο παραλήπτης. Η αποστολή σε διεύθυνση σε λάθος δίκτυο μπορεί να οδηγήσει σε απώλεια κεφαλαίων."; "usdt__pending" = "Εκκρεμεί"; "usdt__received" = "Ελήφθη"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 07b8118fb..d1446112b 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -1847,7 +1847,6 @@ "usdt__failed" = "Failed"; "usdt__fee_note" = "All fees are paid in USDT. Final fee may be lower."; "usdt__hide_details" = "Hide details"; -"usdt__history_sync" = "Older activity is still syncing. Your balance is up to date when the connection is available."; "usdt__maximum_fee" = "Maximum fee"; "usdt__network_warning" = "Select the network used by the recipient. Sending to an address on the wrong network can lose funds."; "usdt__pending" = "Pending"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index ba9bf100c..12a96310c 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -1808,7 +1808,6 @@ "usdt__failed" = "Fallido"; "usdt__fee_note" = "Todas las comisiones se pagan en USDT. La comisión final puede ser menor."; "usdt__hide_details" = "Ocultar detalles"; -"usdt__history_sync" = "La actividad anterior aún se está sincronizando. Su saldo estará actualizado cuando haya conexión."; "usdt__network_warning" = "Seleccione la red que usa el destinatario. Si envía a una dirección en la red equivocada, puede perder los fondos."; "usdt__pending" = "Pendiente"; "usdt__received" = "Recibido"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index d9c7913bd..73936e5bf 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -1685,7 +1685,6 @@ "usdt__failed" = "Fallido"; "usdt__fee_note" = "Todas las comisiones se pagan en USDT. La comisión final puede ser menor."; "usdt__hide_details" = "Ocultar detalles"; -"usdt__history_sync" = "La actividad anterior aún se está sincronizando. Su saldo estará actualizado cuando haya conexión."; "usdt__network_warning" = "Seleccione la red que usa el destinatario. Si envía a una dirección en la red equivocada, puede perder los fondos."; "usdt__pending" = "Pendiente"; "usdt__received" = "Recibido"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index 52e840f68..485b2f9f0 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -1816,7 +1816,6 @@ "usdt__failed" = "Échec"; "usdt__fee_note" = "Tous les frais sont payés en USDT. Les frais finaux peuvent être inférieurs."; "usdt__hide_details" = "Masquer les détails"; -"usdt__history_sync" = "L'activité plus ancienne est encore en cours de synchronisation. Votre solde est à jour lorsque la connexion est disponible."; "usdt__network_warning" = "Sélectionnez le réseau utilisé par le destinataire. Un envoi vers une adresse sur le mauvais réseau peut entraîner une perte de fonds."; "usdt__pending" = "En attente"; "usdt__received" = "Reçu"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index a65e4b51e..c9f0fdf70 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -1775,7 +1775,6 @@ "usdt__failed" = "Fallito"; "usdt__fee_note" = "Tutte le commissioni sono pagate in USDT. La commissione finale potrebbe essere inferiore."; "usdt__hide_details" = "Nascondi dettagli"; -"usdt__history_sync" = "Le attività meno recenti sono ancora in sincronizzazione. Il tuo saldo è aggiornato quando la connessione è disponibile."; "usdt__network_warning" = "Seleziona la rete usata dal destinatario. Inviare a un indirizzo sulla rete sbagliata può causare la perdita dei fondi."; "usdt__pending" = "In attesa"; "usdt__received" = "Ricevuto"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index 5db94734b..a856f111f 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -1810,7 +1810,6 @@ "usdt__failed" = "Mislukt"; "usdt__fee_note" = "Alle vergoedingen worden in USDT betaald. De uiteindelijke vergoeding kan lager zijn."; "usdt__hide_details" = "Details verbergen"; -"usdt__history_sync" = "Oudere activiteit wordt nog gesynchroniseerd. Uw saldo is actueel zodra er verbinding is."; "usdt__network_warning" = "Selecteer het netwerk dat de ontvanger gebruikt. Verzenden naar een adres op het verkeerde netwerk kan tot verlies van geld leiden."; "usdt__pending" = "In behandeling"; "usdt__received" = "Ontvangen"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index afcd7b4a1..b69c3c9ec 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -1815,7 +1815,6 @@ "usdt__failed" = "Niepowodzenie"; "usdt__fee_note" = "Wszystkie opłaty są płacone w USDT. Ostateczna opłata może być niższa."; "usdt__hide_details" = "Ukryj szczegóły"; -"usdt__history_sync" = "Starsza aktywność wciąż się synchronizuje. Saldo jest aktualne, gdy połączenie jest dostępne."; "usdt__network_warning" = "Wybierz sieć używaną przez odbiorcę. Wysłanie na adres w niewłaściwej sieci może skutkować utratą środków."; "usdt__pending" = "W toku"; "usdt__received" = "Otrzymano"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index c84d25a17..a84c0e87b 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -1816,7 +1816,6 @@ "usdt__failed" = "Falhou"; "usdt__fee_note" = "Todas as taxas são pagas em USDT. A taxa final pode ser menor."; "usdt__hide_details" = "Ocultar detalhes"; -"usdt__history_sync" = "A atividade mais antiga ainda está sincronizando. Seu saldo fica atualizado sempre que houver conexão."; "usdt__network_warning" = "Selecione a rede usada pelo destinatário. Enviar para um endereço na rede errada pode causar a perda dos fundos."; "usdt__pending" = "Pendente"; "usdt__received" = "Recebido"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index 3ba9289c8..1da3cd590 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -1462,7 +1462,6 @@ "usdt__failed" = "Falhou"; "usdt__fee_note" = "Todas as taxas são pagas em USDT. A taxa final pode ser inferior."; "usdt__hide_details" = "Ocultar detalhes"; -"usdt__history_sync" = "A atividade mais antiga ainda está a sincronizar. O seu saldo está atualizado sempre que houver ligação."; "usdt__network_warning" = "Selecione a rede usada pelo destinatário. Enviar para um endereço na rede errada pode resultar na perda de fundos."; "usdt__pending" = "Pendente"; "usdt__received" = "Recebido"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index a332ce783..d3ce4f46f 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -1796,7 +1796,6 @@ "usdt__failed" = "Не удалось"; "usdt__fee_note" = "Все комиссии оплачиваются в USDT. Итоговая комиссия может быть ниже."; "usdt__hide_details" = "Скрыть детали"; -"usdt__history_sync" = "Более ранняя активность ещё синхронизируется. Ваш баланс актуален, когда есть подключение."; "usdt__network_warning" = "Выберите сеть, которую использует получатель. Отправка на адрес в неверной сети может привести к потере средств."; "usdt__pending" = "В ожидании"; "usdt__received" = "Получено"; From 7bd57f838fa4e793bc489b5f3e214fc37c50819a Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 06:25:58 -0600 Subject: [PATCH 3/5] fix: allow entry in zero-dollar request drafts --- Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift | 2 +- journeys/usdt-number-format.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift b/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift index 4a70b8317..e2676c3a5 100644 --- a/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift +++ b/Bitkit/Views/PaymentRequests/CreatePaymentRequestView.swift @@ -413,7 +413,7 @@ struct PaymentRequestAmountView: View { if asset == .btc { amountViewModel.updateFromSats(initialDraft.amount.atomic, currency: currency) } else { - dollarAmount = initialDraft.amount.inputValue + dollarAmount = initialDraft.amount.atomic == 0 ? "" : initialDraft.amount.inputValue } } } diff --git a/journeys/usdt-number-format.xml b/journeys/usdt-number-format.xml index 489e74ce4..5a1ae4fb3 100644 --- a/journeys/usdt-number-format.xml +++ b/journeys/usdt-number-format.xml @@ -4,8 +4,8 @@ Set the wallet display unit to dollars. On Home, verify Savings, Spending and USDT all use a dot decimal separator, and the USDT tile shows $ 0.00 for an empty wallet. Open USDT. Verify the balance shows $ 0.00, no refresh-error or history-sync copy appears underneath, and Send and Receive remain available. Return Home and hide balances. Verify the USDT tile is masked. Open USDT and verify its balance is masked. Restore visible balances. - Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the displayed request is 3.50 with a dot. - Open Show Details and verify the exact amount remains 3.50. Return and edit the amount to 0.000001. Verify all six decimals remain visible while editing, the QR payment URI encodes one atomic unit (uint256=1), and the receive overview shows <0.01 rather than 0.00. + Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the QR payment URI encodes 3500000 atomic units (uint256=3500000). + Open Show Details and verify the receiving address matches the QR recipient. Return to the QR and edit the amount to 0.000001. Verify all six decimals remain visible while editing and the QR payment URI encodes one atomic unit (uint256=1). Return Home. Open Send, Enter Manually, and enter 0x1111111111111111111111111111111111111111. Continue to the USDT amount screen. Verify its decimal key is a dot and entering 1.25 displays $ 1.25. Close without submitting. Repeat the empty balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. From 6e5af52c116743f48cd859ea660f34e641dcce89 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 06:27:27 -0600 Subject: [PATCH 4/5] test: expect consistent refund amount formatting --- BitkitTests/UsdtWalletManagerTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BitkitTests/UsdtWalletManagerTests.swift b/BitkitTests/UsdtWalletManagerTests.swift index f047c8e7e..acb5b4f72 100644 --- a/BitkitTests/UsdtWalletManagerTests.swift +++ b/BitkitTests/UsdtWalletManagerTests.swift @@ -376,14 +376,14 @@ final class UsdtWalletManagerTests: XCTestCase { XCTAssertFalse(detail.canRequestRefund) } - func testRefundConfirmationShowsLocalizedAmountNetworkAndFullAddress() { + func testRefundConfirmationShowsExactAmountNetworkAndFullAddress() { let address = "0x1111222233334444555566667777888899990000" let deposit = UsdtDeposit(id: "deposit", network: "bsc", asset: "USDT", amount: 50_000_001, sourceTx: "source-tx", status: "held", code: nil, refundTx: nil) let detail = UsdtDepositDetail(deposit: deposit, order: nil) - for (identifier, amount) in [("en_US", "50.000001"), ("de_DE", "50,000001")] { + for identifier in ["en_US", "de_DE", "nl_NL", "fr_FR"] { let confirmation = detail.refundConfirmation(address: address, hideBalance: false, locale: Locale(identifier: identifier)) - for value in [amount, "BNB Smart Chain (BSC)", address, "Refund fees may apply."] { + for value in ["50.000001", "BNB Smart Chain (BSC)", address, "Refund fees may apply."] { XCTAssertTrue(confirmation.contains(value), value) } } From c323884cb2e1b4accd7b22d6247a01a52c158b6e Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 08:33:55 -0600 Subject: [PATCH 5/5] fix: distinguish unknown usdt balances --- Bitkit/Views/Home/HomeWalletView.swift | 2 +- Bitkit/Views/Wallets/UsdtWalletScreen.swift | 4 ++-- BitkitTests/UsdtDisplayTests.swift | 1 + BitkitUITests/UsdtWalletUITests.swift | 18 +++++++----------- changelog.d/next/1077.fixed.md | 2 +- journeys/usdt-bridge-details.xml | 2 +- journeys/usdt-number-format.xml | 8 ++++---- journeys/usdt-wallet-navigation.xml | 2 +- 8 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Bitkit/Views/Home/HomeWalletView.swift b/Bitkit/Views/Home/HomeWalletView.swift index 34f1270e9..80aa46790 100644 --- a/Bitkit/Views/Home/HomeWalletView.swift +++ b/Bitkit/Views/Home/HomeWalletView.swift @@ -82,7 +82,7 @@ struct HomeWalletView: View { } icon: { Image("tether-circle").resizable().frame(width: 24, height: 24) } amount: { - BodyMSBText(settings.hideBalance ? " • • • • •" : "$ " + usdtOverviewAmount(usdt.balance ?? 0), + BodyMSBText(settings.hideBalance ? " • • • • •" : usdt.balance.map { "$ " + usdtOverviewAmount($0) } ?? "—", kerning: 0, lineHeight: 22) .lineLimit(1).minimumScaleFactor(0.7) } diff --git a/Bitkit/Views/Wallets/UsdtWalletScreen.swift b/Bitkit/Views/Wallets/UsdtWalletScreen.swift index 067eee96f..f1dfa3b97 100644 --- a/Bitkit/Views/Wallets/UsdtWalletScreen.swift +++ b/Bitkit/Views/Wallets/UsdtWalletScreen.swift @@ -27,8 +27,8 @@ struct UsdtWalletScreen: View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 32) { UsdtAmountHeader( - amount: usdtFormatAmount(amount: usdt.balance ?? 0), - displayAmount: usdtOverviewAmount(usdt.balance ?? 0), hideBalance: settings.hideBalance + amount: usdt.balance.map { usdtFormatAmount(amount: $0) } ?? "—", + displayAmount: usdt.balance.map { usdtOverviewAmount($0) }, hideBalance: settings.hideBalance ) .balanceVisibilityToggle() .accessibilityElement(children: .contain) diff --git a/BitkitTests/UsdtDisplayTests.swift b/BitkitTests/UsdtDisplayTests.swift index cb8bc7e73..985bd164c 100644 --- a/BitkitTests/UsdtDisplayTests.swift +++ b/BitkitTests/UsdtDisplayTests.swift @@ -22,6 +22,7 @@ final class UsdtDisplayTests: XCTestCase { XCTAssertEqual(usdtDisplaySats(amount: "0.000001", rate: rate, now: now), 0) XCTAssertNil(usdtDisplaySats(amount: "3.5", rate: nil, now: now)) XCTAssertNil(usdtDisplaySats(amount: "3.5", rate: PaykitExchangeRate(price: "100000", timestamp: now.addingTimeInterval(-601)), now: now)) + XCTAssertNil(usdtDisplaySats(amount: "—", rate: rate, now: now)) XCTAssertNil(usdtDisplaySats(amount: "-1", rate: rate, now: now)) XCTAssertNil(usdtDisplaySats(amount: "999999999999999999999999", rate: rate, now: now)) } diff --git a/BitkitUITests/UsdtWalletUITests.swift b/BitkitUITests/UsdtWalletUITests.swift index 74db3c297..cc0b49d38 100644 --- a/BitkitUITests/UsdtWalletUITests.swift +++ b/BitkitUITests/UsdtWalletUITests.swift @@ -2,7 +2,7 @@ import XCTest final class UsdtWalletUITests: XCTestCase { @MainActor - func testEmptyWalletAndManualEntryUseConsistentAmountsInCommaRegion() throws { + func testUnknownBalanceAndManualEntryUseConsistentAmountsInCommaRegion() throws { continueAfterFailure = false let app = XCUIApplication() app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "de_DE"] @@ -13,12 +13,10 @@ final class UsdtWalletUITests: XCTestCase { if app.buttons["Continue"].waitForExistence(timeout: 3) { app.buttons["Continue"].tap() } - var createdWallet = false if app.buttons["SkipIntro"].waitForExistence(timeout: 3) { app.buttons["SkipIntro"].tap() XCTAssertTrue(app.buttons["NewWallet"].waitForExistence(timeout: 5)) app.buttons["NewWallet"].tap() - createdWallet = true } XCTAssertTrue(app.otherElements["TotalBalance-primary"].waitForExistence(timeout: 90)) if app.buttons["UsdtIntroEnable"].waitForExistence(timeout: 10) { @@ -26,19 +24,17 @@ final class UsdtWalletUITests: XCTestCase { } let homeTile = app.buttons["UsdtWallet"] XCTAssertTrue(homeTile.waitForExistence(timeout: 10), app.debugDescription) - if createdWallet { - XCTAssertTrue(homeTile.label.contains("$ 0.00"), homeTile.label) - } - capture(app, "USDT empty Home balance in German region") + XCTAssertTrue(homeTile.label.contains("—"), homeTile.label) + XCTAssertFalse(homeTile.label.contains("0.00"), homeTile.label) + capture(app, "USDT unknown Home balance in German region") homeTile.tap() XCTAssertTrue(app.buttons["UsdtReceive"].waitForExistence(timeout: 5)) let balance = app.otherElements["UsdtBalance"] - if createdWallet { - XCTAssertTrue(balance.staticTexts["$ 0.00"].exists, app.debugDescription) - } + XCTAssertTrue(balance.staticTexts["$ —"].exists, app.debugDescription) + XCTAssertFalse(balance.staticTexts["$ 0.00"].exists) XCTAssertFalse(app.staticTexts["Could not update USDT. Check your connection and try again."].exists) XCTAssertFalse(app.staticTexts["Older activity is still syncing. Your balance is up to date when the connection is available."].exists) - capture(app, "USDT empty wallet in German region") + capture(app, "USDT unknown wallet in German region") try XCTUnwrap(app.buttons.matching(identifier: "NavigationBack").allElementsBoundByIndex.first(where: \.isHittable)).tap() app.buttons["Send"].tap() XCTAssertTrue(app.buttons["RecipientManual"].waitForExistence(timeout: 5)) diff --git a/changelog.d/next/1077.fixed.md b/changelog.d/next/1077.fixed.md index a5ed912ba..3b3e2a979 100644 --- a/changelog.d/next/1077.fixed.md +++ b/changelog.d/next/1077.fixed.md @@ -1 +1 @@ -Show USDT balances quietly from zero with consistent decimal formatting and a currency symbol on Home. +Show USDT balances quietly with consistent decimals, a currency symbol on Home and an unknown marker until the balance loads. diff --git a/journeys/usdt-bridge-details.xml b/journeys/usdt-bridge-details.xml index 20aa56d20..e247fe0cc 100644 --- a/journeys/usdt-bridge-details.xml +++ b/journeys/usdt-bridge-details.xml @@ -5,7 +5,7 @@ Tap Details (id "UsdtActivityDetails"). Verify the real wallet transaction and available addresses are shown without inventing a provider deposit reference or refund eligibility. Open Cross-Network Deposits (id "UsdtDepositHistory"). Verify the Provider Status note says the spendable balance updates when USDT arrives on Arbitrum, and each row shows its source network, provider status and available amount. Open a supported deposit that needs attention (id "UsdtDepositRow-{depositId}"). Verify the summary appears first with the provider status and receiving network. Do not invent a deposit date or individual fee when the provider does not supply one. Tap Details (id "UsdtDepositDetails"). Verify its own source transaction and deposit reference are shown. Verify the refund instructions and address placeholder name that deposit's source network. - Enter an owned test refund address (id "UsdtRefundAddress") slowly across two background refreshes. Verify the field stays enabled and focused, the keyboard remains open, and every typed character is retained while the provider status updates. Then tap Request refund (id "UsdtDepositRefund"). Verify Confirm Refund shows the exact deposit amount with up to six decimals, network, full refund address and refund fee note. In a comma locale, a 3.5 USDT deposit must display 3,50 USDT. Hidden balances must remain masked, and an unknown amount must remain a dash. Cancel without submitting. + Enter an owned test refund address (id "UsdtRefundAddress") slowly across two background refreshes. Verify the field stays enabled and focused, the keyboard remains open, and every typed character is retained while the provider status updates. Then tap Request refund (id "UsdtDepositRefund"). Verify Confirm Refund shows the exact deposit amount with up to six decimals, network, full refund address and refund fee note. In a comma locale, a 3.5 USDT deposit must display 3.50 USDT. Hidden balances must remain masked, and an unknown amount must remain a dash. Cancel without submitting. Tap Hide details (id "UsdtDepositDetails"), then Details again. Verify the provider status remains separate from wallet activity and the entered refund address is retained. Open completed, refunded and refund-in-progress deposit fixtures. Verify Request Refund is unavailable and the current provider status and available refund transaction are shown. An unsupported asset or network must show support guidance instead of a refund form. 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. Dismiss it and revisit deposit details, then return to the wallet; verify the same receipt does not appear again. diff --git a/journeys/usdt-number-format.xml b/journeys/usdt-number-format.xml index 5a1ae4fb3..71d616719 100644 --- a/journeys/usdt-number-format.xml +++ b/journeys/usdt-number-format.xml @@ -1,12 +1,12 @@ - Use an onboarded unfunded test wallet with USDT enabled. Set the system region to Germany or the Netherlands, keep the app language English, and restart the app. Use the same region on both platforms. No payment is submitted; a configured offline USDT provider is sufficient for empty balances and amount entry. + Use an onboarded unfunded test wallet with USDT enabled. Set the system region to Germany or the Netherlands, keep the app language English, and restart the app. Use the same region on both platforms. No payment is submitted; a configured offline USDT provider is sufficient for unknown balances and amount entry. Confirmed empty-balance checks require a successful read from an owned local test provider. - Set the wallet display unit to dollars. On Home, verify Savings, Spending and USDT all use a dot decimal separator, and the USDT tile shows $ 0.00 for an empty wallet. - Open USDT. Verify the balance shows $ 0.00, no refresh-error or history-sync copy appears underneath, and Send and Receive remain available. + Set the wallet display unit to dollars. On Home with the provider offline after relaunch, verify the USDT tile shows — rather than zero. After a successful balance read, verify a confirmed empty wallet shows $ 0.00 and loaded USDT balances use the same dot decimal separator as Savings and Spending. + Open USDT. Verify an unknown balance shows $ — and a confirmed empty balance shows $ 0.00. No refresh-error or history-sync copy appears underneath, and Send and Receive remain available. Return Home and hide balances. Verify the USDT tile is masked. Open USDT and verify its balance is masked. Restore visible balances. Open Receive, then Edit amount. Verify the decimal key is a dot. Enter 3.50 and verify the entered amount retains its trailing zero. Continue and verify the QR payment URI encodes 3500000 atomic units (uint256=3500000). Open Show Details and verify the receiving address matches the QR recipient. Return to the QR and edit the amount to 0.000001. Verify all six decimals remain visible while editing and the QR payment URI encodes one atomic unit (uint256=1). Return Home. Open Send, Enter Manually, and enter 0x1111111111111111111111111111111111111111. Continue to the USDT amount screen. Verify its decimal key is a dot and entering 1.25 displays $ 1.25. Close without submitting. - Repeat the empty balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. + Repeat the unknown and confirmed empty-balance and amount-entry checks with the system region set to United States. Verify the separators and exact entered values match. diff --git a/journeys/usdt-wallet-navigation.xml b/journeys/usdt-wallet-navigation.xml index e61965445..133232997 100644 --- a/journeys/usdt-wallet-navigation.xml +++ b/journeys/usdt-wallet-navigation.xml @@ -4,7 +4,7 @@ Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow. On Home, verify the Savings/Spending and USDT balance dividers align vertically, including any hardware wallet rows. With an odd number of paired hardware wallets, verify USDT fills the last row’s empty cell; with an even number, it starts the next row. Verify USDT balances and activity amounts show at most two decimals; a nonzero amount below one cent shows <0.01. Open USDT from Home and verify the wallet screen uses the same balance, action buttons and activity layout as the Bitcoin wallet, with its Tether coin illustration anchored at the upper right. - With an empty wallet, verify Home and the USDT wallet show $ 0.00, including before the first successful refresh. Verify no red refresh error or grey history-sync copy appears below the balance while the provider is offline. + Before the first successful balance read, verify Home shows — and the USDT wallet shows $ —. A confirmed empty balance must show $ 0.00. A known nonzero balance must remain visible if a later refresh fails; after relaunching offline it must show unknown rather than zero. Verify no red refresh error or grey history-sync copy appears below the balance while the provider is offline. Open Receive. Verify the network is Arbitrum One. Open details, verify the address starts with 0x, and verify Details Copy contains only the address. Return to the QR and verify Copy and tapping the QR both copy only the same address, while the QR still contains the full USDT payment URI including the network and token. On Android, system Back from Details returns to the USDT QR; Back from the QR closes Receive without switching to Savings. Close Receive, open Send and manual entry. Enter an invalid Bitcoin address and verify the address error prevents continuing. Replace it with an owned valid EVM address. Continue to Amount, enter six decimal places and try a seventh; verify the amount remains limited to six. Close before requesting a quote.