From 20cb477cab3ac9e5e9ad1482eab30516e24f3944 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Fri, 10 Apr 2026 08:12:36 -0700 Subject: [PATCH 01/17] fix: allow the payment button on the nav bar to be clicked --- DashWallet/Sources/UI/Main/MainTabbarController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DashWallet/Sources/UI/Main/MainTabbarController.swift b/DashWallet/Sources/UI/Main/MainTabbarController.swift index 07c160bb6..28d238ad0 100644 --- a/DashWallet/Sources/UI/Main/MainTabbarController.swift +++ b/DashWallet/Sources/UI/Main/MainTabbarController.swift @@ -158,7 +158,7 @@ class MainTabbarController: UITabBarController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - tabBar.addSubview(paymentButton) + view.bringSubviewToFront(paymentButton) } override func viewDidLoad() { @@ -172,8 +172,8 @@ class MainTabbarController: UITabBarController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - // Add Payment Button again to make sure it's at the top - tabBar.addSubview(paymentButton) + // Bring Payment Button to the front to make sure it's at the top + view.bringSubviewToFront(paymentButton) } } @@ -260,7 +260,7 @@ extension MainTabbarController { paymentButton = PaymentButton() paymentButton.translatesAutoresizingMaskIntoConstraints = false paymentButton.addTarget(self, action: #selector(paymentButtonAction), for: .touchUpInside) - tabBar.addSubview(paymentButton) + view.addSubview(paymentButton) NSLayoutConstraint.activate([ paymentButton.centerXAnchor.constraint(equalTo: tabBar.centerXAnchor), From 19a5d888ce22796b328feb281c0bead67aa5d524 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Fri, 10 Apr 2026 08:13:12 -0700 Subject: [PATCH 02/17] fix(dashpay): fix compile issues in HomeViewController+Shortcuts --- .../UI/Home/HomeViewController+Shortcuts.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift b/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift index dbd3a998e..72a58ce66 100644 --- a/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift +++ b/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift @@ -169,7 +169,8 @@ extension HomeViewController: DWLocalCurrencyViewControllerDelegate, ExploreView private func showSendToContact() { #if DASHPAY - let controller = DWContactsViewController() + let controller = DWContactsViewController(payModel: payModel, dataProvider: dataProvider) + controller.intent = .payToSelector controller.payDelegate = self let navigationController = BaseNavigationController(rootViewController: controller) present(navigationController, animated: true, completion: nil) @@ -311,3 +312,14 @@ extension HomeViewController: DWLocalCurrencyViewControllerDelegate, ExploreView showGiftCardDetails(txId: txId) } } +#if DASHPAY +// MARK: DWContactsViewControllerPayDelegate +extension HomeViewController: DWContactsViewControllerPayDelegate { + func contactsViewController(_ controller: DWContactsViewController, payTo item: DWDPBasicUserItem) { + dismiss(animated: true) { [weak self] in + self?.performPay(toUser: item) + } + } +} +#endif + From c268c59e7bc5b4cfa05e7c4278d175f01ae72d43 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 14 Apr 2026 10:08:44 -0700 Subject: [PATCH 03/17] fix: simplify the bottom nav bar, specifically the payments button --- .../UI/Main/MainTabbarController.swift | 92 ++++++++----------- 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/DashWallet/Sources/UI/Main/MainTabbarController.swift b/DashWallet/Sources/UI/Main/MainTabbarController.swift index 28d238ad0..9ca66d382 100644 --- a/DashWallet/Sources/UI/Main/MainTabbarController.swift +++ b/DashWallet/Sources/UI/Main/MainTabbarController.swift @@ -29,10 +29,6 @@ private enum MainTabbarTabs: Int, CaseIterable { } extension MainTabbarTabs { - var isEmpty: Bool { - self == .payment - } - var icon: UIImage { let name: String @@ -42,7 +38,7 @@ extension MainTabbarTabs { case .contacts: name = "tabbar_contacts_icon" case .payment: - return UIImage() + name = "tabbar_pay_button" case .explore: name = "tabbar_discover_icon" case .more: @@ -51,7 +47,7 @@ extension MainTabbarTabs { return UIImage(named: name)!.withRenderingMode(.alwaysOriginal) } - + var selectedIcon: UIImage { let name: String @@ -61,7 +57,7 @@ extension MainTabbarTabs { case .contacts: name = "tabbar_contacts_selected" case .payment: - return UIImage() + name = "tabbar_pay_button" case .explore: name = "tabbar_discover_selected" case .more: @@ -84,7 +80,7 @@ class MainTabbarController: UITabBarController { weak var homeController: HomeViewController? weak var menuNavigationController: MainMenuViewController? - + #if DASHPAY weak var contactsNavigationController: DWRootContactsViewController? weak var exploreNavigationController: ExploreViewController? @@ -94,7 +90,7 @@ class MainTabbarController: UITabBarController { @objc weak var wipeDelegate: DWWipeDelegate? - private var paymentButton: PaymentButton! + private var paymentIsOpened = false @objc var isDemoMode = false @@ -105,7 +101,7 @@ class MainTabbarController: UITabBarController { // TODO: Move it out from here and initialize the model inside home view controller @objc var homeModel: DWHomeProtocol! - + #if DASHPAY // TODO: MOCK_DASHPAY remove when not mocked private var blockchainIdentity: DSBlockchainIdentity? { @@ -146,35 +142,15 @@ class MainTabbarController: UITabBarController { fatalError("init(coder:) has not been implemented") } - // MARK: Actions - - @objc - private func paymentButtonAction() { - showPaymentsController(withActivePage: .none) - } - // MARK: Life Cycle - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - view.bringSubviewToFront(paymentButton) - } - override func viewDidLoad() { super.viewDidLoad() delegate = self - configureHierarchy() + tabBar.barTintColor = .dw_background() setupRatesErrorHandling() } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // Bring Payment Button to the front to make sure it's at the top - view.bringSubviewToFront(paymentButton) - } } // MARK: - Private @@ -212,13 +188,15 @@ extension MainTabbarController { } #endif - // Payment - item = UITabBarItem(title: "", image: UIImage(), tag: 2) + // Payment (tapping this tab opens the payment modal instead of switching tabs) + let paymentImage = Self.makePaymentTabImage() + item = UITabBarItem(title: nil, image: paymentImage, selectedImage: paymentImage) item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) + item.accessibilityIdentifier = "tabbar_payments_button" - let vc = EmptyController() - vc.tabBarItem = item - viewControllers.append(vc) + let paymentVC = EmptyController() + paymentVC.tabBarItem = item + viewControllers.append(paymentVC) #if DASHPAY if identity != nil { @@ -256,25 +234,30 @@ extension MainTabbarController { self.viewControllers = viewControllers } - private func configureHierarchy() { - paymentButton = PaymentButton() - paymentButton.translatesAutoresizingMaskIntoConstraints = false - paymentButton.addTarget(self, action: #selector(paymentButtonAction), for: .touchUpInside) - view.addSubview(paymentButton) + /// Creates a tab bar image with a blue circle background and the payment icon centered on top. + private static func makePaymentTabImage() -> UIImage { + let size: CGFloat = 47 + let rect = CGRect(x: 0, y: 0, width: size, height: size) - NSLayoutConstraint.activate([ - paymentButton.centerXAnchor.constraint(equalTo: tabBar.centerXAnchor), - paymentButton.topAnchor.constraint(equalTo: tabBar.topAnchor, constant: UIDevice.hasHomeIndicator ? 4 : 1), + let renderer = UIGraphicsImageRenderer(size: rect.size) + let image = renderer.image { context in + // Draw blue circle background + UIColor.dw_dashBlue().setFill() + UIBezierPath(ovalIn: rect).fill() - paymentButton.widthAnchor.constraint(equalToConstant: PaymentButton.kCenterCircleSize), - paymentButton.heightAnchor.constraint(equalToConstant: PaymentButton.kCenterCircleSize), - ]) + // Draw icon centered + if let icon = UIImage(named: "tabbar_pay_button") { + let iconSize = CGSize(width: 22, height: 22) + let iconOrigin = CGPoint(x: (size - iconSize.width) / 2, y: (size - iconSize.height) / 2) + icon.draw(in: CGRect(origin: iconOrigin, size: iconSize)) + } + } - tabBar.barTintColor = .dw_background() + return image.withRenderingMode(.alwaysOriginal) } private func closePayments(completion: (() -> Void)? = nil) { - paymentButton.isOpened = false + paymentIsOpened = false guard let top = selectedViewController?.topController(), top != selectedViewController @@ -378,7 +361,7 @@ extension MainTabbarController: PaymentsViewControllerDelegate { } func paymentsViewControllerWantsToImportPrivateKey(_ controller: PaymentsViewController) { - paymentButton.isOpened = false + paymentIsOpened = false controller.dismiss(animated: true) { self.performScanQRCodeAction() @@ -398,7 +381,7 @@ extension MainTabbarController: HomeViewControllerDelegate { } func showPaymentsController(withActivePage pageIndex: PaymentsViewControllerState) { - paymentButton.isOpened = true + paymentIsOpened = true let receiveModel = DWReceiveModel() let payModel = DWPayModel() @@ -427,7 +410,12 @@ extension MainTabbarController: HomeViewControllerDelegate { extension MainTabbarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { - !(viewController is EmptyController) + if viewController is EmptyController { + // Intercept the payment tab tap — show the payment modal instead of switching tabs + showPaymentsController(withActivePage: .none) + return false + } + return true } } From ca983d89da13a0084fe098599ad6912004b78fbd Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 14 Apr 2026 10:11:19 -0700 Subject: [PATCH 04/17] fix: fix padding on Shortcut bar on home screen --- .../Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift b/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift index 76b8535b4..12d4466c4 100644 --- a/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift +++ b/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift @@ -49,6 +49,8 @@ protocol ShortcutsViewDelegate: AnyObject { // MARK: - ShortcutsView class ShortcutsView: UIView { + private static let verticalPadding: CGFloat = 8.0 + private var cancellableBag = Set() private let viewModel: HomeViewModel private var lastLayoutWidth: CGFloat = 0 @@ -140,7 +142,7 @@ class ShortcutsView: UIView { var cellSize = cellSize(for: contentSizeCategory, viewWidth: width) cellSize.height = ceil(cellSize.height) // This fixes the autolayout issue when the size of the cell is higher than the collection view itself - collectionViewHeightConstraint.constant = cellSize.height + collectionViewHeightConstraint.constant = cellSize.height + ShortcutsView.verticalPadding * 2 setNeedsUpdateConstraints() if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { @@ -212,7 +214,7 @@ extension ShortcutsView: UICollectionViewDataSource, UICollectionViewDelegate, U let cellCount = CGFloat(collectionView.numberOfItems(inSection: section)) var inset = (collectionView.bounds.size.width - (cellCount * cellWidth) - ((cellCount - 1) * cellSpacing)) * 0.5 inset = max(inset, 0.0) - return UIEdgeInsets(top: 0.0, left: inset, bottom: 0.0, right: inset) + return UIEdgeInsets(top: ShortcutsView.verticalPadding, left: inset, bottom: ShortcutsView.verticalPadding, right: inset) } } From b5ae4ebe6f9bed396ac1ce94ff5630a3ab34b950 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 14 Apr 2026 10:18:31 -0700 Subject: [PATCH 05/17] fix: update scan button to blue for dark mode --- .../shortcut_scanToPay.imageset/scan.svg | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_scanToPay.imageset/scan.svg b/DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_scanToPay.imageset/scan.svg index 16ef9a903..3a0959c64 100644 --- a/DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_scanToPay.imageset/scan.svg +++ b/DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_scanToPay.imageset/scan.svg @@ -1,13 +1,13 @@ - - - - - - - - - - - + + + + + + + + + + + From 405dfc9c1c36b72afbf72bac65ba66a59f65f00b Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 14 Apr 2026 13:14:31 -0700 Subject: [PATCH 06/17] fix: center back arrow in back buttons --- .../Sources/UI/Views/Navigation/BaseNavigationController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift b/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift index f516962e3..b53e05c2f 100644 --- a/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift +++ b/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift @@ -175,7 +175,6 @@ extension BaseNavigationController: UINavigationControllerDelegate { backButton.frame = .init(x: 0, y: 0, width: 30, height: 30) backButton.setImage(UIImage(systemName: "arrow.backward"), for: .normal) backButton.tintColor = backButtonTintColor - backButton.imageEdgeInsets = .init(top: 0, left: -10, bottom: 0, right: 0) backButton.addTarget(self, action: #selector(backButtonAction), for: .touchUpInside) let item = UIBarButtonItem(customView: backButton) From cfef5e1756db633150394505d5fadd9c64dc2397 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 14 Apr 2026 13:15:13 -0700 Subject: [PATCH 07/17] fix: prevent crash if not FaceID nor TouchID --- .../Advanced Security/Model/DWBaseAdvancedSecurityModel.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m b/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m index 8048e73cd..d400421ef 100644 --- a/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m +++ b/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m @@ -158,7 +158,9 @@ - (NSAttributedString *)currentSpendingConfirmationDescriptionWithFont:(UIFont * else if (self.hasFaceID) { string = NSLocalizedString(@"You can authenticate with Face ID for payments below", nil); } - NSParameterAssert(string); + else { + string = NSLocalizedString(@"You can authenticate with biometrics for payments below", nil); + } string = [string stringByAppendingString:@" "]; From f870628e51560f567157c739d81bbfdd81498329 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 14 Apr 2026 14:03:41 -0700 Subject: [PATCH 08/17] fix: remove invisible button on BuySellPortal --- DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard | 3 --- 1 file changed, 3 deletions(-) diff --git a/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard b/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard index ec4a9a638..3198c6f14 100644 --- a/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard +++ b/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard @@ -586,9 +586,6 @@ - - - From a7d67765a124e5c139be00ae2ee9fe603214b894 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:43:20 +0300 Subject: [PATCH 09/17] feat(search): add SwiftUI search bar styles and assets --- .../UI/SwiftUI Components/Color+DWStyle.swift | 16 +- .../UI/SwiftUI Components/SearchBar.swift | 162 ++++++++++++++++++ .../Black1000Alpha30.colorset/Contents.json | 20 +++ .../Black1000Alpha50.colorset/Contents.json | 20 +++ .../SearchBackground.colorset/Contents.json | 20 +++ 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift create mode 100644 Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha30.colorset/Contents.json create mode 100644 Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha50.colorset/Contents.json create mode 100644 Shared/Resources/SharedAssets.xcassets/Colors/SearchBackground.colorset/Contents.json diff --git a/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift b/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift index c28e98bd0..e6fd5ff47 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift @@ -63,7 +63,13 @@ extension Color { static var systemYellow: Color { Color("SystemYellowColor") } - + +// Search + + static var searchBg: Color { + Color("SearchBackground") + } + // Blue static var blueAlpha5: Color { @@ -80,6 +86,14 @@ extension Color { Color("BlackAlpha40") } + static var black1000Alpha30: Color { + Color("Black1000Alpha30") + } + + static var black1000Alpha50: Color { + Color("Black1000Alpha50") + } + // Gray static var gray50: Color { diff --git a/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift new file mode 100644 index 000000000..095502bad --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift @@ -0,0 +1,162 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 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 SwiftUI + +struct SearchBar: View { + + private enum Layout { + static let fieldHeight: CGFloat = 40 + static let fieldCornerRadius: CGFloat = 14 + static let fieldHorizontalPadding: CGFloat = 14 + static let fieldSpacing: CGFloat = 10 + static let cancelHorizontalPadding: CGFloat = 12 + static let cancelVerticalPadding: CGFloat = 6 + static let animationDuration: CGFloat = 0.25 + } + + @Binding var text: String + @FocusState private var isFocused: Bool + @State private var isEditing: Bool = false + @State private var cancelButtonWidth: CGFloat = 0 + + var body: some View { + GeometryReader { proxy in + HStack(spacing: 0) { + HStack(spacing: Layout.fieldSpacing) { + magnifyingglass + searchField + clearButton + } + .padding(.horizontal, Layout.fieldHorizontalPadding) + .frame(height: Layout.fieldHeight) + .background(Color.searchBg) + .clipShape(.rect(cornerRadius: Layout.fieldCornerRadius)) + .frame(maxWidth: .infinity, alignment: .leading) + + if isEditing { + cancelButton + .transition(.move(edge: .trailing).combined(with: .opacity)) + } + } + .animation(.easeInOut(duration: Layout.animationDuration), value: isEditing) + .onAppear { + isEditing = isFocused + } + .onChange(of: isFocused) { focused in + withAnimation(.easeInOut(duration: Layout.animationDuration)) { + isEditing = focused + } + } + } + } + + @ViewBuilder + private var clearButton: some View { + if !text.isEmpty { + Button(action: { text = "" }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.tertiaryText) + } + } + } + + private var cancelButton: some View { + Button(action: { + text = "" + withAnimation(.easeInOut(duration: Layout.animationDuration)) { + isEditing = false + } + isFocused = false + }) { + Text(NSLocalizedString("Cancel", comment: "")) + .padding(.horizontal, Layout.cancelHorizontalPadding) + .padding(.vertical, Layout.cancelVerticalPadding) + } + .tint(.primaryText) + } + + private var cancelButtonMeasurement: some View { + cancelButton + .fixedSize() + .padding(.leading, Layout.fieldSpacing) + .hidden() + .captureSize { size in + if abs(cancelButtonWidth - size.width) > 0.5 { + cancelButtonWidth = size.width + } + } + } + + private var magnifyingglass: some View { + Image(systemName: "magnifyingglass") + .foregroundColor(Color.black1000Alpha50) + } + + @ViewBuilder + private var searchField: some View { + if #available(iOS 17.0, *) { + TextField( + text: $text, + prompt: Text(NSLocalizedString("Search", comment: "")).foregroundStyle(Color.black1000Alpha30) + ) { + EmptyView() + } + .focused($isFocused) + } else { + // Fallback on earlier versions + TextField(NSLocalizedString("Search", comment: ""), text: $text) + .autocapitalization(.none) + .disableAutocorrection(true) + .foregroundColor(.primaryText) + .focused($isFocused) + } + } +} + +private struct SearchBarSizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } +} + +private extension View { + func captureSize(_ onChange: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometry in + Color.clear + .preference(key: SearchBarSizePreferenceKey.self, value: geometry.size) + } + ) + .onPreferenceChange(SearchBarSizePreferenceKey.self, perform: onChange) + } +} + + +#Preview { + SearchBarPreview() +} + +private struct SearchBarPreview: View { + @State private var text = "" + var body: some View { + SearchBar(text: $text) + .padding() + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha30.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha30.colorset/Contents.json new file mode 100644 index 000000000..e7760a027 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha30.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha50.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha50.colorset/Contents.json new file mode 100644 index 000000000..6ca6d9858 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha50.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/SearchBackground.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/SearchBackground.colorset/Contents.json new file mode 100644 index 000000000..284bfd853 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/SearchBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0x8A", + "green" : "0x80", + "red" : "0x75" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From 303191735e6a8017fe62337b618e75f967030aa9 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:49:49 +0300 Subject: [PATCH 10/17] fix(search): adjust field layout and related color assets --- .../UI/SwiftUI Components/SearchBar.swift | 48 +++++++++---------- .../Contents.json | 22 ++++----- .../Colors/ShadowColor.colorset/Contents.json | 22 ++++----- .../Contents.json | 6 +-- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift index 095502bad..3bb2db530 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift @@ -35,32 +35,30 @@ struct SearchBar: View { @State private var cancelButtonWidth: CGFloat = 0 var body: some View { - GeometryReader { proxy in - HStack(spacing: 0) { - HStack(spacing: Layout.fieldSpacing) { - magnifyingglass - searchField - clearButton - } - .padding(.horizontal, Layout.fieldHorizontalPadding) - .frame(height: Layout.fieldHeight) - .background(Color.searchBg) - .clipShape(.rect(cornerRadius: Layout.fieldCornerRadius)) - .frame(maxWidth: .infinity, alignment: .leading) - - if isEditing { - cancelButton - .transition(.move(edge: .trailing).combined(with: .opacity)) - } + HStack(spacing: 0) { + HStack(spacing: Layout.fieldSpacing) { + magnifyingglass + searchField + clearButton } - .animation(.easeInOut(duration: Layout.animationDuration), value: isEditing) - .onAppear { - isEditing = isFocused + .padding(.horizontal, Layout.fieldHorizontalPadding) + .frame(height: Layout.fieldHeight) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.searchBg) + .clipShape(.rect(cornerRadius: Layout.fieldCornerRadius)) + + if isEditing { + cancelButton + .transition(.move(edge: .trailing).combined(with: .opacity)) } - .onChange(of: isFocused) { focused in - withAnimation(.easeInOut(duration: Layout.animationDuration)) { - isEditing = focused - } + } + .animation(.easeInOut(duration: Layout.animationDuration), value: isEditing) + .onAppear { + isEditing = isFocused + } + .onChange(of: isFocused) { focused in + withAnimation(.easeInOut(duration: Layout.animationDuration)) { + isEditing = focused } } } @@ -70,7 +68,7 @@ struct SearchBar: View { if !text.isEmpty { Button(action: { text = "" }) { Image(systemName: "xmark.circle.fill") - .foregroundColor(.tertiaryText) + .foregroundColor(Color.black1000Alpha30) } } } diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json index c6e5d3d43..a61d481a7 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json @@ -1,20 +1,20 @@ { - "info" : { - "version" : 1, - "author" : "xcode" - }, "colors" : [ { - "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { - "red" : "1.000", "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000" + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" } - } + }, + "idiom" : "universal" } - ] -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json index c60cc7caa..7e8f38fe3 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json @@ -1,20 +1,20 @@ { - "info" : { - "version" : 1, - "author" : "xcode" - }, "colors" : [ { - "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { - "red" : "0.000", "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.000" + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" } - } + }, + "idiom" : "universal" } - ] -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json index eb69d3844..757e0e003 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "250", - "green" : "250", - "red" : "250" + "blue" : "0xFA", + "green" : "0xFA", + "red" : "0xFA" } }, "idiom" : "universal" From 37ebbca1d7fa0be5321f86ac7e85da5adb032483 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:51:15 +0300 Subject: [PATCH 11/17] feat(settings): add SwiftUI local currency selector --- DashWallet.xcodeproj/project.pbxproj | 89 ++++++- .../Cells/LocalCurrencyCellView.swift | 153 +++++++++++ .../LocalCurrency/LocalCurrencyView.swift | 201 +++++++++++++++ .../LocalCurrencyViewModel.swift | 240 ++++++++++++++++++ .../UI/Menu/Settings/SettingsScreen.swift | 35 +-- .../Geometry Readers/FrameReader.swift | 96 +++++++ .../Geometry Readers/LocationReader.swift | 92 +++++++ .../ScrollViewWithOnScrollChanged.swift | 79 ++++++ 8 files changed, 950 insertions(+), 35 deletions(-) create mode 100644 DashWallet/Sources/UI/Menu/Settings/LocalCurrency/Cells/LocalCurrencyCellView.swift create mode 100644 DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyView.swift create mode 100644 DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift create mode 100644 DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/FrameReader.swift create mode 100644 DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/LocationReader.swift create mode 100644 DashWallet/Sources/UI/SwiftUI Components/ScrollViews/ScrollViewWithOnScrollChanged.swift diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index dc1743059..ed75d6f00 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -491,6 +491,22 @@ 47F4B6CD29485A8B00AED4C9 /* ConfirmOrderCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F4B6CC29485A8B00AED4C9 /* ConfirmOrderCells.swift */; }; 47FA3AFF29350929008D58DC /* SyncingActivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA3AFE29350929008D58DC /* SyncingActivityMonitor.swift */; }; 47FA3B0229364991008D58DC /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA3B0129364991008D58DC /* HTTPClient.swift */; }; + 5151C8C62F913BF100F0A604 /* Coinbase-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */; }; + 5151C8C72F913BF100F0A604 /* ZenLedger-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */; }; + 5151C8C82F913BF100F0A604 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */; }; + 5151C8C92F913BF100F0A604 /* Topper-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C42F913BF100F0A604 /* Topper-Info.plist */; }; + 5151C8CA2F913BF100F0A604 /* Coinbase-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */; }; + 5151C8CB2F913BF100F0A604 /* ZenLedger-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */; }; + 5151C8CC2F913BF100F0A604 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */; }; + 5151C8CD2F913BF100F0A604 /* Topper-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C42F913BF100F0A604 /* Topper-Info.plist */; }; + 5151CA232F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */; }; + 5151CA242F92335E00F0A604 /* LocalCurrencyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */; }; + 5151CA252F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */; }; + 5151CA262F92335E00F0A604 /* LocalCurrencyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */; }; + 5151CD622F926D1900F0A604 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CD612F926D1900F0A604 /* SearchBar.swift */; }; + 5151CD632F926D1900F0A604 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CD612F926D1900F0A604 /* SearchBar.swift */; }; + 5151CDF02F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */; }; + 5151CDF12F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */; }; 7502A4872AE401EF00ACDDD3 /* UsernameVotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7502A4862AE401EF00ACDDD3 /* UsernameVotingViewController.swift */; }; 7503643A2C89CFB70029EC0D /* CoinJoinProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */; }; 7503643B2C89CFB70029EC0D /* CoinJoinProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */; }; @@ -698,6 +714,10 @@ 75AA33D82BFB4A5A00F12465 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75AA33D72BFB4A5A00F12465 /* Extensions.swift */; }; 75AA33D92BFB4A5A00F12465 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75AA33D72BFB4A5A00F12465 /* Extensions.swift */; }; 75AE5A7F2A87C363006CD4BA /* DWConfirmUsernameContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C943B5712A40ED4200AF23C5 /* DWConfirmUsernameContentView.xib */; }; + 75B3C1A22E36100100CAFE02 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; + 75B3C1A32E36100100CAFE03 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; + 75B3C1B22E36100100CAFE05 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; + 75B3C1B32E36100100CAFE06 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; 75BDE7AC2BF3287400556791 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75BDE7AB2BF3287400556791 /* Toast.swift */; }; 75BDE7AD2BF3287400556791 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75BDE7AB2BF3287400556791 /* Toast.swift */; }; 75C1F0452AE26AC0006929CA /* CoinJoinLevelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C1F0442AE26AC0006929CA /* CoinJoinLevelsViewModel.swift */; }; @@ -779,8 +799,10 @@ 75FFD6C72BF495800032879E /* HomeViewController+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FFD6C62BF495800032879E /* HomeViewController+Shortcuts.swift */; }; 75FFD6C82BF495800032879E /* HomeViewController+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FFD6C62BF495800032879E /* HomeViewController+Shortcuts.swift */; }; 893AE6F535290DD1CEE89219 /* ImportPrivateKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53435DCAF024BC82E0247F3 /* ImportPrivateKeySheet.swift */; }; + 8A7607606A594C6E99A2FAC7 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; 91F0B8B92F3FAB7C00E713AE /* KeysOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F0B8B82F3FAB7C00E713AE /* KeysOverviewView.swift */; }; 91F0B8BA2F3FAB7C00E713AE /* KeysOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F0B8B82F3FAB7C00E713AE /* KeysOverviewView.swift */; }; + A0795C9369A1402498C00F69 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; A30AF4A11189A2EFB04EB535 /* libPods-WatchApp Extension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C22BC09CF6178544B2300A4 /* libPods-WatchApp Extension.a */; }; AA0000012DUMMYID001234567 /* AddProviderToGiftCardsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000032DUMMYID001234567 /* AddProviderToGiftCardsTable.swift */; }; AA0000022DUMMYID001234567 /* AddProviderToGiftCardsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000032DUMMYID001234567 /* AddProviderToGiftCardsTable.swift */; }; @@ -788,6 +810,8 @@ AA0000052DUMMYID001234567 /* DashSpend/GiftCardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000062DUMMYID001234567 /* DashSpend/GiftCardProvider.swift */; }; AE99C52D130994B720927E8F /* libPods-dashpay.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D6E23A2DA41FE189E773C705 /* libPods-dashpay.a */; }; AEAE60406265A73AFD573FD1 /* libPods-dashwallet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F3BA4BE42F52588AED15BF7E /* libPods-dashwallet.a */; }; + B3A1F2C4D5E6078901234568 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; + B3A1F2C4D5E6078901234569 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; B453B34B871E4125607DACDC /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6615F1246B5935365B05BD /* NavigationBar.swift */; }; BA4A72671BE11AFA00E39C01 /* BRAWWeakTimerTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4A72661BE11AFA00E39C01 /* BRAWWeakTimerTarget.swift */; }; BA54D3CE1B2EA74000C9CB28 /* TodayExtensionAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BA54D3CD1B2EA74000C9CB28 /* TodayExtensionAssets.xcassets */; }; @@ -814,7 +838,6 @@ C3DAD2CF247585C10001624F /* NSPredicate+DWFullTextSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DAD2CE247585C10001624F /* NSPredicate+DWFullTextSearch.m */; }; C909614D29EFF7D600002D82 /* WalletKeysOverviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909614C29EFF7D600002D82 /* WalletKeysOverviewModel.swift */; }; C909615129F158D700002D82 /* DerivationPathKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615029F158D700002D82 /* DerivationPathKeysViewController.swift */; }; - A0795C9369A1402498C00F69 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; C909615329F28E3700002D82 /* DerivationPathKeysModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615229F28E3700002D82 /* DerivationPathKeysModel.swift */; }; C909615529F297DD00002D82 /* DerivationPathKeysCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615429F297DD00002D82 /* DerivationPathKeysCell.swift */; }; C909615929F29C9200002D82 /* KeysOverviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615829F29C9200002D82 /* KeysOverviewCell.swift */; }; @@ -823,7 +846,6 @@ C917024129D462C6008C034D /* PayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C917024029D462C6008C034D /* PayViewController.swift */; }; C91E919729FBACE6003E7883 /* ExtendedPublicKeysModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E919629FBACE6003E7883 /* ExtendedPublicKeysModel.swift */; }; C91E91AE29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E91AD29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift */; }; - B3A1F2C4D5E6078901234568 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; C93078482A6AD4F500906E4B /* ConfirmPaymentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C956AF222A5C7D93002FAB75 /* ConfirmPaymentViewController.swift */; }; C93078492A6AD4FC00906E4B /* UpholdConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C956AF2E2A5D5F6B002FAB75 /* UpholdConfirmViewController.swift */; }; C930784A2A6AD52400906E4B /* UpholdConfirmTransferModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C956AF302A5D601C002FAB75 /* UpholdConfirmTransferModel.swift */; }; @@ -1018,8 +1040,6 @@ C94F5E8829D3E7E30034FD57 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C94F5E8729D3E7E30034FD57 /* GoogleService-Info.plist */; }; C94F5E8A29D3FCCF0034FD57 /* ShortcutAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8929D3FCCF0034FD57 /* ShortcutAction.swift */; }; C94F5E8E29D404850034FD57 /* ShortcutCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8D29D404850034FD57 /* ShortcutCell.swift */; }; - 75B3C1A22E36100100CAFE02 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; - 75B3C1B22E36100100CAFE05 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; C94F5E9029D4060A0034FD57 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8F29D4060A0034FD57 /* ShortcutsView.swift */; }; C956AF0C2A5B591A002FAB75 /* DashInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94D98202A4CC78F00F3BEE1 /* DashInputField.swift */; }; C956AF0D2A5B592E002FAB75 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9829CDD2A562822007132E4 /* TappableLabel.swift */; }; @@ -1102,7 +1122,6 @@ C9D2C6D42A320AA000D15901 /* DWRecoverTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A74EFFA2305464C00C475EB /* DWRecoverTextView.m */; }; C9D2C6D52A320AA000D15901 /* TransactionMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */; }; C9D2C6D62A320AA000D15901 /* ExtendedPublicKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E91AD29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift */; }; - B3A1F2C4D5E6078901234569 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; C9D2C6D82A320AA000D15901 /* AtmListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BDD28C1305E00490F5E /* AtmListViewController.swift */; }; C9D2C6D92A320AA000D15901 /* TxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A5145F2848F75B005A8E3E /* TxDetailViewController.swift */; }; C9D2C6DA2A320AA000D15901 /* NSAttributedString+Builder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472D13E9299E5396006903F1 /* NSAttributedString+Builder.swift */; }; @@ -1395,8 +1414,6 @@ C9D2C84B2A320AA000D15901 /* UITableView+DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F005FE297164600029EB10 /* UITableView+DashWallet.swift */; }; C9D2C84C2A320AA000D15901 /* TransferAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661B128FDC72700028A8D /* TransferAmountViewController.swift */; }; C9D2C84D2A320AA000D15901 /* ShortcutCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8D29D404850034FD57 /* ShortcutCell.swift */; }; - 75B3C1A32E36100100CAFE03 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; - 75B3C1B32E36100100CAFE06 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; C9D2C84F2A320AA000D15901 /* UIColor+DWStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A44312022CCA2A0009BAF7F /* UIColor+DWStyle.m */; }; C9D2C8502A320AA000D15901 /* DWEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = FBEF3AF021823CD800917AB6 /* DWEnvironment.m */; }; C9D2C8512A320AA000D15901 /* AtmItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BC928C1305E00490F5E /* AtmItemCell.swift */; }; @@ -1423,7 +1440,6 @@ C9D2C86F2A320AA000D15901 /* NavigationBarAppearanceCustomizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4730586F295AD62B004641DA /* NavigationBarAppearanceCustomizable.swift */; }; C9D2C8702A320AA000D15901 /* AmountObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471DD1B5290A901200E030C8 /* AmountObject.swift */; }; C9D2C8712A320AA000D15901 /* DerivationPathKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615029F158D700002D82 /* DerivationPathKeysViewController.swift */; }; - 8A7607606A594C6E99A2FAC7 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; C9D2C8722A320AA000D15901 /* DSTransaction+DashWallet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4709C31D2880247C00B4BD48 /* DSTransaction+DashWallet.m */; }; C9D2C8742A320AA000D15901 /* DWLocalCurrencyModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7C1C234B771400451078 /* DWLocalCurrencyModel.m */; }; C9D2C8762A320AA000D15901 /* DWUpholdMainnetConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = FB8ACEB522E0502100EE5035 /* DWUpholdMainnetConstants.m */; }; @@ -2451,6 +2467,14 @@ 4944819B9E17D4CC7DBFF293 /* Pods-WatchApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.release.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.release.xcconfig"; sourceTree = ""; }; 4C22BC09CF6178544B2300A4 /* libPods-WatchApp Extension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WatchApp Extension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 4D37A3F73905BC239A9D127D /* Pods-WatchApp Extension.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.testnet.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.testnet.xcconfig"; sourceTree = ""; }; + 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Coinbase-Info.plist"; sourceTree = ""; }; + 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 5151C8C42F913BF100F0A604 /* Topper-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Topper-Info.plist"; sourceTree = ""; }; + 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ZenLedger-Info.plist"; sourceTree = ""; }; + 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyView.swift; sourceTree = ""; }; + 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyViewModel.swift; sourceTree = ""; }; + 5151CD612F926D1900F0A604 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; + 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyCellView.swift; sourceTree = ""; }; 62C1F7A3ABEE7CF3BBB270C4 /* Pods-DashWalletTests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.testflight.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.testflight.xcconfig"; sourceTree = ""; }; 67217DE34817FB304EDEA8D4 /* Pods-dashpay.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.testnet.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.testnet.xcconfig"; sourceTree = ""; }; 6A44C167AF8F881176AFB256 /* Pods-TodayExtension.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.testflight.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.testflight.xcconfig"; sourceTree = ""; }; @@ -2590,6 +2614,8 @@ 75AA33D12BF9E18E00F12465 /* Color+DWStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+DWStyle.swift"; sourceTree = ""; }; 75AA33D42BF9E1D400F12465 /* Font+DWStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+DWStyle.swift"; sourceTree = ""; }; 75AA33D72BFB4A5A00F12465 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutSelectionView.swift; sourceTree = ""; }; + 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutCustomizeBannerView.swift; sourceTree = ""; }; 75BDE7AB2BF3287400556791 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = ""; }; 75C1F0442AE26AC0006929CA /* CoinJoinLevelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinJoinLevelsViewModel.swift; sourceTree = ""; }; 75C1F09D2AFF675400FE675E /* EnterVotingKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterVotingKeyViewController.swift; sourceTree = ""; }; @@ -2654,12 +2680,14 @@ 8391BA0D088FF516D63DE9A1 /* Pods-dashpay.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.testflight.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.testflight.xcconfig"; sourceTree = ""; }; 86CC430C535DAEBF900E5914 /* Pods-dashwallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.debug.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.debug.xcconfig"; sourceTree = ""; }; 91F0B8B82F3FAB7C00E713AE /* KeysOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysOverviewView.swift; sourceTree = ""; }; + 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysView.swift; sourceTree = ""; }; A47B6DF71195E41A5B7B5F99 /* Pods-dashpay.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.debug.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.debug.xcconfig"; sourceTree = ""; }; AA0000032DUMMYID001234567 /* AddProviderToGiftCardsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderToGiftCardsTable.swift; sourceTree = ""; }; AA0000062DUMMYID001234567 /* DashSpend/GiftCardProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/GiftCardProvider.swift; sourceTree = ""; }; AD6615F1246B5935365B05BD /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; AF918A810315A7BE335BFFA6 /* Pods-WatchApp.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.testnet.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.testnet.xcconfig"; sourceTree = ""; }; B1A681489B9516DC10084A61 /* Pods-DashWalletScreenshotsUITests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.testflight.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.testflight.xcconfig"; sourceTree = ""; }; + B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeySheet.swift; sourceTree = ""; }; BA4A72661BE11AFA00E39C01 /* BRAWWeakTimerTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BRAWWeakTimerTarget.swift; sourceTree = ""; }; BA4EECDF1B351AE200D443A3 /* DWAppGroupConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWAppGroupConstants.h; sourceTree = ""; }; BA54D3CD1B2EA74000C9CB28 /* TodayExtensionAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = TodayExtensionAssets.xcassets; sourceTree = ""; }; @@ -2695,7 +2723,6 @@ C56BFBBABD008D06578BDD9C /* libPods-DashWalletTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DashWalletTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; C909614C29EFF7D600002D82 /* WalletKeysOverviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletKeysOverviewModel.swift; sourceTree = ""; }; C909615029F158D700002D82 /* DerivationPathKeysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysViewController.swift; sourceTree = ""; }; - 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysView.swift; sourceTree = ""; }; C909615229F28E3700002D82 /* DerivationPathKeysModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysModel.swift; sourceTree = ""; }; C909615429F297DD00002D82 /* DerivationPathKeysCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysCell.swift; sourceTree = ""; }; C909615829F29C9200002D82 /* KeysOverviewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysOverviewCell.swift; sourceTree = ""; }; @@ -2704,7 +2731,6 @@ C917024029D462C6008C034D /* PayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayViewController.swift; sourceTree = ""; }; C91E919629FBACE6003E7883 /* ExtendedPublicKeysModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeysModel.swift; sourceTree = ""; }; C91E91AD29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeysViewController.swift; sourceTree = ""; }; - B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeySheet.swift; sourceTree = ""; }; C943B2BA2A408CEC00AF23C5 /* DWUpdatingUserProfileView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWUpdatingUserProfileView.h; sourceTree = ""; }; C943B2BB2A408CEC00AF23C5 /* DWCurrentUserProfileView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWCurrentUserProfileView.h; sourceTree = ""; }; C943B2BC2A408CEC00AF23C5 /* DWErrorUpdatingUserProfileView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWErrorUpdatingUserProfileView.m; sourceTree = ""; }; @@ -3088,8 +3114,6 @@ C94F5E8729D3E7E30034FD57 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; C94F5E8929D3FCCF0034FD57 /* ShortcutAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutAction.swift; sourceTree = ""; }; C94F5E8D29D404850034FD57 /* ShortcutCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutCell.swift; sourceTree = ""; }; - 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutSelectionView.swift; sourceTree = ""; }; - 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutCustomizeBannerView.swift; sourceTree = ""; }; C94F5E8F29D4060A0034FD57 /* ShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsView.swift; sourceTree = ""; }; C956AF152A5C3301002FAB75 /* DWPressableButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWPressableButton.h; sourceTree = ""; }; C956AF162A5C3302002FAB75 /* DWPressableButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWPressableButton.m; sourceTree = ""; }; @@ -3204,6 +3228,11 @@ GG0000032DUMMYID001234567 /* GeoRestrictionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoRestrictionService.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 5151CDF22F929C2300F0A604 /* Geometry Readers */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "Geometry Readers"; sourceTree = ""; }; + 5151CDF62F929D3F00F0A604 /* ScrollViews */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ScrollViews; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 2A4662FD2279DC2F0027533B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -4292,6 +4321,8 @@ 2A7A7C13234B761700451078 /* LocalCurrency */ = { isa = PBXGroup; children = ( + 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */, + 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */, 2A7A7C17234B764C00451078 /* Cells */, 2A7A7C14234B763600451078 /* DWLocalCurrencyViewController.h */, 2A7A7C15234B763600451078 /* DWLocalCurrencyViewController.m */, @@ -4310,6 +4341,7 @@ children = ( 2A7A7C1E234B79B700451078 /* DWLocalCurrencyTableViewCell.h */, 2A7A7C1F234B79B700451078 /* DWLocalCurrencyTableViewCell.m */, + 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */, ); path = Cells; sourceTree = ""; @@ -6144,6 +6176,8 @@ 7566F4882BB6CAD6005238D2 /* SwiftUI Components */ = { isa = PBXGroup; children = ( + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + 5151CDF22F929C2300F0A604 /* Geometry Readers */, 7581B19C2CE3509A00714007 /* Dialogs */, 75EBAA112BB99B6B004488E3 /* BottomSheet.swift */, AD6615F1246B5935365B05BD /* NavigationBar.swift */, @@ -6167,6 +6201,7 @@ 75387B492E0400C300BCCC72 /* MerchantDenominations.swift */, 7545ED5F2DA91FC60075F45C /* NumericKeyboardView.swift */, 756FE7092CDCD6CD00E6C195 /* ValidationCheck.swift */, + 5151CD612F926D1900F0A604 /* SearchBar.swift */, ); path = "SwiftUI Components"; sourceTree = ""; @@ -6346,6 +6381,10 @@ 75D5F3C7191EC270004AB296 /* DashWallet */ = { isa = PBXGroup; children = ( + 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */, + 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */, + 5151C8C42F913BF100F0A604 /* Topper-Info.plist */, + 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */, 2A44313B22CF62FC009BAF7F /* Sources */, 2A4430F022CBD566009BAF7F /* Resources */, FB6DD3811F7FA48500BC1E4D /* dashwallet.entitlements */, @@ -7744,6 +7783,10 @@ BAE12BF11B2DEE7F00895CC5 /* PBXTargetDependency */, BA913BF91BD57E4D005A7C0E /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 5151CDF22F929C2300F0A604 /* Geometry Readers */, + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + ); name = dashwallet; productName = DashWallet; productReference = 75D5F3BE191EC270004AB296 /* dashwallet.app */; @@ -7843,6 +7886,10 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 5151CDF22F929C2300F0A604 /* Geometry Readers */, + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + ); name = dashpay; productName = DashWallet; productReference = C9D2C9532A320AA000D15901 /* dashpay.app */; @@ -7983,6 +8030,10 @@ 4709C30F287E787700B4BD48 /* Migrations.bundle in Resources */, 474C720F298A1A3E00475CA6 /* TxDetailTaxCategoryCell.xib in Resources */, 2ADC722923B5547000D9DD37 /* Localizable.stringsdict in Resources */, + 5151C8CA2F913BF100F0A604 /* Coinbase-Info.plist in Resources */, + 5151C8CB2F913BF100F0A604 /* ZenLedger-Info.plist in Resources */, + 5151C8CC2F913BF100F0A604 /* GoogleService-Info.plist in Resources */, + 5151C8CD2F913BF100F0A604 /* Topper-Info.plist in Resources */, 75E2F3C82AA4CF1900C3B458 /* Topper-Info.plist in Resources */, 753CD8792E2E218800BCF070 /* mixing_anim.json in Resources */, 2A2CD71822F99CAE008C7BC9 /* ShortcutsView.xib in Resources */, @@ -8096,6 +8147,10 @@ C9D2C92A2A320AA000D15901 /* explore.db in Resources */, C9D2C92B2A320AA000D15901 /* (null) in Resources */, C9D2C92C2A320AA000D15901 /* Coinbase.storyboard in Resources */, + 5151C8C62F913BF100F0A604 /* Coinbase-Info.plist in Resources */, + 5151C8C72F913BF100F0A604 /* ZenLedger-Info.plist in Resources */, + 5151C8C82F913BF100F0A604 /* GoogleService-Info.plist in Resources */, + 5151C8C92F913BF100F0A604 /* Topper-Info.plist in Resources */, 75889B892AD2DF0200C17F5D /* CoinJoin.storyboard in Resources */, C9D2C92E2A320AA000D15901 /* ImportWalletInfo.storyboard in Resources */, C9D2C92F2A320AA000D15901 /* ReceiveContentView.xib in Resources */, @@ -8878,6 +8933,7 @@ 47AE8BF528C1306000490F5E /* MerchantListLocationOffCell.swift in Sources */, 1186092529759C4B00279FCC /* CrowdNodeAPIConfirmationTx.swift in Sources */, 2A913E9023A31713006A2A59 /* UIViewController+DWEmbedding.m in Sources */, + 5151CD622F926D1900F0A604 /* SearchBar.swift in Sources */, 2A2CD71322F97B65008C7BC9 /* CALayer+DWShadow.m in Sources */, 47AE8BA528BFADD900490F5E /* MerchantDAO.swift in Sources */, 47AE8BAA28BFAE5800490F5E /* ExploreDatabaseSyncManager.swift in Sources */, @@ -9102,6 +9158,8 @@ C9F452012A0CE6C900825057 /* HomeView.swift in Sources */, 47A2E3AC2972B1A60032A63B /* CoinbaseRatesProvider.swift in Sources */, 7581B19B2CE349BB00714007 /* ConfirmSpendDialog.swift in Sources */, + 5151CA252F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */, + 5151CA262F92335E00F0A604 /* LocalCurrencyView.swift in Sources */, 472D13DF299DF5C6006903F1 /* TaxReportGenerator.swift in Sources */, 119E8D122909513F00D406C1 /* CrowdNodeError.swift in Sources */, 75D9EBC32DE5CD9C009416A2 /* GiftCardsDAO.swift in Sources */, @@ -9130,6 +9188,7 @@ 4774DCDD28F43A68008CF87D /* ServiceDataSource.swift in Sources */, 75387B4A2E0400C300BCCC72 /* MerchantDenominations.swift in Sources */, 47838B7A2900196F0003E8AB /* ConverterView.swift in Sources */, + 5151CDF12F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */, 2A0C69CA23142E11001B8C90 /* DWModalBaseAnimation.m in Sources */, C956AF262A5CACE6002FAB75 /* TitleValueCell.swift in Sources */, 75D657682DF579F300ACE570 /* TransactionFilterDialog.swift in Sources */, @@ -9244,6 +9303,8 @@ C9D2C69F2A320AA000D15901 /* DWPlaceholderFormTableViewCell.m in Sources */, C9D2C6A02A320AA000D15901 /* DWTitleDetailCellView.m in Sources */, C9D2C6A12A320AA000D15901 /* AmountInputControl.swift in Sources */, + 5151CA232F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */, + 5151CA242F92335E00F0A604 /* LocalCurrencyView.swift in Sources */, C9D2C6A22A320AA000D15901 /* DWSegmentSliderFormTableViewCell.m in Sources */, 759609212C4553A400F3BF04 /* BuyCreditsViewController.swift in Sources */, 7549BE5E2DEAF55B004F0BAF /* IconBitmapDAO.swift in Sources */, @@ -9294,6 +9355,7 @@ C9D2C6C02A320AA000D15901 /* Transaction.swift in Sources */, C943B3232A408CED00AF23C5 /* DWFaceDetector.m in Sources */, C9D2C6C12A320AA000D15901 /* AllMerchantLocationsViewController.swift in Sources */, + 5151CDF02F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */, C9D2C6C22A320AA000D15901 /* DWPaymentOutput.m in Sources */, 753FDBEA2AEA422F0005EEC3 /* VotingPrefs.swift in Sources */, C9D2C6C32A320AA000D15901 /* DWPayModelStub.m in Sources */, @@ -9767,6 +9829,7 @@ 7545ED5E2DA91F590075F45C /* DashSpendTermsScreen.swift in Sources */, 75AA33D02BF9D44A00F12465 /* ButtonsGroup.swift in Sources */, C9D2C80C2A320AA000D15901 /* AllMerchantLocationsDataProvider.swift in Sources */, + 5151CD632F926D1900F0A604 /* SearchBar.swift in Sources */, C9D2C80D2A320AA000D15901 /* MainTabbarController.swift in Sources */, 751C05DB2D3D0E8C00475E52 /* JoinDashPayViewModel.swift in Sources */, C9D2C80E2A320AA000D15901 /* NumberFormatter+DashWallet.swift in Sources */, diff --git a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/Cells/LocalCurrencyCellView.swift b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/Cells/LocalCurrencyCellView.swift new file mode 100644 index 000000000..3f9ba34fc --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/Cells/LocalCurrencyCellView.swift @@ -0,0 +1,153 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 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 SwiftUI + +struct LocalCurrencyCellView: View { + + private enum Layout { + static let spacing: CGFloat = 16 + static let horizontalPadding: CGFloat = 10 + static let flagSize: CGFloat = 30 + static let checkboxSize: CGFloat = 22 + static let checkboxSelectedBorderWidth: CGFloat = 5 + static let checkboxDefaultBorderWidth: CGFloat = 1.5 + static let trailingSpacing: CGFloat = 10 + static let infoVerticalPadding: CGFloat = 10 + static let infoSpacing: CGFloat = 1 + } + + let item: CurrencyItem + let isSelected: Bool + let searchQuery: String + + var body: some View { + HStack(spacing: Layout.spacing) { + flagSection + infoSection + trailingSection + } + .padding(.horizontal, Layout.horizontalPadding) + .clipShape(.rect) + } + + private var flagSection: some View { + Group { + if let flagName = item.flagName, UIImage(named: flagName) != nil { + Image(flagName) + .resizable() + .scaledToFill() + } else { + Color.gray200 + } + } + .frame(width: Layout.flagSize, height: Layout.flagSize) + .clipShape(.circle) + } + + private var infoSection: some View { + VStack(alignment: .leading, spacing: Layout.infoSpacing) { + highlightedText(item.name, query: searchQuery) + .font(.subhead.weight(.medium)) + .foregroundColor(.primaryText) + .lineLimit(1) + + Text(item.priceString ?? " ") + .font(.footnote) + .foregroundColor(.tertiaryText) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, Layout.infoVerticalPadding) + } + + private var trailingSection: some View { + HStack(spacing: Layout.trailingSpacing) { + currencyCodeView + checkboxView + } + } + + private var currencyCodeView: some View { + highlightedText(item.code, query: searchQuery) + .font(.caption1) + .foregroundColor(.tertiaryText) + .lineLimit(1) + } + + private var checkboxView: some View { + Circle() + .strokeBorder( + isSelected ? Color.dashBlue : Color.gray300, + lineWidth: isSelected ? Layout.checkboxSelectedBorderWidth : Layout.checkboxDefaultBorderWidth + ) + .frame(width: Layout.checkboxSize, height: Layout.checkboxSize) + } + + /// Builds a SwiftUI Text with the first occurrence of `query` highlighted in dashBlue. + private func highlightedText(_ text: String, query: String) -> Text { + let trimmed = query.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty, + let range = text.range(of: trimmed, options: .caseInsensitive) else { + return Text(text) + } + let before = String(text[.. Void + var onBack: (() -> Void)? + + init( + currencyCode: String? = nil, + onSelect: @escaping (String) -> Void, + onBack: (() -> Void)? = nil + ) { + self._viewModel = StateObject(wrappedValue: LocalCurrencyViewModel(currencyCode: currencyCode)) + self.onSelect = onSelect + self.onBack = onBack + } + + /// Preview / testing init — accepts a pre-built ViewModel. + fileprivate init( + viewModel: LocalCurrencyViewModel, + onSelect: @escaping (String) -> Void, + onBack: (() -> Void)? = nil + ) { + self._viewModel = StateObject(wrappedValue: viewModel) + self.onSelect = onSelect + self.onBack = onBack + } + + var body: some View { + ZStack(alignment: .top) { + // Background + + LocalCurrencyScrollContentView( + headerHeight: fullHeaderSize.height, + onScrollChanged: { offset in + scrollViewOffcet = min(offset.y, 0) + }, + filteredItems: viewModel.filteredItems, + selectedCurrencyCode: viewModel.selectedCurrencyCode, + searchQuery: viewModel.searchQuery, + select: viewModel.select(currencyCode:), + onSelect: onSelect + ) + .padding(.horizontal, 20) + + LocalCurrencyTopOverlayView( + scrollOffset: scrollViewOffcet, + onBack: onBack, + searchQuery: $viewModel.searchQuery + ) + .readingFrame { frame in + if fullHeaderSize == .zero { + fullHeaderSize = frame.size + } + } + } + .background(Color.primaryBackground) + } +} + +private struct LocalCurrencyScrollContentView: View { + + let headerHeight: CGFloat + let onScrollChanged: (CGPoint) -> Void + var filteredItems: [CurrencyItem] + var selectedCurrencyCode: String + var searchQuery: String + var select: (String) -> Void + var onSelect: (String) -> Void + + var body: some View { + ScrollViewWithOnScrollChanged(.vertical, showsIndicators: false) { + VStack { + Rectangle() + .opacity(0) + .frame(height: headerHeight) + + // Currency list + LazyVStack(spacing: 2) { + ForEach(filteredItems) { item in + LocalCurrencyCellView( + item: item, + isSelected: item.code == selectedCurrencyCode, + searchQuery: searchQuery + ) + .onTapGesture { + select(item.code) + onSelect(item.code) + } + } + } + .padding(filteredItems.count > 0 ? 6 : 0) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(Color.secondaryBackground) + ) + } + } onScrollChanged: { offset in + onScrollChanged(offset) + } + } +} + +private struct LocalCurrencyTopOverlayView: View { + let scrollOffset: CGFloat + var onBack: (() -> Void)? + @Binding var searchQuery: String + + var body: some View { + VStack { + header + + if scrollOffset > -20 { + SearchBar(text: $searchQuery) + .transition(.move(edge: .top).combined(with: .opacity)) + .padding(.horizontal, 20) + } + } + .padding(.bottom, 6) + .background(toolbarBackground) + .animation(.smooth, value: scrollOffset) + } + + private var header: some View { + HStack(alignment: .center, spacing: 0) { + ZStack { + if let onBack { + NavBarBack(onBack: onBack) + } + + // Title + Text(NSLocalizedString("Local Currency", comment: "Settings")) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.primaryText) + } + } + } + + @ViewBuilder + private var toolbarBackground: some View { + ZStack(alignment: .bottom) { + Rectangle() + .fill(.clear) + .background(Color.primaryBackground) + .ignoresSafeArea() + + Divider() + .background(Color(red: 176/255, green: 182/255, blue: 188/255, opacity: 0.15)) + .opacity(scrollOffset < -20 ? 1 : 0) + } + + } +} + +// MARK: - Preview + +#if DEBUG +#Preview { + LocalCurrencyView( + viewModel: LocalCurrencyViewModel( + items: [ + CurrencyItem(code: "USD", name: "US Dollar", flagName: "united states", priceString: "42.50"), + CurrencyItem(code: "EUR", name: "Euro", flagName: "european union", priceString: "39.20"), + CurrencyItem(code: "GBP", name: "British Pound", flagName: "united kingdom", priceString: "33.80"), + CurrencyItem(code: "JPY", name: "Japanese Yen", flagName: "japan", priceString: "6380.00"), + CurrencyItem(code: "UAH", name: "Ukrainian Hryvnia", flagName: "ukraine", priceString: "1750.00"), + CurrencyItem(code: "PLN", name: "Polish Zloty", flagName: "poland", priceString: "168.00"), + CurrencyItem(code: "CHF", name: "Swiss Franc", flagName: "switzerland", priceString: "37.10"), + + ], + selectedCode: "USD" + ), + onSelect: { _ in }, + onBack: {} + ) +} +#endif diff --git a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift new file mode 100644 index 000000000..6152811a1 --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift @@ -0,0 +1,240 @@ +// +// Created by Roman Chornyi +// Copyright © 2024 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 + +// MARK: - CurrencyItem + +struct CurrencyItem: Identifiable, Equatable { + var id: String { code } + let code: String + let name: String + let flagName: String? + let priceString: String? +} + +// MARK: - LocalCurrencyViewModel + +@MainActor +class LocalCurrencyViewModel: ObservableObject { + private let allItems: [CurrencyItem] + + @Published var searchQuery: String = "" + @Published var selectedCurrencyCode: String + + var filteredItems: [CurrencyItem] { + let trimmed = searchQuery.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { return allItems } + let query = trimmed.lowercased() + return allItems.filter { + $0.code.lowercased().contains(query) || $0.name.lowercased().contains(query) + } + } + + /// Production init — loads currencies from CurrencyExchangerObjcWrapper. + init(currencyCode: String? = nil) { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 2 + + let items: [CurrencyItem] = CurrencyExchangerObjcWrapper.prices.map { price in + CurrencyItem( + code: price.code, + name: price.name, + flagName: LocalCurrencyViewModel.flagByCode[price.code], + priceString: formatter.string(from: price.price) + ) + } + self.allItems = items + self.selectedCurrencyCode = currencyCode ?? App.fiatCurrency + } + + /// Preview / testing init — accepts pre-built items without DashSync. + init(items: [CurrencyItem], selectedCode: String) { + self.allItems = items + self.selectedCurrencyCode = selectedCode + } + + func select(currencyCode: String) { + selectedCurrencyCode = currencyCode + App.shared.fiatCurrency = currencyCode + } +} + +// MARK: - Flag mapping + +extension LocalCurrencyViewModel { + static let flagByCode: [String: String] = [ + "AED": "united arab emirates", + "AFN": "afghanistan", + "ALL": "albania", + "AMD": "armenia", + "ANG": "sint maarten", + "AOA": "angola", + "ARS": "argentina", + "AUD": "australia", + "AWG": "aruba", + "AZN": "azerbaijan", + "BAM": "bosnia and herzegovina", + "BBD": "barbados", + "BDT": "bangladesh", + "BGN": "bulgaria", + "BHD": "bahrain", + "BIF": "burundi", + "BMD": "bermuda", + "BND": "brunei", + "BOB": "bolivia", + "BRL": "brazil", + "BSD": "bahamas", + "BTN": "bhutan", + "BWP": "botswana", + "BYN": "belarus", + "BZD": "belize", + "CAD": "canada", + "CDF": "democratic republic of congo", + "CHF": "switzerland", + "CLF": "chile", + "CLP": "chile", + "CNY": "china", + "COP": "colombia", + "CRC": "costa rica", + "CUP": "cuba", + "CVE": "cape verde", + "CZK": "czech republic", + "DJF": "djibouti", + "DKK": "denmark", + "DOP": "dominican republic", + "DZD": "Algeria", + "EGP": "egypt", + "ETB": "ethiopia", + "EUR": "european union", + "FJD": "fiji", + "FKP": "falkland islands", + "GBP": "united kingdom", + "GEL": "georgia", + "GHS": "ghana", + "GIP": "gibraltar", + "GMD": "gambia", + "GNF": "guinea", + "GTQ": "guatemala", + "GYD": "guyana", + "HKD": "hong kong", + "HNL": "honduras", + "HRK": "croatia", + "HTG": "haiti", + "HUF": "hungary", + "IDR": "indonesia", + "ILS": "israel", + "INR": "india", + "IQD": "iraq", + "IRR": "iran", + "ISK": "iceland", + "JEP": "jersey", + "JMD": "jamaica", + "JOD": "jordan", + "JPY": "japan", + "KES": "kenya", + "KGS": "kyrgyzstan", + "KHR": "cambodia", + "KMF": "comoros", + "KPW": "north korea", + "KRW": "south korea", + "KWD": "kuwait", + "KYD": "cayman islands", + "KZT": "kazakhstan", + "LAK": "laos", + "LBP": "lebanon", + "LKR": "sri lanka", + "LRD": "liberia", + "LSL": "lesotho", + "LYD": "libya", + "MAD": "morocco", + "MDL": "moldova", + "MGA": "madagascar", + "MKD": "republic of macedonia", + "MMK": "myanmar", + "MNT": "mongolia", + "MOP": "macao", + "MRU": "mauritania", + "MUR": "mauritius", + "MVR": "maldives", + "MWK": "malawi", + "MXN": "mexico", + "MYR": "malaysia", + "MZN": "mozambique", + "NAD": "namibia", + "NGN": "nigeria", + "NIO": "nicaragua", + "NOK": "norway", + "NPR": "nepal", + "NZD": "new zealand", + "OMR": "oman", + "PAB": "panama", + "PEN": "peru", + "PGK": "papua new guinea", + "PHP": "philippines", + "PKR": "pakistan", + "PLN": "poland", + "PYG": "paraguay", + "QAR": "qatar", + "RON": "romania", + "RSD": "serbia", + "RUB": "russia", + "RWF": "rwanda", + "SAR": "saudi arabia", + "SBD": "solomon islands", + "SCR": "seychelles", + "SDG": "sudan", + "SEK": "sweden", + "SGD": "singapore", + "SHP": "united kingdom", + "SLL": "sierra leone", + "SOS": "somalia", + "SRD": "suriname", + "STN": "sao tome and prince", + "SVC": "el salvador", + "SYP": "syria", + "SZL": "swaziland", + "THB": "thailand", + "TJS": "tajikistan", + "TMT": "turkmenistan", + "TND": "tunisia", + "TOP": "tonga", + "TRY": "turkey", + "TTD": "trinidad and tobago", + "TWD": "taiwan", + "TZS": "tanzania", + "UAH": "ukraine", + "UGX": "uganda", + "USD": "united states", + "UYU": "uruguay", + "UZS": "uzbekistan", + "VES": "venezuela", + "VND": "vietnam", + "VUV": "vanuatu", + "WST": "samoa", + "XAF": "central african cfa franc", + "XCD": "anguilla", + "XOF": "benin", + "XPF": "french polynesia", + "YER": "yemen", + "ZAR": "south africa", + "ZMW": "zambia", + "ZWL": "zimbabwe", + ] +} diff --git a/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift b/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift index 6b206712f..4e1581446 100644 --- a/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift +++ b/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift @@ -21,21 +21,17 @@ import Combine struct SettingsScreen: View { private let vc: UINavigationController - private let delegateInternal: DelegateInternal private let onDidRescan: () -> () - + @StateObject private var viewModel = SettingsMenuViewModel() @State private var showNetworkAlert = false @State private var showRescanWarningAlert = false @State private var showRescanActionAlert = false @State private var showCSVExportActivity = false - + init(vc: UINavigationController, onDidRescan: @escaping () -> ()) { self.vc = vc self.onDidRescan = onDidRescan - self.delegateInternal = DelegateInternal(onHide: { - vc.popViewController(animated: true) - }) } var body: some View { @@ -191,8 +187,17 @@ struct SettingsScreen: View { } private func showCurrencySelector() { - let controller = DWLocalCurrencyViewController(navigationAppearance: .default, presentationMode: .screen, currencyCode: nil) - controller.delegate = delegateInternal + let view = LocalCurrencyView( + currencyCode: nil, + onSelect: { [weak vc] _ in + vc?.popViewController(animated: true) + }, + onBack: { [weak vc] in + vc?.popViewController(animated: true) + } + ) + let controller = UIHostingController(rootView: view) + controller.hidesBottomBarWhenPushed = true vc.pushViewController(controller, animated: true) } @@ -231,20 +236,6 @@ struct SettingsScreen: View { } } -extension SettingsScreen { - class DelegateInternal: NSObject, DWLocalCurrencyViewControllerDelegate { - let onHide: () -> () - - init(onHide: @escaping () -> ()) { - self.onHide = onHide - } - - func localCurrencyViewController(_ controller: DWLocalCurrencyViewController, didSelectCurrency currencyCode: String) { - onHide() - } - func localCurrencyViewControllerDidCancel(_ controller: DWLocalCurrencyViewController) { onHide() } - } -} struct ActivityView: UIViewControllerRepresentable { let activityItems: [Any] diff --git a/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/FrameReader.swift b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/FrameReader.swift new file mode 100644 index 000000000..2a8265152 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/FrameReader.swift @@ -0,0 +1,96 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 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 SwiftUI + +@available(iOS 14, *) +/// Adds a transparent View and read it's frame. +/// +/// Adds a GeometryReader with infinity frame. +public struct FrameReader: View { + + let coordinateSpace: CoordinateSpace + let onChange: (_ frame: CGRect) -> Void + + public init(coordinateSpace: CoordinateSpace, onChange: @escaping (_ frame: CGRect) -> Void) { + self.coordinateSpace = coordinateSpace + self.onChange = onChange + } + + public var body: some View { + GeometryReader { geo in + Text("") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear(perform: { + onChange(geo.frame(in: coordinateSpace)) + }) + .onChange(of: geo.frame(in: coordinateSpace), perform: onChange) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +@available(iOS 14, *) +public extension View { + + /// Get the frame of the View + /// + /// Adds a GeometryReader to the background of a View. + func readingFrame(coordinateSpace: CoordinateSpace = .global, onChange: @escaping (_ frame: CGRect) -> ()) -> some View { + background(FrameReader(coordinateSpace: coordinateSpace, onChange: onChange)) + } +} + +@available(iOS 14, *) +struct FrameReader_Previews: PreviewProvider { + + struct PreviewView: View { + + @State private var yOffset: CGFloat = 0 + + var body: some View { + ScrollView(.vertical) { + VStack { + Text("") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + .readingFrame { frame in + yOffset = frame.minY + } + + ForEach(0..<30) { x in + Text("") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + } + } + } + .coordinateSpace(name: "test") + .overlay(Text("Offset: \(yOffset)")) + } + } + + static var previews: some View { + PreviewView() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/LocationReader.swift b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/LocationReader.swift new file mode 100644 index 000000000..d788347d5 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/LocationReader.swift @@ -0,0 +1,92 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 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 SwiftUI + +@available(iOS 14, *) +/// Adds a transparent View and read it's center point. +/// +/// Adds a GeometryReader with 0px by 0px frame. +public struct LocationReader: View { + + let coordinateSpace: CoordinateSpace + let onChange: (_ location: CGPoint) -> Void + + public init(coordinateSpace: CoordinateSpace, onChange: @escaping (_ location: CGPoint) -> Void) { + self.coordinateSpace = coordinateSpace + self.onChange = onChange + } + + public var body: some View { + FrameReader(coordinateSpace: coordinateSpace) { frame in + onChange(CGPoint(x: frame.midX, y: frame.midY)) + } + .frame(width: 0, height: 0, alignment: .center) + } +} + +@available(iOS 14, *) +public extension View { + + /// Get the center point of the View + /// + /// Adds a 0px GeometryReader to the background of a View. + func readingLocation(coordinateSpace: CoordinateSpace = .global, onChange: @escaping (_ location: CGPoint) -> ()) -> some View { + background(LocationReader(coordinateSpace: coordinateSpace, onChange: onChange)) + } + +} + +@available(iOS 14, *) +struct LocationReader_Previews: PreviewProvider { + + struct PreviewView: View { + + @State private var yOffset: CGFloat = 0 + + var body: some View { + ScrollView(.vertical) { + VStack { + Text("Hello, world!") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + .readingLocation { location in + yOffset = location.y + } + + ForEach(0..<30) { x in + Text("") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + } + } + } + .coordinateSpace(name: "test") + .overlay(Text("Offset: \(yOffset)")) + } + } + + static var previews: some View { + PreviewView() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/ScrollViews/ScrollViewWithOnScrollChanged.swift b/DashWallet/Sources/UI/SwiftUI Components/ScrollViews/ScrollViewWithOnScrollChanged.swift new file mode 100644 index 000000000..282dc8f63 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/ScrollViews/ScrollViewWithOnScrollChanged.swift @@ -0,0 +1,79 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 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 SwiftUI + +@available(iOS 14, *) +public struct ScrollViewWithOnScrollChanged: View { + + let axes: Axis.Set + let showsIndicators: Bool + let content: Content + let onScrollChanged: (_ origin: CGPoint) -> () + @State private var coordinateSpaceID: String = UUID().uuidString + + public init( + _ axes: Axis.Set = .vertical, + showsIndicators: Bool = false, + @ViewBuilder content: () -> Content, + onScrollChanged: @escaping (_ origin: CGPoint) -> ()) { + self.axes = axes + self.showsIndicators = showsIndicators + self.content = content() + self.onScrollChanged = onScrollChanged + } + + public var body: some View { + ScrollView(axes, showsIndicators: showsIndicators) { + LocationReader(coordinateSpace: .named(coordinateSpaceID), onChange: onScrollChanged) + content + } + .coordinateSpace(name: coordinateSpaceID) + } +} + +@available(iOS 14, *) +struct ScrollViewWithOnScrollChanged_Previews: PreviewProvider { + + struct PreviewView: View { + + @State private var yPosition: CGFloat = 0 + + var body: some View { + ScrollViewWithOnScrollChanged { + VStack { + ForEach(0..<30) { x in + Text("x: \(x)") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.red) + .padding() + .id(x) + } + } + } onScrollChanged: { origin in + yPosition = origin.y + } + .overlay(Text("Offset: \(yPosition)")) + } + } + + static var previews: some View { + PreviewView() + } +} From c85a865b8b1f0655efa25a941448f1c0f61adcf7 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:25:58 +0300 Subject: [PATCH 12/17] refactor(local-currency): extract CurrencyItem and debounce filtering --- DashWallet.xcodeproj/project.pbxproj | 6 +++ .../Settings/LocalCurrency/CurrencyItem.swift | 26 ++++++++++ .../LocalCurrencyViewModel.swift | 48 +++++++++++-------- 3 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 DashWallet/Sources/UI/Menu/Settings/LocalCurrency/CurrencyItem.swift diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index ed75d6f00..f1040cdde 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -507,6 +507,8 @@ 5151CD632F926D1900F0A604 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CD612F926D1900F0A604 /* SearchBar.swift */; }; 5151CDF02F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */; }; 5151CDF12F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */; }; + 5151CE182F9624D000F0A604 /* CurrencyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CE172F9624D000F0A604 /* CurrencyItem.swift */; }; + 5151CE192F9624D000F0A604 /* CurrencyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CE172F9624D000F0A604 /* CurrencyItem.swift */; }; 7502A4872AE401EF00ACDDD3 /* UsernameVotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7502A4862AE401EF00ACDDD3 /* UsernameVotingViewController.swift */; }; 7503643A2C89CFB70029EC0D /* CoinJoinProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */; }; 7503643B2C89CFB70029EC0D /* CoinJoinProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */; }; @@ -2475,6 +2477,7 @@ 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyViewModel.swift; sourceTree = ""; }; 5151CD612F926D1900F0A604 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyCellView.swift; sourceTree = ""; }; + 5151CE172F9624D000F0A604 /* CurrencyItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyItem.swift; sourceTree = ""; }; 62C1F7A3ABEE7CF3BBB270C4 /* Pods-DashWalletTests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.testflight.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.testflight.xcconfig"; sourceTree = ""; }; 67217DE34817FB304EDEA8D4 /* Pods-dashpay.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.testnet.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.testnet.xcconfig"; sourceTree = ""; }; 6A44C167AF8F881176AFB256 /* Pods-TodayExtension.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.testflight.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.testflight.xcconfig"; sourceTree = ""; }; @@ -4332,6 +4335,7 @@ 2AA87CF926E5681100F0CEA6 /* DWCurrencyObject.m */, 2ACD53E6234BADD300650AD3 /* DWCurrencyItem.h */, 2AA87CFB26E568A100F0CEA6 /* DWCurrencyItemPriceProvider.h */, + 5151CE172F9624D000F0A604 /* CurrencyItem.swift */, ); path = LocalCurrency; sourceTree = ""; @@ -8668,6 +8672,7 @@ 47AE8BF428C1306000490F5E /* FetchingNextPageCell.swift in Sources */, 47081197298CF20C003FCA3D /* Transaction.swift in Sources */, 47AE8BE628C1305F00490F5E /* AllMerchantLocationsViewController.swift in Sources */, + 5151CE192F9624D000F0A604 /* CurrencyItem.swift in Sources */, 2ACCD84D23180E7E00A96B62 /* DWPaymentOutput.m in Sources */, 2AB3415E23A8133A004E37A7 /* DWPayModelStub.m in Sources */, 2A74EFF1230531DA00C475EB /* DWRecoverContentView.m in Sources */, @@ -9337,6 +9342,7 @@ C9D2C6BB2A320AA000D15901 /* DWFormTableViewController.m in Sources */, C943B34D2A40A4C500AF23C5 /* DWInfoPopupViewController.m in Sources */, 75F990822AFD1065006759AB /* UsernameRequestDetailsViewController.swift in Sources */, + 5151CE182F9624D000F0A604 /* CurrencyItem.swift in Sources */, C943B32A2A408CED00AF23C5 /* DWAvatarExternalSourceView.m in Sources */, C9D2C6BC2A320AA000D15901 /* WalletKeysOverviewModel.swift in Sources */, C9D2C6BD2A320AA000D15901 /* UIAssembly.swift in Sources */, diff --git a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/CurrencyItem.swift b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/CurrencyItem.swift new file mode 100644 index 000000000..d21102613 --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/CurrencyItem.swift @@ -0,0 +1,26 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 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 + +struct CurrencyItem: Identifiable, Equatable { + var id: String { code } + let code: String + let name: String + let flagName: String? + let priceString: String? +} diff --git a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift index 6152811a1..ecbf9867d 100644 --- a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift @@ -18,34 +18,15 @@ import Foundation import Combine -// MARK: - CurrencyItem - -struct CurrencyItem: Identifiable, Equatable { - var id: String { code } - let code: String - let name: String - let flagName: String? - let priceString: String? -} - -// MARK: - LocalCurrencyViewModel - @MainActor class LocalCurrencyViewModel: ObservableObject { private let allItems: [CurrencyItem] + private var cancellables = Set() @Published var searchQuery: String = "" + @Published private(set) var filteredItems: [CurrencyItem] = [] @Published var selectedCurrencyCode: String - var filteredItems: [CurrencyItem] { - let trimmed = searchQuery.trimmingCharacters(in: .whitespaces) - guard !trimmed.isEmpty else { return allItems } - let query = trimmed.lowercased() - return allItems.filter { - $0.code.lowercased().contains(query) || $0.name.lowercased().contains(query) - } - } - /// Production init — loads currencies from CurrencyExchangerObjcWrapper. init(currencyCode: String? = nil) { let formatter = NumberFormatter() @@ -62,18 +43,43 @@ class LocalCurrencyViewModel: ObservableObject { } self.allItems = items self.selectedCurrencyCode = currencyCode ?? App.fiatCurrency + self.filteredItems = items + setupSearch() } /// Preview / testing init — accepts pre-built items without DashSync. init(items: [CurrencyItem], selectedCode: String) { self.allItems = items self.selectedCurrencyCode = selectedCode + self.filteredItems = items + setupSearch() } func select(currencyCode: String) { selectedCurrencyCode = currencyCode App.shared.fiatCurrency = currencyCode } + + private func setupSearch() { + $searchQuery + .debounce(for: .milliseconds(150), scheduler: RunLoop.main) + .sink { [weak self] query in + self?.applyFilter(query) + } + .store(in: &cancellables) + } + + private func applyFilter(_ query: String) { + let trimmed = query.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { + filteredItems = allItems + return + } + let lowercased = trimmed.lowercased() + filteredItems = allItems.filter { + $0.code.lowercased().contains(lowercased) || $0.name.lowercased().contains(lowercased) + } + } } // MARK: - Flag mapping From c57de5e23b38b90dc8957114c56d0fa53c5beb06 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:36:44 +0300 Subject: [PATCH 13/17] fix(previews): enable local currency SwiftUI previews --- .../xcschemes/dashwallet.xcscheme | 2 +- DashWallet/AppDelegate.m | 6 ++++ .../LocalCurrency/LocalCurrencyView.swift | 36 ++++++++++++------- .../LocalCurrencyViewModel.swift | 25 ++++++------- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme b/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme index dc4c775c9..b2b6d169e 100644 --- a/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme +++ b/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme @@ -73,7 +73,7 @@ Void, - onBack: (() -> Void)? = nil - ) { - self._viewModel = StateObject(wrappedValue: viewModel) - self.onSelect = onSelect - self.onBack = onBack - } - var body: some View { ZStack(alignment: .top) { - // Background + background LocalCurrencyScrollContentView( headerHeight: fullHeaderSize.height, @@ -77,7 +66,10 @@ struct LocalCurrencyView: View { } } } - .background(Color.primaryBackground) + } + + private var background: some View { + Color.primaryBackground } } @@ -179,6 +171,24 @@ private struct LocalCurrencyTopOverlayView: View { // MARK: - Preview #if DEBUG +extension LocalCurrencyView { + fileprivate init( + viewModel: LocalCurrencyViewModel, + onSelect: @escaping (String) -> Void, + onBack: (() -> Void)? = nil + ) { + self._viewModel = StateObject(wrappedValue: viewModel) + self.onSelect = onSelect + self.onBack = onBack + } +} + +extension LocalCurrencyViewModel { + convenience init(items: [CurrencyItem], selectedCode: String) { + self.init(allItems: items, selectedCurrencyCode: selectedCode) + } +} + #Preview { LocalCurrencyView( viewModel: LocalCurrencyViewModel( diff --git a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift index ecbf9867d..f1fdae22c 100644 --- a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift @@ -27,8 +27,14 @@ class LocalCurrencyViewModel: ObservableObject { @Published private(set) var filteredItems: [CurrencyItem] = [] @Published var selectedCurrencyCode: String - /// Production init — loads currencies from CurrencyExchangerObjcWrapper. - init(currencyCode: String? = nil) { + init(allItems: [CurrencyItem], selectedCurrencyCode: String) { + self.allItems = allItems + self.selectedCurrencyCode = selectedCurrencyCode + self.filteredItems = allItems + setupSearch() + } + + convenience init(currencyCode: String? = nil) { let formatter = NumberFormatter() formatter.maximumFractionDigits = 2 formatter.minimumFractionDigits = 2 @@ -41,18 +47,7 @@ class LocalCurrencyViewModel: ObservableObject { priceString: formatter.string(from: price.price) ) } - self.allItems = items - self.selectedCurrencyCode = currencyCode ?? App.fiatCurrency - self.filteredItems = items - setupSearch() - } - - /// Preview / testing init — accepts pre-built items without DashSync. - init(items: [CurrencyItem], selectedCode: String) { - self.allItems = items - self.selectedCurrencyCode = selectedCode - self.filteredItems = items - setupSearch() + self.init(allItems: items, selectedCurrencyCode: currencyCode ?? App.fiatCurrency) } func select(currencyCode: String) { @@ -82,6 +77,8 @@ class LocalCurrencyViewModel: ObservableObject { } } + + // MARK: - Flag mapping extension LocalCurrencyViewModel { From a74a18448d74622246e23c3c4d672f71efd5c454 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:30:21 +0300 Subject: [PATCH 14/17] ci: enforce Xcode 26.x for release archive pipeline --- .github/workflows/release-xcode26.yml | 62 +++++++++++++++++++++++++++ fastlane/Fastfile | 32 +++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-xcode26.yml diff --git a/.github/workflows/release-xcode26.yml b/.github/workflows/release-xcode26.yml new file mode 100644 index 000000000..216c6d86b --- /dev/null +++ b/.github/workflows/release-xcode26.yml @@ -0,0 +1,62 @@ +name: Release Archive (Xcode 26.x) + +on: + workflow_dispatch: + push: + branches: + - release/* + - release/** + +jobs: + archive: + name: Archive Build + runs-on: macos-latest + timeout-minutes: 90 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Select Xcode 26.x + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.x" + + - name: Verify Xcode version + run: | + mkdir -p build + xcodebuild -version | tee build/xcode-version.txt + if ! grep -Eq '^Xcode 26\.' build/xcode-version.txt; then + echo "Expected Xcode 26.x but got:" + cat build/xcode-version.txt + exit 1 + fi + + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: "1.15.2" + + - name: Ensure fastlane is available + run: | + if ! command -v fastlane >/dev/null 2>&1; then + gem install fastlane -N + fi + fastlane --version + + - name: Install pods + run: pod install --repo-update + + - name: Build release archive + run: fastlane ios release_archive_ci + + - name: Upload build logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: release-xcode26-logs-${{ github.run_number }} + if-no-files-found: warn + path: | + build/xcode-version.txt + build/logs/** + fastlane/report.xml diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 03a8c89d4..66741a59c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,6 +21,7 @@ platform :ios do desc "Push a new beta build to TestFlight" lane :beta do + ensure_xcode_26 ensure_git_status_clean cocoapods_install @@ -28,10 +29,12 @@ platform :ios do increment_build_number build_app( + workspace: ENV["XCODE_WORKSPACE"], scheme: ENV["XCODE_SCHEME"], configuration: "Release", clean: true, - export_method: "app-store" + export_method: "app-store", + buildlog_path: "build/logs" ) upload_to_testflight( @@ -43,6 +46,24 @@ platform :ios do clean_build_artifacts end + desc "Build release archive in CI with Xcode 26.x (no upload)" + lane :release_archive_ci do + ensure_xcode_26 + + cocoapods_install + + build_app( + workspace: ENV["XCODE_WORKSPACE"], + scheme: ENV["XCODE_SCHEME"], + configuration: "Release", + clean: true, + skip_codesigning: true, + skip_package_ipa: true, + archive_path: "build/DashWallet.xcarchive", + buildlog_path: "build/logs" + ) + end + desc "Run tests" lane :test do setup_travis @@ -61,4 +82,13 @@ platform :ios do ) end + private_lane :ensure_xcode_26 do + xcode_version_output = sh("xcodebuild -version") + UI.message("Detected Xcode:\n#{xcode_version_output}") + + unless xcode_version_output.match?(/^Xcode 26\./) + UI.user_error!("Xcode 26.x is required for release lanes. Current version output:\n#{xcode_version_output}") + end + end + end From b12f1c9b1a52283efa46a9ccb1725724bb45bfc3 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:16:51 +0300 Subject: [PATCH 15/17] ci: enable release archive workflow on PR to master --- .github/workflows/release-xcode26.yml | 48 ++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-xcode26.yml b/.github/workflows/release-xcode26.yml index 216c6d86b..b3d5d7c71 100644 --- a/.github/workflows/release-xcode26.yml +++ b/.github/workflows/release-xcode26.yml @@ -4,8 +4,10 @@ on: workflow_dispatch: push: branches: - - release/* - - release/** + - master + pull_request: + branches: + - master jobs: archive: @@ -44,19 +46,49 @@ jobs: fi fastlane --version - - name: Install pods - run: pod install --repo-update - - name: Build release archive run: fastlane ios release_archive_ci + - name: Prepare public-safe artifacts + if: always() + run: | + mkdir -p build/public-artifacts + + if [ -f build/xcode-version.txt ]; then + cp build/xcode-version.txt build/public-artifacts/xcode-version.txt + fi + + if [ -d build/logs ]; then + for src in build/logs/*; do + [ -f "$src" ] || continue + dst="build/public-artifacts/$(basename "$src")" + sed -E \ + -e 's/(Authorization: Bearer )[A-Za-z0-9._-]+/\1[REDACTED]/g' \ + -e 's/([?&](token|access_token|password|api_key|apikey|secret)=)[^&[:space:]]+/\1[REDACTED]/Ig' \ + -e 's/(APPLE_APP_SPECIFIC_PASSWORD=)[^[:space:]]+/\1[REDACTED]/g' \ + -e 's/(FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=)[^[:space:]]+/\1[REDACTED]/g' \ + "$src" > "$dst" + done + fi + + if [ -f fastlane/report.xml ]; then + cp fastlane/report.xml build/public-artifacts/fastlane-report.xml + fi + + { + echo "Archive log summary" + echo "===================" + if [ -d build/public-artifacts ]; then + grep -R -n -E "ARCHIVE SUCCEEDED|ARCHIVE FAILED|BUILD SUCCEEDED|error:" build/public-artifacts || true + fi + } > build/public-artifacts/archive-summary.txt + - name: Upload build logs if: always() uses: actions/upload-artifact@v4 with: name: release-xcode26-logs-${{ github.run_number }} + retention-days: 3 if-no-files-found: warn path: | - build/xcode-version.txt - build/logs/** - fastlane/report.xml + build/public-artifacts/** From 67384d0b1a0ad7737fac884c0ae592ef7598cadb Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Thu, 23 Apr 2026 20:02:17 -0700 Subject: [PATCH 16/17] chore: remove release-code26.yml --- .github/workflows/release-xcode26.yml | 94 --------------------------- 1 file changed, 94 deletions(-) delete mode 100644 .github/workflows/release-xcode26.yml diff --git a/.github/workflows/release-xcode26.yml b/.github/workflows/release-xcode26.yml deleted file mode 100644 index b3d5d7c71..000000000 --- a/.github/workflows/release-xcode26.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Release Archive (Xcode 26.x) - -on: - workflow_dispatch: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - archive: - name: Archive Build - runs-on: macos-latest - timeout-minutes: 90 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Select Xcode 26.x - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "26.x" - - - name: Verify Xcode version - run: | - mkdir -p build - xcodebuild -version | tee build/xcode-version.txt - if ! grep -Eq '^Xcode 26\.' build/xcode-version.txt; then - echo "Expected Xcode 26.x but got:" - cat build/xcode-version.txt - exit 1 - fi - - - name: Setup CocoaPods - uses: maxim-lobanov/setup-cocoapods@v1 - with: - version: "1.15.2" - - - name: Ensure fastlane is available - run: | - if ! command -v fastlane >/dev/null 2>&1; then - gem install fastlane -N - fi - fastlane --version - - - name: Build release archive - run: fastlane ios release_archive_ci - - - name: Prepare public-safe artifacts - if: always() - run: | - mkdir -p build/public-artifacts - - if [ -f build/xcode-version.txt ]; then - cp build/xcode-version.txt build/public-artifacts/xcode-version.txt - fi - - if [ -d build/logs ]; then - for src in build/logs/*; do - [ -f "$src" ] || continue - dst="build/public-artifacts/$(basename "$src")" - sed -E \ - -e 's/(Authorization: Bearer )[A-Za-z0-9._-]+/\1[REDACTED]/g' \ - -e 's/([?&](token|access_token|password|api_key|apikey|secret)=)[^&[:space:]]+/\1[REDACTED]/Ig' \ - -e 's/(APPLE_APP_SPECIFIC_PASSWORD=)[^[:space:]]+/\1[REDACTED]/g' \ - -e 's/(FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=)[^[:space:]]+/\1[REDACTED]/g' \ - "$src" > "$dst" - done - fi - - if [ -f fastlane/report.xml ]; then - cp fastlane/report.xml build/public-artifacts/fastlane-report.xml - fi - - { - echo "Archive log summary" - echo "===================" - if [ -d build/public-artifacts ]; then - grep -R -n -E "ARCHIVE SUCCEEDED|ARCHIVE FAILED|BUILD SUCCEEDED|error:" build/public-artifacts || true - fi - } > build/public-artifacts/archive-summary.txt - - - name: Upload build logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: release-xcode26-logs-${{ github.run_number }} - retention-days: 3 - if-no-files-found: warn - path: | - build/public-artifacts/** From d64385aa39755d9fec535e8756b290d73b7130d4 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Thu, 23 Apr 2026 20:20:42 -0700 Subject: [PATCH 17/17] chore: remove unused field in MainTabbarController --- DashWallet/Sources/UI/Main/MainTabbarController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/DashWallet/Sources/UI/Main/MainTabbarController.swift b/DashWallet/Sources/UI/Main/MainTabbarController.swift index 9d906230e..6e1c5ba57 100644 --- a/DashWallet/Sources/UI/Main/MainTabbarController.swift +++ b/DashWallet/Sources/UI/Main/MainTabbarController.swift @@ -89,8 +89,6 @@ class MainTabbarController: UITabBarController { @objc weak var wipeDelegate: DWWipeDelegate? - private var paymentIsOpened = false - @objc var isDemoMode = false