diff --git a/DashSyncCurrentCommit b/DashSyncCurrentCommit index 5554751eb..8de791cf2 100644 --- a/DashSyncCurrentCommit +++ b/DashSyncCurrentCommit @@ -1 +1 @@ -03f3d451e8ce15e99ec2ef20b4b67fe07b717ca3 +315879b4d157a026fc760d3742f6a55a8883fde5 diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index e2f0db40c..3ca502a2e 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -191,7 +191,7 @@ 2A7A7BC92347E0D700451078 /* DWBaseFormTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BC82347E0D700451078 /* DWBaseFormTableViewCell.m */; }; 2A7A7BCD2347F01B00451078 /* DWSecurityMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BCC2347F01B00451078 /* DWSecurityMenuViewController.m */; }; 2A7A7BD02348A34800451078 /* DWSecurityMenuModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BCF2348A34800451078 /* DWSecurityMenuModel.m */; }; - 2A7A7BD62348CB6600451078 /* DWSettingsMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BD52348CB6600451078 /* DWSettingsMenuViewController.m */; }; + 2A7A7BD62348CB6600451078 /* SettingsMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BD52348CB6600451078 /* SettingsMenuViewController.swift */; }; 2A7A7BD92348CB7300451078 /* DWSettingsMenuModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BD82348CB7300451078 /* DWSettingsMenuModel.m */; }; 2A7A7C16234B763600451078 /* DWLocalCurrencyViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7C15234B763600451078 /* DWLocalCurrencyViewController.m */; }; 2A7A7C1D234B771400451078 /* DWLocalCurrencyModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7C1C234B771400451078 /* DWLocalCurrencyModel.m */; }; @@ -224,7 +224,6 @@ 2A8C24B423336FEA00000D43 /* DWQuickReceiveViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A8C24B323336FEA00000D43 /* DWQuickReceiveViewController.m */; }; 2A8C24B6233370A600000D43 /* QuickReceive.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A8C24B5233370A600000D43 /* QuickReceive.storyboard */; }; 2A8F420921BED16300858B91 /* DashSyncCurrentCommit in Resources */ = {isa = PBXBuildFile; fileRef = 2A8F420821BED16300858B91 /* DashSyncCurrentCommit */; }; - 2A8F420F21BEE95D00858B91 /* DWAboutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A8F420E21BEE95D00858B91 /* DWAboutViewController.m */; }; 2A8F422021BEFEEA00858B91 /* DWAboutModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A8F421F21BEFEEA00858B91 /* DWAboutModel.m */; }; 2A913E6623A11DFE006A2A59 /* DWURLActions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A913E6523A11DFE006A2A59 /* DWURLActions.m */; }; 2A913E6823A1473A006A2A59 /* DWURLRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A913E6723A14739006A2A59 /* DWURLRequestHandler.m */; }; @@ -476,7 +475,6 @@ 47AE8C1828C63F9C00490F5E /* PointOfUseDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8C1728C63F9C00490F5E /* PointOfUseDetailsView.swift */; }; 47AE8C1A28C6A21A00490F5E /* AllMerchantLocationsDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8C1928C6A21A00490F5E /* AllMerchantLocationsDataProvider.swift */; }; 47AE8C1C28C6AA2500490F5E /* PointOfUseLocationServicePopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8C1B28C6AA2400490F5E /* PointOfUseLocationServicePopup.swift */; }; - 47AE8C1E28C7491C00490F5E /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8C1D28C7491C00490F5E /* AboutViewController.swift */; }; 47AF180529070B720025803E /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AF180429070B720025803E /* Types.swift */; }; 47AF18082907B7880025803E /* BaseAmountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AF18072907B7880025803E /* BaseAmountModel.swift */; }; 47B30D78290BFCA60080C326 /* NumberFormatter+DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B30D77290BFCA60080C326 /* NumberFormatter+DashWallet.swift */; }; @@ -522,6 +520,12 @@ 47FA3AFF29350929008D58DC /* SyncingActivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA3AFE29350929008D58DC /* SyncingActivityMonitor.swift */; }; 47FA3B0229364991008D58DC /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA3B0129364991008D58DC /* HTTPClient.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 */; }; + 7503643E2C89D49A0029EC0D /* CoinJoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7503643D2C89D49A0029EC0D /* CoinJoinService.swift */; }; + 7503643F2C89D49A0029EC0D /* CoinJoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7503643D2C89D49A0029EC0D /* CoinJoinService.swift */; }; + 750CED602C94BFD7000FB837 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750CED5F2C94BFD7000FB837 /* SettingsViewModel.swift */; }; + 750CED612C94BFD7000FB837 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750CED5F2C94BFD7000FB837 /* SettingsViewModel.swift */; }; 7513DA882AB175E0005D55F6 /* TopperViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7527720E2AA9F58E0066557E /* TopperViewModel.swift */; }; 7513DA892AB17606005D55F6 /* Topper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75E2F3C92AA4D1B900C3B458 /* Topper.swift */; }; 7513DA8A2AB17666005D55F6 /* SupportedTopperAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7527720C2AA9B2630066557E /* SupportedTopperAssets.swift */; }; @@ -558,6 +562,10 @@ 754495DF2AE91D3500492817 /* UsernameRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754495DE2AE91D3500492817 /* UsernameRequestCell.swift */; }; 754BEA122C0B6BD700E8C93C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754BEA112C0B6BD700E8C93C /* HomeViewModel.swift */; }; 754BEA132C0B6BD700E8C93C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754BEA112C0B6BD700E8C93C /* HomeViewModel.swift */; }; + 755049A92C846299008FA7EB /* DWAboutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 755049A72C846299008FA7EB /* DWAboutViewController.m */; }; + 755049AA2C846299008FA7EB /* DWAboutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 755049A72C846299008FA7EB /* DWAboutViewController.m */; }; + 755049AC2C846576008FA7EB /* MenuItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755049AB2C846576008FA7EB /* MenuItemModel.swift */; }; + 755049AD2C846576008FA7EB /* MenuItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755049AB2C846576008FA7EB /* MenuItemModel.swift */; }; 755A22BD2B1385FD001F170D /* IconAttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A22BC2B1385FD001F170D /* IconAttributedText.swift */; }; 755B4B222B0C903500B844F0 /* DWDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755B4B212B0C903500B844F0 /* DWDateFormatter.swift */; }; 755B4B232B0C903500B844F0 /* DWDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755B4B212B0C903500B844F0 /* DWDateFormatter.swift */; }; @@ -960,8 +968,7 @@ C9D2C6AD2A320AA000D15901 /* HairlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8C1428C6378E00490F5E /* HairlineView.swift */; }; C9D2C6AE2A320AA000D15901 /* DWInitialViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A913E9423A3F75F006A2A59 /* DWInitialViewController.m */; }; C9D2C6AF2A320AA000D15901 /* BuySellPortalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751136B28D9A3DB00223B77 /* BuySellPortalViewController.swift */; }; - C9D2C6B02A320AA000D15901 /* DWSettingsMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BD52348CB6600451078 /* DWSettingsMenuViewController.m */; }; - C9D2C6B12A320AA000D15901 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8C1D28C7491C00490F5E /* AboutViewController.swift */; }; + C9D2C6B02A320AA000D15901 /* SettingsMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BD52348CB6600451078 /* SettingsMenuViewController.swift */; }; C9D2C6B22A320AA000D15901 /* UISpringTimingParameters+DWInit.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C69D92314727F001B8C90 /* UISpringTimingParameters+DWInit.m */; }; C9D2C6B62A320AA000D15901 /* NumberKeyboardButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661AC28F972BD00028A8D /* NumberKeyboardButton.swift */; }; C9D2C6B72A320AA000D15901 /* IsDefaultEmail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11AE3DD72997C599000856EE /* IsDefaultEmail.swift */; }; @@ -1404,7 +1411,6 @@ C9D2C8D12A320AA000D15901 /* DWSeedWordModel+DWLayoutSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AD1CE9622DD0E8E00C99324 /* DWSeedWordModel+DWLayoutSupport.m */; }; C9D2C8D22A320AA000D15901 /* CoinbaseAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EEE242293F436200049E0B /* CoinbaseAPIClient.swift */; }; C9D2C8D32A320AA000D15901 /* SendAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B30D7F291123D30080C326 /* SendAmountViewController.swift */; }; - C9D2C8D42A320AA000D15901 /* DWAboutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A8F420E21BEE95D00858B91 /* DWAboutViewController.m */; }; C9D2C8D52A320AA000D15901 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CF46A0296540EF0067B6EE /* AccountService.swift */; }; C9D2C8D62A320AA000D15901 /* ListHandlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C6E6E6291A90B2003FEDF2 /* ListHandlerView.swift */; }; C9D2C8D72A320AA000D15901 /* DWSecurityMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BCC2347F01B00451078 /* DWSecurityMenuViewController.m */; }; @@ -1914,8 +1920,7 @@ 2A7A7BCC2347F01B00451078 /* DWSecurityMenuViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWSecurityMenuViewController.m; sourceTree = ""; }; 2A7A7BCE2348A34800451078 /* DWSecurityMenuModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWSecurityMenuModel.h; sourceTree = ""; }; 2A7A7BCF2348A34800451078 /* DWSecurityMenuModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWSecurityMenuModel.m; sourceTree = ""; }; - 2A7A7BD42348CB6600451078 /* DWSettingsMenuViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWSettingsMenuViewController.h; sourceTree = ""; }; - 2A7A7BD52348CB6600451078 /* DWSettingsMenuViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWSettingsMenuViewController.m; sourceTree = ""; }; + 2A7A7BD52348CB6600451078 /* SettingsMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuViewController.swift; sourceTree = ""; }; 2A7A7BD72348CB7300451078 /* DWSettingsMenuModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWSettingsMenuModel.h; sourceTree = ""; }; 2A7A7BD82348CB7300451078 /* DWSettingsMenuModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWSettingsMenuModel.m; sourceTree = ""; }; 2A7A7C14234B763600451078 /* DWLocalCurrencyViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWLocalCurrencyViewController.h; sourceTree = ""; }; @@ -1978,8 +1983,6 @@ 2A8E79BC2406772900AA7C3D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 2A8E79BD2406772900AA7C3D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 2A8F420821BED16300858B91 /* DashSyncCurrentCommit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = DashSyncCurrentCommit; sourceTree = SOURCE_ROOT; }; - 2A8F420D21BEE95D00858B91 /* DWAboutViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWAboutViewController.h; sourceTree = ""; }; - 2A8F420E21BEE95D00858B91 /* DWAboutViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWAboutViewController.m; sourceTree = ""; }; 2A8F421E21BEFEEA00858B91 /* DWAboutModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWAboutModel.h; sourceTree = ""; }; 2A8F421F21BEFEEA00858B91 /* DWAboutModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWAboutModel.m; sourceTree = ""; }; 2A913E6023A11DCA006A2A59 /* DWURLRequestHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWURLRequestHandler.h; sourceTree = ""; }; @@ -2358,7 +2361,6 @@ 47AE8C1728C63F9C00490F5E /* PointOfUseDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfUseDetailsView.swift; sourceTree = ""; }; 47AE8C1928C6A21A00490F5E /* AllMerchantLocationsDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMerchantLocationsDataProvider.swift; sourceTree = ""; }; 47AE8C1B28C6AA2400490F5E /* PointOfUseLocationServicePopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfUseLocationServicePopup.swift; sourceTree = ""; }; - 47AE8C1D28C7491C00490F5E /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 47AF180429070B720025803E /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; 47AF18072907B7880025803E /* BaseAmountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAmountModel.swift; sourceTree = ""; }; 47B30D77290BFCA60080C326 /* NumberFormatter+DashWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+DashWallet.swift"; sourceTree = ""; }; @@ -2409,9 +2411,12 @@ 5FD4C91FB8EAB529E8E41227 /* Pods-dashwallet no watch.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet no watch.testnet.xcconfig"; path = "Pods/Target Support Files/Pods-dashwallet no watch/Pods-dashwallet no watch.testnet.xcconfig"; sourceTree = ""; }; 6FBBFC90577C940D8C04E0B1 /* Pods-DashWalletScreenshotsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.debug.xcconfig"; sourceTree = ""; }; 7502A4862AE401EF00ACDDD3 /* UsernameVotingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameVotingViewController.swift; sourceTree = ""; }; + 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinJoinProgressView.swift; sourceTree = ""; }; + 7503643D2C89D49A0029EC0D /* CoinJoinService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinJoinService.swift; sourceTree = ""; }; 7509C10E1AF3076100D03FD5 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 7509C1121AF3720100D03FD5 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 750C6CC01B5C8EB60038AAE9 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + 750CED5F2C94BFD7000FB837 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 7511E8CB1AE5FF240025F1B3 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; lineEnding = 0; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.simpleColoring; }; 7511E8CF1AE5FF2D0025F1B3 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 7511E8D31AE5FF390025F1B3 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; @@ -2436,6 +2441,9 @@ 754495DC2AE91B6300492817 /* GroupedRequestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedRequestCell.swift; sourceTree = ""; }; 754495DE2AE91D3500492817 /* UsernameRequestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameRequestCell.swift; sourceTree = ""; }; 754BEA112C0B6BD700E8C93C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; + 755049A72C846299008FA7EB /* DWAboutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWAboutViewController.m; sourceTree = ""; }; + 755049A82C846299008FA7EB /* DWAboutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWAboutViewController.h; sourceTree = ""; }; + 755049AB2C846576008FA7EB /* MenuItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemModel.swift; sourceTree = ""; }; 755A22BC2B1385FD001F170D /* IconAttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconAttributedText.swift; sourceTree = ""; }; 755B4B212B0C903500B844F0 /* DWDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DWDateFormatter.swift; sourceTree = ""; }; 755C32372C358FBD007DA721 /* BackupSeedPhraseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSeedPhraseViewController.swift; sourceTree = ""; }; @@ -3934,6 +3942,7 @@ 2A44313D22CF632F009BAF7F /* Models */ = { isa = PBXGroup; children = ( + 7503643C2C89D4890029EC0D /* CoinJoin */, 75A8C1612AE571E30042256E /* Voting */, 47081194298CF1E3003FCA3D /* Transactions */, 11BD737F28E7354200A34022 /* CrowdNode */, @@ -4034,6 +4043,7 @@ isa = PBXGroup; children = ( C9F451F22A0C933700825057 /* SyncingHeaderView.swift */, + 750364392C89CFB70029EC0D /* CoinJoinProgressView.swift */, ); path = Cells; sourceTree = ""; @@ -4126,6 +4136,7 @@ 2A7A7BCA2347EF7A00451078 /* Security */, 2A7A7BD32348CB4F00451078 /* Settings */, 2A7A7BDA2348DBE700451078 /* Tools */, + 755049AB2C846576008FA7EB /* MenuItemModel.swift */, ); path = Menu; sourceTree = ""; @@ -4190,10 +4201,10 @@ children = ( 2A7A7C13234B761700451078 /* LocalCurrency */, 2A8F420A21BEE69E00858B91 /* About */, - 2A7A7BD42348CB6600451078 /* DWSettingsMenuViewController.h */, - 2A7A7BD52348CB6600451078 /* DWSettingsMenuViewController.m */, + 2A7A7BD52348CB6600451078 /* SettingsMenuViewController.swift */, 2A7A7BD72348CB7300451078 /* DWSettingsMenuModel.h */, 2A7A7BD82348CB7300451078 /* DWSettingsMenuModel.m */, + 750CED5F2C94BFD7000FB837 /* SettingsViewModel.swift */, ); path = Settings; sourceTree = ""; @@ -4405,12 +4416,11 @@ 2A8F420A21BEE69E00858B91 /* About */ = { isa = PBXGroup; children = ( - 2A8F420D21BEE95D00858B91 /* DWAboutViewController.h */, - 2A8F420E21BEE95D00858B91 /* DWAboutViewController.m */, + 755049A82C846299008FA7EB /* DWAboutViewController.h */, + 755049A72C846299008FA7EB /* DWAboutViewController.m */, 2A8F421E21BEFEEA00858B91 /* DWAboutModel.h */, 2A8F421F21BEFEEA00858B91 /* DWAboutModel.m */, 2AB7C906234DB82700A56795 /* About.storyboard */, - 47AE8C1D28C7491C00490F5E /* AboutViewController.swift */, ); path = About; sourceTree = ""; @@ -5999,6 +6009,14 @@ path = Voting; sourceTree = ""; }; + 7503643C2C89D4890029EC0D /* CoinJoin */ = { + isa = PBXGroup; + children = ( + 7503643D2C89D49A0029EC0D /* CoinJoinService.swift */, + ); + path = CoinJoin; + sourceTree = ""; + }; 7531308F2B47EE480069C9B7 /* Model */ = { isa = PBXGroup; children = ( @@ -8464,8 +8482,7 @@ 47AE8C1528C6378E00490F5E /* HairlineView.swift in Sources */, 2A913E9523A3F75F006A2A59 /* DWInitialViewController.m in Sources */, 4751136C28D9A3DB00223B77 /* BuySellPortalViewController.swift in Sources */, - 2A7A7BD62348CB6600451078 /* DWSettingsMenuViewController.m in Sources */, - 47AE8C1E28C7491C00490F5E /* AboutViewController.swift in Sources */, + 2A7A7BD62348CB6600451078 /* SettingsMenuViewController.swift in Sources */, 2A0C69DA2314727F001B8C90 /* UISpringTimingParameters+DWInit.m in Sources */, 47C661AD28F972BD00028A8D /* NumberKeyboardButton.swift in Sources */, 11AE3DD82997C599000856EE /* IsDefaultEmail.swift in Sources */, @@ -8485,6 +8502,7 @@ 75FFD6BB2BF48DF80032879E /* HomeViewController+JailbreakCheck.swift in Sources */, 477F50102950A55A003C7508 /* Coinbase+Error.swift in Sources */, 2A63003F2327B4BB00827825 /* DWPaymentOutput+DWView.m in Sources */, + 750CED602C94BFD7000FB837 /* SettingsViewModel.swift in Sources */, 2ACD53EE234C9D8E00650AD3 /* UIView+DWRecursiveSubview.m in Sources */, 11860923297598B400279FCC /* AddressStatus.swift in Sources */, C91E919729FBACE6003E7883 /* ExtendedPublicKeysModel.swift in Sources */, @@ -8817,6 +8835,7 @@ 472D13E3299E23B7006903F1 /* BalanceNotifier.swift in Sources */, 2AD1CE6422D9127600C99324 /* DWSeedWordModel.m in Sources */, 7592AA7C2B9B08C000417F9E /* SupportedTopperPaymentMethods.swift in Sources */, + 7503643E2C89D49A0029EC0D /* CoinJoinService.swift in Sources */, 75AA33CC2BF9C82700F12465 /* ModalDialog.swift in Sources */, 2A44314022CF642C009BAF7F /* DWRootModel.m in Sources */, 47C661AF28FDAA3400028A8D /* BaseAmountViewController.swift in Sources */, @@ -8857,6 +8876,7 @@ 75CED09E2ACFD0ED0095F10C /* CoinbaseDepositRequest.swift in Sources */, C94D98212A4CC78F00F3BEE1 /* DashInputField.swift in Sources */, 2A8B9E6822FFE4CC00FF8653 /* DWPayOptionModel.m in Sources */, + 755049A92C846299008FA7EB /* DWAboutViewController.m in Sources */, 0F6EDFC928C896BD000427E7 /* CoinbaseTokenResponse.swift in Sources */, C9F42FB229DD5141001BC549 /* BackupInfoViewController.swift in Sources */, 47C6E6E5291A68B6003FEDF2 /* AppliedFiltersView.swift in Sources */, @@ -8903,6 +8923,7 @@ C909615B29F6535300002D82 /* DerivationPathKeysHeaderView.swift in Sources */, 4751CAD02970224D00F63AC4 /* ConvertCryptoOrderPreviewModel.swift in Sources */, 47A2E3A92972B15F0032A63B /* RatesProvider.swift in Sources */, + 7503643A2C89CFB70029EC0D /* CoinJoinProgressView.swift in Sources */, 47A2A2E9293E612900938DB7 /* CBAuth.swift in Sources */, 7566F48A2BB6CAF2005238D2 /* MenuItem.swift in Sources */, 0F6EDFD128C896BD000427E7 /* CoinbaseCreateAddressesRequest.swift in Sources */, @@ -8949,12 +8970,12 @@ C9F42FB829DFC507001BC549 /* SpendableTransaction.swift in Sources */, 11ED906B29681773003784F9 /* StakingInfoDialogController.swift in Sources */, 0F36937E2919A5DB007F4E91 /* TwoFactorAuthViewController.swift in Sources */, + 755049AC2C846576008FA7EB /* MenuItemModel.swift in Sources */, 75EBAA292BBBE385004488E3 /* ZenLedgerViewModel.swift in Sources */, 471A260A289ACDF70056B7B2 /* Taxes.swift in Sources */, 2AD1CE9722DD0E8E00C99324 /* DWSeedWordModel+DWLayoutSupport.m in Sources */, 47EEE243293F436200049E0B /* CoinbaseAPIClient.swift in Sources */, 47B30D80291123D30080C326 /* SendAmountViewController.swift in Sources */, - 2A8F420F21BEE95D00858B91 /* DWAboutViewController.m in Sources */, 47CF46A1296540EF0067B6EE /* AccountService.swift in Sources */, 47C6E6E7291A90B3003FEDF2 /* ListHandlerView.swift in Sources */, 7566F4832BB6949E005238D2 /* ToolsMenuViewController.swift in Sources */, @@ -9048,6 +9069,7 @@ C9D2C69A2A320AA000D15901 /* CrowdNodeAPI.swift in Sources */, C9D2C69B2A320AA000D15901 /* DWStartModel.m in Sources */, 754495DD2AE91B6300492817 /* GroupedRequestCell.swift in Sources */, + 7503643B2C89CFB70029EC0D /* CoinJoinProgressView.swift in Sources */, C9D2C69C2A320AA000D15901 /* DWAdvancedSecurityModelStub.m in Sources */, C9D2C69D2A320AA000D15901 /* Foundation+Bitcoin.swift in Sources */, C9D2C69E2A320AA000D15901 /* AtmDetailsView.swift in Sources */, @@ -9073,8 +9095,7 @@ C9D2C6AE2A320AA000D15901 /* DWInitialViewController.m in Sources */, C943B52D2A40A54600AF23C5 /* DWDashPayContactsUpdater.m in Sources */, C9D2C6AF2A320AA000D15901 /* BuySellPortalViewController.swift in Sources */, - C9D2C6B02A320AA000D15901 /* DWSettingsMenuViewController.m in Sources */, - C9D2C6B12A320AA000D15901 /* AboutViewController.swift in Sources */, + C9D2C6B02A320AA000D15901 /* SettingsMenuViewController.swift in Sources */, 751B61C52ADFFD0700D1C2EF /* IntegrationViewController+Uphold.swift in Sources */, C943B5022A40A54600AF23C5 /* DWDPGenericStatusItemView.m in Sources */, C9D2C6B22A320AA000D15901 /* UISpringTimingParameters+DWInit.m in Sources */, @@ -9184,6 +9205,7 @@ C9D2C7022A320AA000D15901 /* CrowdNodeWebViewController.swift in Sources */, C9D2C7032A320AA000D15901 /* UIViewController+DWDisplayError.m in Sources */, C9D2C7042A320AA000D15901 /* BRAppleWatchData.m in Sources */, + 755049AD2C846576008FA7EB /* MenuItemModel.swift in Sources */, C9D2C7052A320AA000D15901 /* DWRequestAmountContentView.m in Sources */, C943B3312A408CED00AF23C5 /* DWProfileAboutCellModel.m in Sources */, C943B4C42A40A54600AF23C5 /* DWSearchViewController.m in Sources */, @@ -9215,6 +9237,7 @@ C9D2C7152A320AA000D15901 /* DWBasePayViewController.m in Sources */, C943B4AC2A40A54600AF23C5 /* DWContactsViewController.m in Sources */, C9D2C7162A320AA000D15901 /* CrowdNodeTransferModel.swift in Sources */, + 750CED612C94BFD7000FB837 /* SettingsViewModel.swift in Sources */, C9D2C7172A320AA000D15901 /* DWSetPinViewController.m in Sources */, C943B3322A408CED00AF23C5 /* DWProfileDisplayNameCellModel.m in Sources */, C9D2C7182A320AA000D15901 /* ConfirmOrderController.swift in Sources */, @@ -9301,6 +9324,7 @@ C943B4BC2A40A54600AF23C5 /* DWRootContactsViewController.m in Sources */, C943B4C32A40A54600AF23C5 /* DWTitleActionHeaderView.m in Sources */, 7531308E2B47EC910069C9B7 /* UpholdClient.swift in Sources */, + 755049AA2C846299008FA7EB /* DWAboutViewController.m in Sources */, C9D2C7502A320AA000D15901 /* ServiceOverviewViewController.swift in Sources */, C9D2C7512A320AA000D15901 /* AccountListModel.swift in Sources */, C9D2C7532A320AA000D15901 /* DWRecoverWalletCommand.m in Sources */, @@ -9470,6 +9494,7 @@ C9D2C7D12A320AA000D15901 /* DWMainMenuContentView.m in Sources */, C9D2C7D32A320AA000D15901 /* CrowdNodeAPIConfirmationTx.swift in Sources */, C9D2C7D42A320AA000D15901 /* UIViewController+DWEmbedding.m in Sources */, + 7503643F2C89D49A0029EC0D /* CoinJoinService.swift in Sources */, C9D2C7D62A320AA000D15901 /* CALayer+DWShadow.m in Sources */, C9D2C7D72A320AA000D15901 /* MerchantDAO.swift in Sources */, C9D2C7D82A320AA000D15901 /* ExploreDatabaseSyncManager.swift in Sources */, @@ -9774,7 +9799,6 @@ C943B4ED2A40A54600AF23C5 /* DWPendingContactInfoView.m in Sources */, C9D2C8D32A320AA000D15901 /* SendAmountViewController.swift in Sources */, 757514E02B15D8DE0026AD8E /* VotingConstants.swift in Sources */, - C9D2C8D42A320AA000D15901 /* DWAboutViewController.m in Sources */, C943B51E2A40A54600AF23C5 /* InvitationTopView.swift in Sources */, C9D2C8D52A320AA000D15901 /* AccountService.swift in Sources */, C9D2C8D62A320AA000D15901 /* ListHandlerView.swift in Sources */, diff --git a/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/Contents.json new file mode 100644 index 000000000..f381203f8 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "image.coinjoin.menu.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "image.coinjoin.menu@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "image.coinjoin.menu@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu.png b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu.png new file mode 100644 index 000000000..f65f5980d Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu@2x.png b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu@2x.png new file mode 100644 index 000000000..d13f537c3 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu@2x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu@3x.png b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu@3x.png new file mode 100644 index 000000000..1949a1f54 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/CoinJoin/image.coinjoin.menu.imageset/image.coinjoin.menu@3x.png differ diff --git a/DashWallet/Sources/Models/CoinJoin/CoinJoinService.swift b/DashWallet/Sources/Models/CoinJoin/CoinJoinService.swift new file mode 100644 index 000000000..500d1376b --- /dev/null +++ b/DashWallet/Sources/Models/CoinJoin/CoinJoinService.swift @@ -0,0 +1,298 @@ +// +// Created by Andrei Ashikhmin +// 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 + +enum MixingStatus: Int { + case notStarted + case mixing + case paused + case finished + case error + + var isInProgress: Bool { + get { + return self == .mixing || self == .paused || self == .error + } + } + + var localizedValue: String { + get { + switch self { + case .notStarted: + NSLocalizedString("Not started", comment: "CoinJoin") + case .mixing: + NSLocalizedString("Mixing ·", comment: "CoinJoin") + case .paused: + NSLocalizedString("Mixing Paused ·", comment: "CoinJoin") + case .finished: + NSLocalizedString("Fully mixed", comment: "CoinJoin") + case .error: + NSLocalizedString("Error ·", comment: "CoinJoin") + } + } + } +} + +enum CoinJoinMode { + case none + case intermediate + case advanced +} + +private let kDefaultMultisession = false // for stability, need to investigate +private let kDefaultRounds: Int32 = 1 //4 TODO +private let kDefaultSessions: Int32 = 1 //6 TODO +private let kDefaultDenominationGoal: Int32 = 50 +private let kDefaultDenominationHardcap: Int32 = 300 +private let kCoinJoinMode = "coinJoinModeKey" + +class CoinJoinService: NSObject { + static let shared: CoinJoinService = { + return CoinJoinService() + }() + + private var cancellableBag = Set() + private let updateMutex = NSLock() + private let updateMixingStateMutex = NSLock() + private var coinJoinManager: DSCoinJoinManager? = nil + private var hasAnonymizableBalance: Bool = false + private var networkStatus: NetworkStatus = .online + + @Published private(set) var mode: CoinJoinMode = .none + @Published var mixingState: MixingStatus = .notStarted + @Published private(set) var progress: Double = 0.0 + @Published private(set) var totalBalance: UInt64 = 0 + @Published private(set) var coinJoinBalance: UInt64 = 0 + @Published private(set) var activeSessions: Int = 0 + + override init() { + super.init() + NotificationCenter.default.publisher(for: NSNotification.Name.DSWalletBalanceDidChange) + .sink { [weak self] _ in self?.updateBalance(balance: DWEnvironment.sharedInstance().currentAccount.balance) } + .store(in: &cancellableBag) + } + + func updateMode(mode: CoinJoinMode) { + self.coinJoinManager?.updateOptions(withEnabled: mode != .none) + let account = DWEnvironment.sharedInstance().currentAccount + let balance = account.balance + + if (mode != .none && self.mode == .none) { + configureMixing(amount: balance) + } + + updateBalance(balance: balance) + // TODO: timeskew + updateState(balance: balance, mode: mode, timeSkew: TimeInterval(0), hasAnonymizableBalance: self.hasAnonymizableBalance, networkStatus: self.networkStatus, chain: DWEnvironment.sharedInstance().currentChain) + } + + private func prepareMixing() { + guard let coinJoinManager = self.coinJoinManager ?? createCoinJoinManager() else { return } + + coinJoinManager.setStopOnNothingToDo(true) + coinJoinManager.start() + } + + private func startMixing() { + guard let coinJoinManager = self.coinJoinManager else { return } + + if !coinJoinManager.startMixing() { + print("[SW] CoinJoin: Mixing has been started already.") + } else { + coinJoinManager.refreshUnusedKeys() + coinJoinManager.initMasternodeGroup() + coinJoinManager.doAutomaticDenominating() + + DSLogger.log("[SW] CoinJoin: Mixing \(coinJoinManager.startMixing() ? "started successfully" : "start failed, will retry")") // TODO: failed statuses: \(coinJoinManager.statuses) + } + } + + private func configureMixing(amount: UInt64) { + guard let coinJoinManager = self.coinJoinManager ?? createCoinJoinManager() else { return } + + let rounds: Int32 + switch mode { + case .none: + return + case .intermediate: + rounds = kDefaultRounds + case .advanced: + rounds = kDefaultRounds * 2 + } + + coinJoinManager.configureMixing(withAmount: amount, rounds: rounds, sessions: kDefaultSessions, withMultisession: kDefaultMultisession, denominationGoal: kDefaultDenominationGoal, denominationHardCap: kDefaultDenominationHardcap) + } + + private func updateProgress() { + guard let coinJoinManager = self.coinJoinManager else { return } + self.progress = coinJoinManager.getMixingProgress() + let coinJoinBalance = coinJoinManager.getBalance() + self.totalBalance = coinJoinBalance.myTrusted + self.coinJoinBalance = coinJoinBalance.anonymized + } + + private func createCoinJoinManager() -> DSCoinJoinManager? { + self.coinJoinManager = DSCoinJoinManager.sharedInstance(for: DWEnvironment().currentChain) + coinJoinManager?.managerDelegate = self + return self.coinJoinManager + } + + private func synchronized(_ lock: NSLock, closure: () -> Void) { + lock.lock() + defer { lock.unlock() } + closure() + } + + private func updateBalance(balance: UInt64) { + guard let coinJoinManager = self.coinJoinManager else { return } + + coinJoinManager.updateOptions(withAmount: balance) + DSLogger.log("[SW] CoinJoin: total balance: \(balance)") + let canDenominate = coinJoinManager.doAutomaticDenominating(withDryRun: true) + + let coinJoinBalance = coinJoinManager.getBalance() + DSLogger.log("[SW] CoinJoin: mixed balance: \(coinJoinBalance.anonymized)") + + let anonBalance = coinJoinManager.getAnonymizableBalance(withSkipDenominated: false, skipUnconfirmed: false) + DSLogger.log("[SW] CoinJoin: anonymizable balance \(anonBalance)") + + let smallestDenomination = coinJoinManager.getSmallestDenomination() + let hasPartiallyMixedCoins = (coinJoinBalance.denominatedTrusted - coinJoinBalance.anonymized) > 0 + let hasAnonymizableBalance = anonBalance > smallestDenomination + let hasBalanceLeftToMix: Bool + + if hasPartiallyMixedCoins { + hasBalanceLeftToMix = true + } else if hasAnonymizableBalance && canDenominate { + hasBalanceLeftToMix = true + } else { + hasBalanceLeftToMix = false + } + + DSLogger.log("[SW] CoinJoin: can mix balance: \(hasBalanceLeftToMix) = balance: (\(anonBalance > smallestDenomination) && canDenominate: \(canDenominate)) || partially-mixed: \(hasPartiallyMixedCoins)") + + updateState( + balance: balance, + mode: self.mode, + timeSkew: TimeInterval(0), // TODO + hasAnonymizableBalance: hasBalanceLeftToMix, + networkStatus: self.networkStatus, + chain: DWEnvironment.sharedInstance().currentChain + ) + } + + private func stopMixing() { + self.coinJoinManager?.managerDelegate = nil + self.coinJoinManager?.stop() + } + + private func updateState( + balance: UInt64, + mode: CoinJoinMode, + timeSkew: TimeInterval, + hasAnonymizableBalance: Bool, + networkStatus: NetworkStatus, + chain: DSChain + ) { + synchronized(self.updateMutex) { + DSLogger.log("[SW] CoinJoin: \(mode), \(timeSkew) ms, \(hasAnonymizableBalance), \(networkStatus), synced: \(chain.chainManager!.isSynced)") + + self.networkStatus = networkStatus + self.hasAnonymizableBalance = hasAnonymizableBalance + self.mode = mode + // self.timeSkew = timeSkew + + if mode == .none /*|| !isInsideTimeSkewBounds(timeSkew) || blockchainState.replaying*/ { // TODO + updateMixingState(state: .notStarted) + } else { + configureMixing(amount: balance) + + if hasAnonymizableBalance { + if networkStatus == .online && chain.chainManager!.isSynced { + updateMixingState(state: .mixing) + } else { + updateMixingState(state: .paused) + } + } else { + updateMixingState(state: .finished) + } + } + + updateProgress() + } + } + + private func updateMixingState(state: MixingStatus) { + synchronized(self.updateMixingStateMutex) { + let previousMixingStatus = self.mixingState + DSLogger.log("[SW] CoinJoin: \(previousMixingStatus) -> \(state)") + + if previousMixingStatus == .paused && state != .paused { + DSLogger.log("[SW] CoinJoin: moving from paused to \(state)") + } + + self.mixingState = state + + if state == .mixing && previousMixingStatus != .mixing { + // start mixing + prepareMixing() + startMixing() + } else if previousMixingStatus == .mixing && state != .mixing { + // finish mixing + stopMixing() + } + } + } +} + +extension CoinJoinService: DSCoinJoinManagerDelegate { + func sessionStarted(withId baseId: Int32, clientSessionId clientId: UInt256, denomination denom: UInt32, poolState state: PoolState, poolMessage message: PoolMessage, ipAddress address: UInt128, isJoined joined: Bool) { + updateActiveSessions() + } + + func sessionComplete(withId baseId: Int32, clientSessionId clientId: UInt256, denomination denom: UInt32, poolState state: PoolState, poolMessage message: PoolMessage, ipAddress address: UInt128, isJoined joined: Bool) { + updateActiveSessions() + } + + func mixingStarted() { } + + func mixingComplete(_ withError: Bool) { + if withError { + DSLogger.log("[SW] CoinJoin: Mixing Error. \(progress)% mixed") + } else { + DSLogger.log("[SW] CoinJoin: Mixing Complete. \(progress)% mixed") + } + + self.updateMixingState(state: withError ? .error : .finished) // TODO: paused? + } + + func transactionProcessed(withId txId: UInt256, type: CoinJoinTransactionType) { + self.updateProgress() + } + + private func updateActiveSessions() { + guard let coinJoinManager = self.coinJoinManager else { return } + + let activeSessions = coinJoinManager.getActiveSessionCount() + self.activeSessions = Int(activeSessions) + + DSLogger.log("[SW] CoinJoin: Active sessions: \(activeSessions)") + } +} + diff --git a/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinLevelsViewController.swift b/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinLevelsViewController.swift index b4827eeb8..a095163d0 100644 --- a/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinLevelsViewController.swift +++ b/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinLevelsViewController.swift @@ -32,8 +32,6 @@ class CoinJoinLevelsViewController: UIViewController { @IBOutlet private var advancedTime: UILabel! @IBOutlet private var continueButton: ActionButton! - @Published private(set) var selectedMode: CoinJoinMode = .none - @objc static func controller() -> CoinJoinLevelsViewController { vc(CoinJoinLevelsViewController.self, from: sb("CoinJoin")) @@ -47,9 +45,9 @@ class CoinJoinLevelsViewController: UIViewController { @IBAction func continueButtonAction() { - if viewModel.status == .notStarted { + if viewModel.mixingState == .notStarted { self.navigationController?.popViewController(animated: true) - viewModel.startMixing(mode: selectedMode) + viewModel.startMixing() } else { let alert = UIAlertController(title: NSLocalizedString("Are you sure you want to stop mixing?", comment: "CoinJoin"), message: NSLocalizedString("Any funds that have been mixed will be combined with your un mixed funds", comment: "CoinJoin"), preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("Stop Mixing", comment: "CoinJoin"), style: .destructive, handler: { [weak self] _ in @@ -64,8 +62,6 @@ class CoinJoinLevelsViewController: UIViewController { extension CoinJoinLevelsViewController { private func configureHierarchy() { - selectedMode = viewModel.mode - titleLabel.text = NSLocalizedString("Select mixing level", comment: "CoinJoin") intermediateTitle.text = NSLocalizedString("Intermediate", comment: "CoinJoin") intermediateDescription.text = NSLocalizedString("Advanced users who have a very high level of technical expertise can determine your transaction history", comment: "Coinbase") @@ -92,32 +88,32 @@ extension CoinJoinLevelsViewController { @objc private func selectIntermediate() { - if selectedMode == .intermediate { + if viewModel.selectedMode == .intermediate { return } - if viewModel.status == .mixing { + if viewModel.mixingState == .mixing { confirmFor(.intermediate) } else { - selectedMode = .intermediate + viewModel.selectedMode = .intermediate } } @objc private func selectAdvanced() { - if selectedMode == .advanced { + if viewModel.selectedMode == .advanced { return } - if viewModel.status == .mixing { + if viewModel.mixingState == .mixing { confirmFor(.advanced) } else { - selectedMode = .advanced + viewModel.selectedMode = .advanced } } private func configureObservers() { - $selectedMode + viewModel.$selectedMode .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] mode in guard let self = self else { return } @@ -135,7 +131,7 @@ extension CoinJoinLevelsViewController { }) .store(in: &cancellableBag) - viewModel.$status + viewModel.$mixingState .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] status in guard let self = self else { return } @@ -179,7 +175,7 @@ extension CoinJoinLevelsViewController { let alert = UIAlertController(title: "", message: NSLocalizedString("Are you sure you want to change the privacy level?", comment: "CoinJoin"), preferredStyle: .alert) alert.addAction(UIAlertAction(title: title, style: .default, handler: { [weak self] _ in - self?.selectedMode = mode + self?.viewModel.selectedMode = mode })) let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) alert.addAction(cancelAction) diff --git a/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinViewModel.swift b/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinViewModel.swift index 7a1958fb5..db84313f3 100644 --- a/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinViewModel.swift +++ b/DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinViewModel.swift @@ -16,6 +16,7 @@ // import Foundation +import Combine @objc public class CoinJoinObjcWrapper: NSObject { @@ -25,28 +26,18 @@ public class CoinJoinObjcWrapper: NSObject { } } - -enum CoinJoinMode { - case none - case intermediate - case advanced -} - -enum MixingStatus { - case notStarted - case mixing - case paused - case finished - case error -} - private let kInfoShown = "coinJoinInfoShownKey" -class CoinJoinViewModel { +class CoinJoinViewModel: ObservableObject { static let shared = CoinJoinViewModel() + private var cancellableBag = Set() + private let coinJoinService = CoinJoinService.shared - private(set) var mode: CoinJoinMode = .none - @Published private(set) var status: MixingStatus = .notStarted + @Published var selectedMode: CoinJoinMode = .none + @Published private(set) var mixingState: MixingStatus = .notStarted + @Published private(set) var progress: Double = 0.0 + @Published private(set) var totalBalance: UInt64 = 0 + @Published private(set) var coinJoinBalance: UInt64 = 0 private var _infoShown: Bool? = nil var infoShown: Bool { @@ -57,12 +48,33 @@ class CoinJoinViewModel { } } - func startMixing(mode: CoinJoinMode) { - self.mode = mode - status = .mixing + init() { + coinJoinService.$mixingState + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + self?.mixingState = state + } + .store(in: &cancellableBag) + + coinJoinService.$progress + .receive(on: DispatchQueue.main) + .sink { [weak self] progress in + guard let self = self else { return } + self.progress = progress + self.totalBalance = coinJoinService.totalBalance + self.coinJoinBalance = coinJoinService.coinJoinBalance + } + .store(in: &cancellableBag) + } + + func startMixing() { + if self.selectedMode != .none { + coinJoinService.updateMode(mode: self.selectedMode) + } } func stopMixing() { - status = .notStarted + selectedMode = .none + coinJoinService.updateMode(mode: .none) } } diff --git a/DashWallet/Sources/UI/Home/Views/Cells/CoinJoinProgressView.swift b/DashWallet/Sources/UI/Home/Views/Cells/CoinJoinProgressView.swift new file mode 100644 index 000000000..00f174120 --- /dev/null +++ b/DashWallet/Sources/UI/Home/Views/Cells/CoinJoinProgressView.swift @@ -0,0 +1,98 @@ +// +// Created by Andrei Ashikhmin +// 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 SwiftUI + +struct CoinJoinProgressView: View { + @State var state: MixingStatus + @State var progress: Double + @State var mixed: Double + @State var total: Double + + var body: some View { + HStack(spacing: 12) { + ZStack { + Circle() + .fill(Color.dashBlue.opacity(0.1)) + .frame(width: 38, height: 38) + + Image("image.coinjoin.menu") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + } + + VStack(alignment: .leading, spacing: 4) { + CoinJoinProgressInfo(state: state, progress: progress, mixed: mixed, total: total, textColor: .primaryText, font: .subheadline) + .padding(.leading, -6) + SwiftUI.ProgressView(value: progress) + .progressViewStyle(LinearProgressViewStyle(tint: .dashBlue)) + .frame(height: 6) + .padding(.top, 2) + } + } + .padding(12) + .background(Color.secondaryBackground) + .cornerRadius(8) + } +} + +struct CoinJoinProgressInfo: View { + @State var state: MixingStatus + @State var progress: Double + @State var mixed: Double + @State var total: Double + var textColor: Color + var font: Font + + var body: some View { + HStack(spacing: 0) { + if state == .mixing { + SwiftUI.ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .dashBlue)) + .scaleEffect(0.5) + } + + Text(state.localizedValue) + .foregroundColor(textColor) + .font(font) + .padding(.leading, state == .mixing ? 2 : 5) + + if state.isInProgress { + Text(progress.formatted(.percent.precision(.fractionLength(0...2)))) + .foregroundColor(textColor) + .font(font) + .padding(.leading, 4) + } + + Spacer() + Text("\(mixed, format: .number.precision(.fractionLength(0...3))) of \(total, format: .number.precision(.fractionLength(0...3)))") // TODO + .foregroundColor(textColor) + .font(font) + Image("icon_dash_currency") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: font.pointSize, height: font.pointSize) + .padding(.leading, 2) + .foregroundColor(textColor) + } + } +} + +#Preview { + CoinJoinProgressView(state: .mixing, progress: 0.45, mixed: 0.123, total: 0.321) +} diff --git a/DashWallet/Sources/UI/Home/Views/HomeView.swift b/DashWallet/Sources/UI/Home/Views/HomeView.swift index 95b028243..b2eb3f33b 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeView.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeView.swift @@ -192,6 +192,7 @@ final class HomeView: UIView, DWHomeModelUpdatesObserver, DWDPRegistrationErrorR private func setIdentity(dpInfoHidden: Bool, model: DWHomeProtocol) { headerView.isDPWelcomeViewHidden = dpInfoHidden headerView.isVotingViewHidden = true + viewModel.showJoinDashpay = !headerView.isDPWelcomeViewHidden let status = model.dashPayModel.registrationStatus let completed = model.dashPayModel.registrationCompleted @@ -212,6 +213,7 @@ final class HomeView: UIView, DWHomeModelUpdatesObserver, DWDPRegistrationErrorR let now = Date().timeIntervalSince1970 headerView.isVotingViewHidden = dpInfoHidden || wasClosed || now < VotingConstants.votingEndTime headerView.isDPWelcomeViewHidden = true + viewModel.showJoinDashpay = !headerView.isDPWelcomeViewHidden let dao = UsernameRequestsDAOImpl.shared Task { @@ -287,7 +289,18 @@ struct TransactionList: View { LazyVStack(pinnedViews: [.sectionHeaders]) { balanceHeader() - .frame(height: viewModel.hasNetwork ? 250 : 335) + .frame(height: viewModel.balanceHeaderHeight) + + if viewModel.coinJoinItem.isOn { + CoinJoinProgressView( + state: viewModel.coinJoinItem.state, + progress: viewModel.coinJoinItem.progress, + mixed: viewModel.coinJoinItem.mixed, + total: viewModel.coinJoinItem.total + ) + .padding(.horizontal, 15) + .id(viewModel.coinJoinItem.id) + } syncingHeader() .frame(height: 50) @@ -322,6 +335,9 @@ struct TransactionList: View { .sheet(item: $selectedTxDataItem) { item in TransactionDetailsSheet(item: item) } + .onChange(of: viewModel.coinJoinItem) { new in + DSLogger.log("[SW] CoinJoin: on change of coinJoinItem: \(viewModel.coinJoinItem.description)") + } } @ViewBuilder diff --git a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift index f6b3df166..4ad97c71c 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift @@ -16,17 +16,31 @@ // import Foundation +import Combine + +let kBaseBalanceHeaderHeight: CGFloat = 250 class HomeViewModel: ObservableObject { + private var cancellableBag = Set() + private let coinJoinService = CoinJoinService.shared + @Published var txItems: Array<(DateKey, [TransactionListDataItem])> = [] - @Published var hasNetwork: Bool = true + @Published var balanceHeaderHeight: CGFloat = kBaseBalanceHeaderHeight // TDOO: move back to HomeView when fully transitioned to SwiftUI + @Published var coinJoinItem = CoinJoinMenuItemModel(title: NSLocalizedString("Mixing", comment: "CoinJoin"), isOn: false, state: .notStarted, progress: 0.0, mixed: 0.0, total: 0.0) + private var model: SyncModel = SyncModelImpl() + var showJoinDashpay: Bool = false { + didSet { + self.recalculateHeight() + } + } init() { model.networkStatusDidChange = { status in - self.hasNetwork = status == .online + self.recalculateHeight() } - self.hasNetwork = model.networkStatus == .online + self.recalculateHeight() + self.observeCoinJoin() } func updateItems(transactions: [DSTransaction]) { @@ -60,6 +74,60 @@ class HomeViewModel: ObservableObject { } } } + + private func recalculateHeight() { + var height = kBaseBalanceHeaderHeight + let hasNetwork = model.networkStatus == .online + + if !hasNetwork { + height += 85 + } + + if showJoinDashpay { + height += 50 + } + + self.balanceHeaderHeight = height + } +} + +extension HomeViewModel { + private func observeCoinJoin() { + coinJoinService.$progress + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.refreshCoinJoinItem() + } + .store(in: &cancellableBag) + + coinJoinService.$mode + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.refreshCoinJoinItem() + } + .store(in: &cancellableBag) + + coinJoinService.$mixingState + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.refreshCoinJoinItem() + } + .store(in: &cancellableBag) + } + + private func refreshCoinJoinItem() { + self.coinJoinItem = CoinJoinMenuItemModel( + title: NSLocalizedString("Mixing", comment: "CoinJoin"), + isOn: coinJoinService.mixingState.isInProgress, + state: coinJoinService.mixingState, + progress: coinJoinService.progress, + mixed: Double(coinJoinService.coinJoinBalance) / Double(DUFFS), + total: Double(coinJoinService.totalBalance) / Double(DUFFS) + ) + } } // MARK: - TransactionListDataItem diff --git a/DashWallet/Sources/UI/Menu/Main/DWMainMenuViewController.m b/DashWallet/Sources/UI/Menu/Main/DWMainMenuViewController.m index deebf02c4..ad591d168 100644 --- a/DashWallet/Sources/UI/Menu/Main/DWMainMenuViewController.m +++ b/DashWallet/Sources/UI/Menu/Main/DWMainMenuViewController.m @@ -25,7 +25,6 @@ #import "DWMainMenuContentView.h" #import "DWMainMenuModel.h" #import "DWSecurityMenuViewController.h" -#import "DWSettingsMenuViewController.h" #import "SFSafariViewController+DashWallet.h" #import "dashwallet-Swift.h" diff --git a/DashWallet/Sources/UI/Menu/MenuItemModel.swift b/DashWallet/Sources/UI/Menu/MenuItemModel.swift new file mode 100644 index 000000000..e4ca2a82e --- /dev/null +++ b/DashWallet/Sources/UI/Menu/MenuItemModel.swift @@ -0,0 +1,69 @@ +// +// Created by Andrei Ashikhmin +// 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 SwiftUI + +class MenuItemModel: Identifiable, Equatable { + let id = UUID() + + var title: String + var subtitle: String? = nil + var details: String? = nil + var icon: IconName? = nil + var showInfo: Bool = false + var showChevron: Bool = false + var showToggle: Bool = false + @State var isToggled: Bool = false + var action: (() -> Void)? = nil + + init(title: String, subtitle: String? = nil, details: String? = nil, icon: IconName? = nil, showInfo: Bool = false, showChevron: Bool = false, showToggle: Bool = false, isToggled: Bool = false, action: (() -> Void)? = nil) { + self.title = title + self.subtitle = subtitle + self.details = details + self.icon = icon + self.showInfo = showInfo + self.showChevron = showChevron + self.showToggle = showToggle + self.isToggled = isToggled + self.action = action + } + + static func == (lhs: MenuItemModel, rhs: MenuItemModel) -> Bool { + lhs.id == rhs.id + } +} + +class CoinJoinMenuItemModel: MenuItemModel { + @State var isOn: Bool + @State var state: MixingStatus + @State var progress: Double + @State var mixed: Double + @State var total: Double + + init(title: String, isOn: Bool, state: MixingStatus, progress: Double, mixed: Double, total: Double, action: (() -> Void)? = nil) { + self.isOn = isOn + self.state = state + self.progress = progress + self.mixed = mixed + self.total = total + super.init(title: title, action: action) + } + + var description: String { + return "CoinJoinMenuItemModel(title: \(title), isOn: \(isOn), state: \(state), progress: \(progress), mixed: \(mixed), total: \(total))" + } +} diff --git a/DashWallet/Sources/UI/Menu/Settings/About/AboutViewController.swift b/DashWallet/Sources/UI/Menu/Settings/About/AboutViewController.swift deleted file mode 100644 index f5023df2c..000000000 --- a/DashWallet/Sources/UI/Menu/Settings/About/AboutViewController.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Created by Pavel Tikhonenko -// Copyright © 2022 Dash Core Group. All rights reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import UIKit - -// MARK: - AboutViewController - -@objc -class AboutViewController: UIViewController { - private var model = DWAboutModel() - - private weak var techInfoAlert: UIAlertController? - - @objc - static func controller() -> AboutViewController { - let controller = AboutViewController() - controller.hidesBottomBarWhenPushed = true - controller.title = NSLocalizedString("About", comment: "") - return controller - } - - override func viewDidLoad() { - super.viewDidLoad() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.prefersLargeTitles = true - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - navigationController?.navigationBar.prefersLargeTitles = false - } -} - -extension AboutViewController { - private func configureHierarchy() { } -} - diff --git a/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.h b/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.h index 36e943dc0..d79322fcf 100644 --- a/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.h +++ b/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN @interface DWAboutViewController : UIViewController -+ (instancetype)controller; ++ (instancetype)createController; @end diff --git a/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.m b/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.m index 1191e0c7b..28787a364 100644 --- a/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.m +++ b/DashWallet/Sources/UI/Menu/Settings/About/DWAboutViewController.m @@ -52,7 +52,7 @@ @interface DWAboutViewController () @implementation DWAboutViewController -+ (instancetype)controller { ++ (instancetype)createController { UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"About" bundle:nil]; DWAboutViewController *controller = [storyboard instantiateInitialViewController]; controller.hidesBottomBarWhenPushed = YES; diff --git a/DashWallet/Sources/UI/Menu/Settings/DWSettingsMenuViewController.h b/DashWallet/Sources/UI/Menu/Settings/DWSettingsMenuViewController.h deleted file mode 100644 index 2ad6ca642..000000000 --- a/DashWallet/Sources/UI/Menu/Settings/DWSettingsMenuViewController.h +++ /dev/null @@ -1,36 +0,0 @@ -// -// Created by Andrew Podkovyrin -// Copyright © 2019 Dash Core Group. All rights reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@class DWSettingsMenuViewController; - -@protocol DWSettingsMenuViewControllerDelegate - -- (void)settingsMenuViewControllerDidRescanBlockchain:(DWSettingsMenuViewController *)controller; - -@end - -@interface DWSettingsMenuViewController : UIViewController - -@property (nullable, nonatomic, weak) id delegate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/DashWallet/Sources/UI/Menu/Settings/DWSettingsMenuViewController.m b/DashWallet/Sources/UI/Menu/Settings/DWSettingsMenuViewController.m deleted file mode 100644 index 86be2b2fb..000000000 --- a/DashWallet/Sources/UI/Menu/Settings/DWSettingsMenuViewController.m +++ /dev/null @@ -1,375 +0,0 @@ -// -// Created by Andrew Podkovyrin -// Copyright © 2019 Dash Core Group. All rights reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "DWSettingsMenuViewController.h" - -#import "DWAboutViewController.h" -#import "DWFormTableViewController.h" -#import "DWLocalCurrencyViewController.h" -#import "DWSettingsMenuModel.h" -#import "DWUIKit.h" -#import "UIView+DWHUD.h" -#import "UIViewController+DWDisplayError.h" -#import -#import "dashwallet-Swift.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface DWSettingsMenuViewController () - -@property (null_resettable, nonatomic, strong) DWSettingsMenuModel *model; -@property (nonatomic, strong) DWFormTableViewController *formController; -@property (strong, nonatomic) DWSelectorFormCellModel *localCurrencyCellModel; -@property (strong, nonatomic) DWSelectorFormCellModel *switchNetworkCellModel; - -@end - -@implementation DWSettingsMenuViewController - -- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil { - self = [super initWithNibName:nil bundle:nil]; - if (self) { - self.title = NSLocalizedString(@"Settings", nil); - self.hidesBottomBarWhenPushed = YES; - } - return self; -} - -- (DWSettingsMenuModel *)model { - if (!_model) { - _model = [[DWSettingsMenuModel alloc] init]; - } - - return _model; -} - -- (NSArray *)items { - __weak typeof(self) weakSelf = self; - - NSMutableArray *items = [NSMutableArray array]; - - { - DWSelectorFormCellModel *cellModel = [[DWSelectorFormCellModel alloc] initWithTitle:NSLocalizedString(@"Local Currency", nil)]; - self.localCurrencyCellModel = cellModel; - [self updateLocalCurrencyCellModel]; - cellModel.accessoryType = DWSelectorFormAccessoryType_DisclosureIndicator; - cellModel.didSelectBlock = ^(DWSelectorFormCellModel *_Nonnull cellModel, NSIndexPath *_Nonnull indexPath) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - [strongSelf showCurrencySelector]; - }; - [items addObject:cellModel]; - } - - { - DWSwitcherFormCellModel *cellModel = [[DWSwitcherFormCellModel alloc] initWithTitle:NSLocalizedString(@"Enable Receive Notifications", nil)]; - cellModel.on = self.model.notificationsEnabled; - cellModel.didChangeValueBlock = ^(DWSwitcherFormCellModel *_Nonnull cellModel) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - strongSelf.model.notificationsEnabled = cellModel.on; - }; - [items addObject:cellModel]; - } - - { - DWSelectorFormCellModel *cellModel = [[DWSelectorFormCellModel alloc] initWithTitle:NSLocalizedString(@"Network", nil)]; - self.switchNetworkCellModel = cellModel; - [self updateSwitchNetworkCellModel]; - cellModel.accessoryType = DWSelectorFormAccessoryType_DisclosureIndicator; - cellModel.didSelectBlock = ^(DWSelectorFormCellModel *_Nonnull cellModel, NSIndexPath *_Nonnull indexPath) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - UITableView *tableView = self.formController.tableView; - UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; - [strongSelf showChangeNetworkFromSourceView:tableView sourceRect:cell.frame]; - }; - [items addObject:cellModel]; - } - - { - DWSelectorFormCellModel *cellModel = [[DWSelectorFormCellModel alloc] initWithTitle:NSLocalizedString(@"Rescan Blockchain", nil)]; - cellModel.didSelectBlock = ^(DWSelectorFormCellModel *_Nonnull cellModel, NSIndexPath *_Nonnull indexPath) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - UITableView *tableView = self.formController.tableView; - UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; - [strongSelf showWarningAboutReclassifiedTransactionsRypes:tableView sourceRect:cell.frame]; - }; - [items addObject:cellModel]; - } - - { - DWSelectorFormCellModel *cellModel = [[DWSelectorFormCellModel alloc] initWithTitle:NSLocalizedString(@"About", nil)]; - cellModel.accessoryType = DWSelectorFormAccessoryType_DisclosureIndicator; - cellModel.didSelectBlock = ^(DWSelectorFormCellModel *_Nonnull cellModel, NSIndexPath *_Nonnull indexPath) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - [strongSelf showAboutController]; - }; - [items addObject:cellModel]; - } - -#if DASHPAY - { - DWSelectorFormCellModel *cellModel = [[DWSelectorFormCellModel alloc] initWithTitle:NSLocalizedString(@"CoinJoin", nil)]; - cellModel.didSelectBlock = ^(DWSelectorFormCellModel *_Nonnull cellModel, NSIndexPath *_Nonnull indexPath) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - [strongSelf showCoinJoinController]; - }; - [items addObject:cellModel]; - } - - { - DWSwitcherFormCellModel *cellModel = [[DWSwitcherFormCellModel alloc] initWithTitle:@"Enable Voting"]; - cellModel.on = [VotingPrefsWrapper getIsEnabled]; - cellModel.didChangeValueBlock = ^(DWSwitcherFormCellModel *_Nonnull cellModel) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - [VotingPrefsWrapper setIsEnabledWithValue:cellModel.on]; - }; - [items addObject:cellModel]; - } -#endif - - return [items copy]; -} - -- (NSArray *)sections { - NSMutableArray *sections = [NSMutableArray array]; - - for (DWBaseFormCellModel *item in [self items]) { - DWFormSectionModel *section = [[DWFormSectionModel alloc] init]; - section.items = @[ item ]; - [sections addObject:section]; - } - - return [sections copy]; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - - self.view.backgroundColor = [UIColor dw_secondaryBackgroundColor]; - - DWFormTableViewController *formController = [[DWFormTableViewController alloc] initWithStyle:UITableViewStylePlain]; - [formController setSections:[self sections] placeholderText:nil]; - - [self dw_embedChild:formController]; - self.formController = formController; -} - -- (UIStatusBarStyle)preferredStatusBarStyle { - return UIStatusBarStyleLightContent; -} - -#pragma mark - DWLocalCurrencyViewControllerDelegate - -- (void)localCurrencyViewController:(DWLocalCurrencyViewController *)controller - didSelectCurrency:(nonnull NSString *)currencyCode { - [self updateLocalCurrencyCellModel]; - [self.navigationController popViewControllerAnimated:YES]; -} - -- (void)localCurrencyViewControllerDidCancel:(DWLocalCurrencyViewController *)controller { - NSAssert(NO, @"Not supported"); -} - -#pragma mark - Private - -- (void)updateLocalCurrencyCellModel { - self.localCurrencyCellModel.subTitle = self.model.localCurrencyCode; -} - -- (void)updateSwitchNetworkCellModel { - self.switchNetworkCellModel.subTitle = self.model.networkName; -} - -- (void)rescanBlockchainActionFromSourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect { - [DWSettingsMenuModel rescanBlockchainActionFromController:self - sourceView:sourceView - sourceRect:sourceRect - completion:^(BOOL confirmed) { - if (confirmed) { - [self.delegate settingsMenuViewControllerDidRescanBlockchain:self]; - } - }]; -} - -- (void)showCurrencySelector { - DWLocalCurrencyViewController *controller = - [[DWLocalCurrencyViewController alloc] initWithNavigationAppearance:DWNavigationAppearance_Default - presentationMode:DWCurrencyPickerPresentationMode_Screen - currencyCode:nil]; - controller.delegate = self; - [self.navigationController pushViewController:controller animated:YES]; -} - -- (void)showAboutController { - DWAboutViewController *aboutViewController = [DWAboutViewController controller]; - [self.navigationController pushViewController:aboutViewController animated:YES]; -} - -- (void)showCoinJoinController { - UIViewController *vc; - - if (CoinJoinObjcWrapper.infoShown) { - vc = [CoinJoinLevelsViewController controller]; - } else { - vc = [CoinJoinInfoViewController controller]; - } - - [self.navigationController pushViewController:vc animated:YES]; -} - -- (void)showChangeNetworkFromSourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect { - UIAlertController *actionSheet = [UIAlertController - alertControllerWithTitle:NSLocalizedString(@"Network", nil) - message:nil - preferredStyle:UIAlertControllerStyleActionSheet]; - UIAlertAction *mainnet = [UIAlertAction - actionWithTitle:DSLocalizedString(@"Mainnet", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [DWSettingsMenuModel switchToMainnetWithCompletion:^(BOOL success) { - if (success) { - [self updateSwitchNetworkCellModel]; - } - }]; - }]; - UIAlertAction *testnet = [UIAlertAction - actionWithTitle:DSLocalizedString(@"Testnet", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [DWSettingsMenuModel switchToTestnetWithCompletion:^(BOOL success) { - if (success) { - [self updateSwitchNetworkCellModel]; - } - }]; - }]; - - // UIAlertAction *evonet = [UIAlertAction - // actionWithTitle:DSLocalizedString(@"Evonet", nil) - // style:UIAlertActionStyleDefault - // handler:^(UIAlertAction *action) { - // [DWSettingsMenuModel switchToEvonetWithCompletion:^(BOOL success) { - // if (success) { - // [self updateSwitchNetworkCellModel]; - // } - // }]; - // }]; - - UIAlertAction *cancel = [UIAlertAction - actionWithTitle:NSLocalizedString(@"Cancel", nil) - style:UIAlertActionStyleCancel - handler:nil]; - [actionSheet addAction:mainnet]; - [actionSheet addAction:testnet]; - // [actionSheet addAction:evonet]; - [actionSheet addAction:cancel]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { - actionSheet.popoverPresentationController.sourceView = sourceView; - actionSheet.popoverPresentationController.sourceRect = sourceRect; - } - [self presentViewController:actionSheet - animated:YES - completion:nil]; -} - -- (void)showWarningAboutReclassifiedTransactionsRypes:(UIView *)sourceView sourceRect:(CGRect)sourceRect { - __weak typeof(self) weakSelf = self; - - UIAlertController *actionSheet = [UIAlertController - alertControllerWithTitle:NSLocalizedString(@"You will lose all your manually reclassified transactions types", nil) - message:NSLocalizedString(@"If you would like to save manually reclassified types for transactions you should export a CSV transaction file.", nil) - preferredStyle:UIAlertControllerStyleActionSheet]; - UIAlertAction *continueAction = [UIAlertAction - actionWithTitle:DSLocalizedString(@"Continue", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [self rescanBlockchainActionFromSourceView:sourceView sourceRect:sourceRect]; - }]; - UIAlertAction *export = [UIAlertAction - actionWithTitle:DSLocalizedString(@"Export CSV", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [weakSelf exportTransactionsInCSV]; - }]; - UIAlertAction *cancel = [UIAlertAction - actionWithTitle:NSLocalizedString(@"Cancel", nil) - style:UIAlertActionStyleCancel - handler:nil]; - [actionSheet addAction:export]; - [actionSheet addAction:continueAction]; - [actionSheet addAction:cancel]; - - if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { - actionSheet.popoverPresentationController.sourceView = sourceView; - actionSheet.popoverPresentationController.sourceRect = sourceRect; - } - [self presentViewController:actionSheet - animated:YES - completion:nil]; -} - -- (void)exportTransactionsInCSV { - [self.view dw_showProgressHUDWithMessage:NSLocalizedString(@"Generating CSV Report", nil)]; - __weak typeof(self) weakSelf = self; - - [DWSettingsMenuModel - generateCSVReportWithCompletionHandler:^(NSString *_Nonnull fileName, NSURL *_Nonnull file) { - [weakSelf.view dw_hideProgressHUD]; - - UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[ file ] applicationActivities:nil]; - [activityViewController setValue:fileName forKey:@"subject"]; - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { - activityViewController.popoverPresentationController.sourceView = weakSelf.view; - activityViewController.popoverPresentationController.sourceRect = weakSelf.view.bounds; - } - - [weakSelf presentViewController:activityViewController animated:YES completion:nil]; - } - errorHandler:^(NSError *_Nonnull error) { - [weakSelf.view dw_hideProgressHUD]; - [weakSelf dw_displayErrorModally:error]; - }]; -} -@end - -NS_ASSUME_NONNULL_END diff --git a/DashWallet/Sources/UI/Menu/Settings/SettingsMenuViewController.swift b/DashWallet/Sources/UI/Menu/Settings/SettingsMenuViewController.swift new file mode 100644 index 000000000..d97dbbf76 --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/SettingsMenuViewController.swift @@ -0,0 +1,282 @@ +// +// Created by Andrew Podkovyrin +// Copyright © 2019 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import SwiftUI +import Combine + +@objc(DWSettingsMenuViewControllerDelegate) +protocol SettingsMenuViewControllerDelegate: AnyObject { + func settingsMenuViewControllerDidRescanBlockchain(_ controller: SettingsMenuViewController) +} + +@objc(DWSettingsMenuViewController) +class SettingsMenuViewController: UIViewController, DWLocalCurrencyViewControllerDelegate { + + @objc weak var delegate: SettingsMenuViewControllerDelegate? + + private lazy var model: DWSettingsMenuModel = DWSettingsMenuModel() + private lazy var viewModel: SettingsViewModel = SettingsViewModel(model: model) + private var cancellables = Set() + + init() { + super.init(nibName: nil, bundle: nil) + title = NSLocalizedString("Settings", comment: "") + hidesBottomBarWhenPushed = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .dw_secondaryBackground() + + let content = SettingsMenuContent( + viewModel: self.viewModel, + onLocalCurrencyChange: { [weak self] in + self?.showCurrencySelector() + }, + onNetworkChange: { [weak self] in + self?.showChangeNetwork() + }, + onRescanBlockchain: { [weak self] in + self?.showWarningAboutReclassifiedTransactions() + } + ) + let swiftUIController = UIHostingController(rootView: content) + swiftUIController.view.backgroundColor = .dw_secondaryBackground() + dw_embedChild(swiftUIController) + setupNavigationObserver() + } + + private func setupNavigationObserver() { + viewModel.$navigationDestination + .receive(on: DispatchQueue.main) + .sink { [weak self] dest in + switch dest { + case .coinjoin: + self?.showCoinJoinController() + case .currencySelector: + self?.showCurrencySelector() + case .network: + self?.showChangeNetwork() + case .rescan: + self?.showWarningAboutReclassifiedTransactions() + case .about: + self?.showAboutController() + default: + break + } + } + .store(in: &cancellables) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + // MARK: - LocalCurrencyViewControllerDelegate + + func localCurrencyViewController(_ controller: DWLocalCurrencyViewController, didSelectCurrency currencyCode: String) { + navigationController?.popViewController(animated: true) + } + + func localCurrencyViewControllerDidCancel(_ controller: DWLocalCurrencyViewController) { + assertionFailure("Not supported") + } + + // MARK: - Private + + private func showCurrencySelector() { + let controller = DWLocalCurrencyViewController(navigationAppearance: .default, presentationMode: .screen, currencyCode: nil) + controller.delegate = self + navigationController?.pushViewController(controller, animated: true) + } + + private func showAboutController() { + let aboutViewController = DWAboutViewController.create() + navigationController?.pushViewController(aboutViewController, animated: true) + } + + private func showChangeNetwork() { + let actionSheet = UIAlertController(title: NSLocalizedString("Network", comment: ""), message: nil, preferredStyle: .actionSheet) + + let mainnetAction = UIAlertAction(title: NSLocalizedString("Mainnet", comment: ""), style: .default) { [weak self] _ in + DWSettingsMenuModel.switchToMainnet { success in + if success { + self?.updateView() + } + } + } + + let testnetAction = UIAlertAction(title: NSLocalizedString("Testnet", comment: ""), style: .default) { [weak self] _ in + DWSettingsMenuModel.switchToTestnet { success in + if success { + self?.updateView() + } + } + } + + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil) + + actionSheet.addAction(mainnetAction) + actionSheet.addAction(testnetAction) + actionSheet.addAction(cancelAction) + + if UIDevice.current.userInterfaceIdiom == .pad { + actionSheet.popoverPresentationController?.sourceView = view + actionSheet.popoverPresentationController?.sourceRect = view.bounds + } + + present(actionSheet, animated: true, completion: nil) + } + + private func showWarningAboutReclassifiedTransactions() { + let actionSheet = UIAlertController( + title: NSLocalizedString("You will lose all your manually reclassified transactions types", comment: ""), + message: NSLocalizedString("If you would like to save manually reclassified types for transactions you should export a CSV transaction file.", comment: ""), + preferredStyle: .actionSheet) + + let continueAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { [weak self] _ in + self?.rescanBlockchainAction() + } + + let exportAction = UIAlertAction(title: NSLocalizedString("Export CSV", comment: ""), style: .default) { [weak self] _ in + self?.exportTransactionsInCSV() + } + + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil) + + actionSheet.addAction(exportAction) + actionSheet.addAction(continueAction) + actionSheet.addAction(cancelAction) + + if UIDevice.current.userInterfaceIdiom == .pad { + actionSheet.popoverPresentationController?.sourceView = view + actionSheet.popoverPresentationController?.sourceRect = view.bounds + } + + present(actionSheet, animated: true, completion: nil) + } + + private func rescanBlockchainAction() { + DWSettingsMenuModel.rescanBlockchainAction(from: self, sourceView: view, sourceRect: view.bounds) { [weak self] confirmed in + if confirmed { + self?.delegate?.settingsMenuViewControllerDidRescanBlockchain(self!) + } + } + } + + private func exportTransactionsInCSV() { + view.dw_showProgressHUD(withMessage: NSLocalizedString("Generating CSV Report", comment: "")) + + DWSettingsMenuModel.generateCSVReport { [weak self] fileName, file in + self?.view.dw_hideProgressHUD() + + let activityViewController = UIActivityViewController(activityItems: [file], applicationActivities: nil) + activityViewController.setValue(fileName, forKey: "subject") + + if UIDevice.current.userInterfaceIdiom == .pad { + activityViewController.popoverPresentationController?.sourceView = self?.view + activityViewController.popoverPresentationController?.sourceRect = self?.view.bounds ?? .zero + } + + self?.present(activityViewController, animated: true, completion: nil) + } errorHandler: { [weak self] error in + self?.view.dw_hideProgressHUD() + self?.dw_displayErrorModally(error) + } + } + + private func updateView() { + // Trigger a view update + viewDidLoad() + } +} + +// MARK: - CoinJoin + +extension SettingsMenuViewController { + private func showCoinJoinController() { + let vc: UIViewController + + if CoinJoinViewModel.shared.infoShown { + vc = CoinJoinLevelsViewController.controller() + } else { + vc = CoinJoinInfoViewController.controller() + } + + navigationController?.pushViewController(vc, animated: true) + } +} + +struct SettingsMenuContent: View { + @StateObject var viewModel: SettingsViewModel + var onLocalCurrencyChange: () -> Void + var onNetworkChange: () -> Void + var onRescanBlockchain: () -> Void + + var body: some View { + List(viewModel.items) { item in + Group { + if let cjItem = item as? CoinJoinMenuItemModel { + MenuItem( + title: cjItem.title, + subtitleView: AnyView(CoinJoinSubtitle(cjItem)), + icon: .custom("image.coinjoin.menu"), + action: cjItem.action + ) + } else { + MenuItem( + title: item.title, + subtitle: item.subtitle, + details: item.details, + icon: item.icon, + showInfo: item.showInfo, + showChevron: item.showChevron, + showToggle: item.showToggle, + isToggled: item.isToggled, + action: item.action + ) + } + } + .background(Color.secondaryBackground) + .cornerRadius(8) + .shadow(color: .shadow, radius: 10, x: 0, y: 5) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + .listStyle(.plain) + .background(Color.clear) + } + + @ViewBuilder + private func CoinJoinSubtitle(_ cjItem: CoinJoinMenuItemModel) -> some View { + if cjItem.isOn { + CoinJoinProgressInfo(state: cjItem.state, progress: cjItem.progress, mixed: cjItem.mixed, total: cjItem.total, textColor: .tertiaryText, font: .caption) + } else { + Text(NSLocalizedString("Turned off", comment: "CoinJoin")) + .font(.caption) + .foregroundColor(.tertiaryText) + .padding(.leading, 4) + .padding(.top, 2) + } + } +} diff --git a/DashWallet/Sources/UI/Menu/Settings/SettingsViewModel.swift b/DashWallet/Sources/UI/Menu/Settings/SettingsViewModel.swift new file mode 100644 index 000000000..40da83171 --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/SettingsViewModel.swift @@ -0,0 +1,134 @@ +// +// Created by Andrei Ashikhmin +// 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 Combine + +enum SettingsNavDest { + case coinjoin + case currencySelector + case network + case rescan + case about + case none +} + +class SettingsViewModel: ObservableObject { + private var cancellableBag = Set() + private let coinJoinService = CoinJoinService.shared + private var model: DWSettingsMenuModel + @Published var items: [MenuItemModel] = [] + @Published private(set) var navigationDestination: SettingsNavDest = .none + + init(model: DWSettingsMenuModel) { + self.model = model + refreshMenuItems() + setupCoinJoinObservers() + } + + private func setupCoinJoinObservers() { + coinJoinService.$progress + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.refreshMenuItems() + } + .store(in: &cancellableBag) + + coinJoinService.$mode + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.refreshMenuItems() + } + .store(in: &cancellableBag) + + coinJoinService.$mixingState + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.refreshMenuItems() + } + .store(in: &cancellableBag) + } + + private func refreshMenuItems() { + self.items = [ + MenuItemModel( + title: NSLocalizedString("Local Currency", comment: ""), + subtitle: model.localCurrencyCode, + showChevron: true, + action: { [weak self] in + self?.navigationDestination = .currencySelector + } + ), + MenuItemModel( + title: NSLocalizedString("Enable Receive Notifications", comment: ""), + showToggle: true, + isToggled: model.notificationsEnabled, + action: { [weak self] in + self?.model.notificationsEnabled.toggle() + } + ), + MenuItemModel( + title: NSLocalizedString("Network", comment: ""), + subtitle: model.networkName, + showChevron: true, + action: { [weak self] in + self?.navigationDestination = .network + } + ), + MenuItemModel( + title: NSLocalizedString("Rescan Blockchain", comment: ""), + showChevron: true, + action: { [weak self] in + self?.navigationDestination = .rescan + } + ), + MenuItemModel( + title: NSLocalizedString("About", comment: ""), + showChevron: true, + action: { [weak self] in + self?.navigationDestination = .about + } + ) + ] + + #if DASHPAY + items.append(contentsOf: [ + CoinJoinMenuItemModel( + title: NSLocalizedString("CoinJoin", comment: "CoinJoin"), + isOn: coinJoinService.mode != .none, + state: coinJoinService.mixingState, + progress: coinJoinService.progress, + mixed: Double(coinJoinService.coinJoinBalance) / Double(DUFFS), + total: Double(coinJoinService.totalBalance) / Double(DUFFS), + action: { [weak self] in + self?.navigationDestination = .coinjoin + } + ), + MenuItemModel( + title: "Enable Voting", + showToggle: true, + isToggled: VotingPrefs.shared.votingEnabled, + action: { + VotingPrefs.shared.votingEnabled.toggle() + } + ) + ]) + #endif + } +} diff --git a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewController.swift b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewController.swift index a0fc3e770..92b14f481 100644 --- a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewController.swift +++ b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewController.swift @@ -167,23 +167,6 @@ extension DWImportWalletInfoViewController { } } -struct MenuItemModel: Identifiable, Equatable { - let id = UUID() - - var title: String - var subtitle: String? = nil - var details: String? = nil - var icon: IconName? = nil - var showInfo: Bool = false - var showChevron: Bool = false - var isToggled: Binding? = nil - var action: (() -> Void)? = nil - - static func == (lhs: MenuItemModel, rhs: MenuItemModel) -> Bool { - lhs.id == rhs.id - } -} - struct ToolsMenuContent: View { var items: [MenuItemModel] @State private var showZenLedgerSheet: Bool = false diff --git a/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift b/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift index 4060f0a0d..10bcdf6cb 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift @@ -21,7 +21,7 @@ typealias TransactionPreview = MenuItem struct MenuItem: View { var title: String - var subtitle: String? = nil + @State var subtitleView: AnyView? = nil var details: String? = nil var topText: String? = nil var icon: IconName? = nil @@ -30,8 +30,78 @@ struct MenuItem: View { var showChevron: Bool = false var dashAmount: Int64? = nil var overrideFiatAmount: String? = nil - var isToggled: Binding? = nil + var showToggle: Bool = false + @State var isToggled: Bool = false var action: (() -> Void)? = nil + + init(title: String, + subtitle: String? = nil, + details: String? = nil, + topText: String? = nil, + icon: IconName? = nil, + secondaryIcon: IconName? = nil, + showInfo: Bool = false, + showChevron: Bool = false, + dashAmount: Int64? = nil, + overrideFiatAmount: String? = nil, + showToggle: Bool = false, + isToggled: Bool = false, + action: (() -> Void)? = nil + ) { + self.init( + title: title, + subtitleView: subtitle.map { + AnyView( + Text($0) + .font(.caption) + .lineSpacing(3) + .foregroundColor(.tertiaryText) + .padding(.leading, 4) + .padding(.top, 2) + ) + }, + details: details, + topText: topText, + icon: icon, + secondaryIcon: secondaryIcon, + showInfo: showInfo, + showChevron: showChevron, + dashAmount: dashAmount, + overrideFiatAmount: overrideFiatAmount, + showToggle: showToggle, + isToggled: isToggled, + action: action + ) + } + + init(title: String, + subtitleView: AnyView? = nil, + details: String? = nil, + topText: String? = nil, + icon: IconName? = nil, + secondaryIcon: IconName? = nil, + showInfo: Bool = false, + showChevron: Bool = false, + dashAmount: Int64? = nil, + overrideFiatAmount: String? = nil, + showToggle: Bool = false, + isToggled: Bool = false, + action: (() -> Void)? = nil + ) { + self.title = title + self._subtitleView = State(initialValue: subtitleView) + self.details = details + self.topText = topText + self.icon = icon + self.secondaryIcon = secondaryIcon + self.showInfo = showInfo + self.showChevron = showChevron + self.dashAmount = dashAmount + self.overrideFiatAmount = overrideFiatAmount + self._isToggled = State(initialValue: isToggled) + self.showToggle = showToggle + self.action = action + } var body: some View { HStack(spacing: 4) { @@ -85,15 +155,10 @@ struct MenuItem: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 4) - if let subtitle = subtitle { - Text(subtitle) - .font(.caption) - .lineSpacing(3) - .foregroundColor(.tertiaryText) - .padding(.leading, 4) - .padding(.top, 2) + if let subtitle = subtitleView { + subtitle } - + if let details = details { Text(details) .font(.caption) @@ -105,8 +170,10 @@ struct MenuItem: View { } .frame(maxWidth: .infinity) - if let isToggled = isToggled { - Toggle(isOn: isToggled) { } + if showToggle { + Toggle(isOn: $isToggled) { } + .tint(Color.dashBlue) + .scaleEffect(0.75) .frame(maxWidth: 60) } @@ -137,6 +204,10 @@ struct MenuItem: View { .padding(10) .frame(maxWidth: .infinity, minHeight: 66) .onTapGesture { + if showToggle { + isToggled.toggle() + } + action?() } } @@ -157,6 +228,7 @@ struct MenuItem: View { subtitle: "Easily stake Dash and earn passive income with a few simple steps", icon: .system("faceid"), showInfo: true, - isToggled: .constant(true) + showToggle: true, + isToggled: true ) } diff --git a/DashWallet/dashwallet-Bridging-Header.h b/DashWallet/dashwallet-Bridging-Header.h index edbaa365a..60966195a 100644 --- a/DashWallet/dashwallet-Bridging-Header.h +++ b/DashWallet/dashwallet-Bridging-Header.h @@ -113,7 +113,6 @@ static const bool _SNAPSHOT = 0; #import "UIView+DWEmbedding.h" #import "DWBasePressableControl.h" - #if DASHPAY #import "DWInvitationSetupState.h" #import "DPAlertViewController.h" @@ -161,8 +160,13 @@ static const bool _SNAPSHOT = 0; #import "DWBasePayViewController.h" #import "DWHomeProtocol.h" -//MARK: Tools menu +//MARK: Settings menu #import "UIViewController+DWDisplayError.h" +#import "DWFormTableViewController.h" +#import "DWAboutViewController.h" //MARK: Onboarding #import "DWTransactionStub.h" + +//MARK: CoinJoin +#import "DSCoinJoinManager.h" diff --git a/Podfile b/Podfile index df1cec330..82fe377e5 100644 --- a/Podfile +++ b/Podfile @@ -28,6 +28,7 @@ target 'dashpay' do platform :ios, '14.0' pod 'DashSync', :path => '../DashSync/' + pod 'DashSharedCore', :path => '../dash-shared-core/' pod 'SQLite.swift', '~> 0.13.3' pod 'SQLiteMigrationManager.swift' pod 'CloudInAppMessaging', '0.1.0' diff --git a/Podfile.lock b/Podfile.lock index 2700ece4b..e4d7ba00e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -868,6 +868,6 @@ SPEC CHECKSUMS: TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 UIViewController-KeyboardAdditions: a691dc7e63a49854d341455a778ee8497dfc4662 -PODFILE CHECKSUM: 026ae123f0be550c31403c9f3af202a867705388 +PODFILE CHECKSUM: f2fa4cb9f3987af3acdd6a2624930750c1c1ddee COCOAPODS: 1.15.2