diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index c41c8aff8..ed8aaca28 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 */ @@ -494,6 +494,24 @@ 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 */; }; + 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 */; }; @@ -2460,6 +2478,15 @@ 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 = ""; }; + 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 = ""; }; @@ -3216,6 +3243,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; @@ -4305,6 +4337,8 @@ 2A7A7C13234B761700451078 /* LocalCurrency */ = { isa = PBXGroup; children = ( + 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */, + 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */, 2A7A7C17234B764C00451078 /* Cells */, 2A7A7C14234B763600451078 /* DWLocalCurrencyViewController.h */, 2A7A7C15234B763600451078 /* DWLocalCurrencyViewController.m */, @@ -4314,6 +4348,7 @@ 2AA87CF926E5681100F0CEA6 /* DWCurrencyObject.m */, 2ACD53E6234BADD300650AD3 /* DWCurrencyItem.h */, 2AA87CFB26E568A100F0CEA6 /* DWCurrencyItemPriceProvider.h */, + 5151CE172F9624D000F0A604 /* CurrencyItem.swift */, ); path = LocalCurrency; sourceTree = ""; @@ -4323,6 +4358,7 @@ children = ( 2A7A7C1E234B79B700451078 /* DWLocalCurrencyTableViewCell.h */, 2A7A7C1F234B79B700451078 /* DWLocalCurrencyTableViewCell.m */, + 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */, ); path = Cells; sourceTree = ""; @@ -6159,6 +6195,8 @@ 7566F4882BB6CAD6005238D2 /* SwiftUI Components */ = { isa = PBXGroup; children = ( + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + 5151CDF22F929C2300F0A604 /* Geometry Readers */, 7581B19C2CE3509A00714007 /* Dialogs */, 75EBAA112BB99B6B004488E3 /* BottomSheet.swift */, AD6615F1246B5935365B05BD /* NavigationBar.swift */, @@ -6183,6 +6221,7 @@ 75387B492E0400C300BCCC72 /* MerchantDenominations.swift */, 7545ED5F2DA91FC60075F45C /* NumericKeyboardView.swift */, 756FE7092CDCD6CD00E6C195 /* ValidationCheck.swift */, + 5151CD612F926D1900F0A604 /* SearchBar.swift */, ); path = "SwiftUI Components"; sourceTree = ""; @@ -6362,6 +6401,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 */, @@ -7760,6 +7803,10 @@ BAE12BF11B2DEE7F00895CC5 /* PBXTargetDependency */, BA913BF91BD57E4D005A7C0E /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 5151CDF22F929C2300F0A604 /* Geometry Readers */, + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + ); name = dashwallet; productName = DashWallet; productReference = 75D5F3BE191EC270004AB296 /* dashwallet.app */; @@ -7859,6 +7906,10 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 5151CDF22F929C2300F0A604 /* Geometry Readers */, + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + ); name = dashpay; productName = DashWallet; productReference = C9D2C9532A320AA000D15901 /* dashpay.app */; @@ -7999,6 +8050,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 */, @@ -8112,6 +8167,10 @@ C9D2C92A2A320AA000D15901 /* explore.db in Resources */, C9D2C92B2A320AA000D15901 /* BuildFile 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 */, @@ -8630,6 +8689,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 */, @@ -8896,6 +8956,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 */, @@ -9121,6 +9182,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 */, @@ -9149,6 +9212,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 */, @@ -9271,6 +9335,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 */, @@ -9304,6 +9370,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 */, @@ -9322,6 +9389,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 */, @@ -9797,6 +9865,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.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme b/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme index 6d35021d0..b2b6d169e 100644 --- a/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme +++ b/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme @@ -73,7 +73,7 @@ - - - - - - - - - - - + + + + + + + + + + + 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 @@ - - - diff --git a/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift b/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift index 7e3a9f579..4c9975177 100644 --- a/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift +++ b/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift @@ -181,7 +181,8 @@ extension HomeViewController: DWLocalCurrencyViewControllerDelegate { 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) @@ -310,3 +311,14 @@ extension HomeViewController: DWLocalCurrencyViewControllerDelegate { } } +#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 + 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) } } diff --git a/DashWallet/Sources/UI/Main/MainTabbarController.swift b/DashWallet/Sources/UI/Main/MainTabbarController.swift index 8e4e0943e..6e1c5ba57 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? #endif @@ -93,8 +89,6 @@ class MainTabbarController: UITabBarController { @objc weak var wipeDelegate: DWWipeDelegate? - private var paymentButton: PaymentButton! - @objc var isDemoMode = false @@ -104,7 +98,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? { @@ -145,35 +139,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() - - tabBar.addSubview(paymentButton) - } - override func viewDidLoad() { super.viewDidLoad() delegate = self - configureHierarchy() + tabBar.barTintColor = .dw_background() setupRatesErrorHandling() } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // Add Payment Button again to make sure it's at the top - tabBar.addSubview(paymentButton) - } } // MARK: - Private @@ -211,13 +185,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 { @@ -263,25 +239,30 @@ extension MainTabbarController { self.viewControllers = viewControllers } - private func configureHierarchy() { - paymentButton = PaymentButton() - paymentButton.translatesAutoresizingMaskIntoConstraints = false - paymentButton.addTarget(self, action: #selector(paymentButtonAction), for: .touchUpInside) - tabBar.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 @@ -385,7 +366,7 @@ extension MainTabbarController: PaymentsViewControllerDelegate { } func paymentsViewControllerWantsToImportPrivateKey(_ controller: PaymentsViewController) { - paymentButton.isOpened = false + paymentIsOpened = false controller.dismiss(animated: true) { self.performScanQRCodeAction() @@ -405,7 +386,7 @@ extension MainTabbarController: HomeViewControllerDelegate { } func showPaymentsController(withActivePage pageIndex: PaymentsViewControllerState) { - paymentButton.isOpened = true + paymentIsOpened = true let receiveModel = DWReceiveModel() let payModel = DWPayModel() @@ -434,7 +415,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 } } 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:@" "]; 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 + } + + 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 + } + } + } + } + + private var background: some View { + 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 +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( + 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..f1fdae22c --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift @@ -0,0 +1,243 @@ +// +// 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 + +@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 + + 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 + + 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.init(allItems: items, selectedCurrencyCode: currencyCode ?? App.fiatCurrency) + } + + 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 + +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 d5f311416..6c70c998a 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 { @@ -181,8 +177,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) } @@ -220,20 +225,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/Color+DWStyle.swift b/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift index ed11636f4..323a64724 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 { @@ -84,6 +90,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/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() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift new file mode 100644 index 000000000..3bb2db530 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift @@ -0,0 +1,160 @@ +// +// 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 { + HStack(spacing: 0) { + HStack(spacing: Layout.fieldSpacing) { + magnifyingglass + searchField + clearButton + } + .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)) + } + } + .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(Color.black1000Alpha30) + } + } + } + + 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/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift b/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift index d9ebc03ea..d17d2cb75 100644 --- a/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift +++ b/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift @@ -178,7 +178,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) 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/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/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 + } +} 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" 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