From eee46d946be86c62387329591673f1537e567062 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 9 Feb 2026 13:52:05 +0100 Subject: [PATCH 1/2] fix: place currency symbol correctly based on locale convention --- Bitkit/Components/MoneyText.swift | 14 ++- Bitkit/Components/NumberPadTextField.swift | 18 +++- Bitkit/Models/Currency.swift | 26 +++++ .../ViewModels/Widgets/WeatherViewModel.swift | 2 +- .../Receive/ReceiveCjitConfirmation.swift | 4 +- Bitkit/Views/Wallets/Sheets/BoostSheet.swift | 2 +- BitkitTests/CurrencyTests.swift | 98 +++++++++++++++++++ 7 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 BitkitTests/CurrencyTests.swift diff --git a/Bitkit/Components/MoneyText.swift b/Bitkit/Components/MoneyText.swift index 826f3168..ea2b1b34 100644 --- a/Bitkit/Components/MoneyText.swift +++ b/Bitkit/Components/MoneyText.swift @@ -51,8 +51,14 @@ struct MoneyText: View { private var displayText: String { if showSymbol { let baseSymbol = unit == .bitcoin ? "₿" : fiatSymbol - let symbolPart = prefix != nil ? "\(prefix!) \(baseSymbol)" : "\(baseSymbol)" - return "\(symbolPart) \(formattedValue)" + let isSuffix = unit == .fiat && isFiatSymbolSuffix + if isSuffix { + let prefixPart = prefix != nil ? "\(prefix!) " : "" + return "\(prefixPart)\(formattedValue) \(baseSymbol)" + } else { + let symbolPart = prefix != nil ? "\(prefix!) \(baseSymbol)" : "\(baseSymbol)" + return "\(symbolPart) \(formattedValue)" + } } else { return prefix != nil ? "\(prefix!) \(formattedValue)" : formattedValue } @@ -118,6 +124,10 @@ extension MoneyText { return converted.symbol } + private var isFiatSymbolSuffix: Bool { + isSuffixSymbolCurrency(currency.selectedCurrency) + } + private var formattedValue: String { if hideBalance { return displayDots diff --git a/Bitkit/Components/NumberPadTextField.swift b/Bitkit/Components/NumberPadTextField.swift index f32c7f35..63c11e52 100644 --- a/Bitkit/Components/NumberPadTextField.swift +++ b/Bitkit/Components/NumberPadTextField.swift @@ -77,11 +77,15 @@ struct NumberPadTextField: View { @ViewBuilder private var primaryDisplayView: some View { + let isSuffix = currency.primaryDisplay == .fiat && isSuffixSymbolCurrency(currency.selectedCurrency) + let symbolText = currency.primaryDisplay == .bitcoin ? "₿" : currency.symbol + HStack(spacing: 6) { - // Symbol - Text(currency.primaryDisplay == .bitcoin ? "₿" : currency.symbol) - .font(.custom(Fonts.extraBold, size: 44)) - .foregroundColor(.textSecondary) + if !isSuffix { + Text(symbolText) + .font(.custom(Fonts.extraBold, size: 44)) + .foregroundColor(.textSecondary) + } // Value and placeholder (Text(viewModel.displayText) @@ -89,6 +93,12 @@ struct NumberPadTextField: View { + Text(viewModel.getPlaceholder(currency: currency)) .foregroundColor(isFocused ? .textSecondary : .textPrimary)) .font(.custom(Fonts.black, size: 44)) + + if isSuffix { + Text(symbolText) + .font(.custom(Fonts.extraBold, size: 44)) + .foregroundColor(.textSecondary) + } } } } diff --git a/Bitkit/Models/Currency.swift b/Bitkit/Models/Currency.swift index 6e8a7eb8..32513130 100644 --- a/Bitkit/Models/Currency.swift +++ b/Bitkit/Models/Currency.swift @@ -40,6 +40,8 @@ struct ConvertedAmount { let sats: UInt64 let btcValue: Decimal + var isSymbolSuffix: Bool { isSuffixSymbolCurrency(currency) } + init(value: Decimal, formatted: String, symbol: String, currency: String, flag: String, sats: UInt64) { self.value = value self.formatted = formatted @@ -50,6 +52,10 @@ struct ConvertedAmount { btcValue = Decimal(sats) / 100_000_000 } + func formattedWithSymbol() -> String { + isSymbolSuffix ? "\(formatted)\(symbol)" : "\(symbol)\(formatted)" + } + struct BitcoinDisplayComponents { let symbol: String let value: String @@ -75,3 +81,23 @@ struct ConvertedAmount { } } } + +func isSuffixSymbolCurrency(_ currencyCode: String) -> Bool { + suffixSymbolCurrencies.contains(currencyCode) +} + +private let suffixSymbolCurrencies: Set = [ + "BGN", // Bulgarian Lev (10,00 лв) + "CHF", // Swiss Franc (10.00 CHF) + "CZK", // Czech Koruna (10,00 Kč) + "DKK", // Danish Krone (10,00 kr) + "HRK", // Croatian Kuna (10,00 kn) + "HUF", // Hungarian Forint (10 000 Ft) + "ISK", // Icelandic Króna (10.000 kr) + "NOK", // Norwegian Krone (10,00 kr) + "PLN", // Polish Złoty (0,35 zł) + "RON", // Romanian Leu (10,00 lei) + "RUB", // Russian Ruble (10,00 ₽) + "SEK", // Swedish Krona (10,00 kr) + "TRY", // Turkish Lira (10,00 ₺) +] diff --git a/Bitkit/ViewModels/Widgets/WeatherViewModel.swift b/Bitkit/ViewModels/Widgets/WeatherViewModel.swift index 7342519c..5e1b31de 100644 --- a/Bitkit/ViewModels/Widgets/WeatherViewModel.swift +++ b/Bitkit/ViewModels/Widgets/WeatherViewModel.swift @@ -166,7 +166,7 @@ class WeatherViewModel: ObservableObject { throw AppError(message: "Currency conversion unavailable", debugMessage: "Failed to convert \(fee) satoshis to fiat currency") } - return "\(converted.symbol) \(converted.formatted)" + return converted.formattedWithSymbol() } deinit { diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift index 704823fe..7158683a 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift @@ -14,14 +14,14 @@ struct ReceiveCjitConfirmation: View { guard let converted = currency.convert(sats: entry.networkFeeSat) else { return String(entry.networkFeeSat) } - return "\(converted.symbol)\(converted.formatted)" + return converted.formattedWithSymbol() } private func formattedServiceFee() -> String { guard let converted = currency.convert(sats: entry.serviceFeeSat) else { return String(entry.serviceFeeSat) } - return "\(converted.symbol)\(converted.formatted)" + return converted.formattedWithSymbol() } var receiveAmount: Int { diff --git a/Bitkit/Views/Wallets/Sheets/BoostSheet.swift b/Bitkit/Views/Wallets/Sheets/BoostSheet.swift index 9ee5af76..41403cef 100644 --- a/Bitkit/Views/Wallets/Sheets/BoostSheet.swift +++ b/Bitkit/Views/Wallets/Sheets/BoostSheet.swift @@ -82,7 +82,7 @@ struct BoostSheet: View { else { return "" } - return "\(converted.symbol)\(converted.formatted)" + return converted.formattedWithSymbol() } var body: some View { diff --git a/BitkitTests/CurrencyTests.swift b/BitkitTests/CurrencyTests.swift new file mode 100644 index 00000000..7181b495 --- /dev/null +++ b/BitkitTests/CurrencyTests.swift @@ -0,0 +1,98 @@ +@testable import Bitkit +import XCTest + +final class CurrencyTests: XCTestCase { + // MARK: - isSuffixSymbolCurrency + + func testIsSuffixSymbolCurrency_ReturnsTrueForPLN() { + XCTAssertTrue(isSuffixSymbolCurrency("PLN")) + } + + func testIsSuffixSymbolCurrency_ReturnsTrueForCZK() { + XCTAssertTrue(isSuffixSymbolCurrency("CZK")) + } + + func testIsSuffixSymbolCurrency_ReturnsTrueForSEK() { + XCTAssertTrue(isSuffixSymbolCurrency("SEK")) + } + + func testIsSuffixSymbolCurrency_ReturnsTrueForCHF() { + XCTAssertTrue(isSuffixSymbolCurrency("CHF")) + } + + func testIsSuffixSymbolCurrency_ReturnsFalseForUSD() { + XCTAssertFalse(isSuffixSymbolCurrency("USD")) + } + + func testIsSuffixSymbolCurrency_ReturnsFalseForEUR() { + XCTAssertFalse(isSuffixSymbolCurrency("EUR")) + } + + func testIsSuffixSymbolCurrency_ReturnsFalseForGBP() { + XCTAssertFalse(isSuffixSymbolCurrency("GBP")) + } + + func testIsSuffixSymbolCurrency_ReturnsFalseForUnknownCurrency() { + XCTAssertFalse(isSuffixSymbolCurrency("XYZ")) + } + + // MARK: - ConvertedAmount.isSymbolSuffix + + func testConvertedAmount_IsSymbolSuffix_TrueForPLN() { + let converted = ConvertedAmount( + value: 0.35, formatted: "0.35", symbol: "zł", + currency: "PLN", flag: "🇵🇱", sats: 100 + ) + XCTAssertTrue(converted.isSymbolSuffix) + } + + func testConvertedAmount_IsSymbolSuffix_FalseForUSD() { + let converted = ConvertedAmount( + value: 10.50, formatted: "10.50", symbol: "$", + currency: "USD", flag: "🇺🇸", sats: 1000 + ) + XCTAssertFalse(converted.isSymbolSuffix) + } + + // MARK: - ConvertedAmount.formattedWithSymbol + + func testFormattedWithSymbol_PrefixCurrency() { + let converted = ConvertedAmount( + value: 10.50, formatted: "10.50", symbol: "$", + currency: "USD", flag: "🇺🇸", sats: 1000 + ) + XCTAssertEqual(converted.formattedWithSymbol(), "$10.50") + } + + func testFormattedWithSymbol_SuffixCurrency() { + let converted = ConvertedAmount( + value: 0.35, formatted: "0.35", symbol: "zł", + currency: "PLN", flag: "🇵🇱", sats: 100 + ) + XCTAssertEqual(converted.formattedWithSymbol(), "0.35zł") + } + + func testFormattedWithSymbol_SuffixCurrencyCZK() { + let converted = ConvertedAmount( + value: 250.00, formatted: "250.00", symbol: "Kč", + currency: "CZK", flag: "🇨🇿", sats: 50000 + ) + XCTAssertEqual(converted.formattedWithSymbol(), "250.00Kč") + } + + func testFormattedWithSymbol_PrefixCurrencyEUR() { + let converted = ConvertedAmount( + value: 10.00, formatted: "10.00", symbol: "€", + currency: "EUR", flag: "🇪🇺", sats: 1000 + ) + XCTAssertEqual(converted.formattedWithSymbol(), "€10.00") + } + + func testFormattedWithSymbol_SuffixCurrencyCHF() { + let converted = ConvertedAmount( + value: 50.00, formatted: "50.00", symbol: "CHF", + currency: "CHF", flag: "🇨🇭", sats: 10000 + ) + XCTAssertEqual(converted.formattedWithSymbol(), "50.00CHF") + } +} From 41fca8e6737c5afac2f0b3b6c03477af2455a551 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 10 Feb 2026 13:07:09 +0100 Subject: [PATCH 2/2] fix: preserve spacing in weather fiat fee format --- Bitkit/Models/Currency.swift | 5 +++-- Bitkit/ViewModels/Widgets/WeatherViewModel.swift | 2 +- BitkitTests/CurrencyTests.swift | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Bitkit/Models/Currency.swift b/Bitkit/Models/Currency.swift index 32513130..464e2d24 100644 --- a/Bitkit/Models/Currency.swift +++ b/Bitkit/Models/Currency.swift @@ -52,8 +52,9 @@ struct ConvertedAmount { btcValue = Decimal(sats) / 100_000_000 } - func formattedWithSymbol() -> String { - isSymbolSuffix ? "\(formatted)\(symbol)" : "\(symbol)\(formatted)" + func formattedWithSymbol(withSpace: Bool = false) -> String { + let separator = withSpace ? " " : "" + return isSymbolSuffix ? "\(formatted)\(separator)\(symbol)" : "\(symbol)\(separator)\(formatted)" } struct BitcoinDisplayComponents { diff --git a/Bitkit/ViewModels/Widgets/WeatherViewModel.swift b/Bitkit/ViewModels/Widgets/WeatherViewModel.swift index 5e1b31de..f25f70aa 100644 --- a/Bitkit/ViewModels/Widgets/WeatherViewModel.swift +++ b/Bitkit/ViewModels/Widgets/WeatherViewModel.swift @@ -166,7 +166,7 @@ class WeatherViewModel: ObservableObject { throw AppError(message: "Currency conversion unavailable", debugMessage: "Failed to convert \(fee) satoshis to fiat currency") } - return converted.formattedWithSymbol() + return converted.formattedWithSymbol(withSpace: true) } deinit { diff --git a/BitkitTests/CurrencyTests.swift b/BitkitTests/CurrencyTests.swift index 7181b495..43945eb9 100644 --- a/BitkitTests/CurrencyTests.swift +++ b/BitkitTests/CurrencyTests.swift @@ -95,4 +95,20 @@ final class CurrencyTests: XCTestCase { ) XCTAssertEqual(converted.formattedWithSymbol(), "50.00CHF") } + + func testFormattedWithSymbol_PrefixCurrency_WithSpace() { + let converted = ConvertedAmount( + value: 10.50, formatted: "10.50", symbol: "$", + currency: "USD", flag: "🇺🇸", sats: 1000 + ) + XCTAssertEqual(converted.formattedWithSymbol(withSpace: true), "$ 10.50") + } + + func testFormattedWithSymbol_SuffixCurrency_WithSpace() { + let converted = ConvertedAmount( + value: 0.35, formatted: "0.35", symbol: "zł", + currency: "PLN", flag: "🇵🇱", sats: 100 + ) + XCTAssertEqual(converted.formattedWithSymbol(withSpace: true), "0.35 zł") + } }