From 014417ba382ad1afbcd943f3aa33c94f7a4e2ae6 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Tue, 3 Jun 2025 15:28:43 +0700 Subject: [PATCH 01/13] feat: custom icon metadata --- DashWallet.xcodeproj/project.pbxproj | 18 ++ .../Database/DatabaseConnection.swift | 2 +- .../Migrations/AddIconBitmapsTable.swift | 34 +++ .../Transactions/SendCoinsService.swift | 2 +- DashWallet/Sources/Models/Tx/IconBitmap.swift | 52 +++++ .../Sources/Models/Tx/IconBitmapDAO.swift | 216 ++++++++++++++++++ .../DashSpend/DashSpendPayViewModel.swift | 6 +- .../DashSpend/GiftCardDetailsViewModel.swift | 1 - .../CustomIconMetadataProvider.swift | 162 ++++++++----- .../Sources/UI/Home/Views/HomeViewModel.swift | 4 +- .../UI/Home/Views/MetadataProvider.swift | 12 +- .../Payments/PaymentModels/DWPaymentInput.h | 1 + .../PaymentModels/DWPaymentInputBuilder.m | 9 +- .../PaymentModels/DWPaymentProcessor.m | 2 +- 14 files changed, 458 insertions(+), 63 deletions(-) create mode 100644 DashWallet/Sources/Infrastructure/Database/Migrations/AddIconBitmapsTable.swift create mode 100644 DashWallet/Sources/Models/Tx/IconBitmap.swift create mode 100644 DashWallet/Sources/Models/Tx/IconBitmapDAO.swift diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index 364f6be6a..1221e0687 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -582,6 +582,12 @@ 7549BE542DEAEA18004F0BAF /* MetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE522DEAEA14004F0BAF /* MetadataProvider.swift */; }; 7549BE572DEAECB8004F0BAF /* CustomIconMetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE562DEAECB0004F0BAF /* CustomIconMetadataProvider.swift */; }; 7549BE582DEAECB8004F0BAF /* CustomIconMetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE562DEAECB0004F0BAF /* CustomIconMetadataProvider.swift */; }; + 7549BE5A2DEAF50D004F0BAF /* IconBitmap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE592DEAF50D004F0BAF /* IconBitmap.swift */; }; + 7549BE5B2DEAF50D004F0BAF /* IconBitmap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE592DEAF50D004F0BAF /* IconBitmap.swift */; }; + 7549BE5D2DEAF55B004F0BAF /* IconBitmapDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE5C2DEAF55B004F0BAF /* IconBitmapDAO.swift */; }; + 7549BE5E2DEAF55B004F0BAF /* IconBitmapDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE5C2DEAF55B004F0BAF /* IconBitmapDAO.swift */; }; + 7549BE602DEAF628004F0BAF /* AddIconBitmapsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE5F2DEAF628004F0BAF /* AddIconBitmapsTable.swift */; }; + 7549BE612DEAF628004F0BAF /* AddIconBitmapsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7549BE5F2DEAF628004F0BAF /* AddIconBitmapsTable.swift */; }; 754BEA122C0B6BD700E8C93C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754BEA112C0B6BD700E8C93C /* HomeViewModel.swift */; }; 754BEA132C0B6BD700E8C93C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754BEA112C0B6BD700E8C93C /* HomeViewModel.swift */; }; 754C27C72CC3C0B900BA7B9F /* UsernamePrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754C27C52CC3C0B900BA7B9F /* UsernamePrefs.swift */; }; @@ -2481,6 +2487,9 @@ 7545ED5F2DA91FC60075F45C /* NumericKeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericKeyboardView.swift; sourceTree = ""; }; 7549BE522DEAEA14004F0BAF /* MetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataProvider.swift; sourceTree = ""; }; 7549BE562DEAECB0004F0BAF /* CustomIconMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomIconMetadataProvider.swift; sourceTree = ""; }; + 7549BE592DEAF50D004F0BAF /* IconBitmap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconBitmap.swift; sourceTree = ""; }; + 7549BE5C2DEAF55B004F0BAF /* IconBitmapDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconBitmapDAO.swift; sourceTree = ""; }; + 7549BE5F2DEAF628004F0BAF /* AddIconBitmapsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIconBitmapsTable.swift; sourceTree = ""; }; 754BEA112C0B6BD700E8C93C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 754C27C52CC3C0B900BA7B9F /* UsernamePrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernamePrefs.swift; sourceTree = ""; }; 754C27CB2CC3C14F00BA7B9F /* FeatureSingleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureSingleItem.swift; sourceTree = ""; }; @@ -5167,6 +5176,7 @@ 4709C310287E78BD00B4BD48 /* Migrations */ = { isa = PBXGroup; children = ( + 7549BE5F2DEAF628004F0BAF /* AddIconBitmapsTable.swift */, 75D9EBB72DE5CC3A009416A2 /* AddGiftCardsTable.swift */, 4709C311287E78BD00B4BD48 /* SeedDB.swift */, ); @@ -5525,6 +5535,8 @@ children = ( 754D020C2D1558EA005CA466 /* GroupedTransactions.swift */, 479DBDDC2995168C00F30AF1 /* Transactions.swift */, + 7549BE592DEAF50D004F0BAF /* IconBitmap.swift */, + 7549BE5C2DEAF55B004F0BAF /* IconBitmapDAO.swift */, ); path = Tx; sourceTree = ""; @@ -8797,6 +8809,7 @@ 2A913EA823A79AD2006A2A59 /* DWTransactionListDataProviderStub.m in Sources */, 0F6EDFC628C896BD000427E7 /* CoinbaseBaseIDForCurrencyResponse.swift in Sources */, 759609232C455B2000F3BF04 /* SendIntro.swift in Sources */, + 7549BE5A2DEAF50D004F0BAF /* IconBitmap.swift in Sources */, 47AC8413297822E000BD1B49 /* SpecifyAmountViewController.swift in Sources */, 2AFCB9C423BE76EC00FF59A6 /* DWUpholdTransactionObject+DWView.m in Sources */, 75CDD7862C08D61300F433D2 /* DashAmount.swift in Sources */, @@ -8859,6 +8872,7 @@ 2A858A0F237EE89C0097A7B5 /* DSWatchTransactionDataObject.m in Sources */, 47FA3B0229364991008D58DC /* HTTPClient.swift in Sources */, 7549BE582DEAECB8004F0BAF /* CustomIconMetadataProvider.swift in Sources */, + 7549BE602DEAF628004F0BAF /* AddIconBitmapsTable.swift in Sources */, 2A8B9E6F2302A9C200FF8653 /* DWPasteboardAddressExtractor.m in Sources */, 757111A12CF20ED800A7D452 /* CoinJoinMixingTxSet.swift in Sources */, 477F501529531C07003C7508 /* ViewModel+Coinbase.swift in Sources */, @@ -8884,6 +8898,7 @@ 4709C312287E78BD00B4BD48 /* SeedDB.swift in Sources */, 2ACCD8F0231ECDE000A96B62 /* DWPinField.m in Sources */, 2AD1CEA222DFC80900C99324 /* DWCenteredScrollView.m in Sources */, + 7549BE5D2DEAF55B004F0BAF /* IconBitmapDAO.swift in Sources */, 2A9172C425233DC50024B4C5 /* DWPhraseRepairViewController.m in Sources */, 2A44314C22CF8801009BAF7F /* DWBiometricAuthModel.m in Sources */, 7527720F2AA9F58E0066557E /* TopperViewModel.swift in Sources */, @@ -9157,6 +9172,7 @@ C943B4EE2A40A54600AF23C5 /* DWStretchyHeaderListCollectionLayout.m in Sources */, 75A8C1652AE5726B0042256E /* UsernameRequestsDAO.swift in Sources */, C9D2C6992A320AA000D15901 /* DWStartViewController.m in Sources */, + 7549BE5B2DEAF50D004F0BAF /* IconBitmap.swift in Sources */, C943B4C02A40A54600AF23C5 /* DWContactsSearchInfoHeaderView.m in Sources */, C9D2C69B2A320AA000D15901 /* DWStartModel.m in Sources */, 754495DD2AE91B6300492817 /* GroupedRequestCell.swift in Sources */, @@ -9170,6 +9186,7 @@ C9D2C6A12A320AA000D15901 /* AmountInputControl.swift in Sources */, C9D2C6A22A320AA000D15901 /* DWSegmentSliderFormTableViewCell.m in Sources */, 759609212C4553A400F3BF04 /* BuyCreditsViewController.swift in Sources */, + 7549BE5E2DEAF55B004F0BAF /* IconBitmapDAO.swift in Sources */, C9D2C6A32A320AA000D15901 /* ApiCode.swift in Sources */, 75FEC6AF2CE0C58A00F87B48 /* VerifyIdentityScreen.swift in Sources */, C9D2C6A42A320AA000D15901 /* CoinbaseEntryPointModel.swift in Sources */, @@ -9620,6 +9637,7 @@ C943B4F32A40A54600AF23C5 /* DWDPEstablishedContactObject.m in Sources */, C9D2C7E02A320AA000D15901 /* TransactionFilter.swift in Sources */, C9D2C7E22A320AA000D15901 /* CrowdNodeWithdrawalReceivedTx.swift in Sources */, + 7549BE612DEAF628004F0BAF /* AddIconBitmapsTable.swift in Sources */, C943B4B62A40A54600AF23C5 /* DWFetchedResultsDataSource.m in Sources */, C9D2C7E32A320AA000D15901 /* PointOfUseListSegmentedCell.swift in Sources */, 7513DA892AB17606005D55F6 /* Topper.swift in Sources */, diff --git a/DashWallet/Sources/Infrastructure/Database/DatabaseConnection.swift b/DashWallet/Sources/Infrastructure/Database/DatabaseConnection.swift index bc7df15c1..5be4286e1 100644 --- a/DashWallet/Sources/Infrastructure/Database/DatabaseConnection.swift +++ b/DashWallet/Sources/Infrastructure/Database/DatabaseConnection.swift @@ -70,7 +70,7 @@ extension DatabaseConnection { } static func migrations() -> [Migration] { - [SeedDB(), AddGiftCardsTable()] + [SeedDB(), AddGiftCardsTable(), AddIconBitmapsTable()] } static func migrationsBundle() -> Bundle { diff --git a/DashWallet/Sources/Infrastructure/Database/Migrations/AddIconBitmapsTable.swift b/DashWallet/Sources/Infrastructure/Database/Migrations/AddIconBitmapsTable.swift new file mode 100644 index 000000000..6362860f1 --- /dev/null +++ b/DashWallet/Sources/Infrastructure/Database/Migrations/AddIconBitmapsTable.swift @@ -0,0 +1,34 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SQLite +import SQLiteMigrationManager + +struct AddIconBitmapsTable: Migration { + var version: Int64 = 20250114130000 + + func migrateDatabase(_ db: Connection) throws { + try db.run(IconBitmap.table.create(ifNotExists: true) { t in + t.column(IconBitmap.id, primaryKey: true) + t.column(IconBitmap.imageData) + t.column(IconBitmap.originalUrl) + t.column(IconBitmap.height) + t.column(IconBitmap.width) + }) + } +} \ No newline at end of file diff --git a/DashWallet/Sources/Models/Transactions/SendCoinsService.swift b/DashWallet/Sources/Models/Transactions/SendCoinsService.swift index b978ae983..d1030c566 100644 --- a/DashWallet/Sources/Models/Transactions/SendCoinsService.swift +++ b/DashWallet/Sources/Models/Transactions/SendCoinsService.swift @@ -141,7 +141,7 @@ extension SendCoinsService: DWPaymentProcessorDelegate { public func paymentProcessor(_ processor: DWPaymentProcessor, requestAmountWithDestination sendingDestination: String, details: DSPaymentProtocolDetails?, contactItem: DWDPBasicUserItem?) { // Not needed for gift card payments - completePayment(transaction: nil, error: CTXSpendError.paymentProcessingError("Amount request not supported for gift cards")) + completePayment(transaction: nil, error: CTXSpendError.paymentProcessingError("Request is missing destination")) } public func paymentProcessor(_ processor: DWPaymentProcessor, requestUserActionTitle title: String?, message: String?, actionTitle: String, cancel cancelBlock: (() -> Void)?, actionBlock: (() -> Void)?) { diff --git a/DashWallet/Sources/Models/Tx/IconBitmap.swift b/DashWallet/Sources/Models/Tx/IconBitmap.swift new file mode 100644 index 000000000..02f335efa --- /dev/null +++ b/DashWallet/Sources/Models/Tx/IconBitmap.swift @@ -0,0 +1,52 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SQLite + +// MARK: - IconBitmap + +struct IconBitmap: RowDecodable { + let id: Data + let imageData: Data + let originalUrl: String + let height: Int + let width: Int + + static let table = Table("icon_bitmaps") + static let id = SQLite.Expression("id") + static let imageData = SQLite.Expression("imageData") + static let originalUrl = SQLite.Expression("originalUrl") + static let height = SQLite.Expression("height") + static let width = SQLite.Expression("width") + + init(row: Row) { + self.id = row[IconBitmap.id] + self.imageData = row[IconBitmap.imageData] + self.originalUrl = row[IconBitmap.originalUrl] + self.height = row[IconBitmap.height] + self.width = row[IconBitmap.width] + } + + init(id: Data, imageData: Data, originalUrl: String, height: Int, width: Int) { + self.id = id + self.imageData = imageData + self.originalUrl = originalUrl + self.height = height + self.width = width + } +} \ No newline at end of file diff --git a/DashWallet/Sources/Models/Tx/IconBitmapDAO.swift b/DashWallet/Sources/Models/Tx/IconBitmapDAO.swift new file mode 100644 index 000000000..9f6a4710f --- /dev/null +++ b/DashWallet/Sources/Models/Tx/IconBitmapDAO.swift @@ -0,0 +1,216 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SQLite +import Combine + +// MARK: - IconBitmapDAO + +protocol IconBitmapDAO { + func addBitmap(bitmap: IconBitmap) async + func getBitmap(id: Data) async -> IconBitmap? + func observeBitmaps() -> AnyPublisher<[Data: IconBitmap], Never> + func clear() async +} + +// MARK: - IconBitmapDAOImpl + +class IconBitmapDAOImpl: NSObject, IconBitmapDAO { + private var db: Connection { DatabaseConnection.shared.db } + private var cache: [String: IconBitmap] = [:] + private var bitmapsSubject = CurrentValueSubject<[Data: IconBitmap], Never>([:]) + + static let shared = IconBitmapDAOImpl() + + override init() { + super.init() + Task { + await loadAllBitmaps() + } + } + + func addBitmap(bitmap: IconBitmap) async { + do { + let insert = IconBitmap.table.insert(or: .ignore, + IconBitmap.id <- bitmap.id, + IconBitmap.imageData <- bitmap.imageData, + IconBitmap.originalUrl <- bitmap.originalUrl, + IconBitmap.height <- bitmap.height, + IconBitmap.width <- bitmap.width) + try await execute(insert) + let key = bitmap.id.hexEncodedString() + cache[key] = bitmap + updateBitmapsSubject() + } catch { + print("IconBitmapDAO addBitmap error: \(error)") + } + } + + func getBitmap(id: Data) async -> IconBitmap? { + let statement = IconBitmap.table.filter(IconBitmap.id == id) + + do { + let results: [IconBitmap] = try await prepare(statement) + let bitmap = results.first + if let bitmap = bitmap { + let key = id.hexEncodedString() + cache[key] = bitmap + } + return bitmap + } catch { + print("IconBitmapDAO getBitmap error: \(error)") + } + + return nil + } + + func observeBitmaps() -> AnyPublisher<[Data: IconBitmap], Never> { + return bitmapsSubject.eraseToAnyPublisher() + } + + func clear() async { + do { + let deleteQuery = IconBitmap.table.delete() + try await execute(deleteQuery) + cache.removeAll() + updateBitmapsSubject() + } catch { + print("IconBitmapDAO clear error: \(error)") + } + } + + private func loadAllBitmaps() async { + do { + let bitmaps: [IconBitmap] = try await prepare(IconBitmap.table) + cache.removeAll() + for bitmap in bitmaps { + let key = bitmap.id.hexEncodedString() + cache[key] = bitmap + } + updateBitmapsSubject() + } catch { + print("IconBitmapDAO loadAllBitmaps error: \(error)") + } + } + + private func updateBitmapsSubject() { + let bitmapsDict = cache.reduce(into: [Data: IconBitmap]()) { result, item in + if let data = Data(hex: item.key) { + result[data] = item.value + } + } + bitmapsSubject.send(bitmapsDict) + } +} + +// MARK: - async / await + +extension IconBitmapDAOImpl { + private func execute(_ query: Insert) async throws { + return try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let self = self else { return continuation.resume() } + + do { + try db.run(query) + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + } + } + } + + private func execute(_ query: Delete) async throws { + return try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let self = self else { return continuation.resume() } + + do { + try db.run(query) + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + } + } + } + + private func prepare(_ query: Table) async throws -> [T] { + return try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let self = self else { return continuation.resume(returning: []) } + + var result: [T] = [] + + do { + for row in try db.prepare(query) { + let rowItem = T(row: row) + result.append(rowItem) + } + + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + private func prepare(_ query: QueryType) async throws -> [T] { + return try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let self = self else { return continuation.resume(returning: []) } + + var result: [T] = [] + + do { + for row in try db.prepare(query) { + let rowItem = T(row: row) + result.append(rowItem) + } + + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } +} + +// MARK: - Data Extension + +extension Data { + init?(hex: String) { + let len = hex.count / 2 + var data = Data(capacity: len) + var i = hex.startIndex + for _ in 0..() private let fiatFormatter = NumberFormatter.fiatFormatter(currencyCode: defaultCurrency) private let ctxSpendService = CTXSpendService.shared + private let customIconProvider = CustomIconMetadataProvider.shared private let sendCoinsService = SendCoinsService() private var merchantId: String = "" @@ -147,6 +148,9 @@ class DashSpendPayViewModel: NSObject, ObservableObject { // Payment successful - save gift card information DSLogger.log("Payment transaction completed: \(transaction.txHashHexString)") + // TODO: + // transactionMetadata.markGiftCardTransaction(transaction.txId, giftCardMerchant.logoLocation) + customIconProvider.updateIcon(txId: transaction.txHashData, iconUrl: merchantIconUrl) saveGiftCardDummy(txHashData: transaction.txHashData, giftCardId: response.paymentId) return transaction.txHashData @@ -240,9 +244,7 @@ class DashSpendPayViewModel: NSObject, ObservableObject { } DSLogger.log("Attempting to purchase gift card for merchant \(merchantId) with amount \(amount)") - let fiatAmountString = String(format: "%.2f", Double(truncating: amount as NSDecimalNumber)) - DSLogger.log("Making API request to purchase gift card: merchantId=\(merchantId), amount=\(fiatAmountString)USD") return try await ctxSpendService.purchaseGiftCard( merchantId: merchantId, diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift index 0ddb1d324..434484134 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift @@ -166,7 +166,6 @@ class GiftCardDetailsViewModel: ObservableObject { format: "CODE128" ) } - stopTicker() DSLogger.log("DashSpend: Gift card details fetched successfully") } diff --git a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift b/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift index 95d3a8a41..f502b52c4 100644 --- a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift @@ -17,71 +17,127 @@ import Foundation import Combine +import UIKit +import CryptoKit class CustomIconMetadataProvider: MetadataProvider { + static let shared = CustomIconMetadataProvider() + private var cancellableBag = Set() - private let iconBitmapDao = iconBitmapDAOImpl.shared + private let iconBitmapDao = IconBitmapDAOImpl.shared var availableMetadata: [Data : TxRowMetadata] = [:] let metadataUpdated = PassthroughSubject() init() { loadMetadata() -// self.metadataDao.$lastChange -// .receive(on: DispatchQueue.main) -// .sink { [weak self] change in -// guard let self = self, let change = change else { return } -// -// switch change { -// case .created(let metadata), .updated(let metadata, _): -// onMemoUpdated(metadata: metadata) -// -// case .deleted(let metadata): -// availableMetadata.removeValue(forKey: metadata.txHash) -// metadataUpdated.send(metadata.txHash) -// -// case .deletedAll: -// for metadata in availableMetadata { -// metadataUpdated.send(metadata.key) -// } -// availableMetadata = [:] -// } -// } -// .store(in: &cancellableBag) + + iconBitmapDao.observeBitmaps() + .receive(on: DispatchQueue.main) + .sink { [weak self] bitmaps in + guard let self = self else { return } + + // Update metadata with new icons + for (iconId, iconBitmap) in bitmaps { + if let image = UIImage(data: iconBitmap.imageData) { + // Find transactions that use this icon + for (txHash, metadata) in availableMetadata { + if let customIconId = metadata.customIconId, customIconId == iconId { + var updatedMetadata = metadata + updatedMetadata.icon = image + availableMetadata[txHash] = updatedMetadata + metadataUpdated.send(txHash) + } + } + } + } + } + .store(in: &cancellableBag) } private func loadMetadata() { -// let txMetadata = metadataDao.getPrivateMemos() -// -// for metadata in txMetadata { -// var txRowMetadata = availableMetadata[metadata.txHash] -// -// if txRowMetadata != nil { -// txRowMetadata!.details = metadata.memo -// } else { -// txRowMetadata = TxRowMetadata( -// title: nil, -// details: metadata.memo -// ) -// } -// -// availableMetadata[metadata.txHash] = txRowMetadata -// } + // TODO: Load existing metadata if needed + // This would typically load from transaction metadata that references icon IDs + } + + func updateIcon(txId: Data, iconUrl: String) { + Task { + do { + guard let url = URL(string: iconUrl) else { + print("Invalid icon URL: \(iconUrl)") + return + } + + let (data, _) = try await URLSession.shared.data(from: url) + + guard let image = UIImage(data: data) else { + print("Failed to create image from data for URL: \(iconUrl)") + return + } + + // Calculate hash from original image data + let imageHash = SHA256.hash(data: data) + print("BITMAP: imageHash: \(imageHash)") + let hashData = Data(imageHash) + + let resizedImage = resizeIcon(image: image) + guard let resizedImageData = resizedImage.pngData() else { + print("BITMAP: Failed to get PNG data from resized image") + return + } + + print("Resized image data base64: \(resizedImageData.base64EncodedString())") + + let iconBitmap = IconBitmap( + id: hashData, + imageData: resizedImageData, + originalUrl: iconUrl, + height: Int(resizedImage.size.height), + width: Int(resizedImage.size.width) + ) + + await iconBitmapDao.addBitmap(bitmap: iconBitmap) + + var txRowMetadata = availableMetadata[txId] ?? TxRowMetadata(title: nil, details: nil) + txRowMetadata.customIconId = hashData + txRowMetadata.icon = resizedImage + availableMetadata[txId] = txRowMetadata + + Task { @MainActor in + self.metadataUpdated.send(txId) + } + } catch { + print("Failed to fetch icon from URL \(iconUrl): \(error)") + } + } } -// private func onMemoUpdated(metadata: TransactionMetadata) { -// var txRowMetadata = availableMetadata[metadata.txHash] -// -// if txRowMetadata != nil { -// txRowMetadata!.details = metadata.memo -// } else { -// txRowMetadata = TxRowMetadata( -// title: nil, -// details: metadata.memo -// ) -// } -// -// availableMetadata[metadata.txHash] = txRowMetadata -// metadataUpdated.send(metadata.txHash) -// } + private func resizeIcon(image: UIImage) -> UIImage { + let destSize: CGFloat = 150.0 + var width = image.size.width + var height = image.size.height + + if width > destSize || height > destSize { + if width < height { + let scale = destSize / height + height = destSize + width = width * scale + } else if width > height { + let scale = destSize / width + width = destSize + height = height * scale + } else { + width = destSize + height = destSize + } + } + + let size = CGSize(width: width, height: height) + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + image.draw(in: CGRect(origin: .zero, size: size)) + let resizedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return resizedImage ?? image + } } diff --git a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift index 600ad885c..10d1202ef 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift @@ -450,7 +450,7 @@ extension HomeViewModel { extension HomeViewModel { private func setupMetadataProviders() { -// let privateMemoProvider = PrivateMemoProvider() + let customIconProvider = CustomIconMetadataProvider.shared // privateMemoProvider.metadataUpdated // .receive(on: self.queue) // .sink { [weak self] txHash in @@ -463,7 +463,7 @@ extension HomeViewModel { // } // .store(in: &cancellableBag) // -// self.metadataProviders = [privateMemoProvider] + self.metadataProviders = [customIconProvider] } } diff --git a/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift b/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift index 95c13c6a5..7497e9aa3 100644 --- a/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift @@ -15,10 +15,20 @@ // limitations under the License. // +import UIKit + struct TxRowMetadata: Equatable { var title: String? var details: String? - // TODO: custom icon + var customIconId: Data? + var icon: UIImage? + + static func == (lhs: TxRowMetadata, rhs: TxRowMetadata) -> Bool { + return lhs.title == rhs.title && + lhs.details == rhs.details && + lhs.customIconId == rhs.customIconId + // Note: UIImage is not Equatable, so we don't compare icon directly + } } protocol MetadataProvider { diff --git a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInput.h b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInput.h index d9b8160dd..e7f6d3f31 100644 --- a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInput.h +++ b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInput.h @@ -28,6 +28,7 @@ typedef NS_ENUM(NSUInteger, DWPaymentInputSource) { DWPaymentInputSource_Pasteboard, DWPaymentInputSource_ScanQR, DWPaymentInputSource_NFC, + DWPaymentInputSource_DeepLink, DWPaymentInputSource_URL, DWPaymentInputSource_BlockchainUser, DWPaymentInputSource_PlainAddress diff --git a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInputBuilder.m b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInputBuilder.m index 1fa084742..dc1a03aa7 100644 --- a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInputBuilder.m +++ b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentInputBuilder.m @@ -124,6 +124,8 @@ - (void)payFirstFromArray:(NSArray *)array - (DWPaymentInput *)paymentInputWithURL:(NSURL *)url { DSChain *chain = [DWEnvironment sharedInstance].currentChain; DSPaymentRequest *request = nil; + DWPaymentInputSource sourceType = DWPaymentInputSource_URL; + if ([url.scheme isEqualToString:@"pay"]) { NSString *path = url.absoluteString; if ([path hasPrefix:@"pay:"]) { @@ -135,7 +137,12 @@ - (DWPaymentInput *)paymentInputWithURL:(NSURL *)url { request = [DSPaymentRequest requestWithURL:url onChain:chain]; } - DWPaymentInput *paymentInput = [[DWPaymentInput alloc] initWithSource:DWPaymentInputSource_URL]; + // Check if the request contains a valid Dash address to determine if this is a deep link + if (request && [request.paymentAddress isValidDashAddressOnChain:chain]) { + sourceType = DWPaymentInputSource_DeepLink; + } + + DWPaymentInput *paymentInput = [[DWPaymentInput alloc] initWithSource:sourceType]; paymentInput.request = request; return paymentInput; diff --git a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m index e9ce68724..4a455dd04 100644 --- a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m +++ b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m @@ -159,7 +159,7 @@ - (void)processPaymentInput:(DWPaymentInput *)paymentInput { self.paymentInput = paymentInput; if (paymentInput.request) { - if ((paymentInput.source == DWPaymentInputSource_ScanQR || paymentInput.source == DWPaymentInputSource_URL) && paymentInput.request.isValidAsNonDashpayPaymentRequest) { + if ((paymentInput.source == DWPaymentInputSource_ScanQR || paymentInput.source == DWPaymentInputSource_DeepLink) && paymentInput.request.isValidAsNonDashpayPaymentRequest) { DSPaymentProtocolRequest *protocolRequest = [self protocolRequestFromPaymentRequest:self.paymentInput.request]; [self txManagerRequestingAdditionalInfo:DSRequestingAdditionalInfo_Amount protocolRequest:protocolRequest]; From b9604f073e0d7e598a050e0e7f9d86640b3218da Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Fri, 18 Apr 2025 15:02:46 +0700 Subject: [PATCH 02/13] feat: migrate database & refactoring --- DashWallet.xcodeproj/project.pbxproj | 30 ++++----- DashWallet/Sources/Application/App.swift | 2 +- ...250418145536_more_metadata_tx_userinfo.sql | 5 ++ .../Database/Migrations/SeedDB.swift | 2 +- .../Taxes/Address/AddressUserInfo.swift | 6 +- .../Taxes/Services/TaxReportGenerator.swift | 6 +- DashWallet/Sources/Models/Taxes/Taxes.swift | 14 ++--- .../Transactions/Model/Transaction.swift | 2 +- .../DAO/TransactionMetadataDAO.swift} | 62 +++++++++---------- .../TransactionMetadata.swift} | 43 ++++++++----- .../Sources/Models/Tx/Transactions.swift | 4 +- .../UI/Tx/Details/Model/TxDetailModel.swift | 6 +- 12 files changed, 100 insertions(+), 82 deletions(-) create mode 100644 DashWallet/Sources/Infrastructure/Database/Migrations.bundle/20250418145536_more_metadata_tx_userinfo.sql rename DashWallet/Sources/Models/{Taxes/Tx/DAO/TxUserInfoDAO.swift => Tx Metadata/DAO/TransactionMetadataDAO.swift} (52%) rename DashWallet/Sources/Models/{Taxes/Tx/TxUserInfo.swift => Tx Metadata/TransactionMetadata.swift} (74%) diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index 1221e0687..72cc45ecc 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -328,8 +328,8 @@ 47083B3229892D770010AF71 /* DSTransaction+DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47083B3129892D770010AF71 /* DSTransaction+DashWallet.swift */; }; 4709C30F287E787700B4BD48 /* Migrations.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 4709C30E287E787700B4BD48 /* Migrations.bundle */; }; 4709C312287E78BD00B4BD48 /* SeedDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C311287E78BD00B4BD48 /* SeedDB.swift */; }; - 4709C315287EA11900B4BD48 /* TxUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C314287EA11900B4BD48 /* TxUserInfo.swift */; }; - 4709C318287EA23000B4BD48 /* TxUserInfoDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C317287EA23000B4BD48 /* TxUserInfoDAO.swift */; }; + 4709C315287EA11900B4BD48 /* TransactionMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */; }; + 4709C318287EA23000B4BD48 /* TransactionMetadataDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C317287EA23000B4BD48 /* TransactionMetadataDAO.swift */; }; 4709C31E2880247C00B4BD48 /* DSTransaction+DashWallet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4709C31D2880247C00B4BD48 /* DSTransaction+DashWallet.m */; }; 4709C31F28818BAE00B4BD48 /* TxDetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C31A288020DE00B4BD48 /* TxDetailModel.swift */; }; 4709C32128818D5400B4BD48 /* Foundation+Bitcoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C32028818D5400B4BD48 /* Foundation+Bitcoin.swift */; }; @@ -1068,7 +1068,7 @@ C9D2C6D22A320AA000D15901 /* TransactionWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 119E8D092905413F00D406C1 /* TransactionWrapper.swift */; }; C9D2C6D32A320AA000D15901 /* Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 479DBDDC2995168C00F30AF1 /* Transactions.swift */; }; C9D2C6D42A320AA000D15901 /* DWRecoverTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A74EFFA2305464C00C475EB /* DWRecoverTextView.m */; }; - C9D2C6D52A320AA000D15901 /* TxUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C314287EA11900B4BD48 /* TxUserInfo.swift */; }; + C9D2C6D52A320AA000D15901 /* TransactionMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */; }; C9D2C6D62A320AA000D15901 /* ExtendedPublicKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E91AD29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift */; }; C9D2C6D82A320AA000D15901 /* AtmListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BDD28C1305E00490F5E /* AtmListViewController.swift */; }; C9D2C6D92A320AA000D15901 /* TxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A5145F2848F75B005A8E3E /* TxDetailViewController.swift */; }; @@ -1351,7 +1351,7 @@ C9D2C8382A320AA000D15901 /* DWRootModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A44313F22CF642C009BAF7F /* DWRootModel.m */; }; C9D2C8392A320AA000D15901 /* BaseAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661AE28FDAA3300028A8D /* BaseAmountViewController.swift */; }; C9D2C83A2A320AA000D15901 /* SendingToView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478C982B2942F03500FAA0F0 /* SendingToView.swift */; }; - C9D2C83B2A320AA000D15901 /* TxUserInfoDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C317287EA23000B4BD48 /* TxUserInfoDAO.swift */; }; + C9D2C83B2A320AA000D15901 /* TransactionMetadataDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C317287EA23000B4BD48 /* TransactionMetadataDAO.swift */; }; C9D2C83C2A320AA000D15901 /* DWModalContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C69E223155A0E001B8C90 /* DWModalContentView.m */; }; C9D2C83D2A320AA000D15901 /* DWUpholdConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A9FFDFF2230FF2B00956D5F /* DWUpholdConstants.m */; }; C9D2C83F2A320AA000D15901 /* CoinbaseSwapeTradeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F6EDFA128C896BC000427E7 /* CoinbaseSwapeTradeResponse.swift */; }; @@ -2248,8 +2248,8 @@ 47083B3129892D770010AF71 /* DSTransaction+DashWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DSTransaction+DashWallet.swift"; sourceTree = ""; }; 4709C30E287E787700B4BD48 /* Migrations.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Migrations.bundle; sourceTree = ""; }; 4709C311287E78BD00B4BD48 /* SeedDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedDB.swift; sourceTree = ""; }; - 4709C314287EA11900B4BD48 /* TxUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxUserInfo.swift; sourceTree = ""; }; - 4709C317287EA23000B4BD48 /* TxUserInfoDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxUserInfoDAO.swift; sourceTree = ""; }; + 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionMetadata.swift; sourceTree = ""; }; + 4709C317287EA23000B4BD48 /* TransactionMetadataDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionMetadataDAO.swift; sourceTree = ""; }; 4709C31A288020DE00B4BD48 /* TxDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxDetailModel.swift; sourceTree = ""; }; 4709C31C2880247C00B4BD48 /* DSTransaction+DashWallet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DSTransaction+DashWallet.h"; sourceTree = ""; }; 4709C31D2880247C00B4BD48 /* DSTransaction+DashWallet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DSTransaction+DashWallet.m"; sourceTree = ""; }; @@ -4008,6 +4008,7 @@ 47081194298CF1E3003FCA3D /* Transactions */, 11BD737F28E7354200A34022 /* CrowdNode */, 479DBDDB2995167800F30AF1 /* Tx */, + 4709C313287EA10600B4BD48 /* Tx Metadata */, 0F6EDF9328C894B9000427E7 /* Coinbase */, 47AE8B9328BF873800490F5E /* Explore Dash */, 471A2602289ACCD20056B7B2 /* Taxes */, @@ -5183,19 +5184,19 @@ path = Migrations; sourceTree = ""; }; - 4709C313287EA10600B4BD48 /* Tx */ = { + 4709C313287EA10600B4BD48 /* Tx Metadata */ = { isa = PBXGroup; children = ( 4709C316287EA22600B4BD48 /* DAO */, - 4709C314287EA11900B4BD48 /* TxUserInfo.swift */, + 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */, ); - path = Tx; + path = "Tx Metadata"; sourceTree = ""; }; 4709C316287EA22600B4BD48 /* DAO */ = { isa = PBXGroup; children = ( - 4709C317287EA23000B4BD48 /* TxUserInfoDAO.swift */, + 4709C317287EA23000B4BD48 /* TransactionMetadataDAO.swift */, ); path = DAO; sourceTree = ""; @@ -5205,7 +5206,6 @@ children = ( 472D13DD299DF5B5006903F1 /* Services */, 471A2603289ACCDD0056B7B2 /* Address */, - 4709C313287EA10600B4BD48 /* Tx */, 471A2609289ACDF70056B7B2 /* Taxes.swift */, 75EBAA242BBA9DC2004488E3 /* ZenLedger.swift */, ); @@ -8603,7 +8603,7 @@ 479DBDDD2995168C00F30AF1 /* Transactions.swift in Sources */, 75F3F00D2C48F819004470EA /* RootEditProfileViewController.swift in Sources */, 2A74EFFB2305464C00C475EB /* DWRecoverTextView.m in Sources */, - 4709C315287EA11900B4BD48 /* TxUserInfo.swift in Sources */, + 4709C315287EA11900B4BD48 /* TransactionMetadata.swift in Sources */, C91E91AE29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift in Sources */, 47AE8BFE28C1306000490F5E /* AtmListViewController.swift in Sources */, 754D02082D155564005CA466 /* CoinJoinTransaction.swift in Sources */, @@ -8944,7 +8944,7 @@ 47C661AF28FDAA3400028A8D /* BaseAmountViewController.swift in Sources */, 478C982C2942F03500FAA0F0 /* SendingToView.swift in Sources */, C94D98232A4CC85400F3BEE1 /* UIView+Dash.swift in Sources */, - 4709C318287EA23000B4BD48 /* TxUserInfoDAO.swift in Sources */, + 4709C318287EA23000B4BD48 /* TransactionMetadataDAO.swift in Sources */, 2A0C69E323155A0E001B8C90 /* DWModalContentView.m in Sources */, 2A9FFE042230FF2B00956D5F /* DWUpholdConstants.m in Sources */, 0F6EDFC728C896BD000427E7 /* CoinbaseSwapeTradeResponse.swift in Sources */, @@ -9259,7 +9259,7 @@ C9D2C6D32A320AA000D15901 /* Transactions.swift in Sources */, C9D2C6D42A320AA000D15901 /* DWRecoverTextView.m in Sources */, C943B5012A40A54600AF23C5 /* DWDPGenericItemView.m in Sources */, - C9D2C6D52A320AA000D15901 /* TxUserInfo.swift in Sources */, + C9D2C6D52A320AA000D15901 /* TransactionMetadata.swift in Sources */, C9D2C6D62A320AA000D15901 /* ExtendedPublicKeysViewController.swift in Sources */, C9D2C6D82A320AA000D15901 /* AtmListViewController.swift in Sources */, C9D2C6D92A320AA000D15901 /* TxDetailViewController.swift in Sources */, @@ -9752,7 +9752,7 @@ C9D2C8382A320AA000D15901 /* DWRootModel.m in Sources */, C9D2C8392A320AA000D15901 /* BaseAmountViewController.swift in Sources */, C9D2C83A2A320AA000D15901 /* SendingToView.swift in Sources */, - C9D2C83B2A320AA000D15901 /* TxUserInfoDAO.swift in Sources */, + C9D2C83B2A320AA000D15901 /* TransactionMetadataDAO.swift in Sources */, C9D2C83C2A320AA000D15901 /* DWModalContentView.m in Sources */, 759AFDE02CC63571007072D2 /* JoinDashPayScreen.swift in Sources */, C9D2C83D2A320AA000D15901 /* DWUpholdConstants.m in Sources */, diff --git a/DashWallet/Sources/Application/App.swift b/DashWallet/Sources/Application/App.swift index 3bf47ba93..fb43a438b 100644 --- a/DashWallet/Sources/Application/App.swift +++ b/DashWallet/Sources/Application/App.swift @@ -89,7 +89,7 @@ final class App { static let shared = App() func cleanUp() { - TxUserInfoDAOImpl.shared.deleteAll() + TransactionMetadataDAOImpl.shared.deleteAll() AddressUserInfoDAOImpl.shared.deleteAll() #if DASHPAY UsernameRequestsDAOImpl.shared.deleteAll() diff --git a/DashWallet/Sources/Infrastructure/Database/Migrations.bundle/20250418145536_more_metadata_tx_userinfo.sql b/DashWallet/Sources/Infrastructure/Database/Migrations.bundle/20250418145536_more_metadata_tx_userinfo.sql new file mode 100644 index 000000000..abf002c7c --- /dev/null +++ b/DashWallet/Sources/Infrastructure/Database/Migrations.bundle/20250418145536_more_metadata_tx_userinfo.sql @@ -0,0 +1,5 @@ +-- Adding metadata fields to tx_userinfo table +ALTER TABLE tx_userinfo ADD timestamp BIGINT NULL; +ALTER TABLE tx_userinfo ADD memo TEXT NULL; +ALTER TABLE tx_userinfo ADD service TEXT NULL; +ALTER TABLE tx_userinfo ADD customIconId BLOB NULL; \ No newline at end of file diff --git a/DashWallet/Sources/Infrastructure/Database/Migrations/SeedDB.swift b/DashWallet/Sources/Infrastructure/Database/Migrations/SeedDB.swift index d9985454b..f2e1bbfae 100644 --- a/DashWallet/Sources/Infrastructure/Database/Migrations/SeedDB.swift +++ b/DashWallet/Sources/Infrastructure/Database/Migrations/SeedDB.swift @@ -3,7 +3,7 @@ import SQLite import SQLiteMigrationManager struct SeedDB: Migration { - var version: Int64 = 20241130210940 + var version: Int64 = 20250418145536 func migrateDatabase(_ db: Connection) throws { } } diff --git a/DashWallet/Sources/Models/Taxes/Address/AddressUserInfo.swift b/DashWallet/Sources/Models/Taxes/Address/AddressUserInfo.swift index 1c699c473..85d16944e 100644 --- a/DashWallet/Sources/Models/Taxes/Address/AddressUserInfo.swift +++ b/DashWallet/Sources/Models/Taxes/Address/AddressUserInfo.swift @@ -22,17 +22,17 @@ import SQLite @objc class AddressUserInfo: NSObject { @objc var address: String - @objc var taxCategory: TxUserInfoTaxCategory = .unknown + @objc var taxCategory: TxMetadataTaxCategory = .unknown @objc - init(address: String, taxCategory: TxUserInfoTaxCategory) { + init(address: String, taxCategory: TxMetadataTaxCategory) { self.address = address self.taxCategory = taxCategory } init(row: Row) { address = row[AddressUserInfo.addressColumn] - taxCategory = TxUserInfoTaxCategory(rawValue: row[TxUserInfo.txCategoryColumn]) ?? .unknown + taxCategory = TxMetadataTaxCategory(rawValue: row[TransactionMetadata.txCategoryColumn]) ?? .unknown super.init() } diff --git a/DashWallet/Sources/Models/Taxes/Services/TaxReportGenerator.swift b/DashWallet/Sources/Models/Taxes/Services/TaxReportGenerator.swift index 1a74f57bc..9237cc4cf 100644 --- a/DashWallet/Sources/Models/Taxes/Services/TaxReportGenerator.swift +++ b/DashWallet/Sources/Models/Taxes/Services/TaxReportGenerator.swift @@ -63,8 +63,8 @@ enum TaxReportGenerator { DispatchQueue.global(qos: .default).async { let transactions = transactions - let userInfosArray = TxUserInfoDAOImpl().all() - let userInfos = userInfosArray.reduce(into: [Data: TxUserInfo]()) { partialResult, dto in + let userInfosArray = TransactionMetadataDAOImpl().all() + let userInfos = userInfosArray.reduce(into: [Data: TransactionMetadata]()) { partialResult, dto in partialResult[dto.txHash] = dto } @@ -119,7 +119,7 @@ enum TaxReportGenerator { return fileName } - private static func value(for column: ReportColumns, transaction: DSTransaction, andUserInfo userInfo: TxUserInfo?) -> String { + private static func value(for column: ReportColumns, transaction: DSTransaction, andUserInfo userInfo: TransactionMetadata?) -> String { let transactionDirection = transaction.direction let isOutcoming = transactionDirection == .sent diff --git a/DashWallet/Sources/Models/Taxes/Taxes.swift b/DashWallet/Sources/Models/Taxes/Taxes.swift index c4d3af5e2..ebdbf5a46 100644 --- a/DashWallet/Sources/Models/Taxes/Taxes.swift +++ b/DashWallet/Sources/Models/Taxes/Taxes.swift @@ -20,7 +20,7 @@ import Foundation // MARK: - TxUserInfoTaxCategory @objc -enum TxUserInfoTaxCategory: Int { +enum TxMetadataTaxCategory: Int { /// Unknown case unknown @@ -51,15 +51,15 @@ enum TxUserInfoTaxCategory: Int { class Taxes: NSObject { var addressesUserInfos: AddressUserInfoDAO = AddressUserInfoDAOImpl() - var txUserInfos: TxUserInfoDAO = TxUserInfoDAOImpl.shared + var txUserInfos: TransactionMetadataDAO = TransactionMetadataDAOImpl.shared @objc - func mark(address: String, with taxCategory: TxUserInfoTaxCategory) { + func mark(address: String, with taxCategory: TxMetadataTaxCategory) { addressesUserInfos.create(dto: AddressUserInfo(address: address, taxCategory: taxCategory)) } - func taxCategory(for tx: DSTransaction) -> TxUserInfoTaxCategory { - var taxCategory: TxUserInfoTaxCategory = tx.defaultTaxCategory() + func taxCategory(for tx: DSTransaction) -> TxMetadataTaxCategory { + var taxCategory: TxMetadataTaxCategory = tx.defaultTaxCategory() for outputAddress in tx.outputAddresses { if let address = outputAddress as? String, let txCategory = self.taxCategory(for: address) { @@ -80,11 +80,11 @@ class Taxes: NSObject { return taxCategory } - func taxCategory(for tx: Transaction) -> TxUserInfoTaxCategory { + func taxCategory(for tx: Transaction) -> TxMetadataTaxCategory { taxCategory(for: tx.tx) } - func taxCategory(for address: String) -> TxUserInfoTaxCategory? { + func taxCategory(for address: String) -> TxMetadataTaxCategory? { addressesUserInfos.get(by: address)?.taxCategory } diff --git a/DashWallet/Sources/Models/Transactions/Model/Transaction.swift b/DashWallet/Sources/Models/Transactions/Model/Transaction.swift index b8eb79ff9..c0a18fb94 100644 --- a/DashWallet/Sources/Models/Transactions/Model/Transaction.swift +++ b/DashWallet/Sources/Models/Transactions/Model/Transaction.swift @@ -83,7 +83,7 @@ class Transaction: TransactionDataItem, Identifiable { private lazy var storedFiatAmount = userInfo?.fiatAmountString(from: _dashAmount) ?? NSLocalizedString("Not available", comment: ""); - lazy var userInfo: TxUserInfo? = TxUserInfoDAOImpl.shared.get(by: tx.txHashData) + lazy var userInfo: TransactionMetadata? = TransactionMetadataDAOImpl.shared.get(by: tx.txHashData) var transactionType: `Type` { _transactionType } private lazy var _transactionType: `Type` = tx.type diff --git a/DashWallet/Sources/Models/Taxes/Tx/DAO/TxUserInfoDAO.swift b/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift similarity index 52% rename from DashWallet/Sources/Models/Taxes/Tx/DAO/TxUserInfoDAO.swift rename to DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift index 55dc32497..3fff2d234 100644 --- a/DashWallet/Sources/Models/Taxes/Tx/DAO/TxUserInfoDAO.swift +++ b/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift @@ -20,31 +20,31 @@ import SQLite // MARK: - TxUserInfoDAO -protocol TxUserInfoDAO { - func create(dto: TxUserInfo) - func get(by hash: Data) -> TxUserInfo? - func update(dto: TxUserInfo) - func delete(dto: TxUserInfo) +protocol TransactionMetadataDAO { + func create(dto: TransactionMetadata) + func get(by hash: Data) -> TransactionMetadata? + func update(dto: TransactionMetadata) + func delete(dto: TransactionMetadata) func deleteAll() } // MARK: - TxUserInfoDAOImpl -class TxUserInfoDAOImpl: NSObject, TxUserInfoDAO { +class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO { private var db: Connection { DatabaseConnection.shared.db } - private var cache: [Data: TxUserInfo] = [:] + private var cache: [Data: TransactionMetadata] = [:] - private let queue = DispatchQueue(label: "org.dash.infrastructure.queue.tx-user-info-dao", attributes: .concurrent) + private let queue = DispatchQueue(label: "org.dash.infrastructure.queue.transaction-metadata-dao", attributes: .concurrent) - func create(dto: TxUserInfo) { + func create(dto: TransactionMetadata) { do { - let txUserInfo = TxUserInfo.table.insert(or: .replace, - TxUserInfo.txHashColumn <- dto.txHash, - TxUserInfo.txCategoryColumn <- dto.taxCategory.rawValue, - TxUserInfo.txRateColumn <- dto.rate, - TxUserInfo.txRateCurrencyCodeColumn <- dto.rateCurrency, - TxUserInfo.txRateMaximumFractionDigitsColumn <- dto.rateMaximumFractionDigits) - try db.run(txUserInfo) + let transactionMetadata = TransactionMetadata.table.insert(or: .replace, + TransactionMetadata.txHashColumn <- dto.txHash, + TransactionMetadata.txCategoryColumn <- dto.taxCategory.rawValue, + TransactionMetadata.txRateColumn <- dto.rate, + TransactionMetadata.txRateCurrencyCodeColumn <- dto.rateCurrency, + TransactionMetadata.txRateMaximumFractionDigitsColumn <- dto.rateMaximumFractionDigits) + try db.run(transactionMetadata) } catch { print(error) @@ -55,14 +55,14 @@ class TxUserInfoDAOImpl: NSObject, TxUserInfoDAO { } } - func all() -> [TxUserInfo] { - let txUserInfos = TxUserInfo.table + func all() -> [TransactionMetadata] { + let txUserInfos = TransactionMetadata.table - var userInfos: [TxUserInfo] = [] + var userInfos: [TransactionMetadata] = [] do { for txInfo in try db.prepare(txUserInfos) { - let userInfo = TxUserInfo(row: txInfo) + let userInfo = TransactionMetadata(row: txInfo) userInfos.append(userInfo) } } catch { @@ -72,16 +72,16 @@ class TxUserInfoDAOImpl: NSObject, TxUserInfoDAO { return userInfos } - func get(by hash: Data) -> TxUserInfo? { + func get(by hash: Data) -> TransactionMetadata? { if let cached = cachedValue(by: hash) { return cached } - let txUserInfo = TxUserInfo.table.filter(TxUserInfo.txHashColumn == hash) + let txUserInfo = TransactionMetadata.table.filter(TransactionMetadata.txHashColumn == hash) do { for txInfo in try db.prepare(txUserInfo) { - let userInfo = TxUserInfo(row: txInfo) + let userInfo = TransactionMetadata(row: txInfo) queue.async(flags: .barrier) { [weak self] in self?.cache[hash] = userInfo } @@ -94,8 +94,8 @@ class TxUserInfoDAOImpl: NSObject, TxUserInfoDAO { return nil } - private func cachedValue(by key: Data) -> TxUserInfo? { - var v: TxUserInfo? + private func cachedValue(by key: Data) -> TransactionMetadata? { + var v: TransactionMetadata? queue.sync { v = cache[key] @@ -104,11 +104,11 @@ class TxUserInfoDAOImpl: NSObject, TxUserInfoDAO { return v } - func update(dto: TxUserInfo) { + func update(dto: TransactionMetadata) { create(dto: dto) } - func delete(dto: TxUserInfo) { + func delete(dto: TransactionMetadata) { queue.async(flags: .barrier) { [weak self] in self?.cache[dto.txHash] = nil } @@ -116,7 +116,7 @@ class TxUserInfoDAOImpl: NSObject, TxUserInfoDAO { func deleteAll() { do { - try db.run(TxUserInfo.table.delete()) + try db.run(TransactionMetadata.table.delete()) queue.async(flags: .barrier) { [weak self] in self?.cache = [:] } @@ -125,11 +125,11 @@ class TxUserInfoDAOImpl: NSObject, TxUserInfoDAO { } } - static let shared = TxUserInfoDAOImpl() + static let shared = TransactionMetadataDAOImpl() } -extension TxUserInfoDAOImpl { - func dictionaryOfAllItems() -> [Data: TxUserInfo] { +extension TransactionMetadataDAOImpl { + func dictionaryOfAllItems() -> [Data: TransactionMetadata] { _ = all() return cache } diff --git a/DashWallet/Sources/Models/Taxes/Tx/TxUserInfo.swift b/DashWallet/Sources/Models/Tx Metadata/TransactionMetadata.swift similarity index 74% rename from DashWallet/Sources/Models/Taxes/Tx/TxUserInfo.swift rename to DashWallet/Sources/Models/Tx Metadata/TransactionMetadata.swift index ae9b17246..60b2a74fc 100644 --- a/DashWallet/Sources/Models/Taxes/Tx/TxUserInfo.swift +++ b/DashWallet/Sources/Models/Tx Metadata/TransactionMetadata.swift @@ -19,7 +19,7 @@ import Foundation import SQLite -extension TxUserInfoTaxCategory { +extension TxMetadataTaxCategory { var stringValue: String { switch self { case .unknown: @@ -36,7 +36,7 @@ extension TxUserInfoTaxCategory { } - var nextTaxCategory: TxUserInfoTaxCategory { + var nextTaxCategory: TxMetadataTaxCategory { switch self { case .unknown: return .unknown @@ -52,27 +52,36 @@ extension TxUserInfoTaxCategory { } } -// MARK: - TxUserInfo +// MARK: - TransactionMetadata -struct TxUserInfo { +struct TransactionMetadata { var txHash: Data - var taxCategory: TxUserInfoTaxCategory = .unknown - + var taxCategory: TxMetadataTaxCategory = .unknown + var rate: Int? var rateCurrency: String? var rateMaximumFractionDigits: Int? + + var timestamp: Int64? + var memo: String? + var service: String? + var customIconId: Data? - init(txHash: Data, taxCategory: TxUserInfoTaxCategory) { + init(txHash: Data, taxCategory: TxMetadataTaxCategory) { self.txHash = txHash self.taxCategory = taxCategory } init(row: Row) { - txHash = row[TxUserInfo.txHashColumn] - taxCategory = TxUserInfoTaxCategory(rawValue: row[TxUserInfo.txCategoryColumn]) ?? .unknown - rate = row[TxUserInfo.txRateColumn] - rateCurrency = row[TxUserInfo.txRateCurrencyCodeColumn] - rateMaximumFractionDigits = row[TxUserInfo.txRateMaximumFractionDigitsColumn] + txHash = row[TransactionMetadata.txHashColumn] + taxCategory = TxMetadataTaxCategory(rawValue: row[TransactionMetadata.txCategoryColumn]) ?? .unknown + rate = row[TransactionMetadata.txRateColumn] + rateCurrency = row[TransactionMetadata.txRateCurrencyCodeColumn] + rateMaximumFractionDigits = row[TransactionMetadata.txRateMaximumFractionDigitsColumn] + timestamp = row[TransactionMetadata.timestamp] + memo = row[TransactionMetadata.memo] + service = row[TransactionMetadata.service] + customIconId = row[TransactionMetadata.customIconId] } mutating func update(rate: Int, currency: String, maximumFractionDigits: Int) { @@ -82,7 +91,7 @@ struct TxUserInfo { } } -extension TxUserInfo { +extension TransactionMetadata { func taxCategoryString() -> String { taxCategory.stringValue } @@ -108,19 +117,23 @@ extension TxUserInfo { } } -extension TxUserInfo { +extension TransactionMetadata { static var table: Table { Table("tx_userinfo") } static var txCategoryColumn: SQLite.Expression { Expression("taxCategory") } static var txHashColumn: SQLite.Expression { Expression("txHash") } static var txRateColumn: SQLite.Expression { .init("rate") } static var txRateCurrencyCodeColumn: SQLite.Expression { .init("rateCurrencyCode") } static var txRateMaximumFractionDigitsColumn: SQLite.Expression { .init("rateMaximumFractionDigits") } + static var timestamp: SQLite.Expression { Expression("timestamp") } + static var memo: SQLite.Expression { .init("memo") } + static var service: SQLite.Expression { .init("service") } + static var customIconId: SQLite.Expression { Expression("customIconId") } } @objc extension DSTransaction { @objc - func defaultTaxCategory() -> TxUserInfoTaxCategory { + func defaultTaxCategory() -> TxMetadataTaxCategory { switch direction { case .moved: return .expense diff --git a/DashWallet/Sources/Models/Tx/Transactions.swift b/DashWallet/Sources/Models/Tx/Transactions.swift index d6f3b624e..f5a104024 100644 --- a/DashWallet/Sources/Models/Tx/Transactions.swift +++ b/DashWallet/Sources/Models/Tx/Transactions.swift @@ -28,7 +28,7 @@ final class Tx: NSObject { .init() } - private var txUserInfos: TxUserInfoDAO = TxUserInfoDAOImpl.shared + private var txUserInfos: TransactionMetadataDAO = TransactionMetadataDAOImpl.shared @objc func updateRateIfNeeded(for transaction: DSTransaction) { @@ -59,7 +59,7 @@ final class Tx: NSObject { set(rate: rate, currency: currency, maximumFractionDigits: maximumFractionDigits, for: .init(txHash: transaction.txHashData, taxCategory: transaction.defaultTaxCategory())) } - private func set(rate: Int, currency: String, maximumFractionDigits: Int, for userInfo: TxUserInfo) { + private func set(rate: Int, currency: String, maximumFractionDigits: Int, for userInfo: TransactionMetadata) { var userInfo = userInfo userInfo.update(rate: rate, currency: currency, maximumFractionDigits: maximumFractionDigits) txUserInfos.update(dto: userInfo) diff --git a/DashWallet/Sources/UI/Tx/Details/Model/TxDetailModel.swift b/DashWallet/Sources/UI/Tx/Details/Model/TxDetailModel.swift index d47476961..5cca3ee28 100644 --- a/DashWallet/Sources/UI/Tx/Details/Model/TxDetailModel.swift +++ b/DashWallet/Sources/UI/Tx/Details/Model/TxDetailModel.swift @@ -23,7 +23,7 @@ import Foundation class TxDetailModel: NSObject { var transaction: Transaction var transactionId: String - var txTaxCategory: TxUserInfoTaxCategory + var txTaxCategory: TxMetadataTaxCategory var title: String { direction.title @@ -61,11 +61,11 @@ class TxDetailModel: NSObject { txTaxCategory = txTaxCategory.nextTaxCategory let txHash = transaction.txHashData - var txUserInfo = transaction.userInfo ?? TxUserInfo(txHash: txHash, taxCategory: txTaxCategory) + var txUserInfo = transaction.userInfo ?? TransactionMetadata(txHash: txHash, taxCategory: txTaxCategory) txUserInfo.taxCategory = txTaxCategory // TODO: Move it to Domain layer - TxUserInfoDAOImpl.shared.update(dto: txUserInfo) + TransactionMetadataDAOImpl.shared.update(dto: txUserInfo) } func copyTransactionIdToPasteboard() -> Bool { From e6692ed9e2d7c725bb17add02f489ff323271d6d Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Thu, 5 Jun 2025 15:08:12 +0700 Subject: [PATCH 03/13] chore: merging tx metadata changes --- DashWallet.xcodeproj/project.pbxproj | 4 +- .../DAO}/IconBitmapDAO.swift | 2 +- .../DAO/TransactionMetadataDAO.swift | 118 ++++++++++++++++-- .../{Tx => Tx Metadata}/IconBitmap.swift | 0 .../Tx Metadata/TransactionMetadata.swift | 4 + .../CustomIconMetadataProvider.swift | 9 +- 6 files changed, 120 insertions(+), 17 deletions(-) rename DashWallet/Sources/Models/{Tx => Tx Metadata/DAO}/IconBitmapDAO.swift (99%) rename DashWallet/Sources/Models/{Tx => Tx Metadata}/IconBitmap.swift (100%) diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index 72cc45ecc..93edaedf1 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -5189,6 +5189,7 @@ children = ( 4709C316287EA22600B4BD48 /* DAO */, 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */, + 7549BE592DEAF50D004F0BAF /* IconBitmap.swift */, ); path = "Tx Metadata"; sourceTree = ""; @@ -5197,6 +5198,7 @@ isa = PBXGroup; children = ( 4709C317287EA23000B4BD48 /* TransactionMetadataDAO.swift */, + 7549BE5C2DEAF55B004F0BAF /* IconBitmapDAO.swift */, ); path = DAO; sourceTree = ""; @@ -5535,8 +5537,6 @@ children = ( 754D020C2D1558EA005CA466 /* GroupedTransactions.swift */, 479DBDDC2995168C00F30AF1 /* Transactions.swift */, - 7549BE592DEAF50D004F0BAF /* IconBitmap.swift */, - 7549BE5C2DEAF55B004F0BAF /* IconBitmapDAO.swift */, ); path = Tx; sourceTree = ""; diff --git a/DashWallet/Sources/Models/Tx/IconBitmapDAO.swift b/DashWallet/Sources/Models/Tx Metadata/DAO/IconBitmapDAO.swift similarity index 99% rename from DashWallet/Sources/Models/Tx/IconBitmapDAO.swift rename to DashWallet/Sources/Models/Tx Metadata/DAO/IconBitmapDAO.swift index 9f6a4710f..58cdb9ec5 100644 --- a/DashWallet/Sources/Models/Tx/IconBitmapDAO.swift +++ b/DashWallet/Sources/Models/Tx Metadata/DAO/IconBitmapDAO.swift @@ -213,4 +213,4 @@ extension Data { } self = data } -} \ No newline at end of file +} diff --git a/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift b/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift index 3fff2d234..c8c3e3afb 100644 --- a/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift +++ b/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift @@ -17,25 +17,43 @@ import Foundation import SQLite +import Combine -// MARK: - TxUserInfoDAO +// MARK: - Metadata Change Event + +enum TransactionMetadataChange { + case created(TransactionMetadata) + case updated(TransactionMetadata, previousMetadata: TransactionMetadata) + case deleted(TransactionMetadata) + case deletedAll +} + +// MARK: - TransactionMetadataDAO protocol TransactionMetadataDAO { func create(dto: TransactionMetadata) - func get(by hash: Data) -> TransactionMetadata? + func get(by hash: Data, ignoreCache: Bool) -> TransactionMetadata? func update(dto: TransactionMetadata) func delete(dto: TransactionMetadata) func deleteAll() } -// MARK: - TxUserInfoDAOImpl +extension TransactionMetadataDAO { + func get(by hash: Data) -> TransactionMetadata? { + return get(by: hash, ignoreCache: false) + } +} + +// MARK: - TransactionMetadataDAOImpl -class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO { +class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO, ObservableObject { private var db: Connection { DatabaseConnection.shared.db } private var cache: [Data: TransactionMetadata] = [:] private let queue = DispatchQueue(label: "org.dash.infrastructure.queue.transaction-metadata-dao", attributes: .concurrent) - + + @Published private(set) var lastChange: TransactionMetadataChange? + func create(dto: TransactionMetadata) { do { let transactionMetadata = TransactionMetadata.table.insert(or: .replace, @@ -43,7 +61,11 @@ class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO { TransactionMetadata.txCategoryColumn <- dto.taxCategory.rawValue, TransactionMetadata.txRateColumn <- dto.rate, TransactionMetadata.txRateCurrencyCodeColumn <- dto.rateCurrency, - TransactionMetadata.txRateMaximumFractionDigitsColumn <- dto.rateMaximumFractionDigits) + TransactionMetadata.txRateMaximumFractionDigitsColumn <- dto.rateMaximumFractionDigits, + TransactionMetadata.timestamp <- dto.timestamp, + TransactionMetadata.memo <- dto.memo, + TransactionMetadata.service <- dto.service, + TransactionMetadata.customIconId <- dto.customIconId) try db.run(transactionMetadata) } catch { @@ -52,6 +74,10 @@ class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO { queue.async(flags: .barrier) { [weak self] in self?.cache[dto.txHash] = dto + + DispatchQueue.main.async { + self?.lastChange = .created(dto) + } } } @@ -72,8 +98,8 @@ class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO { return userInfos } - func get(by hash: Data) -> TransactionMetadata? { - if let cached = cachedValue(by: hash) { + func get(by hash: Data, ignoreCache: Bool = false) -> TransactionMetadata? { + if !ignoreCache, let cached = cachedValue(by: hash) { return cached } @@ -105,12 +131,77 @@ class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO { } func update(dto: TransactionMetadata) { - create(dto: dto) + guard let existingDto = get(by: dto.txHash) else { + create(dto: dto) + return + } + + do { + var setters: [Setter] = [] + + if dto.taxCategory != .unknown && existingDto.taxCategory != dto.taxCategory { + setters.append(TransactionMetadata.txCategoryColumn <- dto.taxCategory.rawValue) + } + + if let rate = dto.rate, existingDto.rate != rate { + setters.append(TransactionMetadata.txRateColumn <- rate) + } + + if let rateCurrency = dto.rateCurrency, existingDto.rateCurrency != rateCurrency { + setters.append(TransactionMetadata.txRateCurrencyCodeColumn <- rateCurrency) + } + + if let rateMaximumFractionDigits = dto.rateMaximumFractionDigits, existingDto.rateMaximumFractionDigits != rateMaximumFractionDigits { + setters.append(TransactionMetadata.txRateMaximumFractionDigitsColumn <- rateMaximumFractionDigits) + } + + if let timestamp = dto.timestamp, existingDto.timestamp != timestamp { + setters.append(TransactionMetadata.timestamp <- timestamp) + } + + if let memo = dto.memo, existingDto.memo != memo { + setters.append(TransactionMetadata.memo <- memo) + } + + if let service = dto.service, existingDto.service != service { + setters.append(TransactionMetadata.service <- service) + } + + if let customIconId = dto.customIconId, existingDto.customIconId != customIconId { + setters.append(TransactionMetadata.customIconId <- customIconId) + } + + if !setters.isEmpty { + let txUserInfo = TransactionMetadata.table.filter(TransactionMetadata.txHashColumn == dto.txHash) + try db.run(txUserInfo.update(setters)) + + // Update cache + if let updated = get(by: dto.txHash, ignoreCache: true) { + DispatchQueue.main.async { [weak self] in + self?.lastChange = .updated(updated, previousMetadata: existingDto) + } + } + } + } catch { + print(error) + } } func delete(dto: TransactionMetadata) { - queue.async(flags: .barrier) { [weak self] in - self?.cache[dto.txHash] = nil + do { + let txUserInfo = TransactionMetadata.table.filter(TransactionMetadata.txHashColumn == dto.txHash) + try db.run(txUserInfo.delete()) + + queue.async(flags: .barrier) { [weak self] in + self?.cache[dto.txHash] = nil + + // Publish the deleted metadata event + DispatchQueue.main.async { + self?.lastChange = .deleted(dto) + } + } + } catch { + print(error) } } @@ -119,6 +210,11 @@ class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO { try db.run(TransactionMetadata.table.delete()) queue.async(flags: .barrier) { [weak self] in self?.cache = [:] + + // Publish the delete all event + DispatchQueue.main.async { + self?.lastChange = .deletedAll + } } } catch { print(error) diff --git a/DashWallet/Sources/Models/Tx/IconBitmap.swift b/DashWallet/Sources/Models/Tx Metadata/IconBitmap.swift similarity index 100% rename from DashWallet/Sources/Models/Tx/IconBitmap.swift rename to DashWallet/Sources/Models/Tx Metadata/IconBitmap.swift diff --git a/DashWallet/Sources/Models/Tx Metadata/TransactionMetadata.swift b/DashWallet/Sources/Models/Tx Metadata/TransactionMetadata.swift index 60b2a74fc..a96929917 100644 --- a/DashWallet/Sources/Models/Tx Metadata/TransactionMetadata.swift +++ b/DashWallet/Sources/Models/Tx Metadata/TransactionMetadata.swift @@ -66,6 +66,10 @@ struct TransactionMetadata { var memo: String? var service: String? var customIconId: Data? + + init(txHash: Data) { + self.txHash = txHash + } init(txHash: Data, taxCategory: TxMetadataTaxCategory) { self.txHash = txHash diff --git a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift b/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift index f502b52c4..e09950522 100644 --- a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift @@ -25,6 +25,7 @@ class CustomIconMetadataProvider: MetadataProvider { private var cancellableBag = Set() private let iconBitmapDao = IconBitmapDAOImpl.shared + private let metadataDao = TransactionMetadataDAOImpl.shared var availableMetadata: [Data : TxRowMetadata] = [:] let metadataUpdated = PassthroughSubject() @@ -41,7 +42,7 @@ class CustomIconMetadataProvider: MetadataProvider { for (iconId, iconBitmap) in bitmaps { if let image = UIImage(data: iconBitmap.imageData) { // Find transactions that use this icon - for (txHash, metadata) in availableMetadata { + for (txHash, metadata) in availableMetadata { // TODO: create new metatada if needed if let customIconId = metadata.customIconId, customIconId == iconId { var updatedMetadata = metadata updatedMetadata.icon = image @@ -86,8 +87,6 @@ class CustomIconMetadataProvider: MetadataProvider { return } - print("Resized image data base64: \(resizedImageData.base64EncodedString())") - let iconBitmap = IconBitmap( id: hashData, imageData: resizedImageData, @@ -98,6 +97,10 @@ class CustomIconMetadataProvider: MetadataProvider { await iconBitmapDao.addBitmap(bitmap: iconBitmap) + var metadata = TransactionMetadata(txHash: txId) + metadata.customIconId = hashData + metadataDao.update(dto: metadata) + var txRowMetadata = availableMetadata[txId] ?? TxRowMetadata(title: nil, details: nil) txRowMetadata.customIconId = hashData txRowMetadata.icon = resizedImage From 4e5543986223cb91450a0fe042641fcdb38f17ca Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Thu, 5 Jun 2025 15:32:40 +0700 Subject: [PATCH 04/13] feat: observe custom icon in the gift card details --- .../Views/DashSpend/GiftCardDetailsView.swift | 14 ++++------ .../DashSpend/GiftCardDetailsViewModel.swift | 28 +++++++++++++++++-- .../CustomIconMetadataProvider.swift | 2 -- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift index 7b66d4e1f..b9d7c9e46 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift @@ -40,10 +40,9 @@ struct GiftCardDetailsView: View { // Merchant header HStack(spacing: 15) { ZStack(alignment: .bottomTrailing) { - if let iconUrl = viewModel.uiState.merchantIconUrl { - WebImage(url: URL(string: iconUrl)) + if let icon = viewModel.uiState.merchantIcon { + Image(uiImage: icon) .resizable() - .indicator(.activity) .transition(.fade(duration: 0.3)) .scaledToFit() .frame(width: 48, height: 48) @@ -53,19 +52,16 @@ struct GiftCardDetailsView: View { .resizable() .scaledToFit() .frame(width: 48, height: 48) - .foregroundColor(.dashBlue) } // Secondary icon - if viewModel.uiState.merchantIconUrl != nil { + if viewModel.uiState.merchantIcon != nil { Image("image.explore.dash.wts.payment.gift-card") .resizable() .scaledToFit() - .frame(width: 16, height: 16) - .foregroundColor(.dashBlue) - .frame(width: 24, height: 24) + .frame(width: 20, height: 20) + .frame(width: 23, height: 23) .background(Circle().fill(Color.secondaryBackground)) - .overlay(Circle().stroke(Color.secondaryBackground, lineWidth: 2)) .offset(x: 2, y: 2) } } diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift index 434484134..940ec98cb 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift @@ -21,12 +21,12 @@ import UIKit struct GiftCardDetailsUIState { var merchantName: String = "" - var merchantIconUrl: String? = nil var merchantUrl: String? = nil var formattedPrice: String = "$0.00" var cardNumber: String? = nil var cardPin: String? = nil var barcodeImage: UIImage? = nil + var merchantIcon: UIImage? = nil var purchaseDate: Date? = nil var isLoadingCardDetails: Bool = false var loadingError: Error? = nil @@ -38,6 +38,8 @@ class GiftCardDetailsViewModel: ObservableObject { private var cancellableBag = Set() private let ctxSpendService = CTXSpendService.shared private let giftCardsDAO = GiftCardsDAOImpl.shared + private let customIconDAO = IconBitmapDAOImpl.shared + private let txMetadataDAO = TransactionMetadataDAOImpl.shared private var tickerTimer: Timer? private var retryCount = 0 private let maxRetries = 3 @@ -84,7 +86,29 @@ class GiftCardDetailsViewModel: ObservableObject { } .store(in: &cancellableBag) - // TODO: Observe merchant icon from transaction metadata if available + self.txMetadataDAO.$lastChange // TODO: what if we open this from home screen + .receive(on: DispatchQueue.main) + .sink { [weak self] change in + guard let self = self, let change = change else { return } + + Task { + switch change { + case .created(let metadata), .updated(let metadata, _): + if let customIconId = metadata.customIconId, + let iconBitmap = await self.customIconDAO.getBitmap(id: customIconId) { + guard let image = UIImage(data: iconBitmap.imageData) else { + DSLogger.log("Failed to create image from data for tx icon: \(metadata.txHash.hexEncodedString())") + return + } + + self.uiState.merchantIcon = image + } + default: + break + } + } + } + .store(in: &cancellableBag) } func stopObserving() { diff --git a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift b/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift index e09950522..2dc47a311 100644 --- a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift @@ -78,12 +78,10 @@ class CustomIconMetadataProvider: MetadataProvider { // Calculate hash from original image data let imageHash = SHA256.hash(data: data) - print("BITMAP: imageHash: \(imageHash)") let hashData = Data(imageHash) let resizedImage = resizeIcon(image: image) guard let resizedImageData = resizedImage.pngData() else { - print("BITMAP: Failed to get PNG data from resized image") return } From f42d484a25cfaeecbac1ffe5a5e4412d0e9b5d76 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Thu, 5 Jun 2025 16:12:47 +0700 Subject: [PATCH 05/13] chore: cleanup --- .../Categories/Foundation+Bitcoin.swift | 17 +++++++++++++++ .../Tx Metadata/DAO/IconBitmapDAO.swift | 21 ------------------- .../Models/Uphold/DWUpholdMainnetConstants.m | 1 + .../Sources/UI/Home/Views/HomeViewModel.swift | 1 + 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/DashWallet/Sources/Categories/Foundation+Bitcoin.swift b/DashWallet/Sources/Categories/Foundation+Bitcoin.swift index 30de7fb8b..a70f2d105 100644 --- a/DashWallet/Sources/Categories/Foundation+Bitcoin.swift +++ b/DashWallet/Sources/Categories/Foundation+Bitcoin.swift @@ -22,6 +22,23 @@ extension Data { let rawValue: Int static let upperCase = HexEncodingOptions(rawValue: 1 << 0) } + + init?(hex: String) { + let len = hex.count / 2 + var data = Data(capacity: len) + var i = hex.startIndex + for _ in 0.. String { let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" diff --git a/DashWallet/Sources/Models/Tx Metadata/DAO/IconBitmapDAO.swift b/DashWallet/Sources/Models/Tx Metadata/DAO/IconBitmapDAO.swift index 58cdb9ec5..c95a9e599 100644 --- a/DashWallet/Sources/Models/Tx Metadata/DAO/IconBitmapDAO.swift +++ b/DashWallet/Sources/Models/Tx Metadata/DAO/IconBitmapDAO.swift @@ -193,24 +193,3 @@ extension IconBitmapDAOImpl { } } } - -// MARK: - Data Extension - -extension Data { - init?(hex: String) { - let len = hex.count / 2 - var data = Data(capacity: len) - var i = hex.startIndex - for _ in 0.. Date: Fri, 6 Jun 2025 19:40:14 +0700 Subject: [PATCH 06/13] feat: icons and merchant names on home screen --- DashWallet.xcodeproj/project.pbxproj | 18 +++- .../Explore Dash/Model/DAO/GiftCardsDAO.swift | 34 +++++- .../DAO/TransactionMetadataDAO.swift | 16 +++ .../Models/Tx Metadata/ServiceName.swift | 28 +++++ .../Models/Uphold/DWUpholdMainnetConstants.m | 1 - .../DashSpend/DashSpendPayViewModel.swift | 12 ++- .../CustomIconMetadataProvider.swift | 85 +++++++++++---- .../GiftCardMetadataProvider.swift | 100 ++++++++++++++++++ .../Sources/UI/Home/Views/HomeView.swift | 3 +- .../Sources/UI/Home/Views/HomeViewModel.swift | 44 +++++--- .../UI/Home/Views/MetadataProvider.swift | 9 +- .../Dialogs/ModalDialog.swift | 7 ++ .../Sources/UI/SwiftUI Components/Icon.swift | 32 +++++- .../UI/SwiftUI Components/MenuItem.swift | 4 +- DashWallet/ar.lproj/Localizable.strings | 3 + DashWallet/bg.lproj/Localizable.strings | 3 + DashWallet/ca.lproj/Localizable.strings | 3 + DashWallet/cs.lproj/Localizable.strings | 3 + DashWallet/da.lproj/Localizable.strings | 3 + DashWallet/de.lproj/Localizable.strings | 3 + DashWallet/el.lproj/Localizable.strings | 3 + DashWallet/en.lproj/Localizable.strings | 3 + DashWallet/eo.lproj/Localizable.strings | 3 + DashWallet/es.lproj/Localizable.strings | 3 + DashWallet/et.lproj/Localizable.strings | 3 + DashWallet/fa.lproj/Localizable.strings | 3 + DashWallet/fi.lproj/Localizable.strings | 3 + DashWallet/fil.lproj/Localizable.strings | 3 + DashWallet/fr.lproj/Localizable.strings | 3 + DashWallet/hr.lproj/Localizable.strings | 3 + DashWallet/hu.lproj/Localizable.strings | 3 + DashWallet/id.lproj/Localizable.strings | 3 + DashWallet/it.lproj/Localizable.strings | 3 + DashWallet/ja.lproj/Localizable.strings | 3 + DashWallet/ko.lproj/Localizable.strings | 3 + DashWallet/mk.lproj/Localizable.strings | 3 + DashWallet/ms.lproj/Localizable.strings | 3 + DashWallet/nb.lproj/Localizable.strings | 3 + DashWallet/nl.lproj/Localizable.strings | 3 + DashWallet/pl.lproj/Localizable.strings | 3 + DashWallet/pt.lproj/Localizable.strings | 3 + DashWallet/ro.lproj/Localizable.strings | 3 + DashWallet/ru.lproj/Localizable.strings | 3 + DashWallet/sk.lproj/Localizable.strings | 3 + DashWallet/sl.lproj/Localizable.strings | 3 + DashWallet/sl_SI.lproj/Localizable.strings | 3 + DashWallet/sq.lproj/Localizable.strings | 3 + DashWallet/sr.lproj/Localizable.strings | 3 + DashWallet/sv.lproj/Localizable.strings | 3 + DashWallet/th.lproj/Localizable.strings | 3 + DashWallet/tr.lproj/Localizable.strings | 3 + DashWallet/uk.lproj/Localizable.strings | 3 + DashWallet/vi.lproj/Localizable.strings | 3 + DashWallet/zh-Hans.lproj/Localizable.strings | 3 + .../zh-Hant-TW.lproj/Localizable.strings | 3 + DashWallet/zh.lproj/Localizable.strings | 3 + DashWallet/zh_TW.lproj/Localizable.strings | 3 + 57 files changed, 471 insertions(+), 51 deletions(-) create mode 100644 DashWallet/Sources/Models/Tx Metadata/ServiceName.swift rename DashWallet/Sources/UI/Home/{Custom Icons => Tx Metadata}/CustomIconMetadataProvider.swift (62%) create mode 100644 DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index 93edaedf1..0fde235be 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -508,6 +508,10 @@ 7503643B2C89CFB70029EC0D /* CoinJoinProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */; }; 7503643E2C89D49A0029EC0D /* CoinJoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7503643D2C89D49A0029EC0D /* CoinJoinService.swift */; }; 7503643F2C89D49A0029EC0D /* CoinJoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7503643D2C89D49A0029EC0D /* CoinJoinService.swift */; }; + 75050CCE2DF2CB0F00F586D6 /* GiftCardMetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75050CCD2DF2CB0E00F586D6 /* GiftCardMetadataProvider.swift */; }; + 75050CCF2DF2CB0F00F586D6 /* GiftCardMetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75050CCD2DF2CB0E00F586D6 /* GiftCardMetadataProvider.swift */; }; + 75050CD12DF2FCC200F586D6 /* ServiceName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75050CD02DF2FCBF00F586D6 /* ServiceName.swift */; }; + 75050CD22DF2FCC200F586D6 /* ServiceName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75050CD02DF2FCBF00F586D6 /* ServiceName.swift */; }; 750CED602C94BFD7000FB837 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750CED5F2C94BFD7000FB837 /* SettingsViewModel.swift */; }; 750CED612C94BFD7000FB837 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750CED5F2C94BFD7000FB837 /* SettingsViewModel.swift */; }; 750CEFA12CCA6EA100E87A32 /* TextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750CEFA02CCA6EA100E87A32 /* TextInput.swift */; }; @@ -2438,6 +2442,8 @@ 7502A4862AE401EF00ACDDD3 /* UsernameVotingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameVotingViewController.swift; sourceTree = ""; }; 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinJoinProgressView.swift; sourceTree = ""; }; 7503643D2C89D49A0029EC0D /* CoinJoinService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinJoinService.swift; sourceTree = ""; }; + 75050CCD2DF2CB0E00F586D6 /* GiftCardMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardMetadataProvider.swift; sourceTree = ""; }; + 75050CD02DF2FCBF00F586D6 /* ServiceName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceName.swift; sourceTree = ""; }; 7509C10E1AF3076100D03FD5 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 7509C1121AF3720100D03FD5 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 750C6CC01B5C8EB60038AAE9 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; @@ -4622,7 +4628,7 @@ 2A9CEBB622E1FA0000A50237 /* Home */ = { isa = PBXGroup; children = ( - 7549BE552DEAECA5004F0BAF /* Custom Icons */, + 7549BE552DEAECA5004F0BAF /* Tx Metadata */, 2A1A60B92674083B003DAC65 /* Syncing Views */, 2A913E7223A2EAEF006A2A59 /* Protocols */, 2A2CD6E522F46D1A008C7BC9 /* Models */, @@ -5190,6 +5196,7 @@ 4709C316287EA22600B4BD48 /* DAO */, 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */, 7549BE592DEAF50D004F0BAF /* IconBitmap.swift */, + 75050CD02DF2FCBF00F586D6 /* ServiceName.swift */, ); path = "Tx Metadata"; sourceTree = ""; @@ -6112,12 +6119,13 @@ path = DashSpend; sourceTree = ""; }; - 7549BE552DEAECA5004F0BAF /* Custom Icons */ = { + 7549BE552DEAECA5004F0BAF /* Tx Metadata */ = { isa = PBXGroup; children = ( + 75050CCD2DF2CB0E00F586D6 /* GiftCardMetadataProvider.swift */, 7549BE562DEAECB0004F0BAF /* CustomIconMetadataProvider.swift */, ); - path = "Custom Icons"; + path = "Tx Metadata"; sourceTree = ""; }; 754C27C62CC3C0B900BA7B9F /* Usernames */ = { @@ -8785,6 +8793,7 @@ C956AF2D2A5CEA1F002FAB75 /* SheetViewController.swift in Sources */, C9C1335A2A561FFA00B66651 /* PasteboardContentView.swift in Sources */, 2A7A7BD02348A34800451078 /* DWSecurityMenuModel.m in Sources */, + 75050CCF2DF2CB0F00F586D6 /* GiftCardMetadataProvider.swift in Sources */, C909615929F29C9200002D82 /* KeysOverviewCell.swift in Sources */, 2ACCD86E2319609400A96B62 /* UIViewController+DWShareReceiveInfo.m in Sources */, 114D16B829828BCC009A124C /* OnlineAccountConfirmationController.swift in Sources */, @@ -8897,6 +8906,7 @@ 47AF18082907B7880025803E /* BaseAmountModel.swift in Sources */, 4709C312287E78BD00B4BD48 /* SeedDB.swift in Sources */, 2ACCD8F0231ECDE000A96B62 /* DWPinField.m in Sources */, + 75050CD22DF2FCC200F586D6 /* ServiceName.swift in Sources */, 2AD1CEA222DFC80900C99324 /* DWCenteredScrollView.m in Sources */, 7549BE5D2DEAF55B004F0BAF /* IconBitmapDAO.swift in Sources */, 2A9172C425233DC50024B4C5 /* DWPhraseRepairViewController.m in Sources */, @@ -9283,6 +9293,7 @@ C943B58B2A40ED6F00AF23C5 /* DWInputUsernameViewController.m in Sources */, C9D2C6E62A320AA000D15901 /* CrowdNodeEndpoint.swift in Sources */, C9D2C6E72A320AA000D15901 /* ServiceEntryPointModel.swift in Sources */, + 75050CD12DF2FCC200F586D6 /* ServiceName.swift in Sources */, C943B4B72A40A54600AF23C5 /* DWBaseContactsModel.m in Sources */, C9D2C6E82A320AA000D15901 /* ExploreDash.swift in Sources */, C9D2C6EB2A320AA000D15901 /* UpholdAmountModel.swift in Sources */, @@ -9463,6 +9474,7 @@ C9D2C7602A320AA000D15901 /* DWAppGroupOptions.m in Sources */, C943B5352A40A54600AF23C5 /* DWNotificationsViewController.m in Sources */, C9D2C7612A320AA000D15901 /* DWTransactionListDataItemObject.m in Sources */, + 75050CCE2DF2CB0F00F586D6 /* GiftCardMetadataProvider.swift in Sources */, C943B3362A408CED00AF23C5 /* DWEditProfileBaseCell.m in Sources */, C930784A2A6AD52400906E4B /* UpholdConfirmTransferModel.swift in Sources */, C9D2C7622A320AA000D15901 /* DWUpholdAPIProvider.m in Sources */, diff --git a/DashWallet/Sources/Models/Explore Dash/Model/DAO/GiftCardsDAO.swift b/DashWallet/Sources/Models/Explore Dash/Model/DAO/GiftCardsDAO.swift index 9f1ac080b..2b4277f77 100644 --- a/DashWallet/Sources/Models/Explore Dash/Model/DAO/GiftCardsDAO.swift +++ b/DashWallet/Sources/Models/Explore Dash/Model/DAO/GiftCardsDAO.swift @@ -25,6 +25,7 @@ protocol GiftCardsDAO { func create(dto: GiftCard) async func get(byTxId txId: Data) async -> GiftCard? func observeCard(byTxId txId: Data) -> AnyPublisher + func observeAll() -> AnyPublisher<[GiftCard], Never> func update(dto: GiftCard) async func updateCardDetails(txId: Data, number: String, pin: String?) async func updateBarcode(txId: Data, value: String, format: String) async @@ -38,9 +39,17 @@ class GiftCardsDAOImpl: NSObject, GiftCardsDAO { private var db: Connection { DatabaseConnection.shared.db } private var cache: [String: GiftCard] = [:] private var subjects: [String: CurrentValueSubject] = [:] + private var allCardsSubject = CurrentValueSubject<[GiftCard], Never>([]) static let shared = GiftCardsDAOImpl() + override init() { + super.init() + Task { + await loadAllCards() + } + } + func create(dto: GiftCard) async { do { let insert = GiftCard.table.insert(or: .replace, @@ -57,6 +66,7 @@ class GiftCardsDAOImpl: NSObject, GiftCardsDAO { let key = dto.txId.hexEncodedString() self.cache[key] = dto self.subjects[key]?.send(dto) + updateAllCardsSubject() } catch { print(error) } @@ -92,6 +102,10 @@ class GiftCardsDAOImpl: NSObject, GiftCardsDAO { return subjects[key]!.eraseToAnyPublisher() } + func observeAll() -> AnyPublisher<[GiftCard], Never> { + return allCardsSubject.eraseToAnyPublisher() + } + func update(dto: GiftCard) async { await create(dto: dto) } @@ -120,6 +134,7 @@ class GiftCardsDAOImpl: NSObject, GiftCardsDAO { let key = txId.hexEncodedString() cache[key] = updatedCard subjects[key]?.send(updatedCard) + updateAllCardsSubject() } } catch { print(error) @@ -149,6 +164,7 @@ class GiftCardsDAOImpl: NSObject, GiftCardsDAO { let key = txId.hexEncodedString() cache[key] = updatedCard subjects[key]?.send(updatedCard) + updateAllCardsSubject() } } catch { print(error) @@ -163,6 +179,7 @@ class GiftCardsDAOImpl: NSObject, GiftCardsDAO { do { let deleteQuery = GiftCard.table.filter(GiftCard.txId == txId).delete() try await execute(deleteQuery) + updateAllCardsSubject() } catch { print(error) } @@ -177,6 +194,21 @@ class GiftCardsDAOImpl: NSObject, GiftCardsDAO { return [] } + + private func loadAllCards() async { + let cards = await all() + cache.removeAll() + for card in cards { + let key = card.txId.hexEncodedString() + cache[key] = card + } + updateAllCardsSubject() + } + + private func updateAllCardsSubject() { + let allCards = Array(cache.values) + allCardsSubject.send(allCards) + } } // MARK: - async / await @@ -247,4 +279,4 @@ extension GiftCardsDAOImpl { } } } -} \ No newline at end of file +} diff --git a/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift b/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift index c8c3e3afb..bb42572cf 100644 --- a/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift +++ b/DashWallet/Sources/Models/Tx Metadata/DAO/TransactionMetadataDAO.swift @@ -119,6 +119,22 @@ class TransactionMetadataDAOImpl: NSObject, TransactionMetadataDAO, ObservableOb return nil } + + func getCustomIcons() -> [TransactionMetadata] { + let customIconsQuery = TransactionMetadata.table.filter(TransactionMetadata.customIconId != nil) + var userInfos: [TransactionMetadata] = [] + + do { + for txInfo in try db.prepare(customIconsQuery) { + let userInfo = TransactionMetadata(row: txInfo) + userInfos.append(userInfo) + } + } catch { + print(error) + } + + return userInfos + } private func cachedValue(by key: Data) -> TransactionMetadata? { var v: TransactionMetadata? diff --git a/DashWallet/Sources/Models/Tx Metadata/ServiceName.swift b/DashWallet/Sources/Models/Tx Metadata/ServiceName.swift new file mode 100644 index 000000000..197c46c54 --- /dev/null +++ b/DashWallet/Sources/Models/Tx Metadata/ServiceName.swift @@ -0,0 +1,28 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import Foundation + +enum ServiceName: String, CaseIterable { + case crowdNode = "crowdnode" + case uphold = "uphold" + case coinbase = "coinbase" + case ctxSpend = "ctxspend" + case unknown = "unknown" +} + diff --git a/DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m b/DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m index 15830fe78..a7f171e16 100644 --- a/DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m +++ b/DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m @@ -47,7 +47,6 @@ + (NSString *)transactionURLFormat { + (NSString *)logoutURLString { return @"https://uphold.com/"; - } @end diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift index b37a2a9d2..7bef97bed 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift @@ -26,6 +26,7 @@ class DashSpendPayViewModel: NSObject, ObservableObject { private let fiatFormatter = NumberFormatter.fiatFormatter(currencyCode: defaultCurrency) private let ctxSpendService = CTXSpendService.shared private let customIconProvider = CustomIconMetadataProvider.shared + private let txMetadataDao = TransactionMetadataDAOImpl.shared private let sendCoinsService = SendCoinsService() private var merchantId: String = "" @@ -148,11 +149,11 @@ class DashSpendPayViewModel: NSObject, ObservableObject { // Payment successful - save gift card information DSLogger.log("Payment transaction completed: \(transaction.txHashHexString)") - // TODO: - // transactionMetadata.markGiftCardTransaction(transaction.txId, giftCardMerchant.logoLocation) + markGiftCardTransaction(txId: transaction.txHashData) customIconProvider.updateIcon(txId: transaction.txHashData, iconUrl: merchantIconUrl) saveGiftCardDummy(txHashData: transaction.txHashData, giftCardId: response.paymentId) + return transaction.txHashData } @@ -269,4 +270,11 @@ class DashSpendPayViewModel: NSObject, ObservableObject { await GiftCardsDAOImpl.shared.create(dto: giftCard) } } + + private func markGiftCardTransaction(txId: Data) { + var txMetadata = TransactionMetadata(txHash: txId) + txMetadata.taxCategory = TxMetadataTaxCategory.expense + txMetadata.service = ServiceName.ctxSpend.rawValue + txMetadataDao.update(dto: txMetadata) + } } diff --git a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift b/DashWallet/Sources/UI/Home/Tx Metadata/CustomIconMetadataProvider.swift similarity index 62% rename from DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift rename to DashWallet/Sources/UI/Home/Tx Metadata/CustomIconMetadataProvider.swift index 2dc47a311..c4d31aa33 100644 --- a/DashWallet/Sources/UI/Home/Custom Icons/CustomIconMetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Tx Metadata/CustomIconMetadataProvider.swift @@ -26,39 +26,80 @@ class CustomIconMetadataProvider: MetadataProvider { private var cancellableBag = Set() private let iconBitmapDao = IconBitmapDAOImpl.shared private let metadataDao = TransactionMetadataDAOImpl.shared - var availableMetadata: [Data : TxRowMetadata] = [:] + var availableMetadata: [Data: TxRowMetadata] = [:] let metadataUpdated = PassthroughSubject() init() { - loadMetadata() + Task { + await loadMetadata() + } - iconBitmapDao.observeBitmaps() + self.metadataDao.$lastChange .receive(on: DispatchQueue.main) - .sink { [weak self] bitmaps in - guard let self = self else { return } - - // Update metadata with new icons - for (iconId, iconBitmap) in bitmaps { - if let image = UIImage(data: iconBitmap.imageData) { - // Find transactions that use this icon - for (txHash, metadata) in availableMetadata { // TODO: create new metatada if needed - if let customIconId = metadata.customIconId, customIconId == iconId { - var updatedMetadata = metadata - updatedMetadata.icon = image - availableMetadata[txHash] = updatedMetadata - metadataUpdated.send(txHash) - } - } + .sink { [weak self] change in + guard let self = self, let change = change else { return } + + switch change { + case .created(let metadata), .updated(let metadata, _): + Task { + await self.onMetadataUpdated(metadata: metadata) + } + + case .deleted(let metadata): + availableMetadata.removeValue(forKey: metadata.txHash) + metadataUpdated.send(metadata.txHash) + + case .deletedAll: + for metadata in availableMetadata { + metadataUpdated.send(metadata.key) } + availableMetadata = [:] } } .store(in: &cancellableBag) } - private func loadMetadata() { - // TODO: Load existing metadata if needed - // This would typically load from transaction metadata that references icon IDs + private func loadMetadata() async { + let customIcons = metadataDao.getCustomIcons() + + for iconMetadata in customIcons { + guard let iconId = iconMetadata.customIconId else { continue } + let bitmap = await iconBitmapDao.getBitmap(id: iconId) + guard let data = bitmap?.imageData, let icon = UIImage(data: data) else { continue } + var txRowMetadata = availableMetadata[iconMetadata.txHash] + + if txRowMetadata != nil { + txRowMetadata!.iconId = iconMetadata.customIconId + txRowMetadata!.icon = icon + } else { + txRowMetadata = TxRowMetadata( + iconId: iconMetadata.customIconId, + icon: icon + ) + } + + availableMetadata[iconMetadata.txHash] = txRowMetadata + } + } + + private func onMetadataUpdated(metadata: TransactionMetadata) async { + guard let iconId = metadata.customIconId else { return } + let bitmap = await iconBitmapDao.getBitmap(id: iconId) + guard let data = bitmap?.imageData, let icon = UIImage(data: data) else { return } + var txRowMetadata = availableMetadata[metadata.txHash] + + if txRowMetadata != nil { + txRowMetadata!.details = metadata.memo + } else { + txRowMetadata = TxRowMetadata( + title: nil, + details: metadata.memo + ) + } + + availableMetadata[metadata.txHash] = txRowMetadata + metadataUpdated.send(metadata.txHash) } func updateIcon(txId: Data, iconUrl: String) { @@ -100,7 +141,7 @@ class CustomIconMetadataProvider: MetadataProvider { metadataDao.update(dto: metadata) var txRowMetadata = availableMetadata[txId] ?? TxRowMetadata(title: nil, details: nil) - txRowMetadata.customIconId = hashData + txRowMetadata.iconId = hashData txRowMetadata.icon = resizedImage availableMetadata[txId] = txRowMetadata diff --git a/DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift b/DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift new file mode 100644 index 000000000..0deaefb61 --- /dev/null +++ b/DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift @@ -0,0 +1,100 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import UIKit +import CryptoKit + +class GiftCardMetadataProvider: MetadataProvider { + static let shared = GiftCardMetadataProvider() + + private var cancellableBag = Set() + private let iconBitmapDao = IconBitmapDAOImpl.shared + private let giftCardDao = GiftCardsDAOImpl.shared + private let metadataDao = TransactionMetadataDAOImpl.shared + + var availableMetadata: [Data: TxRowMetadata] = [:] + let metadataUpdated = PassthroughSubject() + + init() { + Task { + await loadMetadata() + } + + self.metadataDao.$lastChange + .receive(on: DispatchQueue.main) + .sink { [weak self] change in + guard let self = self, let change = change else { return } + + switch change { + case .created(let metadata), .updated(let metadata, _): + Task { + await self.onMetadataUpdated(metadata: metadata) + } + + case .deleted(let metadata): + availableMetadata.removeValue(forKey: metadata.txHash) + metadataUpdated.send(metadata.txHash) + + case .deletedAll: + for metadata in availableMetadata { + metadataUpdated.send(metadata.key) + } + availableMetadata = [:] + } + } + .store(in: &cancellableBag) + } + + private func loadMetadata() async { + let giftCards = await giftCardDao.all() + + for giftCard in giftCards { + var txRowMetadata = availableMetadata[giftCard.txId] + let title = String.localizedStringWithFormat(NSLocalizedString("Gift card · %@", comment: "DashSpend"), giftCard.merchantName) + + if txRowMetadata != nil { + txRowMetadata!.title = title + txRowMetadata!.secondaryIcon = .custom("image.explore.dash.wts.payment.gift-card") + } else { + txRowMetadata = TxRowMetadata( + title: title, + secondaryIcon: .custom("image.explore.dash.wts.payment.gift-card") + ) + } + + availableMetadata[giftCard.txId] = txRowMetadata + } + } + + private func onMetadataUpdated(metadata: TransactionMetadata) async { + guard let service = metadata.service, service == ServiceName.ctxSpend.rawValue else { return } + guard let giftCard = await giftCardDao.get(byTxId: metadata.txHash) else { return } + let title = String.localizedStringWithFormat(NSLocalizedString("Gift card · %@", comment: "DashSpend"), giftCard.merchantName) + var txRowMetadata = availableMetadata[metadata.txHash] + + if txRowMetadata != nil { + txRowMetadata!.title = title + } else { + txRowMetadata = TxRowMetadata(title: title) + } + + availableMetadata[metadata.txHash] = txRowMetadata + metadataUpdated.send(metadata.txHash) + } +} diff --git a/DashWallet/Sources/UI/Home/Views/HomeView.swift b/DashWallet/Sources/UI/Home/Views/HomeView.swift index c9c6fe9d1..dc47fcb39 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeView.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeView.swift @@ -418,7 +418,8 @@ struct HomeViewContent: View { title: metadata?.title ?? txItem.stateTitle, subtitle: txItem.shortTimeString, details: metadata?.details?.isEmpty == false ? metadata?.details : nil, - icon: .custom(txItem.iconName), + icon: metadata?.icon == nil ? .custom(txItem.iconName) : .image(metadata!.icon!, effect: .rounded), + secondaryIcon: metadata?.icon == nil ? nil : metadata?.secondaryIcon == nil ? .custom(txItem.iconName) : metadata?.secondaryIcon, dashAmount: txItem.signedDashAmount, overrideFiatAmount: txItem.fiatAmount ) { diff --git a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift index 8fd40adb9..e6bfecc69 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift @@ -384,6 +384,18 @@ class HomeViewModel: ObservableObject { if finalMetadata?.details == nil { finalMetadata?.details = metadata.details } + + if finalMetadata?.icon == nil { + finalMetadata?.icon = metadata.icon + } + + if finalMetadata?.iconId == nil { + finalMetadata?.iconId = metadata.iconId + } + + if finalMetadata?.secondaryIcon == nil { + finalMetadata?.secondaryIcon = metadata.secondaryIcon + } } } } @@ -450,21 +462,23 @@ extension HomeViewModel { extension HomeViewModel { private func setupMetadataProviders() { - let customIconProvider = CustomIconMetadataProvider.shared - // TODO: update tx icons -// privateMemoProvider.metadataUpdated -// .receive(on: self.queue) -// .sink { [weak self] txHash in -// guard let self = self else { return } -// -// let wallet = DWEnvironment.sharedInstance().currentWallet -// if let transaction = wallet.transaction(forHash: txHash.withUnsafeBytes { $0.load(as: UInt256.self) }) { -// self.onTransactionStatusChanged(tx: transaction) -// } -// } -// .store(in: &cancellableBag) -// - self.metadataProviders = [customIconProvider] + let giftCardMetadata = GiftCardMetadataProvider.shared + let customIconMetadata = CustomIconMetadataProvider.shared + self.metadataProviders = [giftCardMetadata, customIconMetadata] + + for provider in self.metadataProviders { + provider.metadataUpdated + .receive(on: self.queue) + .sink { [weak self] txHash in + guard let self = self else { return } + + let wallet = DWEnvironment.sharedInstance().currentWallet + if let transaction = wallet.transaction(forHash: txHash.withUnsafeBytes { $0.load(as: UInt256.self) }) { + self.onTransactionStatusChanged(tx: transaction) + } + } + .store(in: &cancellableBag) + } } } diff --git a/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift b/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift index 7497e9aa3..7de7b81f7 100644 --- a/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Views/MetadataProvider.swift @@ -16,21 +16,24 @@ // import UIKit +import Combine struct TxRowMetadata: Equatable { var title: String? var details: String? - var customIconId: Data? + var iconId: Data? var icon: UIImage? + var secondaryIcon: IconName? static func == (lhs: TxRowMetadata, rhs: TxRowMetadata) -> Bool { return lhs.title == rhs.title && lhs.details == rhs.details && - lhs.customIconId == rhs.customIconId - // Note: UIImage is not Equatable, so we don't compare icon directly + lhs.iconId == rhs.iconId && + lhs.secondaryIcon == rhs.secondaryIcon } } protocol MetadataProvider { var availableMetadata: [Data: TxRowMetadata] { get } + var metadataUpdated: PassthroughSubject { get } } diff --git a/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ModalDialog.swift b/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ModalDialog.swift index 6aecf815d..fb388ce16 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ModalDialog.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ModalDialog.swift @@ -62,6 +62,13 @@ struct ModalDialog: View { .clipShape(Circle()) .padding(.top, 12) .padding(.bottom, 16) + case .image(let image, let maxHeight): + Image(uiImage: image) + .resizable() + .scaledToFit() + .frame(width: 48, height: 48) + .padding(.top, 12) + .padding(.bottom, 16) } } diff --git a/DashWallet/Sources/UI/SwiftUI Components/Icon.swift b/DashWallet/Sources/UI/SwiftUI Components/Icon.swift index 02701cb05..e0fd51786 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/Icon.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/Icon.swift @@ -17,9 +17,28 @@ import SwiftUI -enum IconName { +enum IconName: Equatable { case system(_ name: String) case custom(_ name: String, maxHeight: CGFloat? = nil) + case image(_ uiImage: UIImage, effect: ImageEffect = .none) + + static func == (lhs: IconName, rhs: IconName) -> Bool { + switch (lhs, rhs) { + case (.system(let lhsName), .system(let rhsName)): + return lhsName == rhsName + case (.custom(let lhsName, let lhsHeight), .custom(let rhsName, let rhsHeight)): + return lhsName == rhsName && lhsHeight == rhsHeight + case (.image(_, let lhsEffect), .image(_, let rhsEffect)): + return lhsEffect == rhsEffect + default: + return false + } + } +} + +enum ImageEffect: Equatable { + case none + case rounded } struct Icon: View { @@ -41,6 +60,17 @@ struct Icon: View { .resizable() .scaledToFit() .frame(maxHeight: height) + case .image(let uiImage, let effect): + if effect == .rounded { + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + .clipShape(Circle()) + } else { + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + } } } } diff --git a/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift b/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift index c225b3fe3..dd7299d17 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift @@ -113,7 +113,7 @@ struct MenuItem: View { if let icon = icon { ZStack(alignment: .leading) { Icon(name: icon) - .frame(width: 28, height: 28) + .frame(width: 30, height: 30) .padding(0) if let secondaryIcon = secondaryIcon { @@ -123,7 +123,7 @@ struct MenuItem: View { Spacer() Icon(name: secondaryIcon) .padding(2) - .frame(width: 18, height: 18) + .frame(width: 20, height: 20) .background(Color.secondaryBackground) .clipShape(.circle) .offset(x: 2, y: 2) diff --git a/DashWallet/ar.lproj/Localizable.strings b/DashWallet/ar.lproj/Localizable.strings index 96b70f2a6..74d069e20 100644 --- a/DashWallet/ar.lproj/Localizable.strings +++ b/DashWallet/ar.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "انتقل إلى موقع CrowdNode"; diff --git a/DashWallet/bg.lproj/Localizable.strings b/DashWallet/bg.lproj/Localizable.strings index 53eacff5c..65d271ae3 100644 --- a/DashWallet/bg.lproj/Localizable.strings +++ b/DashWallet/bg.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/ca.lproj/Localizable.strings b/DashWallet/ca.lproj/Localizable.strings index 2c12d3dac..877e61bf6 100644 --- a/DashWallet/ca.lproj/Localizable.strings +++ b/DashWallet/ca.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/cs.lproj/Localizable.strings b/DashWallet/cs.lproj/Localizable.strings index ad6b50fdf..db3ddddb4 100644 --- a/DashWallet/cs.lproj/Localizable.strings +++ b/DashWallet/cs.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/da.lproj/Localizable.strings b/DashWallet/da.lproj/Localizable.strings index 59036f0ce..6a5cb1c0e 100644 --- a/DashWallet/da.lproj/Localizable.strings +++ b/DashWallet/da.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/de.lproj/Localizable.strings b/DashWallet/de.lproj/Localizable.strings index 1d1fd761f..cf498cea6 100644 --- a/DashWallet/de.lproj/Localizable.strings +++ b/DashWallet/de.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Gehe zur CrowdNode Webseite"; diff --git a/DashWallet/el.lproj/Localizable.strings b/DashWallet/el.lproj/Localizable.strings index 4a5e81d12..64cef007d 100644 --- a/DashWallet/el.lproj/Localizable.strings +++ b/DashWallet/el.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Μεταβείτε στον ιστότοπο του CrowdNode"; diff --git a/DashWallet/en.lproj/Localizable.strings b/DashWallet/en.lproj/Localizable.strings index 28060be0a..da6d43092 100644 --- a/DashWallet/en.lproj/Localizable.strings +++ b/DashWallet/en.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/eo.lproj/Localizable.strings b/DashWallet/eo.lproj/Localizable.strings index d6c1d34c9..0787ff50e 100644 --- a/DashWallet/eo.lproj/Localizable.strings +++ b/DashWallet/eo.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/es.lproj/Localizable.strings b/DashWallet/es.lproj/Localizable.strings index cae1f3284..ff90a4890 100644 --- a/DashWallet/es.lproj/Localizable.strings +++ b/DashWallet/es.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Ir al sitio web de CrowdNode"; diff --git a/DashWallet/et.lproj/Localizable.strings b/DashWallet/et.lproj/Localizable.strings index 079d092a1..af2b4f15a 100644 --- a/DashWallet/et.lproj/Localizable.strings +++ b/DashWallet/et.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/fa.lproj/Localizable.strings b/DashWallet/fa.lproj/Localizable.strings index 71625ee63..775a48c10 100644 --- a/DashWallet/fa.lproj/Localizable.strings +++ b/DashWallet/fa.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "رفتن به وب‌سایت کراودنود"; diff --git a/DashWallet/fi.lproj/Localizable.strings b/DashWallet/fi.lproj/Localizable.strings index a5cc50593..21edc6385 100644 --- a/DashWallet/fi.lproj/Localizable.strings +++ b/DashWallet/fi.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/fil.lproj/Localizable.strings b/DashWallet/fil.lproj/Localizable.strings index 2c729519f..53e10eb94 100644 --- a/DashWallet/fil.lproj/Localizable.strings +++ b/DashWallet/fil.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Pumunta sa website ng CrowdNode"; diff --git a/DashWallet/fr.lproj/Localizable.strings b/DashWallet/fr.lproj/Localizable.strings index f7417734a..6098a134e 100644 --- a/DashWallet/fr.lproj/Localizable.strings +++ b/DashWallet/fr.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Aller sur le site CrowdNode"; diff --git a/DashWallet/hr.lproj/Localizable.strings b/DashWallet/hr.lproj/Localizable.strings index 3ac2444f7..60cbfc6ca 100644 --- a/DashWallet/hr.lproj/Localizable.strings +++ b/DashWallet/hr.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/hu.lproj/Localizable.strings b/DashWallet/hu.lproj/Localizable.strings index 99640ec8f..ce9655e14 100644 --- a/DashWallet/hu.lproj/Localizable.strings +++ b/DashWallet/hu.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/id.lproj/Localizable.strings b/DashWallet/id.lproj/Localizable.strings index 7ae163407..c3337ed4c 100644 --- a/DashWallet/id.lproj/Localizable.strings +++ b/DashWallet/id.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Buka situs web CrowdNode"; diff --git a/DashWallet/it.lproj/Localizable.strings b/DashWallet/it.lproj/Localizable.strings index 46531abc6..a2d68d356 100644 --- a/DashWallet/it.lproj/Localizable.strings +++ b/DashWallet/it.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Vai al sito Web CrowdNode"; diff --git a/DashWallet/ja.lproj/Localizable.strings b/DashWallet/ja.lproj/Localizable.strings index e1902b185..abbb85331 100644 --- a/DashWallet/ja.lproj/Localizable.strings +++ b/DashWallet/ja.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "CrowdNodeのウェブサイトへ移動"; diff --git a/DashWallet/ko.lproj/Localizable.strings b/DashWallet/ko.lproj/Localizable.strings index eaaabb170..95d2e4433 100644 --- a/DashWallet/ko.lproj/Localizable.strings +++ b/DashWallet/ko.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "크라우드노드 웹사이트 방문하기"; diff --git a/DashWallet/mk.lproj/Localizable.strings b/DashWallet/mk.lproj/Localizable.strings index 598adccad..74f04caab 100644 --- a/DashWallet/mk.lproj/Localizable.strings +++ b/DashWallet/mk.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/ms.lproj/Localizable.strings b/DashWallet/ms.lproj/Localizable.strings index 51e500826..29599f786 100644 --- a/DashWallet/ms.lproj/Localizable.strings +++ b/DashWallet/ms.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/nb.lproj/Localizable.strings b/DashWallet/nb.lproj/Localizable.strings index ae8c6ee5b..86fde1838 100644 --- a/DashWallet/nb.lproj/Localizable.strings +++ b/DashWallet/nb.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/nl.lproj/Localizable.strings b/DashWallet/nl.lproj/Localizable.strings index 6ce328fda..fd9593600 100644 --- a/DashWallet/nl.lproj/Localizable.strings +++ b/DashWallet/nl.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Ga naar de website van CrowdNode"; diff --git a/DashWallet/pl.lproj/Localizable.strings b/DashWallet/pl.lproj/Localizable.strings index e098bcb31..8b5fd60fd 100644 --- a/DashWallet/pl.lproj/Localizable.strings +++ b/DashWallet/pl.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Przejdź do strony CrowdNode"; diff --git a/DashWallet/pt.lproj/Localizable.strings b/DashWallet/pt.lproj/Localizable.strings index cc8f934ea..1e5c4b0ac 100644 --- a/DashWallet/pt.lproj/Localizable.strings +++ b/DashWallet/pt.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Acesse o site do CrowdNode"; diff --git a/DashWallet/ro.lproj/Localizable.strings b/DashWallet/ro.lproj/Localizable.strings index 5b8f24daa..4030b3293 100644 --- a/DashWallet/ro.lproj/Localizable.strings +++ b/DashWallet/ro.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/ru.lproj/Localizable.strings b/DashWallet/ru.lproj/Localizable.strings index 2b9c5adf0..f75e0bef5 100644 --- a/DashWallet/ru.lproj/Localizable.strings +++ b/DashWallet/ru.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Перейти на сайт CrowdNode"; diff --git a/DashWallet/sk.lproj/Localizable.strings b/DashWallet/sk.lproj/Localizable.strings index 86ea90e5d..c776d5f3a 100644 --- a/DashWallet/sk.lproj/Localizable.strings +++ b/DashWallet/sk.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Prejdite na webovú stránku CrowdNode"; diff --git a/DashWallet/sl.lproj/Localizable.strings b/DashWallet/sl.lproj/Localizable.strings index 1efed565a..2a9abe891 100644 --- a/DashWallet/sl.lproj/Localizable.strings +++ b/DashWallet/sl.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/sl_SI.lproj/Localizable.strings b/DashWallet/sl_SI.lproj/Localizable.strings index b251868ec..71489b04f 100644 --- a/DashWallet/sl_SI.lproj/Localizable.strings +++ b/DashWallet/sl_SI.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/sq.lproj/Localizable.strings b/DashWallet/sq.lproj/Localizable.strings index d2d19265d..a6dc68217 100644 --- a/DashWallet/sq.lproj/Localizable.strings +++ b/DashWallet/sq.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/sr.lproj/Localizable.strings b/DashWallet/sr.lproj/Localizable.strings index 4e7ea71fa..80b0805fe 100644 --- a/DashWallet/sr.lproj/Localizable.strings +++ b/DashWallet/sr.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/sv.lproj/Localizable.strings b/DashWallet/sv.lproj/Localizable.strings index 946088f9e..d2361b224 100644 --- a/DashWallet/sv.lproj/Localizable.strings +++ b/DashWallet/sv.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/th.lproj/Localizable.strings b/DashWallet/th.lproj/Localizable.strings index 6db9a898d..c7e1751d0 100644 --- a/DashWallet/th.lproj/Localizable.strings +++ b/DashWallet/th.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "ไปที่เว็บไซต์ CrowdNode"; diff --git a/DashWallet/tr.lproj/Localizable.strings b/DashWallet/tr.lproj/Localizable.strings index 06aa0cd3b..4c032c796 100644 --- a/DashWallet/tr.lproj/Localizable.strings +++ b/DashWallet/tr.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "CrowdNode web sitesine gidin"; diff --git a/DashWallet/uk.lproj/Localizable.strings b/DashWallet/uk.lproj/Localizable.strings index d77759f97..b23fe83f6 100644 --- a/DashWallet/uk.lproj/Localizable.strings +++ b/DashWallet/uk.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Перейти на веб-сайт CrowdNode"; diff --git a/DashWallet/vi.lproj/Localizable.strings b/DashWallet/vi.lproj/Localizable.strings index 3a863ccf3..b39e4b7fd 100644 --- a/DashWallet/vi.lproj/Localizable.strings +++ b/DashWallet/vi.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/zh-Hans.lproj/Localizable.strings b/DashWallet/zh-Hans.lproj/Localizable.strings index f57315214..f22f97dae 100644 --- a/DashWallet/zh-Hans.lproj/Localizable.strings +++ b/DashWallet/zh-Hans.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/zh-Hant-TW.lproj/Localizable.strings b/DashWallet/zh-Hant-TW.lproj/Localizable.strings index 4e86bd2cb..851bfb46a 100644 --- a/DashWallet/zh-Hant-TW.lproj/Localizable.strings +++ b/DashWallet/zh-Hant-TW.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "Go to CrowdNode website"; diff --git a/DashWallet/zh.lproj/Localizable.strings b/DashWallet/zh.lproj/Localizable.strings index 0c8d79def..68cfb2d72 100644 --- a/DashWallet/zh.lproj/Localizable.strings +++ b/DashWallet/zh.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "访问CrowdNode网站"; diff --git a/DashWallet/zh_TW.lproj/Localizable.strings b/DashWallet/zh_TW.lproj/Localizable.strings index 9cf98180e..8ea2e8065 100644 --- a/DashWallet/zh_TW.lproj/Localizable.strings +++ b/DashWallet/zh_TW.lproj/Localizable.strings @@ -1051,6 +1051,9 @@ /* DashSpend confirmation */ "Gift card total" = "Gift card total"; +/* DashSpend */ +"Gift card · %@" = "Gift card · %@"; + /* No comment provided by engineer. */ "Go to CrowdNode website" = "訪問 CrowdNode 網站"; From dce36f4c0ffd2b8d875076238d84ffeecb473b64 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Sun, 8 Jun 2025 22:18:27 +0700 Subject: [PATCH 07/13] feat: new icons & cleanup --- .../Contents.json | 6 +- .../GiftCard.png | Bin 0 -> 678 bytes .../GiftCard@2x.png | Bin 0 -> 1235 bytes .../GiftCard@3x.png | Bin 0 -> 1690 bytes .../icon_pay_small.png | Bin 247 -> 0 bytes .../icon_pay_small@2x.png | Bin 384 -> 0 bytes .../icon_pay_small@3x.png | Bin 576 -> 0 bytes .../icon_receive_small.imageset/Contents.json | 23 ---- .../icon_receive_small.png | Bin 278 -> 0 bytes .../icon_receive_small@2x.png | Bin 422 -> 0 bytes .../icon_receive_small@3x.png | Bin 595 -> 0 bytes .../icon_selector.imageset/icon_dropDown.png | Bin 413 -> 0 bytes .../icon_dropDown@2x.png | Bin 668 -> 0 bytes .../icon_dropDown@3x.png | Bin 911 -> 0 bytes .../icon_tx_received.imageset/Contents.json | 59 --------- .../icon_tx_received.png | Bin 2125 -> 0 bytes .../icon_tx_received@2x.png | Bin 5058 -> 0 bytes .../icon_tx_received@3x.png | Bin 7995 -> 0 bytes .../received_txn.png | Bin 1568 -> 0 bytes .../received_txn@2x.png | Bin 3139 -> 0 bytes .../received_txn@3x.png | Bin 5060 -> 0 bytes .../icon_tx_sent.imageset/Contents.json | 59 --------- .../icon_tx_sent.imageset/icon_tx_sent.png | Bin 2083 -> 0 bytes .../icon_tx_sent.imageset/icon_tx_sent@2x.png | Bin 4946 -> 0 bytes .../icon_tx_sent.imageset/icon_tx_sent@3x.png | Bin 7903 -> 0 bytes .../icon_tx_sent.imageset/sent_txn.png | Bin 1618 -> 0 bytes .../icon_tx_sent.imageset/sent_txn@2x.png | Bin 3180 -> 0 bytes .../icon_tx_sent.imageset/sent_txn@3x.png | Bin 5057 -> 0 bytes .../Contents.json | 12 +- .../all trans.png | Bin 0 -> 669 bytes .../all trans@2x.png | Bin 0 -> 1109 bytes .../all trans@3x.png | Bin 0 -> 1603 bytes .../Home/TransactionFilterBottomSheet.swift | 103 ++++++++++++++++ .../Home/Views/TransactionFilterDialog.swift | 114 ++++++++++++++++++ 34 files changed, 226 insertions(+), 150 deletions(-) rename DashWallet/Resources/AppAssets.xcassets/{icon_selector.imageset => Explore Dash/image.dashspend.giftcard.imageset}/Contents.json (68%) create mode 100644 DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/GiftCard.png create mode 100644 DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/GiftCard@2x.png create mode 100644 DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/GiftCard@3x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/icon_pay_small.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/icon_pay_small@2x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/icon_pay_small@3x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/Contents.json delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/icon_receive_small.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/icon_receive_small@2x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/icon_receive_small@3x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/icon_dropDown.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/icon_dropDown@2x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/icon_dropDown@3x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/Contents.json delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/icon_tx_received.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/icon_tx_received@2x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/icon_tx_received@3x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn@2x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn@3x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/Contents.json delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/icon_tx_sent.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/icon_tx_sent@2x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/icon_tx_sent@3x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/sent_txn.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/sent_txn@2x.png delete mode 100644 DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/sent_txn@3x.png rename DashWallet/Resources/AppAssets.xcassets/{icon_pay_small.imageset => image.filter.options.imageset}/Contents.json (56%) create mode 100644 DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans.png create mode 100644 DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans@2x.png create mode 100644 DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans@3x.png create mode 100644 DashWallet/Sources/UI/Home/TransactionFilterBottomSheet.swift create mode 100644 DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/Contents.json similarity index 68% rename from DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/Contents.json rename to DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/Contents.json index f2a1c6ce3..35b654f3f 100644 --- a/DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/Contents.json +++ b/DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "icon_dropDown.png", + "filename" : "GiftCard.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "icon_dropDown@2x.png", + "filename" : "GiftCard@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "icon_dropDown@3x.png", + "filename" : "GiftCard@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/GiftCard.png b/DashWallet/Resources/AppAssets.xcassets/Explore Dash/image.dashspend.giftcard.imageset/GiftCard.png new file mode 100644 index 0000000000000000000000000000000000000000..4222cca9d173cf0c54147e2969e7bbb5703da099 GIT binary patch literal 678 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 literal 0 HcmV?d00001 diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/icon_pay_small.png b/DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/icon_pay_small.png deleted file mode 100644 index deb5f43b717b4b9967b76e7ecdb8314af5c3b294..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|HhQ`^hFA!` zo!BUNNP)+-KB77A!OjV#9dWV;tQ@S{t-`CVTI8HEGaj>^V>}}}eZmua4hGfN%lQ&T zdp2#2d~t50pZJ`dH@bLVu`u+Va0olFeaFV_ zEE#XF9@kP~@=98kqM&w8G;VfWgLOi+QTI*l38^glYT=95zPmH~E7PIa!^y`Ne)#)B vU|QPY;`K{Y*Re!cU5}q|X?}SbP0l+XkKeI#I5 diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/icon_pay_small@2x.png b/DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/icon_pay_small@2x.png deleted file mode 100644 index cd862fd6708ff37faeced85dfd6aaba738a7c427..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 384 zcmV-`0e}99P)Px$I!Q!9R9Fe^mP-!8AP_(Y>LILp9rZem-MH}=*FBh@W9VMJK<$8|AwY#`jT=bJ zq!XC;IxR(jE~C0`0e>BaV6Ro+dZX8i9hBoOrxs7ZfD@J{5ij6`^CjX0TH!p2aDgCr zyhNBl6g*Bs(sXZh!G(BO^Q~O7L)2PprjckD@WEv^GpgVP<`@!w0T&$Lu0X}`UkJA!Lz{}TyU~sNZ0~CxM*7324_l`0&Va`ek8`%7}1H3KwogO3zX0TK6sW*A?D5r zPBB12>b***+H0kN8z8*MPgIK?zes!@0TB?6+o=yY)$~dkgAax*h eAa&sH9e4tUNS@}8i`+8+0000AxPx$`bk7VRA>e5n9WMWFc5$zL2*3@ih9}gUG!bNDJa;h=!%G^y?XP>`~%%Rc~I~H z)+S|WH%;4VGPACb!ZO1$eANPYGT8{ehWHh)=pgvVHw&~Tr_U*b^xKeETqEjZr)!3jZe zGM#(h%MR-6c_%hJeE8mhlkKnSXxRaL8ViqkB%%XvmZ%FzEG(Ea2?!RKlsa{QR3^bm zsRM$=C8bUsAeBjQQtE(UaY?CD2S{ZSoRm5sSX@%-)B#eN1SkFb4*UWy*Y&v|qW1g% O0000Px#&`Cr=R5%gsk^K#VFc8Jxp<6hD&f`ZM!vVwrAi9GX$6*MGE>X_D0;UbM!T-Ia zaOL&=?s@>JC>{!~L}UqVlJo}v-3-d-a?-^7LzmZz+YCjML)#kVW9{q7yll2Grlz<5 z3p$5NH3>p9aE0oOMKT-~IUpy%LLfJsn+=6%1ZL~8-mZfILTZaD3W=CmM1o432lNOL zV|&}f=-ufeJNxe2?kW$y5WufKq`#*Fx;!-V_!!_rn15nyrW6NI89HIkA-g%b%?}Sk c9O%2}4Wunw7C4m%EdT%j07*qoM6N<$f|nO?pa1{> diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/icon_receive_small@2x.png b/DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/icon_receive_small@2x.png deleted file mode 100644 index dddb603e7875ef1c04799ee517b72ae57200ffd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmV;X0a^ZuP)Px$U`a$lR9Fe^mOE0zFc5}U4mHrjF?gMT3#FieAwWZ8PtHRwkb~qwojSJgFO3j> z$XY4(424w0UPapP^JcAypxBXbWMk|6S~Oy2kTKUywk!#x3Zx383Zx4BqYC8PCn|O~ zlO6gd@$&ngip{eLbp`V6f=GT6RjWxQ0I$j~lJr6O?i~^tY51!$l60vgmit>3kNNQn zm*+Zd^#XRc2Ot51`(zo-uNFk&%}&*KkSNR5C=yP1&8*qF0(}xh7bi=2;CN{J z6AVcd`@2SkH~|CR6hP#XkabUlNHF1DfiWZk;QazV2>@Ip5RNp%f2IGP5e6#6fwI{n zmB+Q2QrFqpgFa-;dv3nf-EN}xbyxtaT@tbdf>?VFuea)}HrB>|0hff$;kfg|v5_Z$ zm3@`KeDC1Fv6U}?)hr1R0afzGp=-iT QR{#J207*qoM6N<$g62TBO8@`> diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/icon_receive_small@3x.png b/DashWallet/Resources/AppAssets.xcassets/icon_receive_small.imageset/icon_receive_small@3x.png deleted file mode 100644 index e0ec3c73028fc0c65ba7a1222bf17c5e5fd074fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 595 zcmV-Z0<8UsP)Px%4M{{nRA>e5nY~WKFc5$*QC9}$77%~$!8>(A2vHDXD+namc$Ve``V0uE?E*+J zpi)sK_!^lwX;Ry{bJM|I8lQ9L@AE}Uqz2$(^hZf)*={E&jT3?)A$KEEy+G4#@E!0S zAfAAKgop=7PWK%kA0e@T?*Q=t$?3iW;k+6Wrqs|^7odm!k zxR$Vl&kNwJNr@Wp%K|nMkK-M@on_R7$Gx*hNF_1p#7w+cI3x)0)dIMplStqIs1ii@ z)dH?17~u5+HWCPAI+)5gyU({_?5%Zo*275xhVvIDvWmDkHhyR=D^7 zRhe5*C+QnjjjX!16TnI)68PC$I)q^58YbAmFEUE^(FL^ zGy%+Jk*E((7a&W};CTdUOVHu@1gs@^;CTg%CHUa^1+)^p@NNVuCHUdp2`ovtfyZs! h4t!56-WBe)`2)5xBNMA$hX4Qo002ovPDHLkV1mf3|F-}D diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/icon_dropDown.png b/DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/icon_dropDown.png deleted file mode 100644 index 593f00bb63a9a2eafc2fe25966c0227474d474b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(AMxHK?Ar*{oLmmAN8wkkk=G^=VJS!M~G6Kas1$1L9ogb<4wKs*V_jOq+XVtFp=}pD8-EopP43ErT z^gd2qLCU$#rNiyLo)G`Kh&4_VPTl%{syc7q#%`ZOvYUIod<^QjV_fpsUPk51{fUrV z5O2T3*$kp+i*}-sX{=&_T2UOp1 zSWeo%?9pegt>T*uCwa+ixc-3sZJKG@f?_i_^Gd5Tz9#kERU+n=b2b{@w|u5{&FEwj zr*y|V%UR|#<~{Lk@^_0{;Ih8!;>`tjmq?lkd#G+}Kb|Mh>!snf&Ek1X@m*e%JzCkP zKYD8li?5vBsORiEJLsjb>v7e8vfK3~#a_F=5#Gm8dMn_o)4SK{z|dsyboFyt=akR{ E00vy1h5!Hn diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/icon_dropDown@2x.png b/DashWallet/Resources/AppAssets.xcassets/icon_selector.imageset/icon_dropDown@2x.png deleted file mode 100644 index e4fe70f63008eadf1ce603fb7c7f11c495e64670..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 668 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 diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn.png b/DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn.png deleted file mode 100644 index ef8336a18e35b4b242f057f4ac5bb2524d9792e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1568 zcmV+*2H*LKP)Px)+DSw~RA>d=ntN>1RTRg+=hw9zb8290Y+YdyLDWAM;v*5AY*C4V!55Ke!~}^- z40Cf^V&WeIV|+%pArBP;|7bK(6E#BvON>Zj!ZZAA1p?Iq;m*&JGgIO?EqJqYtSEaPTobG(e>3Zw?0sGrjV01a^Z z5R|tEtrfqxZ4SAjP$@!JWEqrN$41}8VYw~6gx{cy9(dGV%n0nkdK$5%Cq>8^;Fd@^ zM&b;m9%aAGPtof?*VexI=kyCfv+Wo7h{Wb{o%1j-(5t6F%>2 z*idmQE18r^)&aZvxeLa&G1wI;dZr*sM_{N2&=NdgrLpV*-&Y|P3b5nvi!n^y`3onO zD|W!h=XkxpA`^!>pi%}-pLv&O%0-z3^Cn+r^Rux}AxoFZV-DCEeu2l_3X}SdvTm8@ z$8}Yvb~w_@j<^P=BJE~M#shXmc&1cOnpJk1x2(pGrPY3?YBI>Hr?5G4?byO}zE%kpu7eF_Z9$9I)XxjWEhA<7IS}{lU`Z~DRl|>O~~NW zuPX}y$CP218VvSc;R43hQki6?_-w`Wx6Vx_sfFyp0tQqF8hj)-GdDk9K4_&1@t~Yk z73ib9>_PQ!fzmM+74alP1q_Hk8PJ{wW%_-r_5ye&K9_UUIsClGT3=gYUk9l^9)onm zj)pi3-9En<@2r@a6G(yk_>MBv1sol5Fd*=)9YB#;wfiqz_FxO zPB_v4+bVn6lRr7+ONd*6%o!Z`21iMN{}t07OqfQy5`ha}*+AwFP5|g7eM2MJMJ3P2 z$KDPn8&RX5i68||7I9aW0^Ohv2@%YydSf)HV-j|Rn}GPMsaa0r+EYWraUB0AhM{4F zB~`w}BJOydGc(ShCbU#<)kWz$NjYIV9{!q-F|N`T+z2&febARs*Zu1`=Zm;s%CsGg_+>lZ&o09#Bd; zLtEL4jk?mb2np?I4L&>8R&y@}18uyH>>FD-Z_`Ad@wj{5%sz9#)ijKv8Fw+{_hv%* zmB-`GC|biSfV{ztd&~i)T2t8&U$$A!f=99crRs1Rm!&|V1G55~vb{rU-O$ePZG01B8{2xRQ+qAR6MlflScm4oH&*1V z1B&*8nk22B@{j`0R?@z#H{dd<=OH!_L)37Q-;V>gBjJ~jN2#rO<=&L%#J2KnUcAb0ZvJidB)eD!g@tIvON&;SW# Sp*}+Z0000; diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn@2x.png b/DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn@2x.png deleted file mode 100644 index c4f734ab3a3a80164b4c36878f06fbb34e409269..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3139 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 diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn@3x.png b/DashWallet/Resources/AppAssets.xcassets/icon_tx_received.imageset/received_txn@3x.png deleted file mode 100644 index 8482d3ee9636535cf975ef83e81de413d0c0ded9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5060 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 QWB>pF07*qoM6N<$f)O`G4FCWD diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/icon_tx_sent@3x.png b/DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/icon_tx_sent@3x.png deleted file mode 100644 index 146043af1fa1eb3a55962ae404790d4147c7283a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7903 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 JPDHLkV1jxUD^UOd diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/sent_txn.png b/DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/sent_txn.png deleted file mode 100644 index dd442d702a555794dc0cc04be4babe30487131a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1618 zcmV-Y2CeytP)Px*3`s;mRA>d=T3JY3R~SBX#~oCNO7)?drfK6kYBgG$N;M%+A5x?ueJJF?hSC@9 zLn)evqR*VT%Gyhrs?K}V3?%ZQxIzI;o2bQ9uqB77Ds6K76SWXi~%0ZnMWjClB^dHncLVgN) zXMB9Tb82ep0qS&pEP80`=;#mz2M5pX@9#H2-|KMtX_b2agq#C_Z_Lln+g0lD6Vn8+ ztgI|)cXzi1z}|({Oh3)q3kv}Hd3t*KExX;mrBJd!RW38lv-S1$c7VG9 znGUK{UiQe*Eh~amRaGZ8Ha2cUVVxYMQQ(=tu&qNh|BhrU zBEaI};^#dc&kz)yjHE1@?HI0euy+xw?P@r?LbygOCRt_#M?PZ>C_)-7EiKInhaqGH zD=RBgwzs!OprkmQf+yXENIrxQXG29YXapM@8{@XNw!SqxIO<&ZNQa8svFnmmE)>+56X<>jolww85wcQY*WYE*@k z!BDj#-KwjriGgUU4(F`7xtXP;q_D28E+Zm&wWg*f zf%`84bi3V`QJKOUv{DW(*qqzj+cOem*4o4n7|F{4$L$yhG80qb6|&<4s8{rqfHN8-){{#g zSBt$}DCGu}Qa!nO=KRbrTsmwbeSIhvIrZ5w;G(4XLC3KE{(i-BN(2Zbb)bePTRMbPh+DQevt9YcE*!HEAYM#mTZtsre@8}+J3zaTo16PHq8Rr7ef>Muc#XhxAOChg6mU}TVkOmdTrl2URTk_d<$I$C z+HX{;FtHFYeJ29Mzro`uteDterLNs;XlVFe1PHVTNH>*gF)Kf?e1?xPzHbA*Axe&> zrlx+12A5f#R^Zn<{t>vz`9*+ZV`H1BZ08L}l_E&j$H&Lr+XQzdmcwaY1*0pb;b>xEJ=wYPG(G>|bUC_SmOi6B859hXO}DAtT5^ z6&4oKEruTuNgJ;-30;bbiK&^HnfW7}KOrN?h3G_%L%_=@|H$i1Ku6_(BR?YoBqllp zK5Fz~$ma37Xi$K=kM!o_7X#TQ zWkpb`4HJ48_p6Ix?0wYJ@y^f(c4*UV#m!%vQgGy$;s{F7;38ZGKEE{~nr|bWlD6rP ze}Limaavm1SGdyruUdVY0CJ-MM>p*+qTGaZ9_0+)ri3C|zkz=UbO=|N@2I)L9B5A$ zfPqRePtRbU(#rc1QZ@jm!Rd75Dbafx4LeG-$F8Dn9^V>=rl+TuP}cSN50r(I6Di)g Q^8f$<07*qoM6N<$f+T+Q!~g&Q diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/sent_txn@2x.png b/DashWallet/Resources/AppAssets.xcassets/icon_tx_sent.imageset/sent_txn@2x.png deleted file mode 100644 index 3fd8d3c487cc0dfc0bb14bdee3392af0f316b4c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3180 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 X8pj-`FN2tK00000NkvXXu0mjfaIa^h diff --git a/DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/Contents.json similarity index 56% rename from DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/Contents.json rename to DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/Contents.json index 78906a146..d07c56191 100644 --- a/DashWallet/Resources/AppAssets.xcassets/icon_pay_small.imageset/Contents.json +++ b/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/Contents.json @@ -1,23 +1,23 @@ { "images" : [ { + "filename" : "all trans.png", "idiom" : "universal", - "filename" : "icon_pay_small.png", "scale" : "1x" }, { + "filename" : "all trans@2x.png", "idiom" : "universal", - "filename" : "icon_pay_small@2x.png", "scale" : "2x" }, { + "filename" : "all trans@3x.png", "idiom" : "universal", - "filename" : "icon_pay_small@3x.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans.png b/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans.png new file mode 100644 index 0000000000000000000000000000000000000000..0f75f9cfd707ec066e91f75bd1db87a61bd8f8c9 GIT binary patch literal 669 zcmV;O0%HA%P)TpiK|Gfv`j* zE*uOJKM~l=j7?&aIO~ATCq<4u>+kc<&g{2@EOd{WfNDT%zM=-rzZgCU1HK|tnf6YDsCVX?bPoET#m(w+ojays^;czVyiM69D!?xHn-qf)L_6_H zZw>od1}8$Zg&{?-ut_H(Jdv-JR=mmqJEJnEZ|~p|kAiSxY!ke@@>=~w(Gbo~Z0*VF zmTop9vnmS0C7gFigv1HOev1&BSb1`8n66jxc(LC+X_R*(3s24qvGyYTVAm}@@VF%l z-&yv!HH+_TjEX%=MRrnWVf*e)53;KS((o*@KwykpOrgyCP_BU600000NkvXXu0mjf DDWov) literal 0 HcmV?d00001 diff --git a/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans@2x.png b/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..223269e0f36049be4bdece09a8258b049716f980 GIT binary patch literal 1109 zcmV-b1giUqP)mH5bO{fDaGoF^2-+kqC~^D*BrO$mba6+QI0dvZ zils<=C?*0Vli175txaGbMJy&uu!Mjq`GX7^VusFgUQgJh_Pw1XpNKinLRP6U$=pZaK(B`U>UusXJX;CLdYA<+1_iWEG2(Rh`Cq3gr)7 zH0s-Q=3+Q#TdTi9Dsr(hVIAHURn(Ei%icl5>GdPl51)}TyBNMQ@MMUQEG$zGx?4zD zW*1SUj&wImb+(Y_GhR1~CNYwYW$Zw^LLALh0Ej|2>xPoii@bh1ScpRN@S<4|s26#% z`tbXZ3eWo(B^ok|7x#*-kA4fOt~Of4gwIN63awRg7>%yxhlgOVX*kv-{{IJodHPWc zcOr&2k8gR$K~O%fWA4L|cho{zsmQ0whXUrD-KTeux_u3Uq*$pT`}Vv31x;U4U9KVK z5r9D}!(@_Uf!b4Un&m^&hhzo;*!uJbgQl?aD^Poy(s)B65q5}^doT|7=3soNT9eoa zsu1HK9dE7y1ZMN|;H?xG(#3(0F0Kw!Lq250t*0zyQ}w3L>9L7%SsRLJwwT`UXrfmn32sE42FY*h>Ge z)@XgK_!FiGtXK9;{fKcV;xM6Ft9uF6kTOf5wSd?6pKL@aQ>YL?U9Pc*D}&r~ARTL^ zjG-e1bvdrjG++&$TFlP%pnwuk{4a>-~wtd|RRfUL3DY6|w18NaVcPb)x z&Vt;K3$#&1ple0Eds7;q>0Q>>-*_SG+csvl zq2K@@^fq4TjU@Sx#7TN6A)W8Z_txC=6JqnyC0R?*=$RA!x%#QzWntkGrmFg-V3UxF zoLz1T2Mq@c^m-K9YV7b0nv+SI#)MSl?5}L1B4UR6y-Oo^E&{k+wrEzHTQv753>W)t bND9I~c57S(p2?b)00000NkvXXu0mjfg=PCO literal 0 HcmV?d00001 diff --git a/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans@3x.png b/DashWallet/Resources/AppAssets.xcassets/image.filter.options.imageset/all trans@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3cba9deaa76e0d845bb5d114b2dfea6a9dc35a14 GIT binary patch literal 1603 zcmV-J2E6%+P)#suPY~Qi_e(ENn0}7@0*BZ`YuJ@WNTqae4zL9$*wO#3qU#Zy_Y%ge$$s z!E7EPId18f(AH;QSax14lW4~nJ<*t)_1hoThxU@;geyI#wyar3^yx*E@NX|!_c8>* zUA0s%jZAyypIwT&#UNh@CS2+HBbc&)@uW=R-S_)p|E_9T+SrQ56sXNB`Tejz;kX?b zKlk+Ff!{W7u&eI;LER>E7n6j@6tH@`()T0%-fZP*oN|nE^c;o;<#`MJ^1S}8YA3go zCK@wi-MiH*@}{?Nwy)2jB$wjPp*Y>gE(3hcCXUqWahWXq-!g zMw;JX?UQ}y=~Ty?0z5_#isjF5V0ZCWB1B_qXH9JWe1^)&D}=(J@e|-uIM?Fr(Wp>C zA$K!_({DdQ5}Ez+PfNs5v_)9gEKFk=Az8ZnE|SX9-RlTV2A4si&7Wi*DgBY+(ztBl z*i99rAp1}MLsHp)`WgzD!4H;JE#VZnDZ_2Z{hwaa@d8Pqacd zF-6mPD1y_G`48Vg+^}f;PgIhQxYVdpJS(gUfI0mVDgH6ud_b-`rOeVMhGr&jQ0|rr z7ZhRS3Y{C2?lk=xv#3FG_E7kD(lb>Lb7l&`r6ydm+xWo-uqfQ{+o}_ zu=V&KL=E3bq!yRYUc+u)U$tcO-c9S85tEDjZj^Mc6L`&J6wOAE$c0nXxlY_Qjn=}8 z$T+H>YiX<*?wb~m{_JbAxT+>|uRvOSG32 z-t5w68jA?r%j%i}A!G%bXuMXJ-QT*du}(obyz{4fRgLa?pm4;$@=M5BNI@`N<8p;# z6BMqZQBpp6rKC~kbCgds3CGd2Z7y)<{5aFxqoJ=nG{6zD{OiBg1jJ#`)}eRc_5E#h z&pMAOr%N;(6jrmd?Jsq=tfm=g`Qbz$(X|xad!55h{V>6s66nRl+8FOx-jtyi`;QGG zv*`7*w{RRiq};=#W0C>&SP0j9^&n6GlGDc%8z_3Xs6+3xn|H~w%`}$XvSLM`(aS|0 z`uA6CGo8FB+r-wOJxK<4^mtDqbAo~e-|KpI;EHhnyFhF|+lA0uv(pX-#I-xV_8thf zea3dZOy5v4Lp^awRIrvU`2BdWnhc+K9Mcrb()^3Y1TSW>DUVZ|$M@pXJ=+PsMUaEN zp%brd9nH-SIOVvYjN$v?Hsl*#IEL5m%jV(8CCQxJinz8xWB9_HU5}FMa(gD;EQH;; zFr3A;lXUCj>NXcI*46CRs!liuUY;(nkr)BlExE?nwk%xDX`{jxa7E?fx Void + + private let filterOptions: [FilterOption] = [ + FilterOption(mode: .all, title: NSLocalizedString("All", comment: ""), icon: "list.bullet"), + FilterOption(mode: .received, title: NSLocalizedString("Received", comment: ""), icon: "arrow.down.circle"), + FilterOption(mode: .sent, title: NSLocalizedString("Sent", comment: ""), icon: "arrow.up.circle"), + FilterOption(mode: .rewards, title: NSLocalizedString("Rewards", comment: ""), icon: "gift.circle") + ] + + var body: some View { + BottomSheet( + title: NSLocalizedString("Filter Transactions", comment: ""), + showBackButton: .constant(false) + ) { + VStack(spacing: 0) { + ForEach(availableFilters) { option in + FilterOptionRow( + option: option, + isSelected: option.mode == selectedFilter + ) { + onFilterSelected(option.mode) + presentationMode.wrappedValue.dismiss() + } + } + } + .padding(.horizontal, 20) + .padding(.vertical, 20) + } + .background(Color.primaryBackground) + } + + private var availableFilters: [FilterOption] { + var filters = filterOptions.filter { $0.mode != .rewards } + + if shouldShowRewards { + let account = DWEnvironment.sharedInstance().currentAccount + if account.hasCoinbaseTransaction { + filters.append(filterOptions.first { $0.mode == .rewards }!) + } + } + + return filters + } +} + +struct FilterOption: Identifiable { + let id = UUID() + let mode: HomeTxDisplayMode + let title: String + let icon: String +} + +struct FilterOptionRow: View { + let option: FilterOption + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 16) { + Image(systemName: option.icon) + .font(.system(size: 20)) + .foregroundColor(.dashBlue) + .frame(width: 24, height: 24) + + Text(option.title) + .font(.body2) + .fontWeight(.medium) + .foregroundColor(.primaryText) + + Spacer() + + if isSelected { + Image(systemName: "checkmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.dashBlue) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(isSelected ? Color.dashBlue.opacity(0.05) : Color.secondaryBackground) + ) + } + .padding(.bottom, 8) + } +} + +#Preview { + TransactionFilterBottomSheet( + selectedFilter: .constant(.all), + shouldShowRewards: true, + onFilterSelected: { _ in } + ) +} \ No newline at end of file diff --git a/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift new file mode 100644 index 000000000..f0420a4a3 --- /dev/null +++ b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift @@ -0,0 +1,114 @@ +import SwiftUI +import UIKit + +struct TransactionFilterDialog: View { + @Environment(\.presentationMode) private var presentationMode + @Binding var selectedFilter: HomeTxDisplayMode + var shouldShowRewards: Bool + var onFilterSelected: (HomeTxDisplayMode) -> Void + + private let filterOptions: [FilterOption] = [ + FilterOption(mode: .all, title: NSLocalizedString("All", comment: ""), icon: .system("line.horizontal.3"), color: .dashBlue), + FilterOption(mode: .sent, title: NSLocalizedString("Sent", comment: ""), icon: .system("arrow.up"), color: .dashBlue), + FilterOption(mode: .received, title: NSLocalizedString("Received", comment: ""), icon: .system("arrow.down"), color: Color.green), + FilterOption(mode: .rewards, title: NSLocalizedString("Gift card", comment: ""), icon: .system("gift"), color: Color.orange) + ] + + var body: some View { + BottomSheet( + title: NSLocalizedString("Filter transactions", comment: ""), + showBackButton: .constant(false) + ) { + VStack(spacing: 24) { + ForEach(availableFilters) { option in + FilterOptionRow( + option: option, + isSelected: option.mode == selectedFilter + ) { + onFilterSelected(option.mode) + presentationMode.wrappedValue.dismiss() + } + } + + Spacer() + } + .padding(.horizontal, 20) + .padding(.top, 32) + } + .background(Color.primaryBackground) + } + + private var availableFilters: [FilterOption] { + var filters = filterOptions.filter { $0.mode != .rewards } + + if shouldShowRewards { + let account = DWEnvironment.sharedInstance().currentAccount + if account.hasCoinbaseTransaction { + filters.append(filterOptions.first { $0.mode == .rewards }!) + } + } + + return filters + } +} + +struct FilterOption: Identifiable { + let id = UUID() + let mode: HomeTxDisplayMode + let title: String + let icon: IconName + let color: Color +} + +struct FilterOptionRow: View { + let option: FilterOption + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 16) { + // Icon circle + ZStack { + Circle() + .fill(option.color) + .frame(width: 40, height: 40) + + Icon(name: option.icon) + .foregroundColor(.white) + .font(.system(size: 18, weight: .medium)) + } + + // Title + Text(option.title) + .font(.system(size: 17, weight: .medium)) + .foregroundColor(.primaryText) + + Spacer() + + // Radio button + ZStack { + Circle() + .stroke(isSelected ? Color.dashBlue : Color.gray400, lineWidth: 2) + .frame(width: 24, height: 24) + + if isSelected { + Circle() + .fill(Color.dashBlue) + .frame(width: 12, height: 12) + } + } + } + .padding(.vertical, 4) + } + .buttonStyle(PlainButtonStyle()) + } +} + +#Preview { + TransactionFilterDialog( + selectedFilter: .constant(.all), + shouldShowRewards: true, + onFilterSelected: { _ in } + ) +} From 1bb1000af14beb772083d0bac0e0456599c86887 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Sun, 8 Jun 2025 22:19:08 +0700 Subject: [PATCH 08/13] fix: concurrency issues in metadata providers --- .../CustomIconMetadataProvider.swift | 88 ++++++++++++------- .../GiftCardMetadataProvider.swift | 70 +++++++++------ 2 files changed, 101 insertions(+), 57 deletions(-) diff --git a/DashWallet/Sources/UI/Home/Tx Metadata/CustomIconMetadataProvider.swift b/DashWallet/Sources/UI/Home/Tx Metadata/CustomIconMetadataProvider.swift index c4d31aa33..6d7fbdd68 100644 --- a/DashWallet/Sources/UI/Home/Tx Metadata/CustomIconMetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Tx Metadata/CustomIconMetadataProvider.swift @@ -20,14 +20,18 @@ import Combine import UIKit import CryptoKit -class CustomIconMetadataProvider: MetadataProvider { +class CustomIconMetadataProvider: MetadataProvider, @unchecked Sendable { static let shared = CustomIconMetadataProvider() private var cancellableBag = Set() private let iconBitmapDao = IconBitmapDAOImpl.shared private let metadataDao = TransactionMetadataDAOImpl.shared + private let metadataQueue = DispatchQueue(label: "CustomIconMetadataProvider.metadata", qos: .utility) - var availableMetadata: [Data: TxRowMetadata] = [:] + private var _availableMetadata: [Data: TxRowMetadata] = [:] + var availableMetadata: [Data: TxRowMetadata] { + return metadataQueue.sync { _availableMetadata } + } let metadataUpdated = PassthroughSubject() init() { @@ -47,14 +51,19 @@ class CustomIconMetadataProvider: MetadataProvider { } case .deleted(let metadata): - availableMetadata.removeValue(forKey: metadata.txHash) + metadataQueue.async { [weak self] in + self?._availableMetadata.removeValue(forKey: metadata.txHash) + } metadataUpdated.send(metadata.txHash) case .deletedAll: - for metadata in availableMetadata { - metadataUpdated.send(metadata.key) + let keys = metadataQueue.sync { self._availableMetadata.keys } + for key in keys { + metadataUpdated.send(key) + } + metadataQueue.async { [weak self] in + self?._availableMetadata = [:] } - availableMetadata = [:] } } .store(in: &cancellableBag) @@ -67,19 +76,23 @@ class CustomIconMetadataProvider: MetadataProvider { guard let iconId = iconMetadata.customIconId else { continue } let bitmap = await iconBitmapDao.getBitmap(id: iconId) guard let data = bitmap?.imageData, let icon = UIImage(data: data) else { continue } - var txRowMetadata = availableMetadata[iconMetadata.txHash] + + metadataQueue.async { [weak self] in + guard let self = self else { return } + var txRowMetadata = self._availableMetadata[iconMetadata.txHash] - if txRowMetadata != nil { - txRowMetadata!.iconId = iconMetadata.customIconId - txRowMetadata!.icon = icon - } else { - txRowMetadata = TxRowMetadata( - iconId: iconMetadata.customIconId, - icon: icon - ) - } + if txRowMetadata != nil { + txRowMetadata!.iconId = iconMetadata.customIconId + txRowMetadata!.icon = icon + } else { + txRowMetadata = TxRowMetadata( + iconId: iconMetadata.customIconId, + icon: icon + ) + } - availableMetadata[iconMetadata.txHash] = txRowMetadata + self._availableMetadata[iconMetadata.txHash] = txRowMetadata + } } } @@ -87,19 +100,27 @@ class CustomIconMetadataProvider: MetadataProvider { guard let iconId = metadata.customIconId else { return } let bitmap = await iconBitmapDao.getBitmap(id: iconId) guard let data = bitmap?.imageData, let icon = UIImage(data: data) else { return } - var txRowMetadata = availableMetadata[metadata.txHash] + + metadataQueue.async { [weak self] in + guard let self = self else { return } + var txRowMetadata = self._availableMetadata[metadata.txHash] - if txRowMetadata != nil { - txRowMetadata!.details = metadata.memo - } else { - txRowMetadata = TxRowMetadata( - title: nil, - details: metadata.memo - ) - } + if txRowMetadata != nil { + txRowMetadata!.iconId = metadata.customIconId + txRowMetadata!.icon = icon + } else { + txRowMetadata = TxRowMetadata( + iconId: metadata.customIconId, + icon: icon + ) + } - availableMetadata[metadata.txHash] = txRowMetadata - metadataUpdated.send(metadata.txHash) + self._availableMetadata[metadata.txHash] = txRowMetadata + + DispatchQueue.main.async { + self.metadataUpdated.send(metadata.txHash) + } + } } func updateIcon(txId: Data, iconUrl: String) { @@ -140,10 +161,13 @@ class CustomIconMetadataProvider: MetadataProvider { metadata.customIconId = hashData metadataDao.update(dto: metadata) - var txRowMetadata = availableMetadata[txId] ?? TxRowMetadata(title: nil, details: nil) - txRowMetadata.iconId = hashData - txRowMetadata.icon = resizedImage - availableMetadata[txId] = txRowMetadata + metadataQueue.async { [weak self] in + guard let self = self else { return } + var txRowMetadata = self._availableMetadata[txId] ?? TxRowMetadata(title: nil, details: nil) + txRowMetadata.iconId = hashData + txRowMetadata.icon = resizedImage + self._availableMetadata[txId] = txRowMetadata + } Task { @MainActor in self.metadataUpdated.send(txId) diff --git a/DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift b/DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift index 0deaefb61..b3c639760 100644 --- a/DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift +++ b/DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift @@ -20,15 +20,19 @@ import Combine import UIKit import CryptoKit -class GiftCardMetadataProvider: MetadataProvider { +class GiftCardMetadataProvider: MetadataProvider, @unchecked Sendable { static let shared = GiftCardMetadataProvider() private var cancellableBag = Set() private let iconBitmapDao = IconBitmapDAOImpl.shared private let giftCardDao = GiftCardsDAOImpl.shared private let metadataDao = TransactionMetadataDAOImpl.shared + private let metadataQueue = DispatchQueue(label: "GiftCardMetadataProvider.metadata", qos: .utility) - var availableMetadata: [Data: TxRowMetadata] = [:] + private var _availableMetadata: [Data: TxRowMetadata] = [:] + var availableMetadata: [Data: TxRowMetadata] { + return metadataQueue.sync { _availableMetadata } + } let metadataUpdated = PassthroughSubject() init() { @@ -48,14 +52,19 @@ class GiftCardMetadataProvider: MetadataProvider { } case .deleted(let metadata): - availableMetadata.removeValue(forKey: metadata.txHash) + metadataQueue.async { [weak self] in + self?._availableMetadata.removeValue(forKey: metadata.txHash) + } metadataUpdated.send(metadata.txHash) case .deletedAll: - for metadata in availableMetadata { - metadataUpdated.send(metadata.key) + let keys = metadataQueue.sync { self._availableMetadata.keys } + for key in keys { + metadataUpdated.send(key) + } + metadataQueue.async { [weak self] in + self?._availableMetadata = [:] } - availableMetadata = [:] } } .store(in: &cancellableBag) @@ -65,20 +74,24 @@ class GiftCardMetadataProvider: MetadataProvider { let giftCards = await giftCardDao.all() for giftCard in giftCards { - var txRowMetadata = availableMetadata[giftCard.txId] let title = String.localizedStringWithFormat(NSLocalizedString("Gift card · %@", comment: "DashSpend"), giftCard.merchantName) + + metadataQueue.async { [weak self] in + guard let self = self else { return } + var txRowMetadata = self._availableMetadata[giftCard.txId] - if txRowMetadata != nil { - txRowMetadata!.title = title - txRowMetadata!.secondaryIcon = .custom("image.explore.dash.wts.payment.gift-card") - } else { - txRowMetadata = TxRowMetadata( - title: title, - secondaryIcon: .custom("image.explore.dash.wts.payment.gift-card") - ) - } + if txRowMetadata != nil { + txRowMetadata!.title = title + txRowMetadata!.secondaryIcon = .custom("image.explore.dash.wts.payment.gift-card") + } else { + txRowMetadata = TxRowMetadata( + title: title, + secondaryIcon: .custom("image.explore.dash.wts.payment.gift-card") + ) + } - availableMetadata[giftCard.txId] = txRowMetadata + self._availableMetadata[giftCard.txId] = txRowMetadata + } } } @@ -86,15 +99,22 @@ class GiftCardMetadataProvider: MetadataProvider { guard let service = metadata.service, service == ServiceName.ctxSpend.rawValue else { return } guard let giftCard = await giftCardDao.get(byTxId: metadata.txHash) else { return } let title = String.localizedStringWithFormat(NSLocalizedString("Gift card · %@", comment: "DashSpend"), giftCard.merchantName) - var txRowMetadata = availableMetadata[metadata.txHash] - if txRowMetadata != nil { - txRowMetadata!.title = title - } else { - txRowMetadata = TxRowMetadata(title: title) + metadataQueue.async { [weak self] in + guard let self = self else { return } + var txRowMetadata = self._availableMetadata[metadata.txHash] + + if txRowMetadata != nil { + txRowMetadata!.title = title + } else { + txRowMetadata = TxRowMetadata(title: title) + } + + self._availableMetadata[metadata.txHash] = txRowMetadata + + DispatchQueue.main.async { + self.metadataUpdated.send(metadata.txHash) + } } - - availableMetadata[metadata.txHash] = txRowMetadata - metadataUpdated.send(metadata.txHash) } } From dc37df4ea3993a1a2f9950bfc2bcbd38a8e17c33 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Sun, 8 Jun 2025 22:19:33 +0700 Subject: [PATCH 09/13] feat: filter dialog redesign --- DashWallet.xcodeproj/project.pbxproj | 12 +- .../Sources/UI/Home/HomeViewController.swift | 6 - .../Home/TransactionFilterBottomSheet.swift | 103 ------------------ .../UI/Home/UIViewController+DWTxFilter.swift | 66 ----------- .../Sources/UI/Home/Views/HomeView.swift | 18 ++- .../Sources/UI/Home/Views/HomeViewModel.swift | 47 ++++---- .../Home/Views/TransactionFilterDialog.swift | 82 ++++---------- DashWallet/ar.lproj/Localizable.strings | 8 +- DashWallet/bg.lproj/Localizable.strings | 8 +- DashWallet/ca.lproj/Localizable.strings | 8 +- DashWallet/cs.lproj/Localizable.strings | 8 +- DashWallet/da.lproj/Localizable.strings | 8 +- DashWallet/de.lproj/Localizable.strings | 8 +- DashWallet/el.lproj/Localizable.strings | 8 +- DashWallet/en.lproj/Localizable.strings | 8 +- DashWallet/eo.lproj/Localizable.strings | 8 +- DashWallet/es.lproj/Localizable.strings | 8 +- DashWallet/et.lproj/Localizable.strings | 8 +- DashWallet/fa.lproj/Localizable.strings | 8 +- DashWallet/fi.lproj/Localizable.strings | 8 +- DashWallet/fil.lproj/Localizable.strings | 8 +- DashWallet/fr.lproj/Localizable.strings | 8 +- DashWallet/hr.lproj/Localizable.strings | 8 +- DashWallet/hu.lproj/Localizable.strings | 8 +- DashWallet/id.lproj/Localizable.strings | 8 +- DashWallet/it.lproj/Localizable.strings | 8 +- DashWallet/ja.lproj/Localizable.strings | 8 +- DashWallet/ko.lproj/Localizable.strings | 8 +- DashWallet/mk.lproj/Localizable.strings | 8 +- DashWallet/ms.lproj/Localizable.strings | 8 +- DashWallet/nb.lproj/Localizable.strings | 8 +- DashWallet/nl.lproj/Localizable.strings | 8 +- DashWallet/pl.lproj/Localizable.strings | 8 +- DashWallet/pt.lproj/Localizable.strings | 8 +- DashWallet/ro.lproj/Localizable.strings | 8 +- DashWallet/ru.lproj/Localizable.strings | 8 +- DashWallet/sk.lproj/Localizable.strings | 8 +- DashWallet/sl.lproj/Localizable.strings | 8 +- DashWallet/sl_SI.lproj/Localizable.strings | 8 +- DashWallet/sq.lproj/Localizable.strings | 8 +- DashWallet/sr.lproj/Localizable.strings | 8 +- DashWallet/sv.lproj/Localizable.strings | 8 +- DashWallet/th.lproj/Localizable.strings | 8 +- DashWallet/tr.lproj/Localizable.strings | 8 +- DashWallet/uk.lproj/Localizable.strings | 8 +- DashWallet/vi.lproj/Localizable.strings | 8 +- DashWallet/zh-Hans.lproj/Localizable.strings | 8 +- .../zh-Hant-TW.lproj/Localizable.strings | 8 +- DashWallet/zh.lproj/Localizable.strings | 8 +- DashWallet/zh_TW.lproj/Localizable.strings | 8 +- 50 files changed, 242 insertions(+), 436 deletions(-) delete mode 100644 DashWallet/Sources/UI/Home/TransactionFilterBottomSheet.swift delete mode 100644 DashWallet/Sources/UI/Home/UIViewController+DWTxFilter.swift diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index 0fde235be..c324a2fd7 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -661,8 +661,6 @@ 759609212C4553A400F3BF04 /* BuyCreditsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759609202C4553A400F3BF04 /* BuyCreditsViewController.swift */; }; 759609232C455B2000F3BF04 /* SendIntro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759609222C455B2000F3BF04 /* SendIntro.swift */; }; 759609242C455B2000F3BF04 /* SendIntro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759609222C455B2000F3BF04 /* SendIntro.swift */; }; - 759A7EA72CFDA707009423AD /* UIViewController+DWTxFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759A7EA62CFDA6FF009423AD /* UIViewController+DWTxFilter.swift */; }; - 759A7EA82CFDA707009423AD /* UIViewController+DWTxFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759A7EA62CFDA6FF009423AD /* UIViewController+DWTxFilter.swift */; }; 759ADD572BF3447400767ACD /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759ADD562BF3447400767ACD /* Button.swift */; }; 759ADD582BF3447400767ACD /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759ADD562BF3447400767ACD /* Button.swift */; }; 759AFDE02CC63571007072D2 /* JoinDashPayScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759AFDDF2CC6356D007072D2 /* JoinDashPayScreen.swift */; }; @@ -710,6 +708,8 @@ 75D5D5342CC928630049ED7B /* UIHostingController+DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D5D5322CC9285B0049ED7B /* UIHostingController+DashWallet.swift */; }; 75D5F3CE191EC270004AB296 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 75D5F3CD191EC270004AB296 /* main.m */; }; 75D6561D2B07936100D1A902 /* UsernameRequests.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 75D656192B0792F500D1A902 /* UsernameRequests.storyboard */; }; + 75D657672DF579F300ACE570 /* TransactionFilterDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D657662DF579F300ACE570 /* TransactionFilterDialog.swift */; }; + 75D657682DF579F300ACE570 /* TransactionFilterDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D657662DF579F300ACE570 /* TransactionFilterDialog.swift */; }; 75D66D332B05E7AE00A8DDA6 /* QuickVoteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D66D322B05E7AE00A8DDA6 /* QuickVoteViewController.swift */; }; 75D6D8E42D96A6DB00E40A6D /* StubTransactionSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D6D8E32D96A6D500E40A6D /* StubTransactionSource.swift */; }; 75D6D8E52D96A6DB00E40A6D /* StubTransactionSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D6D8E32D96A6D500E40A6D /* StubTransactionSource.swift */; }; @@ -2545,7 +2545,6 @@ 759609222C455B2000F3BF04 /* SendIntro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendIntro.swift; sourceTree = ""; }; 759816E519357D6F005060EA /* BRBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BRBubbleView.h; sourceTree = ""; }; 759816E619357D6F005060EA /* BRBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BRBubbleView.m; sourceTree = ""; }; - 759A7EA62CFDA6FF009423AD /* UIViewController+DWTxFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+DWTxFilter.swift"; sourceTree = ""; }; 759ADD562BF3447400767ACD /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; 759AFDDF2CC6356D007072D2 /* JoinDashPayScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinDashPayScreen.swift; sourceTree = ""; }; 759AFDE22CC67E89007072D2 /* VotingInfoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingInfoScreen.swift; sourceTree = ""; }; @@ -2580,6 +2579,7 @@ 75D5F3CF191EC270004AB296 /* DashWallet-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DashWallet-Prefix.pch"; sourceTree = ""; }; 75D5F3E5191EC270004AB296 /* DashWalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DashWalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 75D656192B0792F500D1A902 /* UsernameRequests.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = UsernameRequests.storyboard; sourceTree = ""; }; + 75D657662DF579F300ACE570 /* TransactionFilterDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionFilterDialog.swift; sourceTree = ""; }; 75D66D322B05E7AE00A8DDA6 /* QuickVoteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickVoteViewController.swift; sourceTree = ""; }; 75D6D8E32D96A6D500E40A6D /* StubTransactionSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTransactionSource.swift; sourceTree = ""; }; 75D9EBB72DE5CC3A009416A2 /* AddGiftCardsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGiftCardsTable.swift; sourceTree = ""; }; @@ -3816,6 +3816,7 @@ C9F452002A0CE6C900825057 /* HomeView.swift */, 754BEA112C0B6BD700E8C93C /* HomeViewModel.swift */, 751C05DC2D3E39A600475E52 /* TransactionListDataItem.swift */, + 75D657662DF579F300ACE570 /* TransactionFilterDialog.swift */, ); path = Views; sourceTree = ""; @@ -4633,7 +4634,6 @@ 2A913E7223A2EAEF006A2A59 /* Protocols */, 2A2CD6E522F46D1A008C7BC9 /* Models */, 2A307CB322E6FA7F00A18347 /* Views */, - 759A7EA62CFDA6FF009423AD /* UIViewController+DWTxFilter.swift */, 75FFD6B72BF47BA20032879E /* HomeViewController.swift */, 75FFD6BA2BF48DF80032879E /* HomeViewController+JailbreakCheck.swift */, 75FFD6BD2BF491FC0032879E /* HomeViewController+BackupReminder.swift */, @@ -8607,7 +8607,6 @@ 110C679A29227948006B580C /* UINavigationController+CrowdNode.swift in Sources */, 2A8B9E7A2302E67400FF8653 /* DWReceiveModel.m in Sources */, 119E8D0A2905413F00D406C1 /* TransactionWrapper.swift in Sources */, - 759A7EA82CFDA707009423AD /* UIViewController+DWTxFilter.swift in Sources */, 479DBDDD2995168C00F30AF1 /* Transactions.swift in Sources */, 75F3F00D2C48F819004470EA /* RootEditProfileViewController.swift in Sources */, 2A74EFFB2305464C00C475EB /* DWRecoverTextView.m in Sources */, @@ -9080,6 +9079,7 @@ 47838B7A2900196F0003E8AB /* ConverterView.swift in Sources */, 2A0C69CA23142E11001B8C90 /* DWModalBaseAnimation.m in Sources */, C956AF262A5CACE6002FAB75 /* TitleValueCell.swift in Sources */, + 75D657682DF579F300ACE570 /* TransactionFilterDialog.swift in Sources */, 75EBAA0C2BB9792F004488E3 /* FeatureTopText.swift in Sources */, C9F42FB829DFC507001BC549 /* SpendableTransaction.swift in Sources */, 11ED906B29681773003784F9 /* StakingInfoDialogController.swift in Sources */, @@ -9342,7 +9342,6 @@ C9D2C7072A320AA000D15901 /* AmountInputTypeSwitcher.swift in Sources */, C943B4FA2A40A54600AF23C5 /* DWDPOutgoingRequestNotificationObject.m in Sources */, 75EBAA132BB99B6B004488E3 /* BottomSheet.swift in Sources */, - 759A7EA72CFDA707009423AD /* UIViewController+DWTxFilter.swift in Sources */, C9D2C7082A320AA000D15901 /* DWPlaceholderFormCellModel.m in Sources */, C9D2C7092A320AA000D15901 /* DWOverlapControl.m in Sources */, C9D2C70A2A320AA000D15901 /* CoinsToAddressTxFilter.swift in Sources */, @@ -9416,6 +9415,7 @@ 757514E52B1735370026AD8E /* JoinDashPayView.swift in Sources */, C9D2C73C2A320AA000D15901 /* DWDecimalInputValidator.m in Sources */, C9D2C73D2A320AA000D15901 /* PaymentMethods.swift in Sources */, + 75D657672DF579F300ACE570 /* TransactionFilterDialog.swift in Sources */, C9D2C73E2A320AA000D15901 /* DWTitleDetailCellModel.m in Sources */, 7514E4EA2AF2351F00A0466F /* VotingFiltersModel.swift in Sources */, C943B4E22A40A54600AF23C5 /* DWPassthroughView.m in Sources */, diff --git a/DashWallet/Sources/UI/Home/HomeViewController.swift b/DashWallet/Sources/UI/Home/HomeViewController.swift index 4893af6b7..71cabb9bc 100644 --- a/DashWallet/Sources/UI/Home/HomeViewController.swift +++ b/DashWallet/Sources/UI/Home/HomeViewController.swift @@ -310,12 +310,6 @@ extension HomeViewController: HomeViewDelegate { let action = ShortcutAction(type: .createUsername) performAction(for: action, sender: nil) } - - func homeViewShowTxFilter() { - showTxFilter(displayModeCallback: { [weak self] mode in - self?.viewModel.displayMode = mode - }, shouldShowRewards: true) - } func homeViewShowSyncingStatus() { let controller = SyncingAlertViewController() diff --git a/DashWallet/Sources/UI/Home/TransactionFilterBottomSheet.swift b/DashWallet/Sources/UI/Home/TransactionFilterBottomSheet.swift deleted file mode 100644 index 57b81b5b5..000000000 --- a/DashWallet/Sources/UI/Home/TransactionFilterBottomSheet.swift +++ /dev/null @@ -1,103 +0,0 @@ -import SwiftUI -import UIKit - -struct TransactionFilterBottomSheet: View { - @Environment(\.presentationMode) private var presentationMode - @Binding var selectedFilter: HomeTxDisplayMode - var shouldShowRewards: Bool - var onFilterSelected: (HomeTxDisplayMode) -> Void - - private let filterOptions: [FilterOption] = [ - FilterOption(mode: .all, title: NSLocalizedString("All", comment: ""), icon: "list.bullet"), - FilterOption(mode: .received, title: NSLocalizedString("Received", comment: ""), icon: "arrow.down.circle"), - FilterOption(mode: .sent, title: NSLocalizedString("Sent", comment: ""), icon: "arrow.up.circle"), - FilterOption(mode: .rewards, title: NSLocalizedString("Rewards", comment: ""), icon: "gift.circle") - ] - - var body: some View { - BottomSheet( - title: NSLocalizedString("Filter Transactions", comment: ""), - showBackButton: .constant(false) - ) { - VStack(spacing: 0) { - ForEach(availableFilters) { option in - FilterOptionRow( - option: option, - isSelected: option.mode == selectedFilter - ) { - onFilterSelected(option.mode) - presentationMode.wrappedValue.dismiss() - } - } - } - .padding(.horizontal, 20) - .padding(.vertical, 20) - } - .background(Color.primaryBackground) - } - - private var availableFilters: [FilterOption] { - var filters = filterOptions.filter { $0.mode != .rewards } - - if shouldShowRewards { - let account = DWEnvironment.sharedInstance().currentAccount - if account.hasCoinbaseTransaction { - filters.append(filterOptions.first { $0.mode == .rewards }!) - } - } - - return filters - } -} - -struct FilterOption: Identifiable { - let id = UUID() - let mode: HomeTxDisplayMode - let title: String - let icon: String -} - -struct FilterOptionRow: View { - let option: FilterOption - let isSelected: Bool - let onTap: () -> Void - - var body: some View { - Button(action: onTap) { - HStack(spacing: 16) { - Image(systemName: option.icon) - .font(.system(size: 20)) - .foregroundColor(.dashBlue) - .frame(width: 24, height: 24) - - Text(option.title) - .font(.body2) - .fontWeight(.medium) - .foregroundColor(.primaryText) - - Spacer() - - if isSelected { - Image(systemName: "checkmark") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.dashBlue) - } - } - .padding(.horizontal, 16) - .padding(.vertical, 16) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(isSelected ? Color.dashBlue.opacity(0.05) : Color.secondaryBackground) - ) - } - .padding(.bottom, 8) - } -} - -#Preview { - TransactionFilterBottomSheet( - selectedFilter: .constant(.all), - shouldShowRewards: true, - onFilterSelected: { _ in } - ) -} \ No newline at end of file diff --git a/DashWallet/Sources/UI/Home/UIViewController+DWTxFilter.swift b/DashWallet/Sources/UI/Home/UIViewController+DWTxFilter.swift deleted file mode 100644 index ec6d4c8c9..000000000 --- a/DashWallet/Sources/UI/Home/UIViewController+DWTxFilter.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// Created by Andrew Podkovyrin -// Copyright © 2019 Dash Core Group. All rights reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import UIKit - -extension UIViewController { - @objc - func showTxFilter(displayModeCallback: @escaping (HomeTxDisplayMode) -> Void, shouldShowRewards: Bool) { - let title = NSLocalizedString("Filter Transactions", comment: "") - let alert = UIAlertController(title: title, - message: nil, - preferredStyle: .actionSheet) - - let allAction = UIAlertAction( - title: NSLocalizedString("All", comment: ""), - style: .default) { _ in - displayModeCallback(.all) - } - alert.addAction(allAction) - - let receivedAction = UIAlertAction( - title: NSLocalizedString("Received", comment: ""), - style: .default) { _ in - displayModeCallback(.received) - } - alert.addAction(receivedAction) - - let account = DWEnvironment.sharedInstance().currentAccount - if shouldShowRewards && account.hasCoinbaseTransaction { - let rewardsAction = UIAlertAction( - title: NSLocalizedString("Rewards", comment: ""), - style: .default) { _ in - displayModeCallback(.rewards) - } - alert.addAction(rewardsAction) - } - - let sentAction = UIAlertAction( - title: NSLocalizedString("Sent", comment: ""), - style: .default) { _ in - displayModeCallback(.sent) - } - alert.addAction(sentAction) - - let cancelAction = UIAlertAction( - title: NSLocalizedString("Cancel", comment: ""), - style: .cancel) - alert.addAction(cancelAction) - - present(alert, animated: true) - } -} diff --git a/DashWallet/Sources/UI/Home/Views/HomeView.swift b/DashWallet/Sources/UI/Home/Views/HomeView.swift index dc47fcb39..cee45ea6f 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeView.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeView.swift @@ -22,7 +22,6 @@ import Combine // MARK: - HomeViewDelegate protocol HomeViewDelegate: AnyObject { - func homeViewShowTxFilter() func homeViewShowSyncingStatus() func homeViewShowCoinJoin() @@ -177,6 +176,7 @@ struct TxPreviewModel: Identifiable, Equatable { struct HomeViewContent: View { @State private var selectedTxDataItem: TransactionListDataItem? = nil @State private var shouldShowMixDialog: Bool = false + @State private var showFilterDialog: Bool = false @State private var shouldShowJoinDashPayInfo: Bool = false @State private var navigateToDashPayFlow: Bool = false @State private var navigateToCoinJoin: Bool = false @@ -259,7 +259,7 @@ struct HomeViewContent: View { #endif SyncingHeaderView(onFilterTap: { - delegate?.homeViewShowTxFilter() + showFilterDialog = true }, onSyncTap: { delegate?.homeViewShowSyncingStatus() }) @@ -302,6 +302,20 @@ struct HomeViewContent: View { GiftCardDetailsSheet(txId: txId) } } + .sheet(isPresented: $showFilterDialog) { + let dialog = TransactionFilterDialog( + selectedFilter: .constant(viewModel.displayMode), + onFilterSelected: { mode in + viewModel.displayMode = mode + } + ) + + if #available(iOS 16.0, *) { + dialog.presentationDetents([.height(350)]) + } else { + dialog + } + } #if DASHPAY .sheet(isPresented: $shouldShowMixDialog, onDismiss: { viewModel.shouldShowMixDashDialog = false diff --git a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift index e6bfecc69..ef327d530 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift @@ -373,34 +373,35 @@ class HomeViewModel: ObservableObject { var finalMetadata: TxRowMetadata? = nil for provider in self.metadataProviders { - if let metadata = provider.availableMetadata[txId] { - if finalMetadata == nil { - finalMetadata = metadata - } else { - if finalMetadata?.title == nil { - finalMetadata?.title = metadata.title - } + // Safely access the provider's metadata + let providerMetadata = provider.availableMetadata + guard let metadata = providerMetadata[txId] else { continue } + + if finalMetadata == nil { + finalMetadata = metadata + } else { + if finalMetadata?.title == nil { + finalMetadata?.title = metadata.title + } - if finalMetadata?.details == nil { - finalMetadata?.details = metadata.details - } - - if finalMetadata?.icon == nil { - finalMetadata?.icon = metadata.icon - } - - if finalMetadata?.iconId == nil { - finalMetadata?.iconId = metadata.iconId - } - - if finalMetadata?.secondaryIcon == nil { - finalMetadata?.secondaryIcon = metadata.secondaryIcon - } + if finalMetadata?.details == nil { + finalMetadata?.details = metadata.details + } + + if finalMetadata?.icon == nil { + finalMetadata?.icon = metadata.icon + } + + if finalMetadata?.iconId == nil { + finalMetadata?.iconId = metadata.iconId + } + + if finalMetadata?.secondaryIcon == nil { + finalMetadata?.secondaryIcon = metadata.secondaryIcon } } } - print("METADATA: resolved finalMetadata: \(String(describing: finalMetadata))") return finalMetadata } } diff --git a/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift index f0420a4a3..5c82fd622 100644 --- a/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift +++ b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift @@ -4,14 +4,13 @@ import UIKit struct TransactionFilterDialog: View { @Environment(\.presentationMode) private var presentationMode @Binding var selectedFilter: HomeTxDisplayMode - var shouldShowRewards: Bool var onFilterSelected: (HomeTxDisplayMode) -> Void private let filterOptions: [FilterOption] = [ - FilterOption(mode: .all, title: NSLocalizedString("All", comment: ""), icon: .system("line.horizontal.3"), color: .dashBlue), - FilterOption(mode: .sent, title: NSLocalizedString("Sent", comment: ""), icon: .system("arrow.up"), color: .dashBlue), - FilterOption(mode: .received, title: NSLocalizedString("Received", comment: ""), icon: .system("arrow.down"), color: Color.green), - FilterOption(mode: .rewards, title: NSLocalizedString("Gift card", comment: ""), icon: .system("gift"), color: Color.orange) + FilterOption(mode: .all, title: NSLocalizedString("All", comment: ""), icon: .custom("image.filter.options")), + FilterOption(mode: .sent, title: NSLocalizedString("Sent", comment: ""), icon: .custom("tx.item.sent.icon")), + FilterOption(mode: .received, title: NSLocalizedString("Received", comment: ""), icon: .custom("tx.item.received.icon")), + FilterOption(mode: .rewards, title: NSLocalizedString("Gift card", comment: ""), icon: .custom("image.dashspend.giftcard")) ] var body: some View { @@ -19,8 +18,8 @@ struct TransactionFilterDialog: View { title: NSLocalizedString("Filter transactions", comment: ""), showBackButton: .constant(false) ) { - VStack(spacing: 24) { - ForEach(availableFilters) { option in + VStack(spacing: 0) { + ForEach(filterOptions) { option in FilterOptionRow( option: option, isSelected: option.mode == selectedFilter @@ -29,27 +28,15 @@ struct TransactionFilterDialog: View { presentationMode.wrappedValue.dismiss() } } - - Spacer() } + .padding(.vertical, 6) + .background(Color.secondaryBackground) + .clipShape(RoundedShape(corners: .allCorners, radii: 12)) .padding(.horizontal, 20) - .padding(.top, 32) + .padding(.top, 25) } .background(Color.primaryBackground) } - - private var availableFilters: [FilterOption] { - var filters = filterOptions.filter { $0.mode != .rewards } - - if shouldShowRewards { - let account = DWEnvironment.sharedInstance().currentAccount - if account.hasCoinbaseTransaction { - filters.append(filterOptions.first { $0.mode == .rewards }!) - } - } - - return filters - } } struct FilterOption: Identifiable { @@ -57,7 +44,6 @@ struct FilterOption: Identifiable { let mode: HomeTxDisplayMode let title: String let icon: IconName - let color: Color } struct FilterOptionRow: View { @@ -67,48 +53,28 @@ struct FilterOptionRow: View { var body: some View { Button(action: onTap) { - HStack(spacing: 16) { - // Icon circle - ZStack { - Circle() - .fill(option.color) - .frame(width: 40, height: 40) - - Icon(name: option.icon) - .foregroundColor(.white) - .font(.system(size: 18, weight: .medium)) - } + HStack(alignment: .center, spacing: 16) { + Icon(name: option.icon) + .frame(width: 30, height: 30) + .padding(.vertical, 16) - // Title Text(option.title) - .font(.system(size: 17, weight: .medium)) + .font(.body2) + .fontWeight(.medium) .foregroundColor(.primaryText) Spacer() - // Radio button - ZStack { - Circle() - .stroke(isSelected ? Color.dashBlue : Color.gray400, lineWidth: 2) - .frame(width: 24, height: 24) - - if isSelected { - Circle() - .fill(Color.dashBlue) - .frame(width: 12, height: 12) - } - } + Circle() + .stroke(isSelected ? Color.dashBlue : Color.gray300.opacity(0.5), lineWidth: isSelected ? 6 : 2) + .frame(width: isSelected ? 21 : 24, height: isSelected ? 21 : 24) + .padding(.trailing, isSelected ? 2 : 0) } - .padding(.vertical, 4) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .background(Color.clear) } .buttonStyle(PlainButtonStyle()) } } - -#Preview { - TransactionFilterDialog( - selectedFilter: .constant(.all), - shouldShowRewards: true, - onFilterSelected: { _ in } - ) -} diff --git a/DashWallet/ar.lproj/Localizable.strings b/DashWallet/ar.lproj/Localizable.strings index 74d069e20..d51ec464f 100644 --- a/DashWallet/ar.lproj/Localizable.strings +++ b/DashWallet/ar.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "منقي"; /* No comment provided by engineer. */ -"Filter Transactions" = "تصفية المعاملات"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "حفظ"; diff --git a/DashWallet/bg.lproj/Localizable.strings b/DashWallet/bg.lproj/Localizable.strings index 65d271ae3..d0143e0c4 100644 --- a/DashWallet/bg.lproj/Localizable.strings +++ b/DashWallet/bg.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Филтър"; /* No comment provided by engineer. */ -"Filter Transactions" = "Филтър на транзакциите "; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Възнаграждение"; -/* No comment provided by engineer. */ -"Rewards" = "Възнаграждения"; - /* No comment provided by engineer. */ "Save" = "Съхрани"; diff --git a/DashWallet/ca.lproj/Localizable.strings b/DashWallet/ca.lproj/Localizable.strings index 877e61bf6..e53b7f8c7 100644 --- a/DashWallet/ca.lproj/Localizable.strings +++ b/DashWallet/ca.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Desa"; diff --git a/DashWallet/cs.lproj/Localizable.strings b/DashWallet/cs.lproj/Localizable.strings index db3ddddb4..723a23b69 100644 --- a/DashWallet/cs.lproj/Localizable.strings +++ b/DashWallet/cs.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtr"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filtrovat transakce"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Odměna"; -/* No comment provided by engineer. */ -"Rewards" = "Odměny"; - /* No comment provided by engineer. */ "Save" = "Uložit"; diff --git a/DashWallet/da.lproj/Localizable.strings b/DashWallet/da.lproj/Localizable.strings index 6a5cb1c0e..423f49ebb 100644 --- a/DashWallet/da.lproj/Localizable.strings +++ b/DashWallet/da.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Gem"; diff --git a/DashWallet/de.lproj/Localizable.strings b/DashWallet/de.lproj/Localizable.strings index cf498cea6..594f03ca9 100644 --- a/DashWallet/de.lproj/Localizable.strings +++ b/DashWallet/de.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Transaktionen filtern"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Sortiert nach"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gutscheinkarte"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Belohnung"; -/* No comment provided by engineer. */ -"Rewards" = "Belohnungen"; - /* No comment provided by engineer. */ "Save" = "Speichern"; diff --git a/DashWallet/el.lproj/Localizable.strings b/DashWallet/el.lproj/Localizable.strings index 64cef007d..84b364cc7 100644 --- a/DashWallet/el.lproj/Localizable.strings +++ b/DashWallet/el.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Φίλτρο"; /* No comment provided by engineer. */ -"Filter Transactions" = "Φίλτρο Συναλλαγών"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Φιλτραρισμένο από"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Δωροκάρτα"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Ανταμοιβή"; -/* No comment provided by engineer. */ -"Rewards" = "Ανταμοιβές"; - /* No comment provided by engineer. */ "Save" = "Αποθήκευση"; diff --git a/DashWallet/en.lproj/Localizable.strings b/DashWallet/en.lproj/Localizable.strings index da6d43092..27dcdb937 100644 --- a/DashWallet/en.lproj/Localizable.strings +++ b/DashWallet/en.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Save"; diff --git a/DashWallet/eo.lproj/Localizable.strings b/DashWallet/eo.lproj/Localizable.strings index 0787ff50e..8b5b53a98 100644 --- a/DashWallet/eo.lproj/Localizable.strings +++ b/DashWallet/eo.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Savi"; diff --git a/DashWallet/es.lproj/Localizable.strings b/DashWallet/es.lproj/Localizable.strings index ff90a4890..d4c3afbdb 100644 --- a/DashWallet/es.lproj/Localizable.strings +++ b/DashWallet/es.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtrar"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filtrar Transacciones"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtrado por"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Tarjeta de regalo"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Recompensa"; -/* No comment provided by engineer. */ -"Rewards" = "Recompensas"; - /* No comment provided by engineer. */ "Save" = "Guardar"; diff --git a/DashWallet/et.lproj/Localizable.strings b/DashWallet/et.lproj/Localizable.strings index af2b4f15a..2596fe51b 100644 --- a/DashWallet/et.lproj/Localizable.strings +++ b/DashWallet/et.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Save"; diff --git a/DashWallet/fa.lproj/Localizable.strings b/DashWallet/fa.lproj/Localizable.strings index 775a48c10..90d5f5988 100644 --- a/DashWallet/fa.lproj/Localizable.strings +++ b/DashWallet/fa.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "فیلتر"; /* No comment provided by engineer. */ -"Filter Transactions" = "فیلتر تراکنش‌ها"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "ذخیره"; diff --git a/DashWallet/fi.lproj/Localizable.strings b/DashWallet/fi.lproj/Localizable.strings index 21edc6385..1112645dd 100644 --- a/DashWallet/fi.lproj/Localizable.strings +++ b/DashWallet/fi.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Suodata"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Tallenna"; diff --git a/DashWallet/fil.lproj/Localizable.strings b/DashWallet/fil.lproj/Localizable.strings index 53e10eb94..c621a8a64 100644 --- a/DashWallet/fil.lproj/Localizable.strings +++ b/DashWallet/fil.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Salain"; /* No comment provided by engineer. */ -"Filter Transactions" = "Salain ang transaksyon"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Na-filter ng"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Gantimpala"; -/* No comment provided by engineer. */ -"Rewards" = "Mga gantimpala"; - /* No comment provided by engineer. */ "Save" = "I-save"; diff --git a/DashWallet/fr.lproj/Localizable.strings b/DashWallet/fr.lproj/Localizable.strings index 6098a134e..4e2a1716f 100644 --- a/DashWallet/fr.lproj/Localizable.strings +++ b/DashWallet/fr.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtre"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filtrer les transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtré par"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Carte-cadeau"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Récompense"; -/* No comment provided by engineer. */ -"Rewards" = "Récompenses"; - /* No comment provided by engineer. */ "Save" = "Enregistrer"; diff --git a/DashWallet/hr.lproj/Localizable.strings b/DashWallet/hr.lproj/Localizable.strings index 60cbfc6ca..135c6cbfd 100644 --- a/DashWallet/hr.lproj/Localizable.strings +++ b/DashWallet/hr.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Spremi"; diff --git a/DashWallet/hu.lproj/Localizable.strings b/DashWallet/hu.lproj/Localizable.strings index ce9655e14..16c235d48 100644 --- a/DashWallet/hu.lproj/Localizable.strings +++ b/DashWallet/hu.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Szűrő"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Mentés"; diff --git a/DashWallet/id.lproj/Localizable.strings b/DashWallet/id.lproj/Localizable.strings index c3337ed4c..322bffc2f 100644 --- a/DashWallet/id.lproj/Localizable.strings +++ b/DashWallet/id.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Menyaring"; /* No comment provided by engineer. */ -"Filter Transactions" = "Saring Transaksi"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Difilter menurut"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Kartu ucapan"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Hadiah"; -/* No comment provided by engineer. */ -"Rewards" = "Hadiah"; - /* No comment provided by engineer. */ "Save" = "Simpan"; diff --git a/DashWallet/it.lproj/Localizable.strings b/DashWallet/it.lproj/Localizable.strings index a2d68d356..da87a2b3a 100644 --- a/DashWallet/it.lproj/Localizable.strings +++ b/DashWallet/it.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtro"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filtra Transazioni"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtrato per"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Ricompensa"; -/* No comment provided by engineer. */ -"Rewards" = "Ricompense"; - /* No comment provided by engineer. */ "Save" = "Salva"; diff --git a/DashWallet/ja.lproj/Localizable.strings b/DashWallet/ja.lproj/Localizable.strings index abbb85331..19e884307 100644 --- a/DashWallet/ja.lproj/Localizable.strings +++ b/DashWallet/ja.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "絞り込む"; /* No comment provided by engineer. */ -"Filter Transactions" = "取引を絞り込む"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "で絞り込む"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "ギフトカード"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "報酬"; -/* No comment provided by engineer. */ -"Rewards" = "報酬"; - /* No comment provided by engineer. */ "Save" = "保存"; diff --git a/DashWallet/ko.lproj/Localizable.strings b/DashWallet/ko.lproj/Localizable.strings index 95d2e4433..38a46d0ca 100644 --- a/DashWallet/ko.lproj/Localizable.strings +++ b/DashWallet/ko.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "필터"; /* No comment provided by engineer. */ -"Filter Transactions" = "거래 필터링 하기"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "다음으로 필터"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "기프트 카드"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "리워드"; -/* No comment provided by engineer. */ -"Rewards" = "리워드"; - /* No comment provided by engineer. */ "Save" = "저장"; diff --git a/DashWallet/mk.lproj/Localizable.strings b/DashWallet/mk.lproj/Localizable.strings index 74f04caab..32451effc 100644 --- a/DashWallet/mk.lproj/Localizable.strings +++ b/DashWallet/mk.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Зачувај"; diff --git a/DashWallet/ms.lproj/Localizable.strings b/DashWallet/ms.lproj/Localizable.strings index 29599f786..e24416956 100644 --- a/DashWallet/ms.lproj/Localizable.strings +++ b/DashWallet/ms.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Save"; diff --git a/DashWallet/nb.lproj/Localizable.strings b/DashWallet/nb.lproj/Localizable.strings index 86fde1838..cd624965e 100644 --- a/DashWallet/nb.lproj/Localizable.strings +++ b/DashWallet/nb.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Lagre"; diff --git a/DashWallet/nl.lproj/Localizable.strings b/DashWallet/nl.lproj/Localizable.strings index fd9593600..cd2f2342d 100644 --- a/DashWallet/nl.lproj/Localizable.strings +++ b/DashWallet/nl.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Transacties filteren"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Gefilterd op"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Cadeaukaart"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Beloning"; -/* No comment provided by engineer. */ -"Rewards" = "Beloningen"; - /* No comment provided by engineer. */ "Save" = "Opslaan"; diff --git a/DashWallet/pl.lproj/Localizable.strings b/DashWallet/pl.lproj/Localizable.strings index 8b5fd60fd..c4ec71b52 100644 --- a/DashWallet/pl.lproj/Localizable.strings +++ b/DashWallet/pl.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtrowanie"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filtruj Transakcje"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtruj w zależności od"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Karta podarunkowa"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Nagroda"; -/* No comment provided by engineer. */ -"Rewards" = "Nagrody"; - /* No comment provided by engineer. */ "Save" = "Zapisz"; diff --git a/DashWallet/pt.lproj/Localizable.strings b/DashWallet/pt.lproj/Localizable.strings index 1e5c4b0ac..641d29df7 100644 --- a/DashWallet/pt.lproj/Localizable.strings +++ b/DashWallet/pt.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtro"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filtrar Transações"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtrado por:"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Vale-presente"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Recompensa"; -/* No comment provided by engineer. */ -"Rewards" = "Recompensas"; - /* No comment provided by engineer. */ "Save" = "Salvar"; diff --git a/DashWallet/ro.lproj/Localizable.strings b/DashWallet/ro.lproj/Localizable.strings index 4030b3293..f76c9cf37 100644 --- a/DashWallet/ro.lproj/Localizable.strings +++ b/DashWallet/ro.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtru"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Salvează"; diff --git a/DashWallet/ru.lproj/Localizable.strings b/DashWallet/ru.lproj/Localizable.strings index f75e0bef5..7dd4eb364 100644 --- a/DashWallet/ru.lproj/Localizable.strings +++ b/DashWallet/ru.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Фильтр"; /* No comment provided by engineer. */ -"Filter Transactions" = "Показать транзакции"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Фильтр по"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Подарочный сертификат"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Вознаграждение"; -/* No comment provided by engineer. */ -"Rewards" = "Вознаграждения"; - /* No comment provided by engineer. */ "Save" = "Сохранить"; diff --git a/DashWallet/sk.lproj/Localizable.strings b/DashWallet/sk.lproj/Localizable.strings index c776d5f3a..d307fd7a2 100644 --- a/DashWallet/sk.lproj/Localizable.strings +++ b/DashWallet/sk.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter platieb"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filtrovať transakcie"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtrované podľa"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Darčeková karta"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Odmena"; -/* No comment provided by engineer. */ -"Rewards" = "Odmeny"; - /* No comment provided by engineer. */ "Save" = "Uložiť"; diff --git a/DashWallet/sl.lproj/Localizable.strings b/DashWallet/sl.lproj/Localizable.strings index 2a9abe891..cff42e644 100644 --- a/DashWallet/sl.lproj/Localizable.strings +++ b/DashWallet/sl.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Shranite"; diff --git a/DashWallet/sl_SI.lproj/Localizable.strings b/DashWallet/sl_SI.lproj/Localizable.strings index 71489b04f..b2f731f6d 100644 --- a/DashWallet/sl_SI.lproj/Localizable.strings +++ b/DashWallet/sl_SI.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Save"; diff --git a/DashWallet/sq.lproj/Localizable.strings b/DashWallet/sq.lproj/Localizable.strings index a6dc68217..5e96586a8 100644 --- a/DashWallet/sq.lproj/Localizable.strings +++ b/DashWallet/sq.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtro"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Ruaj"; diff --git a/DashWallet/sr.lproj/Localizable.strings b/DashWallet/sr.lproj/Localizable.strings index 80b0805fe..3bec895e9 100644 --- a/DashWallet/sr.lproj/Localizable.strings +++ b/DashWallet/sr.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtriraj"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Sačuvaj"; diff --git a/DashWallet/sv.lproj/Localizable.strings b/DashWallet/sv.lproj/Localizable.strings index d2361b224..e19023281 100644 --- a/DashWallet/sv.lproj/Localizable.strings +++ b/DashWallet/sv.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Spara"; diff --git a/DashWallet/th.lproj/Localizable.strings b/DashWallet/th.lproj/Localizable.strings index c7e1751d0..8e40b9ec0 100644 --- a/DashWallet/th.lproj/Localizable.strings +++ b/DashWallet/th.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "กรอง"; /* No comment provided by engineer. */ -"Filter Transactions" = "กรองธุรกรรม"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "กรองโดย"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "บัตรของขวัญ"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "รางวัล"; -/* No comment provided by engineer. */ -"Rewards" = "รางวัล"; - /* No comment provided by engineer. */ "Save" = "บันทึก"; diff --git a/DashWallet/tr.lproj/Localizable.strings b/DashWallet/tr.lproj/Localizable.strings index 4c032c796..ff07c38f7 100644 --- a/DashWallet/tr.lproj/Localizable.strings +++ b/DashWallet/tr.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filtre"; /* No comment provided by engineer. */ -"Filter Transactions" = "İşlemleri Filtrele"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Şuna göre filtrelendi:"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Hediye Kartı"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Ödül"; -/* No comment provided by engineer. */ -"Rewards" = "Ödüller"; - /* No comment provided by engineer. */ "Save" = "Kaydet"; diff --git a/DashWallet/uk.lproj/Localizable.strings b/DashWallet/uk.lproj/Localizable.strings index b23fe83f6..2bba88bf3 100644 --- a/DashWallet/uk.lproj/Localizable.strings +++ b/DashWallet/uk.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Фільтр"; /* No comment provided by engineer. */ -"Filter Transactions" = "Фільтрувати транзакції"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Відфільтровано за:"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Подарункова карта"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Нагорода"; -/* No comment provided by engineer. */ -"Rewards" = "Нагороди"; - /* No comment provided by engineer. */ "Save" = "Зберегти"; diff --git a/DashWallet/vi.lproj/Localizable.strings b/DashWallet/vi.lproj/Localizable.strings index b39e4b7fd..908fd3e4e 100644 --- a/DashWallet/vi.lproj/Localizable.strings +++ b/DashWallet/vi.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Lọc"; /* No comment provided by engineer. */ -"Filter Transactions" = "Lọc các giao dịch"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Phần thưởng"; -/* No comment provided by engineer. */ -"Rewards" = "Phần thưởng"; - /* No comment provided by engineer. */ "Save" = "Lưu"; diff --git a/DashWallet/zh-Hans.lproj/Localizable.strings b/DashWallet/zh-Hans.lproj/Localizable.strings index f22f97dae..d840db495 100644 --- a/DashWallet/zh-Hans.lproj/Localizable.strings +++ b/DashWallet/zh-Hans.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Save"; diff --git a/DashWallet/zh-Hant-TW.lproj/Localizable.strings b/DashWallet/zh-Hant-TW.lproj/Localizable.strings index 851bfb46a..ee5cadf2f 100644 --- a/DashWallet/zh-Hant-TW.lproj/Localizable.strings +++ b/DashWallet/zh-Hant-TW.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "Filter"; /* No comment provided by engineer. */ -"Filter Transactions" = "Filter Transactions"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "Filtered by"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "Gift Card"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "Reward"; -/* No comment provided by engineer. */ -"Rewards" = "Rewards"; - /* No comment provided by engineer. */ "Save" = "Save"; diff --git a/DashWallet/zh.lproj/Localizable.strings b/DashWallet/zh.lproj/Localizable.strings index 68cfb2d72..2b7ea3f66 100644 --- a/DashWallet/zh.lproj/Localizable.strings +++ b/DashWallet/zh.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "过滤"; /* No comment provided by engineer. */ -"Filter Transactions" = "过滤交易"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "筛选条件"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "礼品卡"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "奖励"; -/* No comment provided by engineer. */ -"Rewards" = "奖励"; - /* No comment provided by engineer. */ "Save" = "保存"; diff --git a/DashWallet/zh_TW.lproj/Localizable.strings b/DashWallet/zh_TW.lproj/Localizable.strings index 8ea2e8065..03350e403 100644 --- a/DashWallet/zh_TW.lproj/Localizable.strings +++ b/DashWallet/zh_TW.lproj/Localizable.strings @@ -935,7 +935,7 @@ "Filter" = "篩選"; /* No comment provided by engineer. */ -"Filter Transactions" = "過濾交易"; +"Filter transactions" = "Filter transactions"; /* No comment provided by engineer. */ "Filtered by" = "過濾條件"; @@ -1042,6 +1042,9 @@ /* Explore Dash: Filters */ "Gift Card" = "禮物卡"; +/* No comment provided by engineer. */ +"Gift card" = "Gift card"; + /* DashSpend */ "Gift card purchase successful" = "Gift card purchase successful"; @@ -2093,9 +2096,6 @@ /* No comment provided by engineer. */ "Reward" = "報酬"; -/* No comment provided by engineer. */ -"Rewards" = "報酬"; - /* No comment provided by engineer. */ "Save" = "儲存"; From 05853b6642a7d8f8ed80245cef0495adbf69d203 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Mon, 9 Jun 2025 10:07:04 +0700 Subject: [PATCH 10/13] feat: filtering txs by gift cards --- .../Sources/UI/Home/Views/HomeViewModel.swift | 116 +++++++++--------- .../Home/Views/TransactionFilterDialog.swift | 2 +- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift index ef327d530..7dd46e6b7 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift @@ -28,6 +28,7 @@ public enum HomeTxDisplayMode: UInt { case received case sent case rewards + case giftCard } class HomeViewModel: ObservableObject { @@ -179,15 +180,7 @@ class HomeViewModel: ObservableObject { var items: [TransactionListDataItem] = transactions.compactMap { tx -> TransactionListDataItem? in Tx.shared.updateRateIfNeeded(for: tx) - if self.displayMode == .sent && tx.direction != .sent { - return nil - } - - if self.displayMode == .received && (tx.direction != .received || tx is DSCoinbaseTransaction) { - return nil - } - - if self.displayMode == .rewards && !(tx is DSCoinbaseTransaction) { + if !self.passesFilter(tx: tx, displayMode: self.displayMode) { return nil } @@ -250,15 +243,7 @@ class HomeViewModel: ObservableObject { self.queue.async { [weak self] in guard let self = self else { return } - if self.displayMode == .sent && tx.direction != .sent { - return - } - - if self.displayMode == .received && (tx.direction != .received || tx is DSCoinbaseTransaction) { - return - } - - if self.displayMode == .rewards && !(tx is DSCoinbaseTransaction) { + if !self.passesFilter(tx: tx, displayMode: self.displayMode) { return } @@ -367,43 +352,6 @@ class HomeViewModel: ObservableObject { return (false, 0) } } - - - private func resolveMetadata(for txId: Data) -> TxRowMetadata? { - var finalMetadata: TxRowMetadata? = nil - - for provider in self.metadataProviders { - // Safely access the provider's metadata - let providerMetadata = provider.availableMetadata - guard let metadata = providerMetadata[txId] else { continue } - - if finalMetadata == nil { - finalMetadata = metadata - } else { - if finalMetadata?.title == nil { - finalMetadata?.title = metadata.title - } - - if finalMetadata?.details == nil { - finalMetadata?.details = metadata.details - } - - if finalMetadata?.icon == nil { - finalMetadata?.icon = metadata.icon - } - - if finalMetadata?.iconId == nil { - finalMetadata?.iconId = metadata.iconId - } - - if finalMetadata?.secondaryIcon == nil { - finalMetadata?.secondaryIcon = metadata.secondaryIcon - } - } - } - - return finalMetadata - } } // MARK: - CoinJoin @@ -459,7 +407,7 @@ extension HomeViewModel { } } -// MARK: - Metadata Providers +// MARK: - Metadata extension HomeViewModel { private func setupMetadataProviders() { @@ -481,6 +429,62 @@ extension HomeViewModel { .store(in: &cancellableBag) } } + + private func resolveMetadata(for txId: Data) -> TxRowMetadata? { + var finalMetadata: TxRowMetadata? = nil + + // Metadata will not be replaced if already found, so in case + // of conflicts metadataProviders should be sorted by priority + for provider in self.metadataProviders { + let providerMetadata = provider.availableMetadata + guard let metadata = providerMetadata[txId] else { continue } + + if finalMetadata == nil { + finalMetadata = metadata + } else { + if finalMetadata?.title == nil { + finalMetadata?.title = metadata.title + } + + if finalMetadata?.details == nil { + finalMetadata?.details = metadata.details + } + + if finalMetadata?.icon == nil { + finalMetadata?.icon = metadata.icon + } + + if finalMetadata?.iconId == nil { + finalMetadata?.iconId = metadata.iconId + } + + if finalMetadata?.secondaryIcon == nil { + finalMetadata?.secondaryIcon = metadata.secondaryIcon + } + } + } + + return finalMetadata + } + + private func passesFilter(tx: DSTransaction, displayMode: HomeTxDisplayMode) -> Bool { + switch displayMode { + case .all: + return true + case .sent: + return tx.direction == .sent + case .received: + return tx.direction == .received && !(tx is DSCoinbaseTransaction) + case .rewards: + return tx is DSCoinbaseTransaction + case .giftCard: + return isGiftCard(tx: tx) + } + } + + private func isGiftCard(tx: DSTransaction) -> Bool { + return GiftCardMetadataProvider.shared.availableMetadata[tx.txHashData] != nil + } } // MARK: - Shortcuts diff --git a/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift index 5c82fd622..2784d0fc2 100644 --- a/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift +++ b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift @@ -10,7 +10,7 @@ struct TransactionFilterDialog: View { FilterOption(mode: .all, title: NSLocalizedString("All", comment: ""), icon: .custom("image.filter.options")), FilterOption(mode: .sent, title: NSLocalizedString("Sent", comment: ""), icon: .custom("tx.item.sent.icon")), FilterOption(mode: .received, title: NSLocalizedString("Received", comment: ""), icon: .custom("tx.item.received.icon")), - FilterOption(mode: .rewards, title: NSLocalizedString("Gift card", comment: ""), icon: .custom("image.dashspend.giftcard")) + FilterOption(mode: .giftCard, title: NSLocalizedString("Gift card", comment: ""), icon: .custom("image.dashspend.giftcard")) ] var body: some View { From 071fe6db71db9eeeddccfaa6f156e11e366a8861 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Mon, 9 Jun 2025 11:49:43 +0700 Subject: [PATCH 11/13] feat: show saved gift card data on tapping a gift card row --- .../DashSpend/GiftCardDetailsViewModel.swift | 35 +++++++++++++------ .../Sources/UI/Home/Views/HomeView.swift | 23 +++++++----- .../Home/Views/TransactionFilterDialog.swift | 2 +- .../UI/SwiftUI Components/BottomSheet.swift | 2 +- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift index 940ec98cb..18bed2bd6 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift @@ -60,7 +60,8 @@ class GiftCardDetailsViewModel: ObservableObject { } func startObserving() { - // Observe gift card changes + loadExistingMetadata() + giftCardsDAO.observeCard(byTxId: txId) .receive(on: DispatchQueue.main) .sink { [weak self] giftCard in @@ -86,7 +87,7 @@ class GiftCardDetailsViewModel: ObservableObject { } .store(in: &cancellableBag) - self.txMetadataDAO.$lastChange // TODO: what if we open this from home screen + self.txMetadataDAO.$lastChange .receive(on: DispatchQueue.main) .sink { [weak self] change in guard let self = self, let change = change else { return } @@ -94,15 +95,7 @@ class GiftCardDetailsViewModel: ObservableObject { Task { switch change { case .created(let metadata), .updated(let metadata, _): - if let customIconId = metadata.customIconId, - let iconBitmap = await self.customIconDAO.getBitmap(id: customIconId) { - guard let image = UIImage(data: iconBitmap.imageData) else { - DSLogger.log("Failed to create image from data for tx icon: \(metadata.txHash.hexEncodedString())") - return - } - - self.uiState.merchantIcon = image - } + await self.loadIcon(metadata: metadata) default: break } @@ -218,6 +211,26 @@ class GiftCardDetailsViewModel: ObservableObject { } } + private func loadExistingMetadata() { + Task { + if let metadata = txMetadataDAO.get(by: txId) { + await self.loadIcon(metadata: metadata) + } + } + } + + private func loadIcon(metadata: TransactionMetadata) async { + if let customIconId = metadata.customIconId, + let iconBitmap = await self.customIconDAO.getBitmap(id: customIconId) { + guard let image = UIImage(data: iconBitmap.imageData) else { + DSLogger.log("Failed to create image from data for tx icon: \(metadata.txHash.hexEncodedString())") + return + } + + self.uiState.merchantIcon = image + } + } + private func generateBarcode(from string: String, format: String) { guard let filter = CIFilter(name: "CICode128BarcodeGenerator") else { return } diff --git a/DashWallet/Sources/UI/Home/Views/HomeView.swift b/DashWallet/Sources/UI/Home/Views/HomeView.swift index cee45ea6f..80f60a1df 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeView.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeView.swift @@ -295,16 +295,12 @@ struct HomeViewContent: View { .sheet(item: $selectedTxDataItem) { item in TransactionDetailsSheet(item: item) } - .sheet(isPresented: .constant(giftCardTxId != nil), onDismiss: { - giftCardTxId = nil - }) { - if let txId = giftCardTxId { - GiftCardDetailsSheet(txId: txId) - } + .sheet(item: $giftCardTxId) { txId in + GiftCardDetailsSheet(txId: txId) } .sheet(isPresented: $showFilterDialog) { let dialog = TransactionFilterDialog( - selectedFilter: .constant(viewModel.displayMode), + selectedFilter: viewModel.displayMode, onFilterSelected: { mode in viewModel.displayMode = mode } @@ -437,7 +433,12 @@ struct HomeViewContent: View { dashAmount: txItem.signedDashAmount, overrideFiatAmount: txItem.fiatAmount ) { - self.selectedTxDataItem = txDataItem + // Check if this is a gift card transaction + if GiftCardMetadataProvider.shared.availableMetadata[txItem.txHashData] != nil { + self.giftCardTxId = txItem.txHashData + } else { + self.selectedTxDataItem = txDataItem + } } } } @@ -520,3 +521,9 @@ struct TransactionDetailsSheet: View { } } } + +extension Data: Identifiable { + public var id: String { + return self.base64EncodedString() + } +} diff --git a/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift index 2784d0fc2..44be34848 100644 --- a/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift +++ b/DashWallet/Sources/UI/Home/Views/TransactionFilterDialog.swift @@ -3,7 +3,7 @@ import UIKit struct TransactionFilterDialog: View { @Environment(\.presentationMode) private var presentationMode - @Binding var selectedFilter: HomeTxDisplayMode + let selectedFilter: HomeTxDisplayMode var onFilterSelected: (HomeTxDisplayMode) -> Void private let filterOptions: [FilterOption] = [ diff --git a/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift b/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift index c4fd83eb5..fe332cfad 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift @@ -56,7 +56,7 @@ struct BottomSheet: View { .padding(.top, 6) Spacer() - + Button { presentationMode.wrappedValue.dismiss() } label: { From 947a95d31782dc5048ec8d952eeac24de6185ea2 Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Tue, 10 Jun 2025 16:38:57 +0700 Subject: [PATCH 12/13] fix: explore db version --- .gitignore | 1 + .../Database Connection/ExploreDatabaseConnection.swift | 2 +- .../Explore Dash/Services/ExploreDatabaseSyncManager.swift | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 710c043d2..7159cda8b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ TODO.md */Topper-Info.plist */Coinbase-Info.plist */ZenLedger-Info.plist +CLAUDE.md diff --git a/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift b/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift index 31ff726d5..3084cc015 100644 --- a/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift +++ b/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift @@ -46,7 +46,7 @@ class ExploreDatabaseConnection { guard let dbPath = dbPath() else { throw ExploreDatabaseConnectionError.fileNotFound } do { - db = try Connection(nil ?? dbPath) + db = try Connection(dbPath) } catch { print(error) } diff --git a/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift b/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift index 858d6ccda..461f6d899 100644 --- a/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift +++ b/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift @@ -20,7 +20,7 @@ import Foundation import SSZipArchive // TODO: Move it to plist and note in release process -let gsFilePath = "gs://dash-wallet-firebase.appspot.com/explore/explore-v2.db" +let gsFilePath = "gs://dash-wallet-firebase.appspot.com/explore/explore-v3.db" private let fileName = "explore" From bd9e166d8ed4adedef7a5c8eb151a08c9ee4765c Mon Sep 17 00:00:00 2001 From: Andrei Ashikhmin Date: Tue, 10 Jun 2025 16:56:45 +0700 Subject: [PATCH 13/13] chore: disable buy button if syncing or no network --- .../Details/Views/PointOfUseDetailsView.swift | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift index a374d313c..9d0eb4bf2 100644 --- a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift +++ b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift @@ -21,9 +21,14 @@ import Combine // MARK: - PointOfUseDetailsView -class PointOfUseDetailsView: UIView { +class PointOfUseDetailsView: UIView, SyncingActivityMonitorObserver, NetworkReachabilityHandling { private var disposeBag = Set() private let ctxSpendService = CTXSpendService.shared + private let syncMonitor = SyncingActivityMonitor.shared + + // NetworkReachabilityHandling requirements + var networkStatusDidChange: ((NetworkStatus) -> ())? + var reachabilityObserver: Any! public var payWithDashHandler: (()->())? public var sellDashHandler: (()->())? @@ -41,6 +46,7 @@ class PointOfUseDetailsView: UIView { var nameLabel: UILabel! var subLabel: UILabel! var addressLabel: UILabel! + private var payButton: ActionButton! internal let merchant: ExplorePointOfUse internal var isShowAllHidden: Bool @@ -108,6 +114,11 @@ class PointOfUseDetailsView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + syncMonitor.remove(observer: self) + stopNetworkMonitoring() + } @objc func callAction() { @@ -185,8 +196,20 @@ class PointOfUseDetailsView: UIView { .receive(on: DispatchQueue.main) .sink { [weak self] isSignedIn in self?.refreshLoginStatus() + self?.updateButtonState() } .store(in: &disposeBag) + + // Monitor network status + networkStatusDidChange = { [weak self] _ in + DispatchQueue.main.async { + self?.updateButtonState() + } + } + startNetworkMonitoring() + + // Monitor sync status + syncMonitor.add(observer: self) } } @@ -317,7 +340,7 @@ extension PointOfUseDetailsView { @objc internal func configureBottomButton() { - let payButton = ActionButton() + payButton = ActionButton() payButton.translatesAutoresizingMaskIntoConstraints = false payButton.addTarget(self, action: #selector(payAction), for: .touchUpInside) containerView.addArrangedSubview(payButton) @@ -352,6 +375,9 @@ extension PointOfUseDetailsView { NSLayoutConstraint.activate([ payButton.heightAnchor.constraint(equalToConstant: 48), ]) + + // Set initial button state + updateButtonState() } private static func getEmailText() -> String { @@ -394,6 +420,32 @@ extension PointOfUseDetailsView { loginStatusView.isHidden = true } } + + private func updateButtonState() { + guard let payButton = payButton, + case .merchant(let m) = merchant.category, + m.paymentMethod == .giftCard else { + return + } + + let isActive = merchant.active + let isOnline = networkStatus == .online + let isSynced = syncMonitor.state == .syncDone + + payButton.isEnabled = isActive && isOnline && isSynced + } +} + +// MARK: - SyncingActivityMonitorObserver + +extension PointOfUseDetailsView { + func syncingActivityMonitorProgressDidChange(_ progress: Double) { } + + func syncingActivityMonitorStateDidChange(previousState: SyncingActivityMonitor.State, state: SyncingActivityMonitor.State) { + DispatchQueue.main.async { [weak self] in + self?.updateButtonState() + } + } } // MARK: - VerticalButton