diff --git a/DashSyncCurrentCommit b/DashSyncCurrentCommit index c2319bf8a..b7cdec731 100644 --- a/DashSyncCurrentCommit +++ b/DashSyncCurrentCommit @@ -1 +1 @@ -e272a368fa8b5a3fcaa637f5631352980dcc557f +13fc5bea98a6c0f1b80c4e2c146ff5bf536baf76 diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index 024f93fbc..248f6552d 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -3,12 +3,10 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ - 09A6CB66AEDCEA04C9D670DF /* libPods-TodayExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D18D0D69B7365947239D4F97 /* libPods-TodayExtension.a */; }; - 09F4E16CAEAE41E2C6EA7A27 /* libPods-WatchApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 38C4CA9D73A147BA44601741 /* libPods-WatchApp.a */; }; 0F36937E2919A5DB007F4E91 /* TwoFactorAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F36937D2919A5DB007F4E91 /* TwoFactorAuthViewController.swift */; }; 0F36937F2919A70B007F4E91 /* Coinbase.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0F3693812919A70B007F4E91 /* Coinbase.storyboard */; }; 0F6EDFC428C896BD000427E7 /* CoinbaseUserAuthInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F6EDF9D28C896BC000427E7 /* CoinbaseUserAuthInformation.swift */; }; @@ -84,7 +82,10 @@ 11E47BAB28EB38510097CFA0 /* SendCoinsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E47BAA28EB38510097CFA0 /* SendCoinsService.swift */; }; 11E47BAD28EB3A7D0097CFA0 /* CrowdNode+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E47BAC28EB3A7D0097CFA0 /* CrowdNode+Constants.swift */; }; 11ED906B29681773003784F9 /* StakingInfoDialogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED906A29681773003784F9 /* StakingInfoDialogController.swift */; }; + 19BF3F10240A082811AAC1EB /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + 1AF81D8C73A81CB927C3AE0E /* libPods-DashWalletScreenshotsUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B56B94951BA51EAC9E0A6E1 /* libPods-DashWalletScreenshotsUITests.a */; }; 1DD7BE9CD62998F12672140D /* RadioButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708B9F2B4B1E4C31B5010CDF /* RadioButtonRow.swift */; }; + 1F3FF21B0295D5A326F08B74 /* OrderPreviewFeeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1A08496F925D17031493E /* OrderPreviewFeeRow.swift */; }; 222040C61C1A1940005CE1C3 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 222040C51C1A1940005CE1C3 /* WebKit.framework */; }; 222E7F561C46E9B8009AB45D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 222E7F551C46E9B8009AB45D /* Security.framework */; }; 222E7F581C46E9BE009AB45D /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 222E7F571C46E9BE009AB45D /* SystemConfiguration.framework */; }; @@ -92,6 +93,8 @@ 2284B63F1C7BD28F0012ECDF /* BRAWKeypad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F45A0A1C30EAB700B07A15 /* BRAWKeypad.swift */; }; 22B6A4481C0E963900673913 /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 22B6A4471C0E963900673913 /* libbz2.tbd */; }; 22D3613D1C56F2CD0057CF76 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 22D3613C1C56F2CD0057CF76 /* libsqlite3.tbd */; }; + 22E89B6C00B546D294D7F331 /* SelfSizingSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CB7E093E994D2391D36DEC /* SelfSizingSheet.swift */; }; + 279C16F9AF0575C540E312E5 /* MayaConvertCardRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99130732DE8F2E95F635F6B /* MayaConvertCardRow.swift */; }; 2A0C699C23104588001B8C90 /* DWPaymentInput.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C699B23104588001B8C90 /* DWPaymentInput.m */; }; 2A0C69A3231048DA001B8C90 /* DWPaymentProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C69A2231048DA001B8C90 /* DWPaymentProcessor.m */; }; 2A0C69AC23125074001B8C90 /* UIView+DWHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C69AB23125074001B8C90 /* UIView+DWHUD.m */; }; @@ -316,8 +319,11 @@ 2AF26F3B230C0E4C007F9228 /* DWBaseSeedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AF26F3A230C0E4C007F9228 /* DWBaseSeedViewController.m */; }; 2AFCB9C423BE76EC00FF59A6 /* DWUpholdTransactionObject+DWView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AFCB9C323BE76EC00FF59A6 /* DWUpholdTransactionObject+DWView.m */; }; 2AFF01DB243F4559003718DC /* DWDPRegistrationStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AFF01DA243F4559003718DC /* DWDPRegistrationStatus.m */; }; - 3198E8048E48954CD87BAD11 /* libPods-DashWalletTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C56BFBBABD008D06578BDD9C /* libPods-DashWalletTests.a */; }; + 39A9FD561F6418568F3FC109 /* libPods-dashwallet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 797D9070BF54A3533190584E /* libPods-dashwallet.a */; }; + 39DC037E9CF04CCFA3EFBB53 /* SelfSizingSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CB7E093E994D2391D36DEC /* SelfSizingSheet.swift */; }; 3A0C6A2A57DFF02865E36200 /* ImportPrivateKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53435DCAF024BC82E0247F3 /* ImportPrivateKeySheet.swift */; }; + 40AB5A54235FDE2747ECE3F5 /* MayaConvertCardRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99130732DE8F2E95F635F6B /* MayaConvertCardRow.swift */; }; + 452BD4339B58028BD4CF06F7 /* MayaConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7708BBFDD14AFF237FB72D71 /* MayaConstants.swift */; }; 470617D6299A671900DCC667 /* CrowdNodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470617D5299A671900DCC667 /* CrowdNodeCell.swift */; }; 47081197298CF20C003FCA3D /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47081196298CF20B003FCA3D /* Transaction.swift */; }; 4708119F2990F56F003FCA3D /* TransactionDataItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4708119E2990F56F003FCA3D /* TransactionDataItem.swift */; }; @@ -355,7 +361,6 @@ 474C7215298A1FCF00475CA6 /* TxDetailActionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 474C7214298A1FCF00475CA6 /* TxDetailActionCell.xib */; }; 474C7218298A422500475CA6 /* TransactionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474C7217298A422400475CA6 /* TransactionItemView.swift */; }; 4751136C28D9A3DB00223B77 /* BuySellPortalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751136B28D9A3DB00223B77 /* BuySellPortalViewController.swift */; }; - AA00000128D9A3DB00223B77 /* BuySellPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000028D9A3DB00223B77 /* BuySellPortalView.swift */; }; 4751136F28D9B50E00223B77 /* CoinbaseInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751136E28D9B50E00223B77 /* CoinbaseInfoViewController.swift */; }; 4751137528DAF28800223B77 /* UIAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751137428DAF28800223B77 /* UIAssembly.swift */; }; 4751CAC0296EFD2900F63AC4 /* AccountListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751CABF296EFD2900F63AC4 /* AccountListController.swift */; }; @@ -492,6 +497,74 @@ 47F4B6CD29485A8B00AED4C9 /* ConfirmOrderCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F4B6CC29485A8B00AED4C9 /* ConfirmOrderCells.swift */; }; 47FA3AFF29350929008D58DC /* SyncingActivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA3AFE29350929008D58DC /* SyncingActivityMonitor.swift */; }; 47FA3B0229364991008D58DC /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA3B0129364991008D58DC /* HTTPClient.swift */; }; + 4A36341D70A94778AF408757 /* MayaCoinIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20554F992205428BA790F6CF /* MayaCoinIconView.swift */; }; + 514AE85E2FB1F9DB00F9B54C /* DashSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AE85D2FB1F9DB00F9B54C /* DashSwitch.swift */; }; + 514AE85F2FB1F9DB00F9B54C /* DashSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AE85D2FB1F9DB00F9B54C /* DashSwitch.swift */; }; + 514AE9F32FB2F4C500F9B54C /* HaltedToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AE9F22FB2F4C500F9B54C /* HaltedToast.swift */; }; + 514AE9F42FB2F4C500F9B54C /* HaltedToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AE9F22FB2F4C500F9B54C /* HaltedToast.swift */; }; + 514AEA0F2FB31BD800F9B54C /* AddressFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA0E2FB31BD800F9B54C /* AddressFieldView.swift */; }; + 514AEA102FB31BD800F9B54C /* AddressFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA0E2FB31BD800F9B54C /* AddressFieldView.swift */; }; + 514AEA132FB324FA00F9B54C /* MayaConvertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA122FB324FA00F9B54C /* MayaConvertView.swift */; }; + 514AEA142FB324FA00F9B54C /* MayaConvertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA122FB324FA00F9B54C /* MayaConvertView.swift */; }; + 514AEA442FB33B4300F9B54C /* EnterAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA432FB33B4300F9B54C /* EnterAmountView.swift */; }; + 514AEA452FB33B4300F9B54C /* EnterAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA432FB33B4300F9B54C /* EnterAmountView.swift */; }; + 514AEA462FB33B4300F9B54C /* DashPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA472FB33B4300F9B54C /* DashPickerView.swift */; }; + 514AEA482FB33B4300F9B54C /* DashPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA472FB33B4300F9B54C /* DashPickerView.swift */; }; + 514AEA492FB33B4300F9B54C /* MayaConvertHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA4A2FB33B4300F9B54C /* MayaConvertHostingController.swift */; }; + 514AEA4B2FB33B4300F9B54C /* MayaConvertHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA4A2FB33B4300F9B54C /* MayaConvertHostingController.swift */; }; + 514AEA4C2FB33B4300F9B54C /* MayaAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA4D2FB33B4300F9B54C /* MayaAmountView.swift */; }; + 514AEA4E2FB33B4300F9B54C /* MayaAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEA4D2FB33B4300F9B54C /* MayaAmountView.swift */; }; + 514AEAD52FB34F8600F9B54C /* OrderPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEAD42FB34F8600F9B54C /* OrderPreviewView.swift */; }; + 514AEAD62FB34F8600F9B54C /* OrderPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AEAD42FB34F8600F9B54C /* OrderPreviewView.swift */; }; + 514AF1C02FB5F02D00F9B54C /* MayaConvertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AF1BF2FB5F02D00F9B54C /* MayaConvertViewModel.swift */; }; + 514AF1C12FB5F02D00F9B54C /* MayaConvertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514AF1BF2FB5F02D00F9B54C /* MayaConvertViewModel.swift */; }; + 514BEA012FB5F02D00F9B54C /* MayaConvertAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BEA002FB5F02D00F9B54C /* MayaConvertAmount.swift */; }; + 514BEA022FB5F02D00F9B54C /* MayaConvertAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BEA002FB5F02D00F9B54C /* MayaConvertAmount.swift */; }; + 5151C8C62F913BF100F0A604 /* Coinbase-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */; }; + 5151C8C72F913BF100F0A604 /* ZenLedger-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */; }; + 5151C8C82F913BF100F0A604 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */; }; + 5151C8C92F913BF100F0A604 /* Topper-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C42F913BF100F0A604 /* Topper-Info.plist */; }; + 5151C8CA2F913BF100F0A604 /* Coinbase-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */; }; + 5151C8CB2F913BF100F0A604 /* ZenLedger-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */; }; + 5151C8CC2F913BF100F0A604 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */; }; + 5151C8CD2F913BF100F0A604 /* Topper-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5151C8C42F913BF100F0A604 /* Topper-Info.plist */; }; + 5151CA232F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */; }; + 5151CA242F92335E00F0A604 /* LocalCurrencyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */; }; + 5151CA252F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */; }; + 5151CA262F92335E00F0A604 /* LocalCurrencyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */; }; + 5151CD622F926D1900F0A604 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CD612F926D1900F0A604 /* SearchBar.swift */; }; + 5151CD632F926D1900F0A604 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CD612F926D1900F0A604 /* SearchBar.swift */; }; + 5151CDF02F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */; }; + 5151CDF12F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */; }; + 5151CE182F9624D000F0A604 /* CurrencyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CE172F9624D000F0A604 /* CurrencyItem.swift */; }; + 5151CE192F9624D000F0A604 /* CurrencyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5151CE172F9624D000F0A604 /* CurrencyItem.swift */; }; + 516983A32FCAEDEB00BA91A2 /* SuccessIllustration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983A22FCAEDEB00BA91A2 /* SuccessIllustration.swift */; }; + 516983A42FCAEDEB00BA91A2 /* SuccessIllustration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983A22FCAEDEB00BA91A2 /* SuccessIllustration.swift */; }; + 516983A62FCAEF1300BA91A2 /* ErrorIllustration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983A52FCAEF1300BA91A2 /* ErrorIllustration.swift */; }; + 516983A72FCAEF1300BA91A2 /* ErrorIllustration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983A52FCAEF1300BA91A2 /* ErrorIllustration.swift */; }; + 516983B12FCAEDEB00BA91A2 /* LoadingIllustration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983B02FCAEDEB00BA91A2 /* LoadingIllustration.swift */; }; + 516983B22FCAEDEB00BA91A2 /* LoadingIllustration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983B02FCAEDEB00BA91A2 /* LoadingIllustration.swift */; }; + 516983C22FCAEDEB00BA91A2 /* XmarkIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983C12FCAEDEB00BA91A2 /* XmarkIcon.swift */; }; + 516983C32FCAEDEB00BA91A2 /* XmarkIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516983C12FCAEDEB00BA91A2 /* XmarkIcon.swift */; }; + 51A577482FBB3A6000FB4097 /* OrderPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A577472FBB3A6000FB4097 /* OrderPreviewViewModel.swift */; }; + 51A577492FBB3A6000FB4097 /* OrderPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A577472FBB3A6000FB4097 /* OrderPreviewViewModel.swift */; }; + 51A5774B2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5774A2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift */; }; + 51A5774C2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5774A2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift */; }; + 51A57B022FBB41DE00FB4097 /* OrderPreviewTableRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57B012FBB41DE00FB4097 /* OrderPreviewTableRow.swift */; }; + 51A57B032FBB41DE00FB4097 /* OrderPreviewTableRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57B012FBB41DE00FB4097 /* OrderPreviewTableRow.swift */; }; + 51A57B042FBB41DE00FB4097 /* MayaFeeInfoSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57B052FBB41DE00FB4097 /* MayaFeeInfoSheet.swift */; }; + 51A57B062FBB41DE00FB4097 /* MayaFeeInfoSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57B052FBB41DE00FB4097 /* MayaFeeInfoSheet.swift */; }; + 51A57C042FBB5A0000FB4097 /* MayaTransactionSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57C022FBB5A0000FB4097 /* MayaTransactionSuccessView.swift */; }; + 51A57C052FBB5A0000FB4097 /* MayaTransactionSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57C022FBB5A0000FB4097 /* MayaTransactionSuccessView.swift */; }; + 51A57C062FBB5A0000FB4097 /* MayaTransactionFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57C032FBB5A0000FB4097 /* MayaTransactionFailureView.swift */; }; + 51A57C072FBB5A0000FB4097 /* MayaTransactionFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57C032FBB5A0000FB4097 /* MayaTransactionFailureView.swift */; }; + 51A57C092FBB5A0000FB4097 /* MayaTransactionPendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57C082FBB5A0000FB4097 /* MayaTransactionPendingView.swift */; }; + 51A57C0A2FBB5A0000FB4097 /* MayaTransactionPendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A57C082FBB5A0000FB4097 /* MayaTransactionPendingView.swift */; }; + 51F0D10130BF97A900B27E11 /* ExclamationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F0D10030BF97A900B27E11 /* ExclamationIcon.swift */; }; + 51F0D10230BF97A900B27E11 /* ExclamationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F0D10030BF97A900B27E11 /* ExclamationIcon.swift */; }; + 51F0D10430BF98BA00B27E11 /* ArrowDownIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F0D10330BF98BA00B27E11 /* ArrowDownIcon.swift */; }; + 51F0D10530BF98BA00B27E11 /* ArrowDownIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F0D10330BF98BA00B27E11 /* ArrowDownIcon.swift */; }; + 6FD1512E7774A997C1F8466B /* MayaMenuStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0286D4D1CADBBD51F09BE8E8 /* MayaMenuStyles.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 */; }; @@ -571,16 +644,16 @@ 754119E01CDA93FF0042DC51 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BAE12BE61B2DEE7F00895CC5 /* NotificationCenter.framework */; }; 754495DD2AE91B6300492817 /* GroupedRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754495DC2AE91B6300492817 /* GroupedRequestCell.swift */; }; 754495DF2AE91D3500492817 /* UsernameRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754495DE2AE91D3500492817 /* UsernameRequestCell.swift */; }; - 754565C82DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C42DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift */; }; - 754565CA2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C72DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift */; }; - 754565CB2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C52DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift */; }; - 754565CC2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C42DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift */; }; - 754565CE2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C72DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift */; }; - 754565CF2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C52DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift */; }; + 754565C82DAA52A000DA4E8E /* CTXSpendAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C42DAA52A000DA4E8E /* CTXSpendAPI.swift */; }; + 754565CA2DAA52A000DA4E8E /* CTXSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C72DAA52A000DA4E8E /* CTXSpendRepository.swift */; }; + 754565CB2DAA52A000DA4E8E /* CTXSpendEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C52DAA52A000DA4E8E /* CTXSpendEndpoint.swift */; }; + 754565CC2DAA52A000DA4E8E /* CTXSpendAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C42DAA52A000DA4E8E /* CTXSpendAPI.swift */; }; + 754565CE2DAA52A000DA4E8E /* CTXSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C72DAA52A000DA4E8E /* CTXSpendRepository.swift */; }; + 754565CF2DAA52A000DA4E8E /* CTXSpendEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565C52DAA52A000DA4E8E /* CTXSpendEndpoint.swift */; }; 754565D12DABA5F300DA4E8E /* MerchantTypesDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565D02DABA5EB00DA4E8E /* MerchantTypesDialog.swift */; }; 754565D22DABA5F300DA4E8E /* MerchantTypesDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754565D02DABA5EB00DA4E8E /* MerchantTypesDialog.swift */; }; - 7545ED532DA91A2F0075F45C /* DashSpend/CTXConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7545ED522DA91A2F0075F45C /* DashSpend/CTXConstants.swift */; }; - 7545ED542DA91A2F0075F45C /* DashSpend/CTXConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7545ED522DA91A2F0075F45C /* DashSpend/CTXConstants.swift */; }; + 7545ED532DA91A2F0075F45C /* CTXConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7545ED522DA91A2F0075F45C /* CTXConstants.swift */; }; + 7545ED542DA91A2F0075F45C /* CTXConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7545ED522DA91A2F0075F45C /* CTXConstants.swift */; }; 7545ED572DA91AF10075F45C /* DashSpendUserAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7545ED562DA91AEA0075F45C /* DashSpendUserAuthViewModel.swift */; }; 7545ED582DA91AF10075F45C /* DashSpendUserAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7545ED562DA91AEA0075F45C /* DashSpendUserAuthViewModel.swift */; }; 7545ED5D2DA91F590075F45C /* DashSpendTermsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7545ED5C2DA91F590075F45C /* DashSpendTermsScreen.swift */; }; @@ -683,8 +756,17 @@ 759C8FA02B593589004B1305 /* CrowdNodeAPYView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759C8F9E2B593589004B1305 /* CrowdNodeAPYView.swift */; }; 75A0A3F32CA7DBCF003ED48B /* TimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A0A3F22CA7DBCF003ED48B /* TimeUtils.swift */; }; 75A0A3F42CA7DBCF003ED48B /* TimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A0A3F22CA7DBCF003ED48B /* TimeUtils.swift */; }; - 75A2F3032DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A2F3022DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift */; }; - 75A2F3042DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A2F3022DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift */; }; + 75DFA00175DFA00175DFA001 /* DashStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DFA10175DFA10175DFA101 /* DashStepper.swift */; }; + 75DFA00275DFA00275DFA002 /* SegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DFA10275DFA10275DFA102 /* SegmentedControl.swift */; }; + 75A2F3032DE48C860046BE17 /* CTXSpendTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A2F3022DE48C860046BE17 /* CTXSpendTokenService.swift */; }; + 75A2F3042DE48C860046BE17 /* CTXSpendTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A2F3022DE48C860046BE17 /* CTXSpendTokenService.swift */; }; + D5A0C001D5A0C001D5A0C001 /* DashSpendAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0B001D5A0B001D5A0B001 /* DashSpendAmountView.swift */; }; + D5A0C002D5A0C002D5A0C002 /* DashSpendDenominationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0B002D5A0B002D5A0B002 /* DashSpendDenominationRow.swift */; }; + D5A0C003D5A0C003D5A0C003 /* DashSpendFixedContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0B003D5A0B003D5A0B003 /* DashSpendFixedContent.swift */; }; + D5A0C004D5A0C004D5A0C004 /* DashSpendFlexibleContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0B004D5A0B004D5A0B004 /* DashSpendFlexibleContent.swift */; }; + D5A0C005D5A0C005D5A0C005 /* DashSpendMultiplePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0B005D5A0B005D5A0B005 /* DashSpendMultiplePanel.swift */; }; + D5A0C006D5A0C006D5A0C006 /* DashSpendPayIntro.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0B006D5A0B006D5A0B006 /* DashSpendPayIntro.swift */; }; + D5A0C007D5A0C007D5A0C007 /* DashSpendSinglePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0B007D5A0B007D5A0B007 /* DashSpendSinglePanel.swift */; }; 75A8C1652AE5726B0042256E /* UsernameRequestsDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A8C1632AE5725C0042256E /* UsernameRequestsDAO.swift */; }; 75A8C1672AE5734A0042256E /* UsernameRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A8C1662AE5734A0042256E /* UsernameRequest.swift */; }; 75A8C1692AE6A1AC0042256E /* VotingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A8C1682AE6A1AC0042256E /* VotingViewModel.swift */; }; @@ -699,6 +781,10 @@ 75AA33D82BFB4A5A00F12465 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75AA33D72BFB4A5A00F12465 /* Extensions.swift */; }; 75AA33D92BFB4A5A00F12465 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75AA33D72BFB4A5A00F12465 /* Extensions.swift */; }; 75AE5A7F2A87C363006CD4BA /* DWConfirmUsernameContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C943B5712A40ED4200AF23C5 /* DWConfirmUsernameContentView.xib */; }; + 75B3C1A22E36100100CAFE02 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; + 75B3C1A32E36100100CAFE03 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; + 75B3C1B22E36100100CAFE05 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; + 75B3C1B32E36100100CAFE06 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; 75BDE7AC2BF3287400556791 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75BDE7AB2BF3287400556791 /* Toast.swift */; }; 75BDE7AD2BF3287400556791 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75BDE7AB2BF3287400556791 /* Toast.swift */; }; 75C1F0452AE26AC0006929CA /* CoinJoinLevelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C1F0442AE26AC0006929CA /* CoinJoinLevelsViewModel.swift */; }; @@ -737,6 +823,11 @@ 75D9EBC72DE5CE14009416A2 /* GiftCardDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D9EBC42DE5CE14009416A2 /* GiftCardDetailsView.swift */; }; 75D9EBC82DE5CE14009416A2 /* GiftCardDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D9EBC52DE5CE14009416A2 /* GiftCardDetailsViewModel.swift */; }; 75D9EBC92DE5CE14009416A2 /* GiftCardDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D9EBC42DE5CE14009416A2 /* GiftCardDetailsView.swift */; }; + D6C0C001D6C0C001D6C0C001 /* GiftCardDetailsInfoCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C0B001D6C0B001D6C0B001 /* GiftCardDetailsInfoCard.swift */; }; + D6C0C002D6C0C002D6C0C002 /* GiftCardDetailsMerchantHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C0B002D6C0B002D6C0B002 /* GiftCardDetailsMerchantHeader.swift */; }; + D6C0C003D6C0C003D6C0C003 /* GiftCardPurchaseSelectionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C0B003D6C0B003D6C0B003 /* GiftCardPurchaseSelectionSheet.swift */; }; + D6C0C004D6C0C004D6C0C004 /* GiftCardDetailsPoweredBySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C0B004D6C0B004D6C0B004 /* GiftCardDetailsPoweredBySection.swift */; }; + D6C0C005D6C0C005D6C0C005 /* GiftCardDetailsHowToUseSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C0B005D6C0B005D6C0B005 /* GiftCardDetailsHowToUseSection.swift */; }; 75DFAF2E2CF7066600CCA68B /* VoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DFAF2D2CF7066200CCA68B /* VoteButton.swift */; }; 75E2F3C82AA4CF1900C3B458 /* Topper-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 75E2F3C72AA4CF1900C3B458 /* Topper-Info.plist */; }; 75E2F3CA2AA4D1B900C3B458 /* Topper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75E2F3C92AA4D1B900C3B458 /* Topper.swift */; }; @@ -779,16 +870,72 @@ 75FFD6C52BF494E20032879E /* HomeViewController+ImportPrivateKeyDelegateImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FFD6C32BF494E20032879E /* HomeViewController+ImportPrivateKeyDelegateImpl.swift */; }; 75FFD6C72BF495800032879E /* HomeViewController+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FFD6C62BF495800032879E /* HomeViewController+Shortcuts.swift */; }; 75FFD6C82BF495800032879E /* HomeViewController+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FFD6C62BF495800032879E /* HomeViewController+Shortcuts.swift */; }; + 76766A50AFDB7BC138F569DC /* NetworkUnavailableStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C4C2E6AC61005536FF85E /* NetworkUnavailableStateView.swift */; }; + 7B4F28EE00BA451DA8675B06 /* MayaCoinIconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2F0351CAD174EEB8E64012A /* MayaCoinIconLoader.swift */; }; + 7BDE6E52D5F0A428E2858824 /* libPods-TodayExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0755F840BCC1E0754D807B04 /* libPods-TodayExtension.a */; }; 893AE6F535290DD1CEE89219 /* ImportPrivateKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53435DCAF024BC82E0247F3 /* ImportPrivateKeySheet.swift */; }; + 8A7607606A594C6E99A2FAC7 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; + 8DF3D24EE3B247B79AE9CAB5 /* BuySellPortalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B94B3697D7742BFA991B5CC /* BuySellPortalScreen.swift */; }; 91F0B8B92F3FAB7C00E713AE /* KeysOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F0B8B82F3FAB7C00E713AE /* KeysOverviewView.swift */; }; 91F0B8BA2F3FAB7C00E713AE /* KeysOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F0B8B82F3FAB7C00E713AE /* KeysOverviewView.swift */; }; - A30AF4A11189A2EFB04EB535 /* libPods-WatchApp Extension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C22BC09CF6178544B2300A4 /* libPods-WatchApp Extension.a */; }; + 97C1167F9600BF7E489A8545 /* MayaTransactionStatusHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B5469F5588395FA0668C4B /* MayaTransactionStatusHostingController.swift */; }; + A0795C9369A1402498C00F69 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; + A6DB8889C5594B1988BDE0CE /* MayaCoinIconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2F0351CAD174EEB8E64012A /* MayaCoinIconLoader.swift */; }; + A70C83C906AC1349DF3B9C98 /* DashBalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF95E0E0B636AFB810D62116 /* DashBalanceView.swift */; }; + AA00000128D9A3DB00223B77 /* BuySellPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000028D9A3DB00223B77 /* BuySellPortalView.swift */; }; AA0000012DUMMYID001234567 /* AddProviderToGiftCardsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000032DUMMYID001234567 /* AddProviderToGiftCardsTable.swift */; }; + AA00000228D9A3DB00223B77 /* BuySellPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000028D9A3DB00223B77 /* BuySellPortalView.swift */; }; AA0000022DUMMYID001234567 /* AddProviderToGiftCardsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000032DUMMYID001234567 /* AddProviderToGiftCardsTable.swift */; }; - AA0000042DUMMYID001234567 /* DashSpend/GiftCardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000062DUMMYID001234567 /* DashSpend/GiftCardProvider.swift */; }; - AA0000052DUMMYID001234567 /* DashSpend/GiftCardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000062DUMMYID001234567 /* DashSpend/GiftCardProvider.swift */; }; - AE99C52D130994B720927E8F /* libPods-dashpay.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D6E23A2DA41FE189E773C705 /* libPods-dashpay.a */; }; - AEAE60406265A73AFD573FD1 /* libPods-dashwallet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F3BA4BE42F52588AED15BF7E /* libPods-dashwallet.a */; }; + AA0000042DUMMYID001234567 /* GiftCardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000062DUMMYID001234567 /* GiftCardProvider.swift */; }; + AA0000052DUMMYID001234567 /* GiftCardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000062DUMMYID001234567 /* GiftCardProvider.swift */; }; + AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */; }; + AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */; }; + AA0001052AA9F58E00A1B202 /* MayaPortalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */; }; + AA0001062AA9F58E00A1B202 /* MayaPortalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */; }; + AA0002022BA0F58E00A1B301 /* MayaCryptoCurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */; }; + AA0002032BA0F58E00A1B301 /* MayaCryptoCurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */; }; + AA0002052BA0F58E00A1B302 /* MayaPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002042BA0F58E00A1B302 /* MayaPool.swift */; }; + AA0002062BA0F58E00A1B302 /* MayaPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002042BA0F58E00A1B302 /* MayaPool.swift */; }; + AA0002082BA0F58E00A1B303 /* MayaEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */; }; + AA0002092BA0F58E00A1B303 /* MayaEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */; }; + AA00020B2BA0F58E00A1B304 /* MayaAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */; }; + AA00020C2BA0F58E00A1B304 /* MayaAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */; }; + AA00020E2BA0F58E00A1B305 /* SelectCoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */; }; + AA00020F2BA0F58E00A1B305 /* SelectCoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */; }; + AA0002112BA0F58E00A1B306 /* CoinRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002102BA0F58E00A1B306 /* CoinRowView.swift */; }; + AA0002122BA0F58E00A1B306 /* CoinRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002102BA0F58E00A1B306 /* CoinRowView.swift */; }; + AA0002142BA0F58E00A1B307 /* SelectCoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */; }; + AA0002152BA0F58E00A1B307 /* SelectCoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */; }; + AA0002172BA0F58E00A1B308 /* SelectCoinHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */; }; + AA0002182BA0F58E00A1B308 /* SelectCoinHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */; }; + AA0003012CA0F58E00A1B401 /* MayaAddressValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */; }; + AA0003022CA0F58E00A1B401 /* MayaAddressValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */; }; + AA0003022CA0F58E00B1B401 /* EnterAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */; }; + AA0003032CA0F58E00B1B401 /* EnterAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */; }; + AA0003042CA0F58E00A1B402 /* MayaAddressValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003032CA0F58E00A1B402 /* MayaAddressValidatorTests.swift */; }; + AA0003052CA0F58E00B1B402 /* EnterAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */; }; + AA0003062CA0F58E00B1B402 /* EnterAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */; }; + AA0003082CA0F58E00B1B403 /* EnterAddressHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */; }; + AA0003092CA0F58E00B1B403 /* EnterAddressHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */; }; + AA00030B2CA0F58E00B1B404 /* GenericQRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */; }; + AA00030C2CA0F58E00B1B404 /* GenericQRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */; }; + AA00030E2CA0F58E00B1B405 /* QRCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */; }; + AA00030F2CA0F58E00B1B405 /* QRCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */; }; + AA0003112CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */; }; + AA0003122CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */; }; + AA0004012DA0F58E00C1C501 /* AddressSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */; }; + AA0004022DA0F58E00C1C501 /* AddressSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */; }; + AA0004042DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */; }; + AA0004052DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */; }; + AA0020012FA0000000000014 /* SwapExecutionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0020012FA0000000000013 /* SwapExecutionData.swift */; }; + AA0020012FA0000000000015 /* SwapExecutionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0020012FA0000000000013 /* SwapExecutionData.swift */; }; + AA29B7041CF55787428E1A04 /* libPods-WatchApp Extension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B9BF7550EF8D4BA009CB1EB5 /* libPods-WatchApp Extension.a */; }; + ABA7230759067F4D4EB39AE9 /* MayaTransactionStatusHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B5469F5588395FA0668C4B /* MayaTransactionStatusHostingController.swift */; }; + AEAE60406265A73AFD573FD1 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + B059B6E200A57A699E6EED84 /* libPods-DashWalletTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1940CA64E8FD6DDD0CFC14A2 /* libPods-DashWalletTests.a */; }; + B3A1F2C4D5E6078901234568 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; + B3A1F2C4D5E6078901234569 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; + B3D26F853F876FE4F2081CFF /* libPods-dashpay.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B6C652479D19F2169F565A6C /* libPods-dashpay.a */; }; B453B34B871E4125607DACDC /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6615F1246B5935365B05BD /* NavigationBar.swift */; }; BA4A72671BE11AFA00E39C01 /* BRAWWeakTimerTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4A72661BE11AFA00E39C01 /* BRAWWeakTimerTarget.swift */; }; BA54D3CE1B2EA74000C9CB28 /* TodayExtensionAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BA54D3CD1B2EA74000C9CB28 /* TodayExtensionAssets.xcassets */; }; @@ -805,17 +952,18 @@ BAA6E3F11BD5CA5900773205 /* BRAWReceiveMoneyInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA6E3F01BD5CA5900773205 /* BRAWReceiveMoneyInterfaceController.swift */; }; BAE12BF21B2DEE7F00895CC5 /* TodayExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BAE12BE51B2DEE7F00895CC5 /* TodayExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; BAE12C061B2DEEF700895CC5 /* DWTodayViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BAE12C051B2DEEF700895CC5 /* DWTodayViewController.m */; }; - BB0000012DUMMYID001234567 /* DashSpend/DashSpendError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000072DUMMYID001234567 /* DashSpend/DashSpendError.swift */; }; - BB0000022DUMMYID001234567 /* DashSpend/DashSpendError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000072DUMMYID001234567 /* DashSpend/DashSpendError.swift */; }; - BB0000032DUMMYID001234567 /* DashSpend/DashSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000082DUMMYID001234567 /* DashSpend/DashSpendRepository.swift */; }; - BB0000042DUMMYID001234567 /* DashSpend/DashSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000082DUMMYID001234567 /* DashSpend/DashSpendRepository.swift */; }; - BB0000052DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000092DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift */; }; - BB0000062DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000092DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift */; }; + BB0000012DUMMYID001234567 /* DashSpendError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000072DUMMYID001234567 /* DashSpendError.swift */; }; + BB0000022DUMMYID001234567 /* DashSpendError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000072DUMMYID001234567 /* DashSpendError.swift */; }; + BB0000032DUMMYID001234567 /* DashSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000082DUMMYID001234567 /* DashSpendRepository.swift */; }; + BB0000042DUMMYID001234567 /* DashSpendRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000082DUMMYID001234567 /* DashSpendRepository.swift */; }; + BB0000052DUMMYID001234567 /* DashSpendRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000092DUMMYID001234567 /* DashSpendRepositoryFactory.swift */; }; + BB0000062DUMMYID001234567 /* DashSpendRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000092DUMMYID001234567 /* DashSpendRepositoryFactory.swift */; }; + C049F721C394463EB97CA8FF /* DSAccount+SpentInputCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BC16BFBFFFA4E488C09164D /* DSAccount+SpentInputCheck.m */; }; + C124DF94278D4238FAE0975D /* OrderPreviewFeeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1A08496F925D17031493E /* OrderPreviewFeeRow.swift */; }; C3DAD268246AA6F10001624F /* DWScreenshotWarningViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DAD267246AA6F10001624F /* DWScreenshotWarningViewController.m */; }; C3DAD2CF247585C10001624F /* NSPredicate+DWFullTextSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DAD2CE247585C10001624F /* NSPredicate+DWFullTextSearch.m */; }; C909614D29EFF7D600002D82 /* WalletKeysOverviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909614C29EFF7D600002D82 /* WalletKeysOverviewModel.swift */; }; C909615129F158D700002D82 /* DerivationPathKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615029F158D700002D82 /* DerivationPathKeysViewController.swift */; }; - A0795C9369A1402498C00F69 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; C909615329F28E3700002D82 /* DerivationPathKeysModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615229F28E3700002D82 /* DerivationPathKeysModel.swift */; }; C909615529F297DD00002D82 /* DerivationPathKeysCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615429F297DD00002D82 /* DerivationPathKeysCell.swift */; }; C909615929F29C9200002D82 /* KeysOverviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615829F29C9200002D82 /* KeysOverviewCell.swift */; }; @@ -824,7 +972,6 @@ C917024129D462C6008C034D /* PayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C917024029D462C6008C034D /* PayViewController.swift */; }; C91E919729FBACE6003E7883 /* ExtendedPublicKeysModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E919629FBACE6003E7883 /* ExtendedPublicKeysModel.swift */; }; C91E91AE29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E91AD29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift */; }; - B3A1F2C4D5E6078901234568 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; C93078482A6AD4F500906E4B /* ConfirmPaymentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C956AF222A5C7D93002FAB75 /* ConfirmPaymentViewController.swift */; }; C93078492A6AD4FC00906E4B /* UpholdConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C956AF2E2A5D5F6B002FAB75 /* UpholdConfirmViewController.swift */; }; C930784A2A6AD52400906E4B /* UpholdConfirmTransferModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C956AF302A5D601C002FAB75 /* UpholdConfirmTransferModel.swift */; }; @@ -1019,8 +1166,6 @@ C94F5E8829D3E7E30034FD57 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C94F5E8729D3E7E30034FD57 /* GoogleService-Info.plist */; }; C94F5E8A29D3FCCF0034FD57 /* ShortcutAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8929D3FCCF0034FD57 /* ShortcutAction.swift */; }; C94F5E8E29D404850034FD57 /* ShortcutCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8D29D404850034FD57 /* ShortcutCell.swift */; }; - 75B3C1A22E36100100CAFE02 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; - 75B3C1B22E36100100CAFE05 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; C94F5E9029D4060A0034FD57 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8F29D4060A0034FD57 /* ShortcutsView.swift */; }; C956AF0C2A5B591A002FAB75 /* DashInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94D98202A4CC78F00F3BEE1 /* DashInputField.swift */; }; C956AF0D2A5B592E002FAB75 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9829CDD2A562822007132E4 /* TappableLabel.swift */; }; @@ -1070,7 +1215,6 @@ 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 */; }; - AA00000228D9A3DB00223B77 /* BuySellPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000028D9A3DB00223B77 /* BuySellPortalView.swift */; }; C9D2C6B02A320AA000D15901 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BD52348CB6600451078 /* SettingsScreen.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 */; }; @@ -1104,7 +1248,6 @@ C9D2C6D42A320AA000D15901 /* DWRecoverTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A74EFFA2305464C00C475EB /* DWRecoverTextView.m */; }; C9D2C6D52A320AA000D15901 /* TransactionMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4709C314287EA11900B4BD48 /* TransactionMetadata.swift */; }; C9D2C6D62A320AA000D15901 /* ExtendedPublicKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E91AD29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift */; }; - B3A1F2C4D5E6078901234569 /* ExtendedPublicKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */; }; C9D2C6D82A320AA000D15901 /* AtmListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BDD28C1305E00490F5E /* AtmListViewController.swift */; }; C9D2C6D92A320AA000D15901 /* TxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A5145F2848F75B005A8E3E /* TxDetailViewController.swift */; }; C9D2C6DA2A320AA000D15901 /* NSAttributedString+Builder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472D13E9299E5396006903F1 /* NSAttributedString+Builder.swift */; }; @@ -1151,7 +1294,7 @@ C9D2C70C2A320AA000D15901 /* DWUpholdLogoutTutorialViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A9FFE652230FF4600956D5F /* DWUpholdLogoutTutorialViewController.m */; }; C9D2C70D2A320AA000D15901 /* CBAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CF46A429654E190067B6EE /* CBAccount.swift */; }; C9D2C70E2A320AA000D15901 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AF180429070B720025803E /* Types.swift */; }; - C9D2C70F2A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C70F2A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C7112A320AA000D15901 /* Coinbase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4774DCDA28F3FA9C008CF87D /* Coinbase.swift */; }; C9D2C7122A320AA000D15901 /* SyncModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F452072A11F28600825057 /* SyncModel.swift */; }; C9D2C7132A320AA000D15901 /* ReceiveContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F42FAA29DC1098001BC549 /* ReceiveContentView.swift */; }; @@ -1171,7 +1314,7 @@ C9D2C7242A320AA000D15901 /* WKWebView+CrowdNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110D1780298BA9AF005BEB30 /* WKWebView+CrowdNode.swift */; }; C9D2C7252A320AA000D15901 /* SyncingActivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA3AFE29350929008D58DC /* SyncingActivityMonitor.swift */; }; C9D2C7262A320AA000D15901 /* EmptyUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F42FAF29DC27F4001BC549 /* EmptyUIView.swift */; }; - C9D2C7272A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C7272A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C7282A320AA000D15901 /* DWQRScanModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AC92C891FEB0B8B008CAEE0 /* DWQRScanModel.m */; }; C9D2C72A2A320AA000D15901 /* SyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F451F62A0CAE1300825057 /* SyncView.swift */; }; C9D2C72B2A320AA000D15901 /* SuccessfulOperationStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47522F4F2927CB9000EE143E /* SuccessfulOperationStatusViewController.swift */; }; @@ -1202,7 +1345,7 @@ C9D2C7492A320AA000D15901 /* DWCurrencyObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AA87CF926E5681100F0CEA6 /* DWCurrencyObject.m */; }; C9D2C74A2A320AA000D15901 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8F29D4060A0034FD57 /* ShortcutsView.swift */; }; C9D2C74B2A320AA000D15901 /* DWFormSectionModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A9FFE3E2230FF4600956D5F /* DWFormSectionModel.m */; }; - C9D2C74C2A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C74C2A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C74D2A320AA000D15901 /* CoinbasePlaceBuyOrderResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F6EDFA628C896BC000427E7 /* CoinbasePlaceBuyOrderResponse.swift */; }; C9D2C74E2A320AA000D15901 /* ExploreMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BDC28C1305E00490F5E /* ExploreMapView.swift */; }; C9D2C74F2A320AA000D15901 /* DWSeedPhraseRow.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AD1CE9322DD078600C99324 /* DWSeedPhraseRow.m */; }; @@ -1226,7 +1369,7 @@ C9D2C7642A320AA000D15901 /* BasePageSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F2C67C28602D4F00C2B774 /* BasePageSheetViewController.swift */; }; C9D2C7662A320AA000D15901 /* CrowdNodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 119E8D0329051F9900D406C1 /* CrowdNodeRequest.swift */; }; C9D2C7672A320AA000D15901 /* CoinbaseExchangeRateResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F6EDFAF28C896BC000427E7 /* CoinbaseExchangeRateResponse.swift */; }; - C9D2C7692A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C7692A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C76A2A320AA000D15901 /* CrowdNodePortalItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 117728A2297A7D24006F1553 /* CrowdNodePortalItem.swift */; }; C9D2C76B2A320AA000D15901 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661B528FE75A700028A8D /* BaseViewController.swift */; }; C9D2C76C2A320AA000D15901 /* AccountListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751CABF296EFD2900F63AC4 /* AccountListController.swift */; }; @@ -1276,12 +1419,12 @@ C9D2C7A52A320AA000D15901 /* DWModalPresentationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C69C523142AA4001B8C90 /* DWModalPresentationController.m */; }; C9D2C7A72A320AA000D15901 /* ActionButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661B328FDCF7800028A8D /* ActionButtonViewController.swift */; }; C9D2C7A82A320AA000D15901 /* BuySellServiceItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4774DCE428F4668B008CF87D /* BuySellServiceItemCell.swift */; }; - C9D2C7AA2A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C7AA2A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C7AB2A320AA000D15901 /* DWBaseFormTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BC82347E0D700451078 /* DWBaseFormTableViewCell.m */; }; C9D2C7AC2A320AA000D15901 /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751CAC6296FAEBB00F63AC4 /* AccountCell.swift */; }; C9D2C7AD2A320AA000D15901 /* DWTransactionListDataProviderStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A913EA723A79AD2006A2A59 /* DWTransactionListDataProviderStub.m */; }; C9D2C7B12A320AA000D15901 /* CoinbaseBaseIDForCurrencyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F6EDFA028C896BC000427E7 /* CoinbaseBaseIDForCurrencyResponse.swift */; }; - C9D2C7B22A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C7B22A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C7B32A320AA000D15901 /* SpecifyAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AC8412297822E000BD1B49 /* SpecifyAmountViewController.swift */; }; C9D2C7B42A320AA000D15901 /* DWUpholdTransactionObject+DWView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AFCB9C323BE76EC00FF59A6 /* DWUpholdTransactionObject+DWView.m */; }; C9D2C7B62A320AA000D15901 /* CrowdNodeDepositTx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114CFECF296469D9005F421B /* CrowdNodeDepositTx.swift */; }; @@ -1292,14 +1435,14 @@ C9D2C7BD2A320AA000D15901 /* DWSharedUIConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A8B9E2822FB1C5D00FF8653 /* DWSharedUIConstants.m */; }; C9D2C7BE2A320AA000D15901 /* BRAppleWatchTransactionData.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A858A0E237EE89B0097A7B5 /* BRAppleWatchTransactionData.m */; }; C9D2C7C02A320AA000D15901 /* HomeHeaderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F4520A2A1209D100825057 /* HomeHeaderModel.swift */; }; - C9D2C7C22A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C7C22A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C7C32A320AA000D15901 /* DWDPRegistrationStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AFF01DA243F4559003718DC /* DWDPRegistrationStatus.m */; }; C9D2C7C42A320AA000D15901 /* CurrencyExchanger_Objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 472D13EC299E6579006903F1 /* CurrencyExchanger_Objc.m */; }; C9D2C7C62A320AA000D15901 /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471DD1B7290A92CD00E030C8 /* Tools.swift */; }; C9D2C7C72A320AA000D15901 /* BuySellPortalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478A2C7028DC554200AD1420 /* BuySellPortalModel.swift */; }; C9D2C7C92A320AA000D15901 /* BuyDashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478C98252942DC2700FAA0F0 /* BuyDashViewController.swift */; }; C9D2C7CA2A320AA000D15901 /* DWModalDismissalAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C69CF23143435001B8C90 /* DWModalDismissalAnimation.m */; }; - C9D2C7CB2A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C7CB2A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C7CD2A320AA000D15901 /* UIViewController+DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CDEECD294A3CF2008AE06D /* UIViewController+DashWallet.swift */; }; C9D2C7CE2A320AA000D15901 /* CrowdNodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470617D5299A671900DCC667 /* CrowdNodeCell.swift */; }; C9D2C7CF2A320AA000D15901 /* MerchantListLocationOffCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BD128C1305E00490F5E /* MerchantListLocationOffCell.swift */; }; @@ -1393,12 +1536,10 @@ C9D2C8472A320AA000D15901 /* DWBaseTransactionListDataProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A913EAD23A7AC86006A2A59 /* DWBaseTransactionListDataProvider.m */; }; C9D2C8482A320AA000D15901 /* CoinbaseTransactionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F6EDFA828C896BC000427E7 /* CoinbaseTransactionsRequest.swift */; }; C9D2C8492A320AA000D15901 /* DWAppRootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A9CEBAC22E1DA4000A50237 /* DWAppRootViewController.m */; }; - C9D2C84A2A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C84A2A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C84B2A320AA000D15901 /* UITableView+DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F005FE297164600029EB10 /* UITableView+DashWallet.swift */; }; C9D2C84C2A320AA000D15901 /* TransferAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661B128FDC72700028A8D /* TransferAmountViewController.swift */; }; C9D2C84D2A320AA000D15901 /* ShortcutCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94F5E8D29D404850034FD57 /* ShortcutCell.swift */; }; - 75B3C1A32E36100100CAFE03 /* ShortcutSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */; }; - 75B3C1B32E36100100CAFE06 /* ShortcutCustomizeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */; }; C9D2C84F2A320AA000D15901 /* UIColor+DWStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A44312022CCA2A0009BAF7F /* UIColor+DWStyle.m */; }; C9D2C8502A320AA000D15901 /* DWEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = FBEF3AF021823CD800917AB6 /* DWEnvironment.m */; }; C9D2C8512A320AA000D15901 /* AtmItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BC928C1305E00490F5E /* AtmItemCell.swift */; }; @@ -1418,14 +1559,13 @@ C9D2C8622A320AA000D15901 /* BackupInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F42FB129DD5141001BC549 /* BackupInfoViewController.swift */; }; C9D2C8652A320AA000D15901 /* DWCaptureSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7BAD234770C900451078 /* DWCaptureSessionManager.m */; }; C9D2C8662A320AA000D15901 /* DWDataMigrationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A11F59E2194BD6200E7B563 /* DWDataMigrationManager.m */; }; - C9D2C86A2A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C86A2A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C86B2A320AA000D15901 /* AmountPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472D13E5299E3C81006903F1 /* AmountPreviewView.swift */; }; C9D2C86C2A320AA000D15901 /* ModalNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4759D513292FEFFB002F20DC /* ModalNavigationController.swift */; }; C9D2C86E2A320AA000D15901 /* TransferAmountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A50F3A2913BC0900C70123 /* TransferAmountModel.swift */; }; C9D2C86F2A320AA000D15901 /* NavigationBarAppearanceCustomizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4730586F295AD62B004641DA /* NavigationBarAppearanceCustomizable.swift */; }; C9D2C8702A320AA000D15901 /* AmountObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471DD1B5290A901200E030C8 /* AmountObject.swift */; }; C9D2C8712A320AA000D15901 /* DerivationPathKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C909615029F158D700002D82 /* DerivationPathKeysViewController.swift */; }; - 8A7607606A594C6E99A2FAC7 /* DerivationPathKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */; }; C9D2C8722A320AA000D15901 /* DSTransaction+DashWallet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4709C31D2880247C00B4BD48 /* DSTransaction+DashWallet.m */; }; C9D2C8742A320AA000D15901 /* DWLocalCurrencyModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7A7C1C234B771400451078 /* DWLocalCurrencyModel.m */; }; C9D2C8762A320AA000D15901 /* DWUpholdMainnetConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = FB8ACEB522E0502100EE5035 /* DWUpholdMainnetConstants.m */; }; @@ -1452,7 +1592,7 @@ C9D2C8952A320AA000D15901 /* ConvertCryptoOrderPreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4751CACF2970224D00F63AC4 /* ConvertCryptoOrderPreviewModel.swift */; }; C9D2C8972A320AA000D15901 /* RatesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A2E3A82972B15F0032A63B /* RatesProvider.swift */; }; C9D2C8982A320AA000D15901 /* CBAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A2A2E8293E612900938DB7 /* CBAuth.swift */; }; - C9D2C8992A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C8992A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C89A2A320AA000D15901 /* CoinbaseCreateAddressesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F6EDFAE28C896BC000427E7 /* CoinbaseCreateAddressesRequest.swift */; }; C9D2C89B2A320AA000D15901 /* DWImportWalletInfoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A10EB342358996700C38B61 /* DWImportWalletInfoViewController.m */; }; C9D2C89D2A320AA000D15901 /* SyncingAlertContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F451ED2A0BF1F500825057 /* SyncingAlertContentView.swift */; }; @@ -1503,7 +1643,7 @@ C9D2C8DD2A320AA000D15901 /* AtmDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47AE8BD528C1305E00490F5E /* AtmDataProvider.swift */; }; C9D2C8DE2A320AA000D15901 /* GettingStartedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110C67942921147F006B580C /* GettingStartedViewController.swift */; }; C9D2C8DF2A320AA000D15901 /* NetworkUnavailableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477A963D292CD27D0013605B /* NetworkUnavailableView.swift */; }; - C9D2C8E02A320AA000D15901 /* (null) in Sources */ = {isa = PBXBuildFile; }; + C9D2C8E02A320AA000D15901 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; C9D2C8E12A320AA000D15901 /* DWLockScreenViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A63004D2328F37C00827825 /* DWLockScreenViewController.m */; }; C9D2C8E22A320AA000D15901 /* UpholdTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4789D2302981069700BAFEFA /* UpholdTransferViewController.swift */; }; C9D2C8E32A320AA000D15901 /* DWVerifySeedPhraseViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2AD1CE8322DC9B5D00C99324 /* DWVerifySeedPhraseViewController.m */; }; @@ -1562,7 +1702,7 @@ C9D2C9272A320AA000D15901 /* SyncView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2A4E531922EA382B00E5168A /* SyncView.xib */; }; C9D2C9292A320AA000D15901 /* BackupInfoItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C9F42FB529DD8702001BC549 /* BackupInfoItemView.xib */; }; C9D2C92A2A320AA000D15901 /* explore.db in Resources */ = {isa = PBXBuildFile; fileRef = 47AE8BAF28BFF28400490F5E /* explore.db */; }; - C9D2C92B2A320AA000D15901 /* (null) in Resources */ = {isa = PBXBuildFile; }; + C9D2C92B2A320AA000D15901 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; C9D2C92C2A320AA000D15901 /* Coinbase.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0F3693812919A70B007F4E91 /* Coinbase.storyboard */; }; C9D2C92E2A320AA000D15901 /* ImportWalletInfo.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A10EB3D2358BDA500C38B61 /* ImportWalletInfo.storyboard */; }; C9D2C92F2A320AA000D15901 /* ReceiveContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C9F42FAC29DC115A001BC549 /* ReceiveContentView.xib */; }; @@ -1625,13 +1765,20 @@ CC0000182DUMMYID001234567 /* PiggyCardsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000092DUMMYID001234567 /* PiggyCardsRepository.swift */; }; CC0000192DUMMYID001234567 /* PiggyCardsTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000102DUMMYID001234567 /* PiggyCardsTokenService.swift */; }; CC0000202DUMMYID001234567 /* PiggyCardsTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0000102DUMMYID001234567 /* PiggyCardsTokenService.swift */; }; - D0B1968E57F8D7B118C22CED /* libPods-DashWalletScreenshotsUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C20BCC772DF4075F8EADAE0 /* libPods-DashWalletScreenshotsUITests.a */; }; - DE3A167A235B79D705C0A962 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + D0D3F0F33D9F162622F0A6E5 /* DashBalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF95E0E0B636AFB810D62116 /* DashBalanceView.swift */; }; + DA38F1C5CD32A52891D02D0E /* MayaConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7708BBFDD14AFF237FB72D71 /* MayaConstants.swift */; }; + DE3A167A235B79D705C0A962 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + E5767AEAEF7C4921886E915A /* BuySellPortalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B94B3697D7742BFA991B5CC /* BuySellPortalScreen.swift */; }; + E88FE13A3079496C9A5EA8D4 /* MayaCoinIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20554F992205428BA790F6CF /* MayaCoinIconView.swift */; }; + EC26E6EBECE04736903F77A2 /* DSAccount+SpentInputCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BC16BFBFFFA4E488C09164D /* DSAccount+SpentInputCheck.m */; }; + ECF67981D8BA06E1323B0818 /* NetworkUnavailableStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C4C2E6AC61005536FF85E /* NetworkUnavailableStateView.swift */; }; EE0000012DUMMYID001234567 /* POIDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0000032DUMMYID001234567 /* POIDetailsViewModel.swift */; }; EE0000022DUMMYID001234567 /* POIDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0000032DUMMYID001234567 /* POIDetailsViewModel.swift */; }; EE0000042DUMMYID001234567 /* PercentageFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0000052DUMMYID001234567 /* PercentageFormatter.swift */; }; EE0000062DUMMYID001234567 /* PercentageFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0000052DUMMYID001234567 /* PercentageFormatter.swift */; }; EE5F3D5C458E92B69D14BFD8 /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6615F1246B5935365B05BD /* NavigationBar.swift */; }; + F264E9A940EF10070D2F941E /* libPods-WatchApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DC11A0747AD00DA7524204A /* libPods-WatchApp.a */; }; + F8BA4B9BBB650F7F50AD936F /* MayaMenuStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0286D4D1CADBBD51F09BE8E8 /* MayaMenuStyles.swift */; }; FB248B5D1F73803100405AE0 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB248B5C1F73803100405AE0 /* UserNotifications.framework */; }; FB248B631F79BB7C00405AE0 /* SafariServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB248B621F79BB7C00405AE0 /* SafariServices.framework */; }; FB2E5537218BA161003A1B7C /* DWVersionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB2E5536218BA161003A1B7C /* DWVersionManager.m */; }; @@ -1645,53 +1792,20 @@ FBF3F42D1E42B00C00C7248E /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBF3F42C1E42B00C00C7248E /* UIKit.framework */; }; FBF3F42F1E42B01E00C7248E /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBF3F42E1E42B01E00C7248E /* CoreGraphics.framework */; }; FBF3F4311E42B02800C7248E /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBF3F4301E42B02800C7248E /* ImageIO.framework */; }; -AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */; }; - AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */; }; - AA0001052AA9F58E00A1B202 /* MayaPortalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */; }; - AA0001062AA9F58E00A1B202 /* MayaPortalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */; }; - AA0002022BA0F58E00A1B301 /* MayaCryptoCurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */; }; - AA0002032BA0F58E00A1B301 /* MayaCryptoCurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */; }; - AA0003012CA0F58E00A1B401 /* MayaAddressValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */; }; - AA0003022CA0F58E00A1B401 /* MayaAddressValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */; }; - AA0003042CA0F58E00A1B402 /* MayaAddressValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003032CA0F58E00A1B402 /* MayaAddressValidatorTests.swift */; }; - AA0002052BA0F58E00A1B302 /* MayaPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002042BA0F58E00A1B302 /* MayaPool.swift */; }; - AA0002062BA0F58E00A1B302 /* MayaPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002042BA0F58E00A1B302 /* MayaPool.swift */; }; - AA0002082BA0F58E00A1B303 /* MayaEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */; }; - AA0002092BA0F58E00A1B303 /* MayaEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */; }; - AA00020B2BA0F58E00A1B304 /* MayaAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */; }; - AA00020C2BA0F58E00A1B304 /* MayaAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */; }; - AA00020E2BA0F58E00A1B305 /* SelectCoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */; }; - AA00020F2BA0F58E00A1B305 /* SelectCoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */; }; - AA0002112BA0F58E00A1B306 /* CoinRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002102BA0F58E00A1B306 /* CoinRowView.swift */; }; - AA0002122BA0F58E00A1B306 /* CoinRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002102BA0F58E00A1B306 /* CoinRowView.swift */; }; - AA0002142BA0F58E00A1B307 /* SelectCoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */; }; - AA0002152BA0F58E00A1B307 /* SelectCoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */; }; - AA0002172BA0F58E00A1B308 /* SelectCoinHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */; }; - AA0002182BA0F58E00A1B308 /* SelectCoinHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */; }; - AA0003022CA0F58E00B1B401 /* EnterAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */; }; - AA0003032CA0F58E00B1B401 /* EnterAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */; }; - AA0003052CA0F58E00B1B402 /* EnterAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */; }; - AA0003062CA0F58E00B1B402 /* EnterAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */; }; - AA0003082CA0F58E00B1B403 /* EnterAddressHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */; }; - AA0003092CA0F58E00B1B403 /* EnterAddressHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */; }; - AA00030B2CA0F58E00B1B404 /* GenericQRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */; }; - AA00030C2CA0F58E00B1B404 /* GenericQRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */; }; - AA00030E2CA0F58E00B1B405 /* QRCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */; }; - AA00030F2CA0F58E00B1B405 /* QRCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */; }; - AA0003112CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */; }; - AA0003122CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */; }; - AA0004012DA0F58E00C1C501 /* AddressSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */; }; - AA0004022DA0F58E00C1C501 /* AddressSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */; }; - AA0004042DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */; }; - AA0004052DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */; }; - FF0000012DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000042DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift */; }; - FF0000022DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000042DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift */; }; + FF0000012DUMMYID001234567 /* PiggyCardsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000042DUMMYID001234567 /* PiggyCardsConstants.swift */; }; + FF0000022DUMMYID001234567 /* PiggyCardsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000042DUMMYID001234567 /* PiggyCardsConstants.swift */; }; FF0000052DUMMYID001234567 /* PiggyCardsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000062DUMMYID001234567 /* PiggyCardsModels.swift */; }; FF0000072DUMMYID001234567 /* PiggyCardsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000062DUMMYID001234567 /* PiggyCardsModels.swift */; }; FF0000082DUMMYID001234567 /* BarcodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000092DUMMYID001234567 /* BarcodeScanner.swift */; }; FF0000102DUMMYID001234567 /* BarcodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0000092DUMMYID001234567 /* BarcodeScanner.swift */; }; GG0000012DUMMYID001234567 /* GeoRestrictionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = GG0000032DUMMYID001234567 /* GeoRestrictionService.swift */; }; GG0000022DUMMYID001234567 /* GeoRestrictionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = GG0000032DUMMYID001234567 /* GeoRestrictionService.swift */; }; + RC00A0012FA0000000000001 /* TopIntro.swift in Sources */ = {isa = PBXBuildFile; fileRef = RC00A0032FA0000000000001 /* TopIntro.swift */; }; + RC00A0022FA0000000000001 /* TopIntro.swift in Sources */ = {isa = PBXBuildFile; fileRef = RC00A0032FA0000000000001 /* TopIntro.swift */; }; + RC00A0042FA0000000000001 /* ExploreMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = RC00A0062FA0000000000001 /* ExploreMenuScreen.swift */; }; + RC00A0052FA0000000000001 /* ExploreMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = RC00A0062FA0000000000001 /* ExploreMenuScreen.swift */; }; + RC00C0012FA0000000000001 /* CSVExportSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = RC00C0032FA0000000000001 /* CSVExportSheet.swift */; }; + RC00C0022FA0000000000001 /* CSVExportSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = RC00C0032FA0000000000001 /* CSVExportSheet.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1769,8 +1883,10 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 04D182469170A3A2843F51CA /* Pods-WatchApp.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.testflight.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.testflight.xcconfig"; sourceTree = ""; }; - 0995737D764092DF77CB88AC /* Pods-WatchApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.debug.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.debug.xcconfig"; sourceTree = ""; }; + 0286D4D1CADBBD51F09BE8E8 /* MayaMenuStyles.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MayaMenuStyles.swift; sourceTree = ""; }; + 0755F840BCC1E0754D807B04 /* libPods-TodayExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TodayExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 09950CBE38F14286AC06BB78 /* DSAccount+SpentInputCheck.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DSAccount+SpentInputCheck.h"; sourceTree = ""; }; + 0CE6B29178211061196A0936 /* Pods-WatchApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.debug.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.debug.xcconfig"; sourceTree = ""; }; 0F36937D2919A5DB007F4E91 /* TwoFactorAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorAuthViewController.swift; sourceTree = ""; }; 0F3693802919A70B007F4E91 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Coinbase.storyboard; sourceTree = ""; }; 0F6EDF9D28C896BC000427E7 /* CoinbaseUserAuthInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinbaseUserAuthInformation.swift; sourceTree = ""; }; @@ -1846,16 +1962,20 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 11E47BAA28EB38510097CFA0 /* SendCoinsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendCoinsService.swift; sourceTree = ""; }; 11E47BAC28EB3A7D0097CFA0 /* CrowdNode+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CrowdNode+Constants.swift"; sourceTree = ""; }; 11ED906A29681773003784F9 /* StakingInfoDialogController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingInfoDialogController.swift; sourceTree = ""; }; - 1C3070D5E872E975F8473A68 /* Pods-dashwallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.release.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.release.xcconfig"; sourceTree = ""; }; + 180C7A2E60359C7F8FB7B918 /* Pods-DashWalletScreenshotsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.release.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.release.xcconfig"; sourceTree = ""; }; + 1940CA64E8FD6DDD0CFC14A2 /* libPods-DashWalletTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DashWalletTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1AF20341A5E313C9D309BF39 /* Pods-DashWalletTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.release.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.release.xcconfig"; sourceTree = ""; }; + 1E7370138411E3E74B42F4CC /* Pods-dashwallet.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.testflight.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.testflight.xcconfig"; sourceTree = ""; }; + 20554F992205428BA790F6CF /* MayaCoinIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaCoinIconView.swift; sourceTree = ""; }; 222040C51C1A1940005CE1C3 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 222E7F551C46E9B8009AB45D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 222E7F571C46E9BE009AB45D /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 225383001C694D7400968BEE /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; + 226154FFBC684D1FDFCA35E4 /* Pods-DashWalletScreenshotsUITests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.testflight.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.testflight.xcconfig"; sourceTree = ""; }; 22B6A4471C0E963900673913 /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; 22D3613C1C56F2CD0057CF76 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 22F45A0A1C30EAB700B07A15 /* BRAWKeypad.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BRAWKeypad.swift; sourceTree = ""; }; - 281D63D6AC9610BCF4765D07 /* Pods-DashWalletTests.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.testnet.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.testnet.xcconfig"; sourceTree = ""; }; - 28AA9F2D282325F800A4F3AA /* Pods-dashpay.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.release.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.release.xcconfig"; sourceTree = ""; }; + 2537FCF252FB799EB4103150 /* Pods-dashwallet.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.testnet.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.testnet.xcconfig"; sourceTree = ""; }; 2A0C699A23104588001B8C90 /* DWPaymentInput.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWPaymentInput.h; sourceTree = ""; }; 2A0C699B23104588001B8C90 /* DWPaymentInput.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWPaymentInput.m; sourceTree = ""; }; 2A0C699D231045E7001B8C90 /* DWPaymentInput+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DWPaymentInput+Private.h"; sourceTree = ""; }; @@ -2304,9 +2424,16 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 2AFCB9C323BE76EC00FF59A6 /* DWUpholdTransactionObject+DWView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DWUpholdTransactionObject+DWView.m"; sourceTree = ""; }; 2AFF01D9243F4559003718DC /* DWDPRegistrationStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWDPRegistrationStatus.h; sourceTree = ""; }; 2AFF01DA243F4559003718DC /* DWDPRegistrationStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWDPRegistrationStatus.m; sourceTree = ""; }; - 2E17397DF35152CAFBB79068 /* Pods-WatchApp Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.debug.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.debug.xcconfig"; sourceTree = ""; }; - 38C4CA9D73A147BA44601741 /* libPods-WatchApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WatchApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3C20BCC772DF4075F8EADAE0 /* libPods-DashWalletScreenshotsUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DashWalletScreenshotsUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 34B8A8461A3EDA41E40156CE /* Pods-dashpay.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.testnet.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.testnet.xcconfig"; sourceTree = ""; }; + 351EC86529F9D2F4DC5CCE1F /* Pods-WatchApp.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.testflight.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.testflight.xcconfig"; sourceTree = ""; }; + 38ED8068CF6060EE8343E195 /* Pods-WatchApp.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.testnet.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.testnet.xcconfig"; sourceTree = ""; }; + 393F2EE6876F067BC90A103C /* Pods-TodayExtension.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.testflight.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.testflight.xcconfig"; sourceTree = ""; }; + 3B0E86557754B86DD3A389CC /* Pods-TodayExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.debug.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.debug.xcconfig"; sourceTree = ""; }; + 3B889D25725F4A7EBFD60324 /* Pods-DashWalletScreenshotsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.debug.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.debug.xcconfig"; sourceTree = ""; }; + 3BA20785E162F1AF2F455D9C /* Pods-WatchApp Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.debug.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.debug.xcconfig"; sourceTree = ""; }; + 3DC11A0747AD00DA7524204A /* libPods-WatchApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WatchApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 41D6F206BA803FDFA21B31F5 /* Pods-DashWalletTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.debug.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.debug.xcconfig"; sourceTree = ""; }; + 4457C976B1DB0CF402FF3C5F /* Pods-WatchApp Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.release.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.release.xcconfig"; sourceTree = ""; }; 470617D5299A671900DCC667 /* CrowdNodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdNodeCell.swift; sourceTree = ""; }; 47081196298CF20B003FCA3D /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = ""; }; 4708119E2990F56F003FCA3D /* TransactionDataItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDataItem.swift; sourceTree = ""; }; @@ -2346,7 +2473,6 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 474C7214298A1FCF00475CA6 /* TxDetailActionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TxDetailActionCell.xib; sourceTree = ""; }; 474C7217298A422400475CA6 /* TransactionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionItemView.swift; sourceTree = ""; }; 4751136B28D9A3DB00223B77 /* BuySellPortalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuySellPortalViewController.swift; sourceTree = ""; }; - AA00000028D9A3DB00223B77 /* BuySellPortalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuySellPortalView.swift; sourceTree = ""; }; 4751136E28D9B50E00223B77 /* CoinbaseInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinbaseInfoViewController.swift; sourceTree = ""; }; 4751137428DAF28800223B77 /* UIAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAssembly.swift; sourceTree = ""; }; 4751CABF296EFD2900F63AC4 /* AccountListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListController.swift; sourceTree = ""; }; @@ -2490,16 +2616,48 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 47F4B6CC29485A8B00AED4C9 /* ConfirmOrderCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmOrderCells.swift; sourceTree = ""; }; 47FA3AFE29350929008D58DC /* SyncingActivityMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncingActivityMonitor.swift; sourceTree = ""; }; 47FA3B0129364991008D58DC /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; - 4944819B9E17D4CC7DBFF293 /* Pods-WatchApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.release.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.release.xcconfig"; sourceTree = ""; }; - 4C22BC09CF6178544B2300A4 /* libPods-WatchApp Extension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WatchApp Extension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 4D37A3F73905BC239A9D127D /* Pods-WatchApp Extension.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.testnet.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.testnet.xcconfig"; sourceTree = ""; }; - 62C1F7A3ABEE7CF3BBB270C4 /* Pods-DashWalletTests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.testflight.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.testflight.xcconfig"; sourceTree = ""; }; - 67217DE34817FB304EDEA8D4 /* Pods-dashpay.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.testnet.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.testnet.xcconfig"; sourceTree = ""; }; - 6A44C167AF8F881176AFB256 /* Pods-TodayExtension.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.testflight.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.testflight.xcconfig"; sourceTree = ""; }; - 6DC4B39C45C824674E05DB22 /* Pods-dashwallet.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.testflight.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.testflight.xcconfig"; sourceTree = ""; }; + 487C4C2E6AC61005536FF85E /* NetworkUnavailableStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkUnavailableStateView.swift; sourceTree = ""; }; + 514AE85D2FB1F9DB00F9B54C /* DashSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSwitch.swift; sourceTree = ""; }; + 514AE9F22FB2F4C500F9B54C /* HaltedToast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HaltedToast.swift; sourceTree = ""; }; + 514AEA0E2FB31BD800F9B54C /* AddressFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressFieldView.swift; sourceTree = ""; }; + 514AEA122FB324FA00F9B54C /* MayaConvertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaConvertView.swift; sourceTree = ""; }; + 514AEA432FB33B4300F9B54C /* EnterAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAmountView.swift; sourceTree = ""; }; + 514AEA472FB33B4300F9B54C /* DashPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashPickerView.swift; sourceTree = ""; }; + 514AEA4A2FB33B4300F9B54C /* MayaConvertHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaConvertHostingController.swift; sourceTree = ""; }; + 514AEA4D2FB33B4300F9B54C /* MayaAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaAmountView.swift; sourceTree = ""; }; + 514AEAD42FB34F8600F9B54C /* OrderPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPreviewView.swift; sourceTree = ""; }; + 514AF1BF2FB5F02D00F9B54C /* MayaConvertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaConvertViewModel.swift; sourceTree = ""; }; + 514BEA002FB5F02D00F9B54C /* MayaConvertAmount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaConvertAmount.swift; sourceTree = ""; }; + 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Coinbase-Info.plist"; sourceTree = ""; }; + 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 5151C8C42F913BF100F0A604 /* Topper-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Topper-Info.plist"; sourceTree = ""; }; + 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ZenLedger-Info.plist"; sourceTree = ""; }; + 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyView.swift; sourceTree = ""; }; + 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyViewModel.swift; sourceTree = ""; }; + 5151CD612F926D1900F0A604 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; + 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCurrencyCellView.swift; sourceTree = ""; }; + 5151CE172F9624D000F0A604 /* CurrencyItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyItem.swift; sourceTree = ""; }; + 516983A22FCAEDEB00BA91A2 /* SuccessIllustration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuccessIllustration.swift; sourceTree = ""; }; + 516983A52FCAEF1300BA91A2 /* ErrorIllustration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorIllustration.swift; sourceTree = ""; }; + 516983B02FCAEDEB00BA91A2 /* LoadingIllustration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIllustration.swift; sourceTree = ""; }; + 516983C12FCAEDEB00BA91A2 /* XmarkIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XmarkIcon.swift; sourceTree = ""; }; + 516C4047DB43CFFBB8ED541B /* Pods-DashWalletScreenshotsUITests.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.testnet.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.testnet.xcconfig"; sourceTree = ""; }; + 51A577472FBB3A6000FB4097 /* OrderPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPreviewViewModel.swift; sourceTree = ""; }; + 51A5774A2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPreviewHostingController.swift; sourceTree = ""; }; + 51A57B012FBB41DE00FB4097 /* OrderPreviewTableRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPreviewTableRow.swift; sourceTree = ""; }; + 51A57B052FBB41DE00FB4097 /* MayaFeeInfoSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaFeeInfoSheet.swift; sourceTree = ""; }; + 51A57C022FBB5A0000FB4097 /* MayaTransactionSuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaTransactionSuccessView.swift; sourceTree = ""; }; + 51A57C032FBB5A0000FB4097 /* MayaTransactionFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaTransactionFailureView.swift; sourceTree = ""; }; + 51A57C082FBB5A0000FB4097 /* MayaTransactionPendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaTransactionPendingView.swift; sourceTree = ""; }; + 51F0D10030BF97A900B27E11 /* ExclamationIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclamationIcon.swift; sourceTree = ""; }; + 51F0D10330BF98BA00B27E11 /* ArrowDownIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowDownIcon.swift; sourceTree = ""; }; + 52B5469F5588395FA0668C4B /* MayaTransactionStatusHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaTransactionStatusHostingController.swift; sourceTree = ""; }; + 75DFA10175DFA10175DFA101 /* DashStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashStepper.swift; sourceTree = ""; }; + 75DFA10275DFA10275DFA102 /* SegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControl.swift; sourceTree = ""; }; + 5CBC0F417B9E8DE631F9DE2A /* Pods-DashWalletTests.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.testnet.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.testnet.xcconfig"; sourceTree = ""; }; + 62613C0D937CA301FBBF2356 /* Pods-dashpay.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.debug.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.debug.xcconfig"; sourceTree = ""; }; 708B9F2B4B1E4C31B5010CDF /* RadioButtonRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadioButtonRow.swift; path = "DashWallet/Sources/UI/SwiftUI Components/RadioButtonRow.swift"; sourceTree = ""; }; - 718A7E2BCD93948084E43A8F /* Pods-DashWalletScreenshotsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.release.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.release.xcconfig"; sourceTree = ""; }; - 74F4F603C555A521F738DC9E /* Pods-TodayExtension.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.testnet.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.testnet.xcconfig"; sourceTree = ""; }; + 726054552E79221F6B9D97AE /* Pods-dashwallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.debug.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.debug.xcconfig"; sourceTree = ""; }; 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 = ""; }; @@ -2516,6 +2674,7 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 7511E8D31AE5FF390025F1B3 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; 7511E8E31AE5FFCC0025F1B3 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; lineEnding = 0; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.simpleColoring; }; 7511E8EB1AE600220025F1B3 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 7513056584D6F9A1B5E8E79B /* Pods-dashpay.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.release.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.release.xcconfig"; sourceTree = ""; }; 7513DA8B2AB9643F005D55F6 /* BaseIntegrationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseIntegrationModel.swift; sourceTree = ""; }; 7514E4E72AF233CF00A0466F /* VotingFiltersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingFiltersViewController.swift; sourceTree = ""; }; 7514E4E92AF2351F00A0466F /* VotingFiltersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingFiltersModel.swift; sourceTree = ""; }; @@ -2552,11 +2711,11 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 753FDBED2AECF52B0005EEC3 /* UsernameVoting.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UsernameVoting.storyboard; sourceTree = ""; }; 754495DC2AE91B6300492817 /* GroupedRequestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedRequestCell.swift; sourceTree = ""; }; 754495DE2AE91D3500492817 /* UsernameRequestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameRequestCell.swift; sourceTree = ""; }; - 754565C42DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendAPI.swift; sourceTree = ""; }; - 754565C52DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendEndpoint.swift; sourceTree = ""; }; - 754565C72DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendRepository.swift; sourceTree = ""; }; + 754565C42DAA52A000DA4E8E /* CTXSpendAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendAPI.swift; sourceTree = ""; }; + 754565C52DAA52A000DA4E8E /* CTXSpendEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendEndpoint.swift; sourceTree = ""; }; + 754565C72DAA52A000DA4E8E /* CTXSpendRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendRepository.swift; sourceTree = ""; }; 754565D02DABA5EB00DA4E8E /* MerchantTypesDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MerchantTypesDialog.swift; sourceTree = ""; }; - 7545ED522DA91A2F0075F45C /* DashSpend/CTXConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTXConstants.swift; sourceTree = ""; }; + 7545ED522DA91A2F0075F45C /* CTXConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTXConstants.swift; sourceTree = ""; }; 7545ED562DA91AEA0075F45C /* DashSpendUserAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendUserAuthViewModel.swift; sourceTree = ""; }; 7545ED5C2DA91F590075F45C /* DashSpendTermsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendTermsScreen.swift; sourceTree = ""; }; 7545ED5F2DA91FC60075F45C /* NumericKeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericKeyboardView.swift; sourceTree = ""; }; @@ -2623,7 +2782,7 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 759AFDE22CC67E89007072D2 /* VotingInfoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingInfoScreen.swift; sourceTree = ""; }; 759C8F9E2B593589004B1305 /* CrowdNodeAPYView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdNodeAPYView.swift; sourceTree = ""; }; 75A0A3F22CA7DBCF003ED48B /* TimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeUtils.swift; sourceTree = ""; }; - 75A2F3022DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendTokenService.swift; sourceTree = ""; }; + 75A2F3022DE48C860046BE17 /* CTXSpendTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/CTX/CTXSpendTokenService.swift; sourceTree = ""; }; 75A8C1632AE5725C0042256E /* UsernameRequestsDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameRequestsDAO.swift; sourceTree = ""; }; 75A8C1662AE5734A0042256E /* UsernameRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameRequest.swift; sourceTree = ""; }; 75A8C1682AE6A1AC0042256E /* VotingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingViewModel.swift; sourceTree = ""; }; @@ -2632,6 +2791,8 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 75AA33D12BF9E18E00F12465 /* Color+DWStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+DWStyle.swift"; sourceTree = ""; }; 75AA33D42BF9E1D400F12465 /* Font+DWStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+DWStyle.swift"; sourceTree = ""; }; 75AA33D72BFB4A5A00F12465 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutSelectionView.swift; sourceTree = ""; }; + 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutCustomizeBannerView.swift; sourceTree = ""; }; 75BDE7AB2BF3287400556791 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = ""; }; 75C1F0442AE26AC0006929CA /* CoinJoinLevelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinJoinLevelsViewModel.swift; sourceTree = ""; }; 75C1F09D2AFF675400FE675E /* EnterVotingKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterVotingKeyViewController.swift; sourceTree = ""; }; @@ -2660,6 +2821,11 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 75D9EBC12DE5CD9C009416A2 /* GiftCardsDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardsDAO.swift; sourceTree = ""; }; 75D9EBC42DE5CE14009416A2 /* GiftCardDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardDetailsView.swift; sourceTree = ""; }; 75D9EBC52DE5CE14009416A2 /* GiftCardDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardDetailsViewModel.swift; sourceTree = ""; }; + D6C0B001D6C0B001D6C0B001 /* GiftCardDetailsInfoCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardDetailsInfoCard.swift; sourceTree = ""; }; + D6C0B002D6C0B002D6C0B002 /* GiftCardDetailsMerchantHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardDetailsMerchantHeader.swift; sourceTree = ""; }; + D6C0B003D6C0B003D6C0B003 /* GiftCardPurchaseSelectionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardPurchaseSelectionSheet.swift; sourceTree = ""; }; + D6C0B004D6C0B004D6C0B004 /* GiftCardDetailsPoweredBySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardDetailsPoweredBySection.swift; sourceTree = ""; }; + D6C0B005D6C0B005D6C0B005 /* GiftCardDetailsHowToUseSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardDetailsHowToUseSection.swift; sourceTree = ""; }; 75DE02411B0EB79A0027FF08 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 75DE02491B0EB7C80027FF08 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; 75DE024D1B0EB7FC0027FF08 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -2692,16 +2858,47 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild 75FFD6C02BF493A60032879E /* HomeViewController+SecureWalletDelegateImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeViewController+SecureWalletDelegateImpl.swift"; sourceTree = ""; }; 75FFD6C32BF494E20032879E /* HomeViewController+ImportPrivateKeyDelegateImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeViewController+ImportPrivateKeyDelegateImpl.swift"; sourceTree = ""; }; 75FFD6C62BF495800032879E /* HomeViewController+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeViewController+Shortcuts.swift"; sourceTree = ""; }; - 77215C3978EE089C06A41979 /* Pods-DashWalletTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.debug.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.debug.xcconfig"; sourceTree = ""; }; - 8391BA0D088FF516D63DE9A1 /* Pods-dashpay.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.testflight.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.testflight.xcconfig"; sourceTree = ""; }; - 86CC430C535DAEBF900E5914 /* Pods-dashwallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.debug.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.debug.xcconfig"; sourceTree = ""; }; + 7708BBFDD14AFF237FB72D71 /* MayaConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaConstants.swift; sourceTree = ""; }; + 797D9070BF54A3533190584E /* libPods-dashwallet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-dashwallet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7B56B94951BA51EAC9E0A6E1 /* libPods-DashWalletScreenshotsUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DashWalletScreenshotsUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7B94B3697D7742BFA991B5CC /* BuySellPortalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuySellPortalScreen.swift; sourceTree = ""; }; + 7BC16BFBFFFA4E488C09164D /* DSAccount+SpentInputCheck.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DSAccount+SpentInputCheck.m"; sourceTree = ""; }; + 8571065059CF5168F45C86E4 /* Pods-dashpay.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.testflight.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.testflight.xcconfig"; sourceTree = ""; }; 91F0B8B82F3FAB7C00E713AE /* KeysOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysOverviewView.swift; sourceTree = ""; }; - A47B6DF71195E41A5B7B5F99 /* Pods-dashpay.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashpay.debug.xcconfig"; path = "Target Support Files/Pods-dashpay/Pods-dashpay.debug.xcconfig"; sourceTree = ""; }; + 96CB7E093E994D2391D36DEC /* SelfSizingSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingSheet.swift; sourceTree = ""; }; + 97E3D9AE002847AABFDA5537 /* Pods-TodayExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.release.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.release.xcconfig"; sourceTree = ""; }; + 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysView.swift; sourceTree = ""; }; + AA00000028D9A3DB00223B77 /* BuySellPortalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuySellPortalView.swift; sourceTree = ""; }; AA0000032DUMMYID001234567 /* AddProviderToGiftCardsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderToGiftCardsTable.swift; sourceTree = ""; }; - AA0000062DUMMYID001234567 /* DashSpend/GiftCardProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/GiftCardProvider.swift; sourceTree = ""; }; + AA0000062DUMMYID001234567 /* GiftCardProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/GiftCardProvider.swift; sourceTree = ""; }; + AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaPortalView.swift; sourceTree = ""; }; + AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaPortalViewController.swift; sourceTree = ""; }; + AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaCryptoCurrency.swift; sourceTree = ""; }; + AA0002042BA0F58E00A1B302 /* MayaPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaPool.swift; sourceTree = ""; }; + AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaEndpoint.swift; sourceTree = ""; }; + AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaAPIService.swift; sourceTree = ""; }; + AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCoinViewModel.swift; sourceTree = ""; }; + AA0002102BA0F58E00A1B306 /* CoinRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinRowView.swift; sourceTree = ""; }; + AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCoinView.swift; sourceTree = ""; }; + AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCoinHostingController.swift; sourceTree = ""; }; + AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaAddressValidator.swift; sourceTree = ""; }; + AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAddressViewModel.swift; sourceTree = ""; }; + AA0003032CA0F58E00A1B402 /* MayaAddressValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaAddressValidatorTests.swift; sourceTree = ""; }; + AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAddressView.swift; sourceTree = ""; }; + AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAddressHostingController.swift; sourceTree = ""; }; + AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericQRScannerController.swift; sourceTree = ""; }; + AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCaptureView.swift; sourceTree = ""; }; + AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericQRScannerView.swift; sourceTree = ""; }; + AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSourceView.swift; sourceTree = ""; }; + AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaExchangeAddressProvider.swift; sourceTree = ""; }; + AA0020012FA0000000000013 /* SwapExecutionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapExecutionData.swift; sourceTree = ""; }; + AA29A8515217AC4232F794C2 /* Pods-TodayExtension.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.testnet.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.testnet.xcconfig"; sourceTree = ""; }; AD6615F1246B5935365B05BD /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; - AF918A810315A7BE335BFFA6 /* Pods-WatchApp.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.testnet.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.testnet.xcconfig"; sourceTree = ""; }; - B1A681489B9516DC10084A61 /* Pods-DashWalletScreenshotsUITests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.testflight.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.testflight.xcconfig"; sourceTree = ""; }; + AF95E0E0B636AFB810D62116 /* DashBalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashBalanceView.swift; sourceTree = ""; }; + B2F0351CAD174EEB8E64012A /* MayaCoinIconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaCoinIconLoader.swift; sourceTree = ""; }; + B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeySheet.swift; sourceTree = ""; }; + B6C652479D19F2169F565A6C /* libPods-dashpay.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-dashpay.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + B9BF7550EF8D4BA009CB1EB5 /* libPods-WatchApp Extension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WatchApp Extension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BA4A72661BE11AFA00E39C01 /* BRAWWeakTimerTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BRAWWeakTimerTarget.swift; sourceTree = ""; }; BA4EECDF1B351AE200D443A3 /* DWAppGroupConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWAppGroupConstants.h; sourceTree = ""; }; BA54D3CD1B2EA74000C9CB28 /* TodayExtensionAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = TodayExtensionAssets.xcassets; sourceTree = ""; }; @@ -2724,20 +2921,16 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild BAE12BE61B2DEE7F00895CC5 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; BAE12BEA1B2DEE7F00895CC5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BAE12C051B2DEEF700895CC5 /* DWTodayViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWTodayViewController.m; sourceTree = ""; }; - BB0000072DUMMYID001234567 /* DashSpend/DashSpendError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/DashSpendError.swift; sourceTree = ""; }; - BB0000082DUMMYID001234567 /* DashSpend/DashSpendRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/DashSpendRepository.swift; sourceTree = ""; }; - BB0000092DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/DashSpendRepositoryFactory.swift; sourceTree = ""; }; - BC46CEE26088DC2B21D22315 /* Pods-DashWalletScreenshotsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.debug.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.debug.xcconfig"; sourceTree = ""; }; - C09FD41E863456F8BF96B2DB /* Pods-dashwallet.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.testnet.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.testnet.xcconfig"; sourceTree = ""; }; - C1AFBC4E154DF42D982613E4 /* Pods-DashWalletScreenshotsUITests.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletScreenshotsUITests.testnet.xcconfig"; path = "Target Support Files/Pods-DashWalletScreenshotsUITests/Pods-DashWalletScreenshotsUITests.testnet.xcconfig"; sourceTree = ""; }; + BB0000072DUMMYID001234567 /* DashSpendError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/DashSpendError.swift; sourceTree = ""; }; + BB0000082DUMMYID001234567 /* DashSpendRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/DashSpendRepository.swift; sourceTree = ""; }; + BB0000092DUMMYID001234567 /* DashSpendRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/DashSpendRepositoryFactory.swift; sourceTree = ""; }; + C0D1A08496F925D17031493E /* OrderPreviewFeeRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OrderPreviewFeeRow.swift; sourceTree = ""; }; C3DAD266246AA6F10001624F /* DWScreenshotWarningViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWScreenshotWarningViewController.h; sourceTree = ""; }; C3DAD267246AA6F10001624F /* DWScreenshotWarningViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWScreenshotWarningViewController.m; sourceTree = ""; }; C3DAD2CD247585C10001624F /* NSPredicate+DWFullTextSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSPredicate+DWFullTextSearch.h"; sourceTree = ""; }; C3DAD2CE247585C10001624F /* NSPredicate+DWFullTextSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSPredicate+DWFullTextSearch.m"; sourceTree = ""; }; - C56BFBBABD008D06578BDD9C /* libPods-DashWalletTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DashWalletTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; C909614C29EFF7D600002D82 /* WalletKeysOverviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletKeysOverviewModel.swift; sourceTree = ""; }; C909615029F158D700002D82 /* DerivationPathKeysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysViewController.swift; sourceTree = ""; }; - 9EC1F4DB87C442BEABDF0DB2 /* DerivationPathKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysView.swift; sourceTree = ""; }; C909615229F28E3700002D82 /* DerivationPathKeysModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysModel.swift; sourceTree = ""; }; C909615429F297DD00002D82 /* DerivationPathKeysCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivationPathKeysCell.swift; sourceTree = ""; }; C909615829F29C9200002D82 /* KeysOverviewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysOverviewCell.swift; sourceTree = ""; }; @@ -2746,7 +2939,6 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild C917024029D462C6008C034D /* PayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayViewController.swift; sourceTree = ""; }; C91E919629FBACE6003E7883 /* ExtendedPublicKeysModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeysModel.swift; sourceTree = ""; }; C91E91AD29FFC8A1003E7883 /* ExtendedPublicKeysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeysViewController.swift; sourceTree = ""; }; - B3A1F2C4D5E6078901234567 /* ExtendedPublicKeySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPublicKeySheet.swift; sourceTree = ""; }; C943B2BA2A408CEC00AF23C5 /* DWUpdatingUserProfileView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWUpdatingUserProfileView.h; sourceTree = ""; }; C943B2BB2A408CEC00AF23C5 /* DWCurrentUserProfileView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWCurrentUserProfileView.h; sourceTree = ""; }; C943B2BC2A408CEC00AF23C5 /* DWErrorUpdatingUserProfileView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWErrorUpdatingUserProfileView.m; sourceTree = ""; }; @@ -3130,8 +3322,6 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild C94F5E8729D3E7E30034FD57 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; C94F5E8929D3FCCF0034FD57 /* ShortcutAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutAction.swift; sourceTree = ""; }; C94F5E8D29D404850034FD57 /* ShortcutCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutCell.swift; sourceTree = ""; }; - 75B3C1A12E36100100CAFE01 /* ShortcutSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutSelectionView.swift; sourceTree = ""; }; - 75B3C1B12E36100100CAFE04 /* ShortcutCustomizeBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutCustomizeBannerView.swift; sourceTree = ""; }; C94F5E8F29D4060A0034FD57 /* ShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsView.swift; sourceTree = ""; }; C956AF152A5C3301002FAB75 /* DWPressableButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWPressableButton.h; sourceTree = ""; }; C956AF162A5C3302002FAB75 /* DWPressableButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWPressableButton.m; sourceTree = ""; }; @@ -3183,6 +3373,13 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild C9F452002A0CE6C900825057 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; C9F452072A11F28600825057 /* SyncModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncModel.swift; sourceTree = ""; }; C9F4520A2A1209D100825057 /* HomeHeaderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeHeaderModel.swift; sourceTree = ""; }; + D5A0B001D5A0B001D5A0B001 /* DashSpendAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendAmountView.swift; sourceTree = ""; }; + D5A0B002D5A0B002D5A0B002 /* DashSpendDenominationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendDenominationRow.swift; sourceTree = ""; }; + D5A0B003D5A0B003D5A0B003 /* DashSpendFixedContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendFixedContent.swift; sourceTree = ""; }; + D5A0B004D5A0B004D5A0B004 /* DashSpendFlexibleContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendFlexibleContent.swift; sourceTree = ""; }; + D5A0B005D5A0B005D5A0B005 /* DashSpendMultiplePanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendMultiplePanel.swift; sourceTree = ""; }; + D5A0B006D5A0B006D5A0B006 /* DashSpendPayIntro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendPayIntro.swift; sourceTree = ""; }; + D5A0B007D5A0B007D5A0B007 /* DashSpendSinglePanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendSinglePanel.swift; sourceTree = ""; }; C9FAABB42AB793CE00878224 /* BuySellPortal.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BuySellPortal.storyboard; sourceTree = ""; }; C9FAABB62AB799AE00878224 /* UpholdPortalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpholdPortalModel.swift; sourceTree = ""; }; CC0000062DUMMYID001234567 /* PiggyCardsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsAPI.swift; sourceTree = ""; }; @@ -3190,16 +3387,15 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild CC0000082DUMMYID001234567 /* PiggyCardsEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsEndpoint.swift; sourceTree = ""; }; CC0000092DUMMYID001234567 /* PiggyCardsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsRepository.swift; sourceTree = ""; }; CC0000102DUMMYID001234567 /* PiggyCardsTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsTokenService.swift; sourceTree = ""; }; - D18D0D69B7365947239D4F97 /* libPods-TodayExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TodayExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; D53435DCAF024BC82E0247F3 /* ImportPrivateKeySheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImportPrivateKeySheet.swift; sourceTree = ""; }; - D6E23A2DA41FE189E773C705 /* libPods-dashpay.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-dashpay.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - D8BD81810F2B0C11C86E7A5E /* Pods-DashWalletTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.release.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.release.xcconfig"; sourceTree = ""; }; - D96AAF71E3E67F7AD309BCC4 /* Pods-TodayExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.release.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.release.xcconfig"; sourceTree = ""; }; - E57EEDA37189E949FBAAA627 /* Pods-WatchApp Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.release.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.release.xcconfig"; sourceTree = ""; }; - EDE78DB35CE393ADB4F3C5DD /* Pods-TodayExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodayExtension.debug.xcconfig"; path = "Target Support Files/Pods-TodayExtension/Pods-TodayExtension.debug.xcconfig"; sourceTree = ""; }; + D99130732DE8F2E95F635F6B /* MayaConvertCardRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaConvertCardRow.swift; sourceTree = ""; }; + E2A832899A499C24C7A43E47 /* Pods-WatchApp Extension.testnet.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.testnet.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.testnet.xcconfig"; sourceTree = ""; }; + E83D8CEAFF69B6D666C02116 /* Pods-WatchApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp.release.xcconfig"; path = "Target Support Files/Pods-WatchApp/Pods-WatchApp.release.xcconfig"; sourceTree = ""; }; EE0000032DUMMYID001234567 /* POIDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POIDetailsViewModel.swift; sourceTree = ""; }; EE0000052DUMMYID001234567 /* PercentageFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentageFormatter.swift; sourceTree = ""; }; - F3BA4BE42F52588AED15BF7E /* libPods-dashwallet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-dashwallet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + EE15E7C0EEFF1E6930A90DA6 /* Pods-WatchApp Extension.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.testflight.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.testflight.xcconfig"; sourceTree = ""; }; + F203E5FB195609E2B3F255C7 /* Pods-dashwallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dashwallet.release.xcconfig"; path = "Target Support Files/Pods-dashwallet/Pods-dashwallet.release.xcconfig"; sourceTree = ""; }; + F5121ED6CEEEAEDBE95414EE /* Pods-DashWalletTests.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DashWalletTests.testflight.xcconfig"; path = "Target Support Files/Pods-DashWalletTests/Pods-DashWalletTests.testflight.xcconfig"; sourceTree = ""; }; FB0ACB9E1F9AB76800F4AB52 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; FB0ACBA01F9ABB6200F4AB52 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; FB1212A91FFFB0F3000E407E /* dashwalletTodayExtension.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = dashwalletTodayExtension.entitlements; path = DashWallet/dashwalletTodayExtension.entitlements; sourceTree = SOURCE_ROOT; }; @@ -3239,39 +3435,57 @@ AA0001022AA9F58E00A1B201 /* MayaPortalView.swift in Sources */ = {isa = PBXBuild FBF3F42C1E42B00C00C7248E /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; FBF3F42E1E42B01E00C7248E /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; FBF3F4301E42B02800C7248E /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; -AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaPortalView.swift; sourceTree = ""; }; - AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaPortalViewController.swift; sourceTree = ""; }; - AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaCryptoCurrency.swift; sourceTree = ""; }; - AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaAddressValidator.swift; sourceTree = ""; }; - AA0003032CA0F58E00A1B402 /* MayaAddressValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaAddressValidatorTests.swift; sourceTree = ""; }; - AA0002042BA0F58E00A1B302 /* MayaPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaPool.swift; sourceTree = ""; }; - AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaEndpoint.swift; sourceTree = ""; }; - AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaAPIService.swift; sourceTree = ""; }; - AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCoinViewModel.swift; sourceTree = ""; }; - AA0002102BA0F58E00A1B306 /* CoinRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinRowView.swift; sourceTree = ""; }; - AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCoinView.swift; sourceTree = ""; }; - AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCoinHostingController.swift; sourceTree = ""; }; - AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAddressViewModel.swift; sourceTree = ""; }; - AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAddressView.swift; sourceTree = ""; }; - AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterAddressHostingController.swift; sourceTree = ""; }; - AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericQRScannerController.swift; sourceTree = ""; }; - AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCaptureView.swift; sourceTree = ""; }; - AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericQRScannerView.swift; sourceTree = ""; }; - AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSourceView.swift; sourceTree = ""; }; - AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaExchangeAddressProvider.swift; sourceTree = ""; }; - FC016B99DFFB0CBAC3D19290 /* Pods-WatchApp Extension.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchApp Extension.testflight.xcconfig"; path = "Target Support Files/Pods-WatchApp Extension/Pods-WatchApp Extension.testflight.xcconfig"; sourceTree = ""; }; - FF0000042DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/PiggyCardsConstants.swift; sourceTree = ""; }; + FF0000042DUMMYID001234567 /* PiggyCardsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpend/PiggyCardsConstants.swift; sourceTree = ""; }; FF0000062DUMMYID001234567 /* PiggyCardsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiggyCardsModels.swift; sourceTree = ""; }; FF0000092DUMMYID001234567 /* BarcodeScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScanner.swift; sourceTree = ""; }; GG0000032DUMMYID001234567 /* GeoRestrictionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoRestrictionService.swift; sourceTree = ""; }; + RC00A0032FA0000000000001 /* TopIntro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopIntro.swift; sourceTree = ""; }; + RC00A0062FA0000000000001 /* ExploreMenuScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreMenuScreen.swift; sourceTree = ""; }; + RC00C0032FA0000000000001 /* CSVExportSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVExportSheet.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 5151CDF22F929C2300F0A604 /* Geometry Readers */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = "Geometry Readers"; + sourceTree = ""; + }; + 5151CDF62F929D3F00F0A604 /* ScrollViews */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = ScrollViews; + sourceTree = ""; + }; + 51DFA7FB2FA8EAF2001EDB3A /* Components */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Components; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 2A4662FD2279DC2F0027533B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D0B1968E57F8D7B118C22CED /* libPods-DashWalletScreenshotsUITests.a in Frameworks */, + 1AF81D8C73A81CB927C3AE0E /* libPods-DashWalletScreenshotsUITests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3298,8 +3512,9 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 222E7F561C46E9B8009AB45D /* Security.framework in Frameworks */, 222040C61C1A1940005CE1C3 /* WebKit.framework in Frameworks */, 22B6A4481C0E963900673913 /* libbz2.tbd in Frameworks */, - DE3A167A235B79D705C0A962 /* (null) in Frameworks */, - AEAE60406265A73AFD573FD1 /* libPods-dashwallet.a in Frameworks */, + DE3A167A235B79D705C0A962 /* BuildFile in Frameworks */, + AEAE60406265A73AFD573FD1 /* BuildFile in Frameworks */, + 39A9FD561F6418568F3FC109 /* libPods-dashwallet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3307,7 +3522,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3198E8048E48954CD87BAD11 /* libPods-DashWalletTests.a in Frameworks */, + 19BF3F10240A082811AAC1EB /* BuildFile in Frameworks */, + B059B6E200A57A699E6EED84 /* libPods-DashWalletTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3315,7 +3531,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 09F4E16CAEAE41E2C6EA7A27 /* libPods-WatchApp.a in Frameworks */, + F264E9A940EF10070D2F941E /* libPods-WatchApp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3323,7 +3539,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A30AF4A11189A2EFB04EB535 /* libPods-WatchApp Extension.a in Frameworks */, + AA29B7041CF55787428E1A04 /* libPods-WatchApp Extension.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3332,7 +3548,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l buildActionMask = 2147483647; files = ( 754119E01CDA93FF0042DC51 /* NotificationCenter.framework in Frameworks */, - 09A6CB66AEDCEA04C9D670DF /* libPods-TodayExtension.a in Frameworks */, + 7BDE6E52D5F0A428E2858824 /* libPods-TodayExtension.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3359,7 +3575,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C9062A320AA000D15901 /* Security.framework in Frameworks */, C9D2C9072A320AA000D15901 /* WebKit.framework in Frameworks */, C9D2C9082A320AA000D15901 /* libbz2.tbd in Frameworks */, - AE99C52D130994B720927E8F /* libPods-dashpay.a in Frameworks */, + B3D26F853F876FE4F2081CFF /* libPods-dashpay.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4099,7 +4315,6 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 11C5F51128E5D0C500F6F135 /* CrowdNode */, 47A50F362912D9A800C70123 /* Payment Controller */, 4751136A28D9A3BE00223B77 /* Buy Sell */, - 0F6EDFE028C8AE32000427E7 /* Coinbase */, 47AE8BB328C1305E00490F5E /* Explore Dash */, 2AB231CF2196E23200A6E7E6 /* Start */, 2A913E6C23A26469006A2A59 /* Onboarding */, @@ -4121,43 +4336,11 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l path = UI; sourceTree = ""; }; - AA0001042AA9F58E00A1B203 /* Maya */ = { - isa = PBXGroup; - children = ( - AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */, - AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */, - AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */, - AA0002102BA0F58E00A1B306 /* CoinRowView.swift */, - AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */, - AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */, - AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */, - AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */, - AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */, - AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */, - AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */, - AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */, - AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */, - ); - path = Maya; - sourceTree = ""; - }; - AA0002192BA0F58E00A1B309 /* Maya */ = { - isa = PBXGroup; - children = ( - AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */, - AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */, - AA0002042BA0F58E00A1B302 /* MayaPool.swift */, - AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */, - AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */, - AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */, - ); - path = Maya; - sourceTree = ""; - }; 2A44313D22CF632F009BAF7F /* Models */ = { isa = PBXGroup; children = ( AA0002192BA0F58E00A1B309 /* Maya */, + AA0020012FA0000000000001 /* Swap */, 7503643C2C89D4890029EC0D /* CoinJoin */, 75A8C1612AE571E30042256E /* Voting */, 47081194298CF1E3003FCA3D /* Transactions */, @@ -4382,6 +4565,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2A10EB322358994400C38B61 /* ImportWallet */, FB3FF0B0222641210059A9A5 /* Masternode Keys */, 7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */, + RC00C0032FA0000000000001 /* CSVExportSheet.swift */, 752F81A82E323FD000ADA76D /* ToolsMenuViewModel.swift */, ); path = Tools; @@ -4390,6 +4574,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2A7A7C13234B761700451078 /* LocalCurrency */ = { isa = PBXGroup; children = ( + 5151CA212F92335E00F0A604 /* LocalCurrencyView.swift */, + 5151CA222F92335E00F0A604 /* LocalCurrencyViewModel.swift */, 2A7A7C17234B764C00451078 /* Cells */, 2A7A7C14234B763600451078 /* DWLocalCurrencyViewController.h */, 2A7A7C15234B763600451078 /* DWLocalCurrencyViewController.m */, @@ -4399,6 +4585,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2AA87CF926E5681100F0CEA6 /* DWCurrencyObject.m */, 2ACD53E6234BADD300650AD3 /* DWCurrencyItem.h */, 2AA87CFB26E568A100F0CEA6 /* DWCurrencyItemPriceProvider.h */, + 5151CE172F9624D000F0A604 /* CurrencyItem.swift */, ); path = LocalCurrency; sourceTree = ""; @@ -4408,6 +4595,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l children = ( 2A7A7C1E234B79B700451078 /* DWLocalCurrencyTableViewCell.h */, 2A7A7C1F234B79B700451078 /* DWLocalCurrencyTableViewCell.m */, + 5151CDEF2F928D7D00F0A604 /* LocalCurrencyCellView.swift */, ); path = Cells; sourceTree = ""; @@ -5261,6 +5449,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47081195298CF1FE003FCA3D /* Model */, 472D13E2299E23B7006903F1 /* BalanceNotifier.swift */, 11E47BAA28EB38510097CFA0 /* SendCoinsService.swift */, + 09950CBE38F14286AC06BB78 /* DSAccount+SpentInputCheck.h */, + 7BC16BFBFFFA4E488C09164D /* DSAccount+SpentInputCheck.m */, ); path = Transactions; sourceTree = ""; @@ -5390,6 +5580,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 478A2C6F28DC551E00AD1420 /* Model */, 4751136B28D9A3DB00223B77 /* BuySellPortalViewController.swift */, AA00000028D9A3DB00223B77 /* BuySellPortalView.swift */, + 7B94B3697D7742BFA991B5CC /* BuySellPortalScreen.swift */, C9FAABB42AB793CE00878224 /* BuySellPortal.storyboard */, 47838B86290670630003E8AB /* IntegrationViewController.swift */, ); @@ -5770,9 +5961,9 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47AE8B9628BFACE800490F5E /* Model */ = { isa = PBXGroup; children = ( - 7545ED522DA91A2F0075F45C /* DashSpend/CTXConstants.swift */, - AA0000062DUMMYID001234567 /* DashSpend/GiftCardProvider.swift */, - FF0000042DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift */, + 7545ED522DA91A2F0075F45C /* CTXConstants.swift */, + AA0000062DUMMYID001234567 /* GiftCardProvider.swift */, + FF0000042DUMMYID001234567 /* PiggyCardsConstants.swift */, 47AE8B9A28BFAD2000490F5E /* Entites */, 47AE8B9928BFAD0B00490F5E /* DAO */, ); @@ -5792,15 +5983,15 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXGroup; children = ( FF0000092DUMMYID001234567 /* BarcodeScanner.swift */, - 75A2F3022DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift */, - 754565C42DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift */, - 754565C52DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift */, - 754565C72DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift */, - BB0000072DUMMYID001234567 /* DashSpend/DashSpendError.swift */, - BB0000082DUMMYID001234567 /* DashSpend/DashSpendRepository.swift */, - BB0000092DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift */, + 75A2F3022DE48C860046BE17 /* CTXSpendTokenService.swift */, + 754565C42DAA52A000DA4E8E /* CTXSpendAPI.swift */, + 754565C52DAA52A000DA4E8E /* CTXSpendEndpoint.swift */, + 754565C72DAA52A000DA4E8E /* CTXSpendRepository.swift */, + BB0000072DUMMYID001234567 /* DashSpendError.swift */, + BB0000082DUMMYID001234567 /* DashSpendRepository.swift */, + BB0000092DUMMYID001234567 /* DashSpendRepositoryFactory.swift */, GG0000032DUMMYID001234567 /* GeoRestrictionService.swift */, - DD0000012DUMMYID001234567 /* DashSpend/PiggyCards */, + DD0000012DUMMYID001234567 /* PiggyCards */, 47AE8BAC28BFAE6700490F5E /* DWLocationManager.swift */, 47AE8BA828BFAE5800490F5E /* ExploreDatabaseSyncManager.swift */, ); @@ -5858,6 +6049,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47AE8BB428C1305E00490F5E /* Merchants & ATMs */, 47AE8BDF28C1305E00490F5E /* Views */, 7569D6822DE6DA6800768BFF /* ExploreViewController.swift */, + RC00A0062FA0000000000001 /* ExploreMenuScreen.swift */, ); path = "Explore Dash"; sourceTree = ""; @@ -6155,6 +6347,100 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l path = Networking; sourceTree = ""; }; + 514AE9F12FB2F48F00F9B54C /* Components */ = { + isa = PBXGroup; + children = ( + AA0002102BA0F58E00A1B306 /* CoinRowView.swift */, + 514AE9F22FB2F4C500F9B54C /* HaltedToast.swift */, + 20554F992205428BA790F6CF /* MayaCoinIconView.swift */, + ); + path = Components; + sourceTree = ""; + }; + 514AEA0D2FB31BBC00F9B54C /* Components */ = { + isa = PBXGroup; + children = ( + AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */, + ); + path = Components; + sourceTree = ""; + }; + 514AEA112FB324DA00F9B54C /* Convert */ = { + isa = PBXGroup; + children = ( + 514AEA152FB3258F00F9B54C /* Components */, + 514AEA122FB324FA00F9B54C /* MayaConvertView.swift */, + 514AEA4A2FB33B4300F9B54C /* MayaConvertHostingController.swift */, + 514AF1BF2FB5F02D00F9B54C /* MayaConvertViewModel.swift */, + 514BEA002FB5F02D00F9B54C /* MayaConvertAmount.swift */, + ); + path = Convert; + sourceTree = ""; + }; + 514AEA152FB3258F00F9B54C /* Components */ = { + isa = PBXGroup; + children = ( + AF95E0E0B636AFB810D62116 /* DashBalanceView.swift */, + D99130732DE8F2E95F635F6B /* MayaConvertCardRow.swift */, + 514AEA432FB33B4300F9B54C /* EnterAmountView.swift */, + 514AEA472FB33B4300F9B54C /* DashPickerView.swift */, + 514AEA4D2FB33B4300F9B54C /* MayaAmountView.swift */, + ); + path = Components; + sourceTree = ""; + }; + 514AEA162FB3291A00F9B54C /* OrderPreview */ = { + isa = PBXGroup; + children = ( + 51A5774D2FBB3A7100FB4097 /* Components */, + 514AEAD42FB34F8600F9B54C /* OrderPreviewView.swift */, + 51A577472FBB3A6000FB4097 /* OrderPreviewViewModel.swift */, + 51A5774A2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift */, + ); + path = OrderPreview; + sourceTree = ""; + }; + 516983A12FCAEDD600BA91A2 /* Illustrations */ = { + isa = PBXGroup; + children = ( + 516983A22FCAEDEB00BA91A2 /* SuccessIllustration.swift */, + 516983B02FCAEDEB00BA91A2 /* LoadingIllustration.swift */, + 516983A52FCAEF1300BA91A2 /* ErrorIllustration.swift */, + ); + path = Illustrations; + sourceTree = ""; + }; + 516983C02FCAEDD600BA91A2 /* Icons */ = { + isa = PBXGroup; + children = ( + 51F0D10330BF98BA00B27E11 /* ArrowDownIcon.swift */, + 51F0D10030BF97A900B27E11 /* ExclamationIcon.swift */, + 516983C12FCAEDEB00BA91A2 /* XmarkIcon.swift */, + ); + path = Icons; + sourceTree = ""; + }; + 51A5774D2FBB3A7100FB4097 /* Components */ = { + isa = PBXGroup; + children = ( + C0D1A08496F925D17031493E /* OrderPreviewFeeRow.swift */, + 51A57B012FBB41DE00FB4097 /* OrderPreviewTableRow.swift */, + 51A57B052FBB41DE00FB4097 /* MayaFeeInfoSheet.swift */, + ); + path = Components; + sourceTree = ""; + }; + 51A57C012FBB5A0000FB4097 /* TransactionStatus */ = { + isa = PBXGroup; + children = ( + 51A57C022FBB5A0000FB4097 /* MayaTransactionSuccessView.swift */, + 51A57C032FBB5A0000FB4097 /* MayaTransactionFailureView.swift */, + 52B5469F5588395FA0668C4B /* MayaTransactionStatusHostingController.swift */, + 51A57C082FBB5A0000FB4097 /* MayaTransactionPendingView.swift */, + ); + path = TransactionStatus; + sourceTree = ""; + }; 7502A4852AE401DA00ACDDD3 /* Voting */ = { isa = PBXGroup; children = ( @@ -6209,8 +6495,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 7545ED552DA91AC80075F45C /* DashSpend */ = { isa = PBXGroup; children = ( - 75D9EBC42DE5CE14009416A2 /* GiftCardDetailsView.swift */, - 75D9EBC52DE5CE14009416A2 /* GiftCardDetailsViewModel.swift */, + D5A0D000D5A0D000D5A0D000 /* Components */, + D6C0D000D6C0D000D6C0D000 /* GiftCardDetails */, 753F75332DD0D41900D40DFE /* DashSpendPayScreen.swift */, 75EE9F412DE2F74B000AD1AD /* DashSpendConfirmationDialog.swift */, 753F75362DD0D75F00D40DFE /* DashSpendPayViewModel.swift */, @@ -6222,29 +6508,71 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l path = DashSpend; sourceTree = ""; }; - 7549BE552DEAECA5004F0BAF /* Tx Metadata */ = { + D5A0D000D5A0D000D5A0D000 /* Components */ = { isa = PBXGroup; children = ( - 75050CCD2DF2CB0E00F586D6 /* GiftCardMetadataProvider.swift */, - 7549BE562DEAECB0004F0BAF /* CustomIconMetadataProvider.swift */, + D5A0B001D5A0B001D5A0B001 /* DashSpendAmountView.swift */, + D5A0B002D5A0B002D5A0B002 /* DashSpendDenominationRow.swift */, + D5A0B003D5A0B003D5A0B003 /* DashSpendFixedContent.swift */, + D5A0B004D5A0B004D5A0B004 /* DashSpendFlexibleContent.swift */, + D5A0B005D5A0B005D5A0B005 /* DashSpendMultiplePanel.swift */, + D5A0B006D5A0B006D5A0B006 /* DashSpendPayIntro.swift */, + D5A0B007D5A0B007D5A0B007 /* DashSpendSinglePanel.swift */, ); - path = "Tx Metadata"; + path = Components; sourceTree = ""; }; - 754C27C62CC3C0B900BA7B9F /* Usernames */ = { + D6C0D000D6C0D000D6C0D000 /* GiftCardDetails */ = { isa = PBXGroup; children = ( - 751C05D82D3BB6A200475E52 /* CurrentUserProfileModel.swift */, - 754C27C52CC3C0B900BA7B9F /* UsernamePrefs.swift */, + 75D9EBC42DE5CE14009416A2 /* GiftCardDetailsView.swift */, + 75D9EBC52DE5CE14009416A2 /* GiftCardDetailsViewModel.swift */, + D6C0D001D6C0D001D6C0D001 /* Components */, ); - path = Usernames; + path = GiftCardDetails; sourceTree = ""; }; - 7566F4882BB6CAD6005238D2 /* SwiftUI Components */ = { + D6C0D001D6C0D001D6C0D001 /* Components */ = { isa = PBXGroup; children = ( - 7581B19C2CE3509A00714007 /* Dialogs */, - 75EBAA112BB99B6B004488E3 /* BottomSheet.swift */, + D6C0B001D6C0B001D6C0B001 /* GiftCardDetailsInfoCard.swift */, + D6C0B002D6C0B002D6C0B002 /* GiftCardDetailsMerchantHeader.swift */, + D6C0B003D6C0B003D6C0B003 /* GiftCardPurchaseSelectionSheet.swift */, + D6C0B004D6C0B004D6C0B004 /* GiftCardDetailsPoweredBySection.swift */, + D6C0B005D6C0B005D6C0B005 /* GiftCardDetailsHowToUseSection.swift */, + ); + path = Components; + sourceTree = ""; + }; + 7549BE552DEAECA5004F0BAF /* Tx Metadata */ = { + isa = PBXGroup; + children = ( + 75050CCD2DF2CB0E00F586D6 /* GiftCardMetadataProvider.swift */, + 7549BE562DEAECB0004F0BAF /* CustomIconMetadataProvider.swift */, + ); + path = "Tx Metadata"; + sourceTree = ""; + }; + 754C27C62CC3C0B900BA7B9F /* Usernames */ = { + isa = PBXGroup; + children = ( + 751C05D82D3BB6A200475E52 /* CurrentUserProfileModel.swift */, + 754C27C52CC3C0B900BA7B9F /* UsernamePrefs.swift */, + ); + path = Usernames; + sourceTree = ""; + }; + 7566F4882BB6CAD6005238D2 /* SwiftUI Components */ = { + isa = PBXGroup; + children = ( + 516983A12FCAEDD600BA91A2 /* Illustrations */, + 516983C02FCAEDD600BA91A2 /* Icons */, + 514AEA0E2FB31BD800F9B54C /* AddressFieldView.swift */, + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + 5151CDF22F929C2300F0A604 /* Geometry Readers */, + 7581B19C2CE3509A00714007 /* Dialogs */, + 75EBAA112BB99B6B004488E3 /* BottomSheet.swift */, + 96CB7E093E994D2391D36DEC /* SelfSizingSheet.swift */, AD6615F1246B5935365B05BD /* NavigationBar.swift */, 759ADD562BF3447400767ACD /* Button.swift */, 75CDD7852C08D61300F433D2 /* DashAmount.swift */, @@ -6254,6 +6582,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 75EBAA0B2BB9792F004488E3 /* FeatureTopText.swift */, 754C27CB2CC3C14F00BA7B9F /* FeatureSingleItem.swift */, 75EBAA0E2BB99036004488E3 /* TextIntro.swift */, + RC00A0032FA0000000000001 /* TopIntro.swift */, 758C4C4E2DFAC1C30028549B /* RadioButtonRow.swift */, 75BDE7AB2BF3287400556791 /* Toast.swift */, 75AA33CE2BF9D44A00F12465 /* ButtonsGroup.swift */, @@ -6266,6 +6595,11 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 75387B492E0400C300BCCC72 /* MerchantDenominations.swift */, 7545ED5F2DA91FC60075F45C /* NumericKeyboardView.swift */, 756FE7092CDCD6CD00E6C195 /* ValidationCheck.swift */, + 5151CD612F926D1900F0A604 /* SearchBar.swift */, + 75DFA10175DFA10175DFA101 /* DashStepper.swift */, + 75DFA10275DFA10275DFA102 /* SegmentedControl.swift */, + 487C4C2E6AC61005536FF85E /* NetworkUnavailableStateView.swift */, + 514AE85D2FB1F9DB00F9B54C /* DashSwitch.swift */, ); path = "SwiftUI Components"; sourceTree = ""; @@ -6431,13 +6765,13 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 22B6A4471C0E963900673913 /* libbz2.tbd */, 75F2E0B61BE2D5F000EAE861 /* Accelerate.framework */, BAE12BE61B2DEE7F00895CC5 /* NotificationCenter.framework */, - 3C20BCC772DF4075F8EADAE0 /* libPods-DashWalletScreenshotsUITests.a */, - C56BFBBABD008D06578BDD9C /* libPods-DashWalletTests.a */, - D18D0D69B7365947239D4F97 /* libPods-TodayExtension.a */, - 38C4CA9D73A147BA44601741 /* libPods-WatchApp.a */, - 4C22BC09CF6178544B2300A4 /* libPods-WatchApp Extension.a */, - D6E23A2DA41FE189E773C705 /* libPods-dashpay.a */, - F3BA4BE42F52588AED15BF7E /* libPods-dashwallet.a */, + B9BF7550EF8D4BA009CB1EB5 /* libPods-WatchApp Extension.a */, + B6C652479D19F2169F565A6C /* libPods-dashpay.a */, + 7B56B94951BA51EAC9E0A6E1 /* libPods-DashWalletScreenshotsUITests.a */, + 1940CA64E8FD6DDD0CFC14A2 /* libPods-DashWalletTests.a */, + 0755F840BCC1E0754D807B04 /* libPods-TodayExtension.a */, + 3DC11A0747AD00DA7524204A /* libPods-WatchApp.a */, + 797D9070BF54A3533190584E /* libPods-dashwallet.a */, ); name = Frameworks; sourceTree = ""; @@ -6445,6 +6779,10 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 75D5F3C7191EC270004AB296 /* DashWallet */ = { isa = PBXGroup; children = ( + 5151C8C22F913BF100F0A604 /* Coinbase-Info.plist */, + 5151C8C32F913BF100F0A604 /* GoogleService-Info.plist */, + 5151C8C42F913BF100F0A604 /* Topper-Info.plist */, + 5151C8C52F913BF100F0A604 /* ZenLedger-Info.plist */, 2A44313B22CF62FC009BAF7F /* Sources */, 2A4430F022CBD566009BAF7F /* Resources */, FB6DD3811F7FA48500BC1E4D /* dashwallet.entitlements */, @@ -6521,38 +6859,117 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 8AE9CD5705796B04FF0033B6 /* Pods */ = { isa = PBXGroup; children = ( - BC46CEE26088DC2B21D22315 /* Pods-DashWalletScreenshotsUITests.debug.xcconfig */, - C1AFBC4E154DF42D982613E4 /* Pods-DashWalletScreenshotsUITests.testnet.xcconfig */, - 718A7E2BCD93948084E43A8F /* Pods-DashWalletScreenshotsUITests.release.xcconfig */, - B1A681489B9516DC10084A61 /* Pods-DashWalletScreenshotsUITests.testflight.xcconfig */, - 77215C3978EE089C06A41979 /* Pods-DashWalletTests.debug.xcconfig */, - 281D63D6AC9610BCF4765D07 /* Pods-DashWalletTests.testnet.xcconfig */, - D8BD81810F2B0C11C86E7A5E /* Pods-DashWalletTests.release.xcconfig */, - 62C1F7A3ABEE7CF3BBB270C4 /* Pods-DashWalletTests.testflight.xcconfig */, - EDE78DB35CE393ADB4F3C5DD /* Pods-TodayExtension.debug.xcconfig */, - 74F4F603C555A521F738DC9E /* Pods-TodayExtension.testnet.xcconfig */, - D96AAF71E3E67F7AD309BCC4 /* Pods-TodayExtension.release.xcconfig */, - 6A44C167AF8F881176AFB256 /* Pods-TodayExtension.testflight.xcconfig */, - 0995737D764092DF77CB88AC /* Pods-WatchApp.debug.xcconfig */, - AF918A810315A7BE335BFFA6 /* Pods-WatchApp.testnet.xcconfig */, - 4944819B9E17D4CC7DBFF293 /* Pods-WatchApp.release.xcconfig */, - 04D182469170A3A2843F51CA /* Pods-WatchApp.testflight.xcconfig */, - 2E17397DF35152CAFBB79068 /* Pods-WatchApp Extension.debug.xcconfig */, - 4D37A3F73905BC239A9D127D /* Pods-WatchApp Extension.testnet.xcconfig */, - E57EEDA37189E949FBAAA627 /* Pods-WatchApp Extension.release.xcconfig */, - FC016B99DFFB0CBAC3D19290 /* Pods-WatchApp Extension.testflight.xcconfig */, - A47B6DF71195E41A5B7B5F99 /* Pods-dashpay.debug.xcconfig */, - 67217DE34817FB304EDEA8D4 /* Pods-dashpay.testnet.xcconfig */, - 28AA9F2D282325F800A4F3AA /* Pods-dashpay.release.xcconfig */, - 8391BA0D088FF516D63DE9A1 /* Pods-dashpay.testflight.xcconfig */, - 86CC430C535DAEBF900E5914 /* Pods-dashwallet.debug.xcconfig */, - C09FD41E863456F8BF96B2DB /* Pods-dashwallet.testnet.xcconfig */, - 1C3070D5E872E975F8473A68 /* Pods-dashwallet.release.xcconfig */, - 6DC4B39C45C824674E05DB22 /* Pods-dashwallet.testflight.xcconfig */, + 3B889D25725F4A7EBFD60324 /* Pods-DashWalletScreenshotsUITests.debug.xcconfig */, + 41D6F206BA803FDFA21B31F5 /* Pods-DashWalletTests.debug.xcconfig */, + F5121ED6CEEEAEDBE95414EE /* Pods-DashWalletTests.testflight.xcconfig */, + 393F2EE6876F067BC90A103C /* Pods-TodayExtension.testflight.xcconfig */, + 351EC86529F9D2F4DC5CCE1F /* Pods-WatchApp.testflight.xcconfig */, + 3BA20785E162F1AF2F455D9C /* Pods-WatchApp Extension.debug.xcconfig */, + 4457C976B1DB0CF402FF3C5F /* Pods-WatchApp Extension.release.xcconfig */, + 34B8A8461A3EDA41E40156CE /* Pods-dashpay.testnet.xcconfig */, + 726054552E79221F6B9D97AE /* Pods-dashwallet.debug.xcconfig */, + 2537FCF252FB799EB4103150 /* Pods-dashwallet.testnet.xcconfig */, + F203E5FB195609E2B3F255C7 /* Pods-dashwallet.release.xcconfig */, + 516C4047DB43CFFBB8ED541B /* Pods-DashWalletScreenshotsUITests.testnet.xcconfig */, + 180C7A2E60359C7F8FB7B918 /* Pods-DashWalletScreenshotsUITests.release.xcconfig */, + 226154FFBC684D1FDFCA35E4 /* Pods-DashWalletScreenshotsUITests.testflight.xcconfig */, + 5CBC0F417B9E8DE631F9DE2A /* Pods-DashWalletTests.testnet.xcconfig */, + 1AF20341A5E313C9D309BF39 /* Pods-DashWalletTests.release.xcconfig */, + 3B0E86557754B86DD3A389CC /* Pods-TodayExtension.debug.xcconfig */, + AA29A8515217AC4232F794C2 /* Pods-TodayExtension.testnet.xcconfig */, + 97E3D9AE002847AABFDA5537 /* Pods-TodayExtension.release.xcconfig */, + 0CE6B29178211061196A0936 /* Pods-WatchApp.debug.xcconfig */, + 38ED8068CF6060EE8343E195 /* Pods-WatchApp.testnet.xcconfig */, + E83D8CEAFF69B6D666C02116 /* Pods-WatchApp.release.xcconfig */, + E2A832899A499C24C7A43E47 /* Pods-WatchApp Extension.testnet.xcconfig */, + EE15E7C0EEFF1E6930A90DA6 /* Pods-WatchApp Extension.testflight.xcconfig */, + 62613C0D937CA301FBBF2356 /* Pods-dashpay.debug.xcconfig */, + 7513056584D6F9A1B5E8E79B /* Pods-dashpay.release.xcconfig */, + 8571065059CF5168F45C86E4 /* Pods-dashpay.testflight.xcconfig */, + 1E7370138411E3E74B42F4CC /* Pods-dashwallet.testflight.xcconfig */, ); path = Pods; sourceTree = ""; }; + AA0001042AA9F58E00A1B203 /* Maya */ = { + isa = PBXGroup; + children = ( + 0286D4D1CADBBD51F09BE8E8 /* MayaMenuStyles.swift */, + 514AEA162FB3291A00F9B54C /* OrderPreview */, + 514AEA112FB324DA00F9B54C /* Convert */, + AA0005012EA0F58E00D1D601 /* MayaPortal */, + AA0005022EA0F58E00D1D602 /* SelectCoin */, + AA0005032EA0F58E00D1D603 /* EnterAddress */, + AA0005042EA0F58E00D1D604 /* QRScanner */, + 51A57C012FBB5A0000FB4097 /* TransactionStatus */, + ); + path = Maya; + sourceTree = ""; + }; + AA0002192BA0F58E00A1B309 /* Maya */ = { + isa = PBXGroup; + children = ( + AA0003002CA0F58E00A1B401 /* MayaAddressValidator.swift */, + AA0002012BA0F58E00A1B301 /* MayaCryptoCurrency.swift */, + B2F0351CAD174EEB8E64012A /* MayaCoinIconLoader.swift */, + AA0002042BA0F58E00A1B302 /* MayaPool.swift */, + 7708BBFDD14AFF237FB72D71 /* MayaConstants.swift */, + AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */, + AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */, + AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */, + ); + path = Maya; + sourceTree = ""; + }; + AA0005012EA0F58E00D1D601 /* MayaPortal */ = { + isa = PBXGroup; + children = ( + AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */, + AA0001042AA9F58E00A1B202 /* MayaPortalViewController.swift */, + ); + path = MayaPortal; + sourceTree = ""; + }; + AA0005022EA0F58E00D1D602 /* SelectCoin */ = { + isa = PBXGroup; + children = ( + 514AE9F12FB2F48F00F9B54C /* Components */, + AA0002132BA0F58E00A1B307 /* SelectCoinView.swift */, + AA00020D2BA0F58E00A1B305 /* SelectCoinViewModel.swift */, + AA0002162BA0F58E00A1B308 /* SelectCoinHostingController.swift */, + ); + path = SelectCoin; + sourceTree = ""; + }; + AA0005032EA0F58E00D1D603 /* EnterAddress */ = { + isa = PBXGroup; + children = ( + 514AEA0D2FB31BBC00F9B54C /* Components */, + AA0003042CA0F58E00B1B402 /* EnterAddressView.swift */, + AA0003012CA0F58E00B1B401 /* EnterAddressViewModel.swift */, + AA0003072CA0F58E00B1B403 /* EnterAddressHostingController.swift */, + ); + path = EnterAddress; + sourceTree = ""; + }; + AA0005042EA0F58E00D1D604 /* QRScanner */ = { + isa = PBXGroup; + children = ( + AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */, + AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */, + AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */, + ); + path = QRScanner; + sourceTree = ""; + }; + AA0020012FA0000000000001 /* Swap */ = { + isa = PBXGroup; + children = ( + AA0020012FA0000000000013 /* SwapExecutionData.swift */, + ); + path = Swap; + sourceTree = ""; + }; BA913BDF1BD57E4D005A7C0E /* WatchApp */ = { isa = PBXGroup; children = ( @@ -7778,7 +8195,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l path = "Sync View"; sourceTree = ""; }; - DD0000012DUMMYID001234567 /* DashSpend/PiggyCards */ = { + DD0000012DUMMYID001234567 /* PiggyCards */ = { isa = PBXGroup; children = ( CC0000062DUMMYID001234567 /* PiggyCardsAPI.swift */, @@ -7806,7 +8223,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXNativeTarget; buildConfigurationList = 2A46630B2279DC2F0027533B /* Build configuration list for PBXNativeTarget "DashWalletScreenshotsUITests" */; buildPhases = ( - 910A25650B9BB7C3EDEB829B /* [CP] Check Pods Manifest.lock */, + 47303C478666FDFF3EFBBE40 /* [CP] Check Pods Manifest.lock */, 2ADC722623B4071700D9DD37 /* Run Script Swift Format */, 2A4662FC2279DC2F0027533B /* Sources */, 2A4662FD2279DC2F0027533B /* Frameworks */, @@ -7826,7 +8243,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXNativeTarget; buildConfigurationList = 75D5F3F6191EC270004AB296 /* Build configuration list for PBXNativeTarget "dashwallet" */; buildPhases = ( - 08F6DAB00DF73D2DA860DE1D /* [CP] Check Pods Manifest.lock */, + 28DD713340F825108DBF3E6C /* [CP] Check Pods Manifest.lock */, 2AF134F821DE95FC004B9859 /* Run Script - Uphold Constants */, 2A4431D722D6391F009BAF7F /* Run Script - clang-format */, FB0ACB9A1F9A99E400F4AB52 /* Run Script - bartycrouch */, @@ -7836,7 +8253,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l BAE12BF71B2DEE7F00895CC5 /* Embed Foundation Extensions */, BA913C021BD57E4E005A7C0E /* Embed Watch Content */, 2A1AF9FD23C86A4400442AF5 /* Run Script - Cleanup Uphold Constants */, - 345EF76B976730DE3012526D /* [CP] Copy Pods Resources */, + 622C18F9EEFCADF772D1DBB5 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -7844,6 +8261,11 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l BAE12BF11B2DEE7F00895CC5 /* PBXTargetDependency */, BA913BF91BD57E4D005A7C0E /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 5151CDF22F929C2300F0A604 /* Geometry Readers */, + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + 51DFA7FB2FA8EAF2001EDB3A /* Components */, + ); name = dashwallet; productName = DashWallet; productReference = 75D5F3BE191EC270004AB296 /* dashwallet.app */; @@ -7853,7 +8275,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXNativeTarget; buildConfigurationList = 75D5F3F9191EC270004AB296 /* Build configuration list for PBXNativeTarget "DashWalletTests" */; buildPhases = ( - 7C9E00747676DAE0ABAA0C52 /* [CP] Check Pods Manifest.lock */, + A5BD49B2A59A7ED61D52013B /* [CP] Check Pods Manifest.lock */, 75D5F3E1191EC270004AB296 /* Sources */, 75D5F3E2191EC270004AB296 /* Frameworks */, 75D5F3E3191EC270004AB296 /* Resources */, @@ -7872,7 +8294,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXNativeTarget; buildConfigurationList = BA913C011BD57E4E005A7C0E /* Build configuration list for PBXNativeTarget "WatchApp" */; buildPhases = ( - 6761AFE7CA3EDFF4FF268B23 /* [CP] Check Pods Manifest.lock */, + A00464195619E3C43C141D1A /* [CP] Check Pods Manifest.lock */, BA913BDC1BD57E4D005A7C0E /* Resources */, BA913C001BD57E4E005A7C0E /* Embed Foundation Extensions */, B361A57803894CC7C6E59498 /* Frameworks */, @@ -7891,7 +8313,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXNativeTarget; buildConfigurationList = BA913BFF1BD57E4E005A7C0E /* Build configuration list for PBXNativeTarget "WatchApp Extension" */; buildPhases = ( - 7E9BC050F05F77EC2D12DEAD /* [CP] Check Pods Manifest.lock */, + 4E1E4D28B3D968C9E5392A41 /* [CP] Check Pods Manifest.lock */, 2A858A15237EED350097A7B5 /* Swift Format */, 2A858A16237EED5D0097A7B5 /* Swift Lint */, BA913BE61BD57E4D005A7C0E /* Sources */, @@ -7911,7 +8333,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXNativeTarget; buildConfigurationList = BAE12BF61B2DEE7F00895CC5 /* Build configuration list for PBXNativeTarget "TodayExtension" */; buildPhases = ( - 81600197311AC4FC9835F1D9 /* [CP] Check Pods Manifest.lock */, + 3235F5C725A891BC8E502835 /* [CP] Check Pods Manifest.lock */, BAE12BE11B2DEE7F00895CC5 /* Sources */, BAE12BE21B2DEE7F00895CC5 /* Frameworks */, BAE12BE31B2DEE7F00895CC5 /* Resources */, @@ -7929,7 +8351,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l isa = PBXNativeTarget; buildConfigurationList = C9D2C94E2A320AA000D15901 /* Build configuration list for PBXNativeTarget "dashpay" */; buildPhases = ( - E89FBBEF1ECEF3BD173AC8EB /* [CP] Check Pods Manifest.lock */, + 1F064526D281DEF5E5CC2729 /* [CP] Check Pods Manifest.lock */, C9D2C6902A320AA000D15901 /* Run Script - Uphold Constants */, C9D2C6912A320AA000D15901 /* Run Script - clang-format */, C9D2C6922A320AA000D15901 /* Run Script - bartycrouch */, @@ -7937,12 +8359,17 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C8F52A320AA000D15901 /* Frameworks */, C9D2C90A2A320AA000D15901 /* Resources */, C9D2C94D2A320AA000D15901 /* Run Script - Cleanup Uphold Constants */, - E5B46042D96D592661436332 /* [CP] Copy Pods Resources */, + 525BB766C4C0A1764D4BD70A /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 5151CDF22F929C2300F0A604 /* Geometry Readers */, + 5151CDF62F929D3F00F0A604 /* ScrollViews */, + 51DFA7FB2FA8EAF2001EDB3A /* Components */, + ); name = dashpay; productName = DashWallet; productReference = C9D2C9532A320AA000D15901 /* dashpay.app */; @@ -8083,6 +8510,10 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 4709C30F287E787700B4BD48 /* Migrations.bundle in Resources */, 474C720F298A1A3E00475CA6 /* TxDetailTaxCategoryCell.xib in Resources */, 2ADC722923B5547000D9DD37 /* Localizable.stringsdict in Resources */, + 5151C8CA2F913BF100F0A604 /* Coinbase-Info.plist in Resources */, + 5151C8CB2F913BF100F0A604 /* ZenLedger-Info.plist in Resources */, + 5151C8CC2F913BF100F0A604 /* GoogleService-Info.plist in Resources */, + 5151C8CD2F913BF100F0A604 /* Topper-Info.plist in Resources */, 75E2F3C82AA4CF1900C3B458 /* Topper-Info.plist in Resources */, 753CD8792E2E218800BCF070 /* mixing_anim.json in Resources */, 2A2CD71822F99CAE008C7BC9 /* ShortcutsView.xib in Resources */, @@ -8194,8 +8625,12 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C9272A320AA000D15901 /* SyncView.xib in Resources */, C9D2C9292A320AA000D15901 /* BackupInfoItemView.xib in Resources */, C9D2C92A2A320AA000D15901 /* explore.db in Resources */, - C9D2C92B2A320AA000D15901 /* (null) in Resources */, + C9D2C92B2A320AA000D15901 /* BuildFile in Resources */, C9D2C92C2A320AA000D15901 /* Coinbase.storyboard in Resources */, + 5151C8C62F913BF100F0A604 /* Coinbase-Info.plist in Resources */, + 5151C8C72F913BF100F0A604 /* ZenLedger-Info.plist in Resources */, + 5151C8C82F913BF100F0A604 /* GoogleService-Info.plist in Resources */, + 5151C8C92F913BF100F0A604 /* Topper-Info.plist in Resources */, 75889B892AD2DF0200C17F5D /* CoinJoin.storyboard in Resources */, C9D2C92E2A320AA000D15901 /* ImportWalletInfo.storyboard in Resources */, C9D2C92F2A320AA000D15901 /* ReceiveContentView.xib in Resources */, @@ -8230,7 +8665,29 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 08F6DAB00DF73D2DA860DE1D /* [CP] Check Pods Manifest.lock */ = { + 1F064526D281DEF5E5CC2729 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-dashpay-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 28DD713340F825108DBF3E6C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -8361,45 +8818,29 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l shellPath = /bin/sh; shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ] ; then\n if [ ! -f \"DWUpholdMainnetConstants__Release.m\" ]; then\n echo \"error: DWUpholdMainnetConstants__Release.m file not found!\"\n exit 1\n fi\n\n cp \"DWUpholdMainnetConstants__Release.m\" \"DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m\"\nelse\n # check if original file exist (in case it was moved or deleted while refactoring)\n if [ ! -f \"DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m\" ]; then\n echo \"error: DWUpholdMainnetConstants.m file not found! Fix Uphold Constants build phase\"\n exit 1\n fi\nfi\n"; }; - 345EF76B976730DE3012526D /* [CP] Copy Pods Resources */ = { + 3235F5C725A891BC8E502835 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-dashwallet/Pods-dashwallet-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/DashSync/DashSync.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Protobuf/Protobuf_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageSwiftUI/SDWebImageSwiftUI.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SQLite.swift/SQLite.swift.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/gRPC/gRPCCertificates.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Alamofire.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DashSync.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Protobuf_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImageSwiftUI.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SQLite.swift.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle", + "$(DERIVED_FILE_DIR)/Pods-TodayExtension-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dashwallet/Pods-dashwallet-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6761AFE7CA3EDFF4FF268B23 /* [CP] Check Pods Manifest.lock */ = { + 47303C478666FDFF3EFBBE40 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -8414,14 +8855,14 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-WatchApp-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-DashWalletScreenshotsUITests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 7C9E00747676DAE0ABAA0C52 /* [CP] Check Pods Manifest.lock */ = { + 4E1E4D28B3D968C9E5392A41 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -8436,36 +8877,92 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-DashWalletTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-WatchApp Extension-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 7E9BC050F05F77EC2D12DEAD /* [CP] Check Pods Manifest.lock */ = { + 525BB766C4C0A1764D4BD70A /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", + "${PODS_ROOT}/Target Support Files/Pods-dashpay/Pods-dashpay-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/DashSync/DashSync.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/Protobuf/Protobuf_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageSwiftUI/SDWebImageSwiftUI.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SQLite.swift/SQLite.swift.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/gRPC/gRPCCertificates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Alamofire.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DashSync.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Protobuf_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImageSwiftUI.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SQLite.swift.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dashpay/Pods-dashpay-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 622C18F9EEFCADF772D1DBB5 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-dashwallet/Pods-dashwallet-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/DashSync/DashSync.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/Protobuf/Protobuf_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageSwiftUI/SDWebImageSwiftUI.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SQLite.swift/SQLite.swift.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/gRPC/gRPCCertificates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle", + ); + name = "[CP] Copy Pods Resources"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-WatchApp Extension-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Alamofire.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DashSync.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Protobuf_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImageSwiftUI.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SQLite.swift.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dashwallet/Pods-dashwallet-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 81600197311AC4FC9835F1D9 /* [CP] Check Pods Manifest.lock */ = { + A00464195619E3C43C141D1A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -8480,14 +8977,14 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-TodayExtension-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-WatchApp-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 910A25650B9BB7C3EDEB829B /* [CP] Check Pods Manifest.lock */ = { + A5BD49B2A59A7ED61D52013B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -8502,7 +8999,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-DashWalletScreenshotsUITests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-DashWalletTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -8578,68 +9075,6 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l shellPath = /bin/sh; shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ \"${CONFIGURATION}\" = \"Release\" ] ; then\nif [ ! -f \"DWUpholdMainnetConstants__Empty.m\" ]; then\n echo \"error: DWUpholdMainnetConstants__Empty.m file not found!\"\n exit 1\nfi\n\ncp \"DWUpholdMainnetConstants__Empty.m\" \"DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m\"\nelse\n # check if original file exist (in case it was moved or deleted while refactoring)\n if [ ! -f \"DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m\" ]; then\n echo \"error: DWUpholdMainnetConstants.m file not found! Fix Uphold Constants build phase\"\n exit 1\n fi\nfi\n"; }; - E5B46042D96D592661436332 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-dashpay/Pods-dashpay-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/DashSync/DashSync.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Protobuf/Protobuf_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImageSwiftUI/SDWebImageSwiftUI.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SQLite.swift/SQLite.swift.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/gRPC/gRPCCertificates.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Alamofire.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DashSync.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Protobuf_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImageSwiftUI.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SQLite.swift.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dashpay/Pods-dashpay-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - E89FBBEF1ECEF3BD173AC8EB /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-dashpay-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; FB0ACB9A1F9A99E400F4AB52 /* Run Script - bartycrouch */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -8691,7 +9126,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9F451EB2A0BF10B00825057 /* SyncingAlertViewController.swift in Sources */, 11B8449628F5B9F80082770C /* CrowdNodeResponse.swift in Sources */, 47AE8BB228BFF61A00490F5E /* FileManager+DashWallet.swift in Sources */, - 7545ED532DA91A2F0075F45C /* DashSpend/CTXConstants.swift in Sources */, + 7545ED532DA91A2F0075F45C /* CTXConstants.swift in Sources */, 47AE8BF228C1306000490F5E /* PointOfUseItemCell.swift in Sources */, 47083B3229892D770010AF71 /* DSTransaction+DashWallet.swift in Sources */, 751B61C82AE0EDD000D1C2EF /* CoinJoinLevelsViewController.swift in Sources */, @@ -8701,12 +9136,15 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2A913E9523A3F75F006A2A59 /* DWInitialViewController.m in Sources */, 4751136C28D9A3DB00223B77 /* BuySellPortalViewController.swift in Sources */, AA00000128D9A3DB00223B77 /* BuySellPortalView.swift in Sources */, + 8DF3D24EE3B247B79AE9CAB5 /* BuySellPortalScreen.swift in Sources */, + 514AEA0F2FB31BD800F9B54C /* AddressFieldView.swift in Sources */, 2A7A7BD62348CB6600451078 /* SettingsScreen.swift in Sources */, 2A0C69DA2314727F001B8C90 /* UISpringTimingParameters+DWInit.m in Sources */, 47C661AD28F972BD00028A8D /* NumberKeyboardButton.swift in Sources */, 11AE3DD82997C599000856EE /* IsDefaultEmail.swift in Sources */, + 516983A62FCAEF1300BA91A2 /* ErrorIllustration.swift in Sources */, 2A9FFF2A2233E60F00956D5F /* DWUpholdAccountObject.m in Sources */, - 75A2F3042DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift in Sources */, + 75A2F3042DE48C860046BE17 /* CTXSpendTokenService.swift in Sources */, 2A9FFE812230FF4700956D5F /* DWFormTableViewController.m in Sources */, C909614D29EFF7D600002D82 /* WalletKeysOverviewModel.swift in Sources */, 4751137528DAF28800223B77 /* UIAssembly.swift in Sources */, @@ -8714,6 +9152,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47AE8BF428C1306000490F5E /* FetchingNextPageCell.swift in Sources */, 47081197298CF20C003FCA3D /* Transaction.swift in Sources */, 47AE8BE628C1305F00490F5E /* AllMerchantLocationsViewController.swift in Sources */, + 5151CE192F9624D000F0A604 /* CurrencyItem.swift in Sources */, 2ACCD84D23180E7E00A96B62 /* DWPaymentOutput.m in Sources */, 2AB3415E23A8133A004E37A7 /* DWPayModelStub.m in Sources */, 2A74EFF1230531DA00C475EB /* DWRecoverContentView.m in Sources */, @@ -8724,6 +9163,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2A63003F2327B4BB00827825 /* DWPaymentOutput+DWView.m in Sources */, 750CED602C94BFD7000FB837 /* SettingsMenuViewModel.swift in Sources */, 75D5D5342CC928630049ED7B /* UIHostingController+DashWallet.swift in Sources */, + 51A57B032FBB41DE00FB4097 /* OrderPreviewTableRow.swift in Sources */, + 51A57B042FBB41DE00FB4097 /* MayaFeeInfoSheet.swift in Sources */, 2ACD53EE234C9D8E00650AD3 /* UIView+DWRecursiveSubview.m in Sources */, 11860923297598B400279FCC /* AddressStatus.swift in Sources */, C91E919729FBACE6003E7883 /* ExtendedPublicKeysModel.swift in Sources */, @@ -8774,6 +9215,9 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47AE8BFA28C1306000490F5E /* PointOfUseListModel.swift in Sources */, C3DAD2CF247585C10001624F /* NSPredicate+DWFullTextSearch.m in Sources */, 47F2C6842860513900C2B774 /* TxReclassifyTransactionsWhereToChangeViewController.swift in Sources */, + 514AE9F32FB2F4C500F9B54C /* HaltedToast.swift in Sources */, + A6DB8889C5594B1988BDE0CE /* MayaCoinIconLoader.swift in Sources */, + 4A36341D70A94778AF408757 /* MayaCoinIconView.swift in Sources */, 2A7A7C16234B763600451078 /* DWLocalCurrencyViewController.m in Sources */, 752D03B22E2F7B3C00B88784 /* MainMenuViewControllerDelegate.swift in Sources */, C9F451FD2A0CC4A300825057 /* BadgeView.swift in Sources */, @@ -8823,6 +9267,13 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9F451F72A0CAE1300825057 /* SyncView.swift in Sources */, 47522F502927CB9000EE143E /* SuccessfulOperationStatusViewController.swift in Sources */, 47E94B9A296D7F99000FE68E /* CustodialSwapsModel.swift in Sources */, + ECF67981D8BA06E1323B0818 /* NetworkUnavailableStateView.swift in Sources */, + 514AEA142FB324FA00F9B54C /* MayaConvertView.swift in Sources */, + 514AEA492FB33B4300F9B54C /* MayaConvertHostingController.swift in Sources */, + 51A57C042FBB5A0000FB4097 /* MayaTransactionSuccessView.swift in Sources */, + 51A57C062FBB5A0000FB4097 /* MayaTransactionFailureView.swift in Sources */, + ABA7230759067F4D4EB39AE9 /* MayaTransactionStatusHostingController.swift in Sources */, + 51A57C092FBB5A0000FB4097 /* MayaTransactionPendingView.swift in Sources */, 2A7A7C20234B79B700451078 /* DWLocalCurrencyTableViewCell.m in Sources */, 757422242DF87B5200CB0175 /* TerritoryPickerView.swift in Sources */, 757422252DF87B5200CB0175 /* MerchantFiltersView.swift in Sources */, @@ -8886,6 +9337,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 75EAFBB12B04C057005ABC6A /* CustomHUDView.swift in Sources */, 47C661B628FE75A700028A8D /* BaseViewController.swift in Sources */, 4751CAC0296EFD2900F63AC4 /* AccountListController.swift in Sources */, + 514AE85F2FB1F9DB00F9B54C /* DashSwitch.swift in Sources */, 2A9FFE032230FF2B00956D5F /* DWUpholdTransactionObject.m in Sources */, 2AF26F3B230C0E4C007F9228 /* DWBaseSeedViewController.m in Sources */, 2A8B9E4D22FEBF7A00FF8653 /* DWProgressAnimator.mm in Sources */, @@ -8932,6 +9384,11 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2ACCD86E2319609400A96B62 /* UIViewController+DWShareReceiveInfo.m in Sources */, 114D16B829828BCC009A124C /* OnlineAccountConfirmationController.swift in Sources */, 47838B8C29068F110003E8AB /* BaseResponse.swift in Sources */, + A70C83C906AC1349DF3B9C98 /* DashBalanceView.swift in Sources */, + 279C16F9AF0575C540E312E5 /* MayaConvertCardRow.swift in Sources */, + 514AEA442FB33B4300F9B54C /* EnterAmountView.swift in Sources */, + 514AEA462FB33B4300F9B54C /* DashPickerView.swift in Sources */, + 514AEA4C2FB33B4300F9B54C /* MayaAmountView.swift in Sources */, 2AB3417723A92978004E37A7 /* DWDemoAdvancedSecurityViewController.m in Sources */, C9829CDE2A562822007132E4 /* TappableLabel.swift in Sources */, 111C3C50296D5A4700788E18 /* TxWithinTimePeriod.swift in Sources */, @@ -8979,6 +9436,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47AE8BF528C1306000490F5E /* MerchantListLocationOffCell.swift in Sources */, 1186092529759C4B00279FCC /* CrowdNodeAPIConfirmationTx.swift in Sources */, 2A913E9023A31713006A2A59 /* UIViewController+DWEmbedding.m in Sources */, + 5151CD622F926D1900F0A604 /* SearchBar.swift in Sources */, 2A2CD71322F97B65008C7BC9 /* CALayer+DWShadow.m in Sources */, 47AE8BA528BFADD900490F5E /* MerchantDAO.swift in Sources */, 47AE8BAA28BFAE5800490F5E /* ExploreDatabaseSyncManager.swift in Sources */, @@ -9006,6 +9464,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2AD85A9F245881740045B480 /* DWQRScanStatusView.m in Sources */, 2A741DC62363A06000840ADF /* DWURLParser.m in Sources */, 2A8B9E6C23029D2B00FF8653 /* DWPaymentInputBuilder.m in Sources */, + 51A5774C2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift in Sources */, 2AC92C841FEB0A6D008CAEE0 /* DWQRScanViewController.m in Sources */, 2A9FFE802230FF4600956D5F /* DWSelectorFormTableViewCell.m in Sources */, 75C3EE032DA7C63C00A4E9C0 /* DashSpendUserAuthScreen.swift in Sources */, @@ -9016,10 +9475,10 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 7549BE582DEAECB8004F0BAF /* CustomIconMetadataProvider.swift in Sources */, 7549BE602DEAF628004F0BAF /* AddIconBitmapsTable.swift in Sources */, AA0000012DUMMYID001234567 /* AddProviderToGiftCardsTable.swift in Sources */, - AA0000042DUMMYID001234567 /* DashSpend/GiftCardProvider.swift in Sources */, - BB0000012DUMMYID001234567 /* DashSpend/DashSpendError.swift in Sources */, - BB0000032DUMMYID001234567 /* DashSpend/DashSpendRepository.swift in Sources */, - BB0000052DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift in Sources */, + AA0000042DUMMYID001234567 /* GiftCardProvider.swift in Sources */, + BB0000012DUMMYID001234567 /* DashSpendError.swift in Sources */, + BB0000032DUMMYID001234567 /* DashSpendRepository.swift in Sources */, + BB0000052DUMMYID001234567 /* DashSpendRepositoryFactory.swift in Sources */, GG0000012DUMMYID001234567 /* GeoRestrictionService.swift in Sources */, CC0000112DUMMYID001234567 /* PiggyCardsAPI.swift in Sources */, CC0000132DUMMYID001234567 /* PiggyCardsCache.swift in Sources */, @@ -9027,8 +9486,9 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l CC0000172DUMMYID001234567 /* PiggyCardsRepository.swift in Sources */, CC0000192DUMMYID001234567 /* PiggyCardsTokenService.swift in Sources */, EE0000012DUMMYID001234567 /* POIDetailsViewModel.swift in Sources */, + 51A577482FBB3A6000FB4097 /* OrderPreviewViewModel.swift in Sources */, EE0000042DUMMYID001234567 /* PercentageFormatter.swift in Sources */, - FF0000012DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift in Sources */, + FF0000012DUMMYID001234567 /* PiggyCardsConstants.swift in Sources */, FF0000052DUMMYID001234567 /* PiggyCardsModels.swift in Sources */, FF0000082DUMMYID001234567 /* BarcodeScanner.swift in Sources */, 2A8B9E6F2302A9C200FF8653 /* DWPasteboardAddressExtractor.m in Sources */, @@ -9038,15 +9498,22 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47A2A2EE293E622700938DB7 /* CBSecureTokenService.swift in Sources */, C9F42FA129DA95F5001BC549 /* PayTableViewCell.swift in Sources */, 75EBAA0F2BB99036004488E3 /* TextIntro.swift in Sources */, + RC00A0012FA0000000000001 /* TopIntro.swift in Sources */, + RC00A0042FA0000000000001 /* ExploreMenuScreen.swift in Sources */, 0F71317728F436920072F454 /* ServiceOverviewTableCell.swift in Sources */, 47838B87290670630003E8AB /* IntegrationViewController.swift in Sources */, 758A8F172C4D35A80012FA41 /* NSLayoutConstraint+DashWallet.swift in Sources */, 2AD1CE8022DC92BF00C99324 /* NSString+DWTextSize.m in Sources */, 47AE8C0528C1F74A00490F5E /* PointOfUseListFiltersCell.swift in Sources */, 47AE8C1A28C6A21A00490F5E /* AllMerchantLocationsDataProvider.swift in Sources */, - 754565CC2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift in Sources */, - 754565CE2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift in Sources */, - 754565CF2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift in Sources */, + 754565CC2DAA52A000DA4E8E /* CTXSpendAPI.swift in Sources */, + 516983A32FCAEDEB00BA91A2 /* SuccessIllustration.swift in Sources */, + 516983B12FCAEDEB00BA91A2 /* LoadingIllustration.swift in Sources */, + 51F0D10430BF98BA00B27E11 /* ArrowDownIcon.swift in Sources */, + 516983C22FCAEDEB00BA91A2 /* XmarkIcon.swift in Sources */, + 51F0D10130BF97A900B27E11 /* ExclamationIcon.swift in Sources */, + 754565CE2DAA52A000DA4E8E /* CTXSpendRepository.swift in Sources */, + 754565CF2DAA52A000DA4E8E /* CTXSpendEndpoint.swift in Sources */, C9F451E52A0B986E00825057 /* MainTabbarController.swift in Sources */, 7545ED612DA91FC60075F45C /* NumericKeyboardView.swift in Sources */, 47B30D78290BFCA60080C326 /* NumberFormatter+DashWallet.swift in Sources */, @@ -9066,6 +9533,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2AD1CEAB22E18F1800C99324 /* DWGlobalOptions.m in Sources */, 754BEA122C0B6BD700E8C93C /* HomeViewModel.swift in Sources */, 2A4430E722CBB6EC009BAF7F /* AppDelegate.m in Sources */, + 514AF1C12FB5F02D00F9B54C /* MayaConvertViewModel.swift in Sources */, + 514BEA022FB5F02D00F9B54C /* MayaConvertAmount.swift in Sources */, 75D2B4A12D9D28AC00228DB0 /* BlockExplorerSelectionView.swift in Sources */, C917023F29D44E0B008C034D /* SendReceivePageController.swift in Sources */, 753FD7E22CA44BDD00B7751F /* CoinJoinProgress.swift in Sources */, @@ -9117,6 +9586,20 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 0F6EDFCC28C896BD000427E7 /* CoinbaseTransactionsRequest.swift in Sources */, 2A9CEBAD22E1DA4000A50237 /* DWAppRootViewController.m in Sources */, 753F75352DD0D42300D40DFE /* DashSpendPayScreen.swift in Sources */, + D6C0C001D6C0C001D6C0C001 /* GiftCardDetailsInfoCard.swift in Sources */, + D6C0C002D6C0C002D6C0C002 /* GiftCardDetailsMerchantHeader.swift in Sources */, + D6C0C003D6C0C003D6C0C003 /* GiftCardPurchaseSelectionSheet.swift in Sources */, + D6C0C004D6C0C004D6C0C004 /* GiftCardDetailsPoweredBySection.swift in Sources */, + D6C0C005D6C0C005D6C0C005 /* GiftCardDetailsHowToUseSection.swift in Sources */, + D5A0C003D5A0C003D5A0C003 /* DashSpendFixedContent.swift in Sources */, + D5A0C004D5A0C004D5A0C004 /* DashSpendFlexibleContent.swift in Sources */, + D5A0C001D5A0C001D5A0C001 /* DashSpendAmountView.swift in Sources */, + D5A0C002D5A0C002D5A0C002 /* DashSpendDenominationRow.swift in Sources */, + D5A0C005D5A0C005D5A0C005 /* DashSpendMultiplePanel.swift in Sources */, + D5A0C006D5A0C006D5A0C006 /* DashSpendPayIntro.swift in Sources */, + D5A0C007D5A0C007D5A0C007 /* DashSpendSinglePanel.swift in Sources */, + 75DFA00175DFA00175DFA001 /* DashStepper.swift in Sources */, + 75DFA00275DFA00275DFA002 /* SegmentedControl.swift in Sources */, 47F005FF297164600029EB10 /* UITableView+DashWallet.swift in Sources */, 47C661B228FDC72700028A8D /* TransferAmountViewController.swift in Sources */, C94F5E8E29D404850034FD57 /* ShortcutCell.swift in Sources */, @@ -9137,6 +9620,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 2A6300452328D07500827825 /* DWLockPinInputView.m in Sources */, 2A0C69AC23125074001B8C90 /* UIView+DWHUD.m in Sources */, 75EBAA122BB99B6B004488E3 /* BottomSheet.swift in Sources */, + 22E89B6C00B546D294D7F331 /* SelfSizingSheet.swift in Sources */, B453B34B871E4125607DACDC /* NavigationBar.swift in Sources */, 75CED09E2ACFD0ED0095F10C /* CoinbaseDepositRequest.swift in Sources */, C94D98212A4CC78F00F3BEE1 /* DashInputField.swift in Sources */, @@ -9197,12 +9681,16 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9F451EE2A0BF1F500825057 /* SyncingAlertContentView.swift in Sources */, 471A2605289ACD5C0056B7B2 /* AddressUserInfo.swift in Sources */, 11E47BAB28EB38510097CFA0 /* SendCoinsService.swift in Sources */, + EC26E6EBECE04736903F77A2 /* DSAccount+SpentInputCheck.m in Sources */, C94F5E8A29D3FCCF0034FD57 /* ShortcutAction.swift in Sources */, 2A0C69E0231556D6001B8C90 /* DWModalChevronView.m in Sources */, 2A58815921A5906C00FD4D2C /* DWBaseLegacyViewController.m in Sources */, C9F452012A0CE6C900825057 /* HomeView.swift in Sources */, 47A2E3AC2972B1A60032A63B /* CoinbaseRatesProvider.swift in Sources */, 7581B19B2CE349BB00714007 /* ConfirmSpendDialog.swift in Sources */, + 5151CA252F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */, + 5151CA262F92335E00F0A604 /* LocalCurrencyView.swift in Sources */, + 514AEAD52FB34F8600F9B54C /* OrderPreviewView.swift in Sources */, 472D13DF299DF5C6006903F1 /* TaxReportGenerator.swift in Sources */, 119E8D122909513F00D406C1 /* CrowdNodeError.swift in Sources */, 75D9EBC32DE5CD9C009416A2 /* GiftCardsDAO.swift in Sources */, @@ -9231,6 +9719,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 4774DCDD28F43A68008CF87D /* ServiceDataSource.swift in Sources */, 75387B4A2E0400C300BCCC72 /* MerchantDenominations.swift in Sources */, 47838B7A2900196F0003E8AB /* ConverterView.swift in Sources */, + 5151CDF12F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */, 2A0C69CA23142E11001B8C90 /* DWModalBaseAnimation.m in Sources */, C956AF262A5CACE6002FAB75 /* TitleValueCell.swift in Sources */, 75D657682DF579F300ACE570 /* TransactionFilterDialog.swift in Sources */, @@ -9248,6 +9737,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 47CF46A1296540EF0067B6EE /* AccountService.swift in Sources */, 47C6E6E7291A90B3003FEDF2 /* ListHandlerView.swift in Sources */, 7566F4832BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */, + RC00C0012FA0000000000001 /* CSVExportSheet.swift in Sources */, 754D020E2D1558EB005CA466 /* GroupedTransactions.swift in Sources */, 11BD738128E7356100A34022 /* CrowdNode.swift in Sources */, 47AE8B9F28BFAD8200490F5E /* SQLite+ExloreDash.swift in Sources */, @@ -9281,8 +9771,10 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l AA0003012CA0F58E00A1B401 /* MayaAddressValidator.swift in Sources */, AA0002022BA0F58E00A1B301 /* MayaCryptoCurrency.swift in Sources */, AA0002052BA0F58E00A1B302 /* MayaPool.swift in Sources */, + 452BD4339B58028BD4CF06F7 /* MayaConstants.swift in Sources */, AA0002082BA0F58E00A1B303 /* MayaEndpoint.swift in Sources */, AA00020B2BA0F58E00A1B304 /* MayaAPIService.swift in Sources */, + AA0020012FA0000000000014 /* SwapExecutionData.swift in Sources */, AA00020E2BA0F58E00A1B305 /* SelectCoinViewModel.swift in Sources */, AA0002112BA0F58E00A1B306 /* CoinRowView.swift in Sources */, AA0002142BA0F58E00A1B307 /* SelectCoinView.swift in Sources */, @@ -9296,6 +9788,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l AA0004012DA0F58E00C1C501 /* AddressSourceView.swift in Sources */, AA0004042DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */, 893AE6F535290DD1CEE89219 /* ImportPrivateKeySheet.swift in Sources */, + 6FD1512E7774A997C1F8466B /* MayaMenuStyles.swift in Sources */, + 1F3FF21B0295D5A326F08B74 /* OrderPreviewFeeRow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9365,6 +9859,9 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C69F2A320AA000D15901 /* DWPlaceholderFormTableViewCell.m in Sources */, C9D2C6A02A320AA000D15901 /* DWTitleDetailCellView.m in Sources */, C9D2C6A12A320AA000D15901 /* AmountInputControl.swift in Sources */, + 5151CA232F92335E00F0A604 /* LocalCurrencyViewModel.swift in Sources */, + 5151CA242F92335E00F0A604 /* LocalCurrencyView.swift in Sources */, + 516983A72FCAEF1300BA91A2 /* ErrorIllustration.swift in Sources */, C9D2C6A22A320AA000D15901 /* DWSegmentSliderFormTableViewCell.m in Sources */, 759609212C4553A400F3BF04 /* BuyCreditsViewController.swift in Sources */, 7549BE5E2DEAF55B004F0BAF /* IconBitmapDAO.swift in Sources */, @@ -9387,6 +9884,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C943B52D2A40A54600AF23C5 /* DWDashPayContactsUpdater.m in Sources */, C9D2C6AF2A320AA000D15901 /* BuySellPortalViewController.swift in Sources */, AA00000228D9A3DB00223B77 /* BuySellPortalView.swift in Sources */, + E5767AEAEF7C4921886E915A /* BuySellPortalScreen.swift in Sources */, C9D2C6B02A320AA000D15901 /* SettingsScreen.swift in Sources */, 751B61C52ADFFD0700D1C2EF /* IntegrationViewController+Uphold.swift in Sources */, C943B5022A40A54600AF23C5 /* DWDPGenericStatusItemView.m in Sources */, @@ -9398,6 +9896,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C6BB2A320AA000D15901 /* DWFormTableViewController.m in Sources */, C943B34D2A40A4C500AF23C5 /* DWInfoPopupViewController.m in Sources */, 75F990822AFD1065006759AB /* UsernameRequestDetailsViewController.swift in Sources */, + 5151CE182F9624D000F0A604 /* CurrencyItem.swift in Sources */, C943B32A2A408CED00AF23C5 /* DWAvatarExternalSourceView.m in Sources */, C9D2C6BC2A320AA000D15901 /* WalletKeysOverviewModel.swift in Sources */, C9D2C6BD2A320AA000D15901 /* UIAssembly.swift in Sources */, @@ -9416,6 +9915,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C6C02A320AA000D15901 /* Transaction.swift in Sources */, C943B3232A408CED00AF23C5 /* DWFaceDetector.m in Sources */, C9D2C6C12A320AA000D15901 /* AllMerchantLocationsViewController.swift in Sources */, + 5151CDF02F928D7D00F0A604 /* LocalCurrencyCellView.swift in Sources */, C9D2C6C22A320AA000D15901 /* DWPaymentOutput.m in Sources */, 753FDBEA2AEA422F0005EEC3 /* VotingPrefs.swift in Sources */, C9D2C6C32A320AA000D15901 /* DWPayModelStub.m in Sources */, @@ -9446,6 +9946,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C6D62A320AA000D15901 /* ExtendedPublicKeysViewController.swift in Sources */, B3A1F2C4D5E6078901234569 /* ExtendedPublicKeySheet.swift in Sources */, C9D2C6D82A320AA000D15901 /* AtmListViewController.swift in Sources */, + 514AE85E2FB1F9DB00F9B54C /* DashSwitch.swift in Sources */, C9D2C6D92A320AA000D15901 /* TxDetailViewController.swift in Sources */, C9D2C6DA2A320AA000D15901 /* NSAttributedString+Builder.swift in Sources */, 75D9EBB92DE5CC3A009416A2 /* AddGiftCardsTable.swift in Sources */, @@ -9478,7 +9979,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C943B31D2A408CED00AF23C5 /* DWUserProfileContainerView.m in Sources */, C9D2C6F02A320AA000D15901 /* UIImage+Utils.m in Sources */, C943B4BE2A40A54600AF23C5 /* DWGlobalMatchFailedHeaderView.m in Sources */, - 75A2F3032DE48C860046BE17 /* DashSpend/CTX/CTXSpendTokenService.swift in Sources */, + 75A2F3032DE48C860046BE17 /* CTXSpendTokenService.swift in Sources */, C943B4C22A40A54600AF23C5 /* BaseCollectionReusableView.m in Sources */, C9D2C6F22A320AA000D15901 /* DWSetupViewController.m in Sources */, C9D2C6F32A320AA000D15901 /* ExploreDatabaseConnection.swift in Sources */, @@ -9487,6 +9988,13 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 752D03AC2E2F758B00B88784 /* MainMenuViewController.swift in Sources */, C943B31B2A408CED00AF23C5 /* DWUpdatingUserProfileView.m in Sources */, C9D2C6F52A320AA000D15901 /* CoinbaseTransactionResponse.swift in Sources */, + 76766A50AFDB7BC138F569DC /* NetworkUnavailableStateView.swift in Sources */, + 514AEA132FB324FA00F9B54C /* MayaConvertView.swift in Sources */, + 514AEA4B2FB33B4300F9B54C /* MayaConvertHostingController.swift in Sources */, + 51A57C052FBB5A0000FB4097 /* MayaTransactionSuccessView.swift in Sources */, + 51A57C072FBB5A0000FB4097 /* MayaTransactionFailureView.swift in Sources */, + 97C1167F9600BF7E489A8545 /* MayaTransactionStatusHostingController.swift in Sources */, + 51A57C0A2FBB5A0000FB4097 /* MayaTransactionPendingView.swift in Sources */, C943B52F2A40A54600AF23C5 /* DWNotificationsData.m in Sources */, C9D2C6F72A320AA000D15901 /* TxDetailModel.swift in Sources */, 757514E22B15DB510026AD8E /* ShadowView.swift in Sources */, @@ -9509,6 +10017,9 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7032A320AA000D15901 /* UIViewController+DWDisplayError.m in Sources */, C9D2C7042A320AA000D15901 /* BRAppleWatchData.m in Sources */, 755049AD2C846576008FA7EB /* MenuItemModel.swift in Sources */, + 514AE9F42FB2F4C500F9B54C /* HaltedToast.swift in Sources */, + 7B4F28EE00BA451DA8675B06 /* MayaCoinIconLoader.swift in Sources */, + E88FE13A3079496C9A5EA8D4 /* MayaCoinIconView.swift in Sources */, C9D2C7052A320AA000D15901 /* DWRequestAmountContentView.m in Sources */, C943B3312A408CED00AF23C5 /* DWProfileAboutCellModel.m in Sources */, C943B4C42A40A54600AF23C5 /* DWSearchViewController.m in Sources */, @@ -9518,6 +10029,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7072A320AA000D15901 /* AmountInputTypeSwitcher.swift in Sources */, C943B4FA2A40A54600AF23C5 /* DWDPOutgoingRequestNotificationObject.m in Sources */, 75EBAA132BB99B6B004488E3 /* BottomSheet.swift in Sources */, + 39DC037E9CF04CCFA3EFBB53 /* SelfSizingSheet.swift in Sources */, EE5F3D5C458E92B69D14BFD8 /* NavigationBar.swift in Sources */, C9D2C7082A320AA000D15901 /* DWPlaceholderFormCellModel.m in Sources */, C9D2C7092A320AA000D15901 /* DWOverlapControl.m in Sources */, @@ -9530,7 +10042,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 75A8C1672AE5734A0042256E /* UsernameRequest.swift in Sources */, C9D2C70D2A320AA000D15901 /* CBAccount.swift in Sources */, C9D2C70E2A320AA000D15901 /* Types.swift in Sources */, - C9D2C70F2A320AA000D15901 /* (null) in Sources */, + C9D2C70F2A320AA000D15901 /* BuildFile in Sources */, C943B33B2A408CED00AF23C5 /* DWUploadAvatarChildView.m in Sources */, C9D2C7112A320AA000D15901 /* Coinbase.swift in Sources */, C9D2C7122A320AA000D15901 /* SyncModel.swift in Sources */, @@ -9547,6 +10059,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7182A320AA000D15901 /* ConfirmOrderController.swift in Sources */, C9D2C71B2A320AA000D15901 /* DWResetWalletInfoViewController.m in Sources */, C9D2C71C2A320AA000D15901 /* DWLockScreenModel.m in Sources */, + 514AF1C02FB5F02D00F9B54C /* MayaConvertViewModel.swift in Sources */, + 514BEA012FB5F02D00F9B54C /* MayaConvertAmount.swift in Sources */, C943B5182A40A54600AF23C5 /* DWInvitationPreviewViewController.m in Sources */, C9D2C71D2A320AA000D15901 /* DWVerifySeedPhraseContentView.m in Sources */, C9D2C71E2A320AA000D15901 /* Coinbase+Constants.swift in Sources */, @@ -9562,7 +10076,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7252A320AA000D15901 /* SyncingActivityMonitor.swift in Sources */, 75EE38D82CBFE52E00845FFF /* ContactsPlaceholderViewController.swift in Sources */, C9D2C7262A320AA000D15901 /* EmptyUIView.swift in Sources */, - C9D2C7272A320AA000D15901 /* (null) in Sources */, + C9D2C7272A320AA000D15901 /* BuildFile in Sources */, C9D2C7282A320AA000D15901 /* DWQRScanModel.m in Sources */, C943B3292A408CED00AF23C5 /* DWExternalSourceViewController.m in Sources */, C9D2C72A2A320AA000D15901 /* SyncView.swift in Sources */, @@ -9585,10 +10099,31 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7382A320AA000D15901 /* CoinbasePlaceBuyOrderRequest.swift in Sources */, C943B5262A40A54600AF23C5 /* DWDashPayAnimationView.m in Sources */, 753F75342DD0D42300D40DFE /* DashSpendPayScreen.swift in Sources */, + D6C0C001D6C0C001D6C0C001 /* GiftCardDetailsInfoCard.swift in Sources */, + D6C0C002D6C0C002D6C0C002 /* GiftCardDetailsMerchantHeader.swift in Sources */, + D6C0C003D6C0C003D6C0C003 /* GiftCardPurchaseSelectionSheet.swift in Sources */, + D6C0C004D6C0C004D6C0C004 /* GiftCardDetailsPoweredBySection.swift in Sources */, + D6C0C005D6C0C005D6C0C005 /* GiftCardDetailsHowToUseSection.swift in Sources */, + D5A0C003D5A0C003D5A0C003 /* DashSpendFixedContent.swift in Sources */, + D5A0C004D5A0C004D5A0C004 /* DashSpendFlexibleContent.swift in Sources */, + D5A0C001D5A0C001D5A0C001 /* DashSpendAmountView.swift in Sources */, + D5A0C002D5A0C002D5A0C002 /* DashSpendDenominationRow.swift in Sources */, + D5A0C005D5A0C005D5A0C005 /* DashSpendMultiplePanel.swift in Sources */, + D5A0C006D5A0C006D5A0C006 /* DashSpendPayIntro.swift in Sources */, + D5A0C007D5A0C007D5A0C007 /* DashSpendSinglePanel.swift in Sources */, + 75DFA00175DFA00175DFA001 /* DashStepper.swift in Sources */, + 75DFA00275DFA00275DFA002 /* SegmentedControl.swift in Sources */, C943B4EF2A40A54600AF23C5 /* DWUserProfileHeaderView.m in Sources */, C9D2C7392A320AA000D15901 /* CoinbaseAmount.swift in Sources */, C9D2C73B2A320AA000D15901 /* SendAmountModel.swift in Sources */, + 516983A42FCAEDEB00BA91A2 /* SuccessIllustration.swift in Sources */, + 516983B22FCAEDEB00BA91A2 /* LoadingIllustration.swift in Sources */, + 51F0D10530BF98BA00B27E11 /* ArrowDownIcon.swift in Sources */, + 516983C32FCAEDEB00BA91A2 /* XmarkIcon.swift in Sources */, + 51F0D10230BF97A900B27E11 /* ExclamationIcon.swift in Sources */, 75EBAA102BB99036004488E3 /* TextIntro.swift in Sources */, + RC00A0022FA0000000000001 /* TopIntro.swift in Sources */, + RC00A0052FA0000000000001 /* ExploreMenuScreen.swift in Sources */, 757514E52B1735370026AD8E /* JoinDashPayView.swift in Sources */, C9D2C73C2A320AA000D15901 /* DWDecimalInputValidator.m in Sources */, C9D2C73D2A320AA000D15901 /* PaymentMethods.swift in Sources */, @@ -9619,9 +10154,14 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C74A2A320AA000D15901 /* ShortcutsView.swift in Sources */, C9D2C74B2A320AA000D15901 /* DWFormSectionModel.m in Sources */, 754C27CF2CC3C7AF00BA7B9F /* MixDashDialog.swift in Sources */, - C9D2C74C2A320AA000D15901 /* (null) in Sources */, + C9D2C74C2A320AA000D15901 /* BuildFile in Sources */, C9D2C74D2A320AA000D15901 /* CoinbasePlaceBuyOrderResponse.swift in Sources */, C9D2C74E2A320AA000D15901 /* ExploreMapView.swift in Sources */, + D0D3F0F33D9F162622F0A6E5 /* DashBalanceView.swift in Sources */, + 40AB5A54235FDE2747ECE3F5 /* MayaConvertCardRow.swift in Sources */, + 514AEA452FB33B4300F9B54C /* EnterAmountView.swift in Sources */, + 514AEA482FB33B4300F9B54C /* DashPickerView.swift in Sources */, + 514AEA4E2FB33B4300F9B54C /* MayaAmountView.swift in Sources */, C943B32F2A408CED00AF23C5 /* DWSaveAlertViewController.m in Sources */, C930784C2A6AD59700906E4B /* TitleValueCell.swift in Sources */, 7566F48B2BB6CAF2005238D2 /* MenuItem.swift in Sources */, @@ -9639,7 +10179,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7552A320AA000D15901 /* DWPaymentInput.m in Sources */, C9D2C7572A320AA000D15901 /* OnlineAccountDetailsController.swift in Sources */, C9D2C7582A320AA000D15901 /* UIApplication+DashWallet.swift in Sources */, - 7545ED542DA91A2F0075F45C /* DashSpend/CTXConstants.swift in Sources */, + 7545ED542DA91A2F0075F45C /* CTXConstants.swift in Sources */, C9D2C7592A320AA000D15901 /* OnlineAccountInfoController.swift in Sources */, C9D2C75A2A320AA000D15901 /* PaymentController.swift in Sources */, C9D2C75B2A320AA000D15901 /* CALayer+MBAnimationPersistence.m in Sources */, @@ -9668,7 +10208,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 75EBAA0A2BB9791B004488E3 /* Icon.swift in Sources */, 754C27C72CC3C0B900BA7B9F /* UsernamePrefs.swift in Sources */, C9D2C7672A320AA000D15901 /* CoinbaseExchangeRateResponse.swift in Sources */, - C9D2C7692A320AA000D15901 /* (null) in Sources */, + C9D2C7692A320AA000D15901 /* BuildFile in Sources */, C9D2C76A2A320AA000D15901 /* CrowdNodePortalItem.swift in Sources */, C943B4C82A40A54600AF23C5 /* DWSearchStateViewController.m in Sources */, C9D2C76B2A320AA000D15901 /* BaseViewController.swift in Sources */, @@ -9755,17 +10295,18 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7A72A320AA000D15901 /* ActionButtonViewController.swift in Sources */, C9D2C7A82A320AA000D15901 /* BuySellServiceItemCell.swift in Sources */, C956AF122A5B5949002FAB75 /* PasteboardContentView.swift in Sources */, - C9D2C7AA2A320AA000D15901 /* (null) in Sources */, + C9D2C7AA2A320AA000D15901 /* BuildFile in Sources */, C943B5002A40A54600AF23C5 /* DWDPTxItemView.m in Sources */, C9D2C7AB2A320AA000D15901 /* DWBaseFormTableViewCell.m in Sources */, C9D2C7AC2A320AA000D15901 /* AccountCell.swift in Sources */, C9D2C7AD2A320AA000D15901 /* DWTransactionListDataProviderStub.m in Sources */, 7566F4842BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */, + RC00C0022FA0000000000001 /* CSVExportSheet.swift in Sources */, C943B33A2A408CED00AF23C5 /* DWUploadAvatarModel.m in Sources */, C943B3392A408CED00AF23C5 /* DWUploadAvatarViewController.m in Sources */, 759609242C455B2000F3BF04 /* SendIntro.swift in Sources */, C9D2C7B12A320AA000D15901 /* CoinbaseBaseIDForCurrencyResponse.swift in Sources */, - C9D2C7B22A320AA000D15901 /* (null) in Sources */, + C9D2C7B22A320AA000D15901 /* BuildFile in Sources */, C9D2C7B32A320AA000D15901 /* SpecifyAmountViewController.swift in Sources */, C943B4EA2A40A54600AF23C5 /* DWUserProfileViewController.m in Sources */, 756557B62CE84FFA0060348D /* FeeInfo.swift in Sources */, @@ -9786,7 +10327,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C956AF212A5C33E5002FAB75 /* DWButton.m in Sources */, C9D2C7C02A320AA000D15901 /* HomeHeaderModel.swift in Sources */, C943B5942A40ED7B00AF23C5 /* DWPlanetarySystemView.m in Sources */, - C9D2C7C22A320AA000D15901 /* (null) in Sources */, + C9D2C7C22A320AA000D15901 /* BuildFile in Sources */, C9D2C7C32A320AA000D15901 /* DWDPRegistrationStatus.m in Sources */, C9D2C7C42A320AA000D15901 /* CurrencyExchanger_Objc.m in Sources */, 752F81982E30F55E00ADA76D /* SecurityMenuScreen.swift in Sources */, @@ -9800,7 +10341,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7C92A320AA000D15901 /* BuyDashViewController.swift in Sources */, C956AF0D2A5B592E002FAB75 /* TappableLabel.swift in Sources */, C9D2C7CA2A320AA000D15901 /* DWModalDismissalAnimation.m in Sources */, - C9D2C7CB2A320AA000D15901 /* (null) in Sources */, + C9D2C7CB2A320AA000D15901 /* BuildFile in Sources */, C9D2C7CD2A320AA000D15901 /* UIViewController+DashWallet.swift in Sources */, 75D5D5332CC928630049ED7B /* UIHostingController+DashWallet.swift in Sources */, C9D2C7CE2A320AA000D15901 /* CrowdNodeCell.swift in Sources */, @@ -9822,6 +10363,8 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7DD2A320AA000D15901 /* UISearchBar+DWAdditions.m in Sources */, C943B5122A40A54600AF23C5 /* DWInvitationHistoryModel.m in Sources */, C943B31A2A408CED00AF23C5 /* DWCurrentUserProfileView.m in Sources */, + 51A57B022FBB41DE00FB4097 /* OrderPreviewTableRow.swift in Sources */, + 51A57B062FBB41DE00FB4097 /* MayaFeeInfoSheet.swift in Sources */, 751B61C22ADFF99D00D1C2EF /* CoinbaseDepositResponse.swift in Sources */, 753E46E92DE1E24300A3FF2A /* CTXSpendModels.swift in Sources */, C943B4F32A40A54600AF23C5 /* DWDPEstablishedContactObject.m in Sources */, @@ -9829,10 +10372,10 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C7E22A320AA000D15901 /* CrowdNodeWithdrawalReceivedTx.swift in Sources */, 7549BE612DEAF628004F0BAF /* AddIconBitmapsTable.swift in Sources */, AA0000022DUMMYID001234567 /* AddProviderToGiftCardsTable.swift in Sources */, - AA0000052DUMMYID001234567 /* DashSpend/GiftCardProvider.swift in Sources */, - BB0000022DUMMYID001234567 /* DashSpend/DashSpendError.swift in Sources */, - BB0000042DUMMYID001234567 /* DashSpend/DashSpendRepository.swift in Sources */, - BB0000062DUMMYID001234567 /* DashSpend/DashSpendRepositoryFactory.swift in Sources */, + AA0000052DUMMYID001234567 /* GiftCardProvider.swift in Sources */, + BB0000022DUMMYID001234567 /* DashSpendError.swift in Sources */, + BB0000042DUMMYID001234567 /* DashSpendRepository.swift in Sources */, + BB0000062DUMMYID001234567 /* DashSpendRepositoryFactory.swift in Sources */, GG0000022DUMMYID001234567 /* GeoRestrictionService.swift in Sources */, CC0000122DUMMYID001234567 /* PiggyCardsAPI.swift in Sources */, CC0000142DUMMYID001234567 /* PiggyCardsCache.swift in Sources */, @@ -9841,7 +10384,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l CC0000202DUMMYID001234567 /* PiggyCardsTokenService.swift in Sources */, EE0000022DUMMYID001234567 /* POIDetailsViewModel.swift in Sources */, EE0000062DUMMYID001234567 /* PercentageFormatter.swift in Sources */, - FF0000022DUMMYID001234567 /* DashSpend/PiggyCardsConstants.swift in Sources */, + FF0000022DUMMYID001234567 /* PiggyCardsConstants.swift in Sources */, FF0000072DUMMYID001234567 /* PiggyCardsModels.swift in Sources */, FF0000102DUMMYID001234567 /* BarcodeScanner.swift in Sources */, C943B4B62A40A54600AF23C5 /* DWFetchedResultsDataSource.m in Sources */, @@ -9889,6 +10432,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 7545ED5E2DA91F590075F45C /* DashSpendTermsScreen.swift in Sources */, 75AA33D02BF9D44A00F12465 /* ButtonsGroup.swift in Sources */, C9D2C80C2A320AA000D15901 /* AllMerchantLocationsDataProvider.swift in Sources */, + 5151CD632F926D1900F0A604 /* SearchBar.swift in Sources */, C9D2C80D2A320AA000D15901 /* MainTabbarController.swift in Sources */, 751C05DB2D3D0E8C00475E52 /* JoinDashPayViewModel.swift in Sources */, C9D2C80E2A320AA000D15901 /* NumberFormatter+DashWallet.swift in Sources */, @@ -9974,7 +10518,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C8482A320AA000D15901 /* CoinbaseTransactionsRequest.swift in Sources */, C943B3372A408CED00AF23C5 /* DWEditProfileTextFieldCell.m in Sources */, C9D2C8492A320AA000D15901 /* DWAppRootViewController.m in Sources */, - C9D2C84A2A320AA000D15901 /* (null) in Sources */, + C9D2C84A2A320AA000D15901 /* BuildFile in Sources */, 7513DA882AB175E0005D55F6 /* TopperViewModel.swift in Sources */, C9D2C84B2A320AA000D15901 /* UITableView+DashWallet.swift in Sources */, C9D2C84C2A320AA000D15901 /* TransferAmountViewController.swift in Sources */, @@ -9992,6 +10536,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C8562A320AA000D15901 /* TransactionItemView.swift in Sources */, C9D2C8572A320AA000D15901 /* UIView+Reuse.swift in Sources */, C9D2C8582A320AA000D15901 /* DWUpholdClient.m in Sources */, + 514AEA102FB31BD800F9B54C /* AddressFieldView.swift in Sources */, C9D2C8592A320AA000D15901 /* CrowdNodePortalViewController.swift in Sources */, C9D2C85A2A320AA000D15901 /* ConfirmOrderModel.swift in Sources */, C943B53F2A40A6BE00AF23C5 /* DPAlertChildContentsView.m in Sources */, @@ -10010,7 +10555,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C8652A320AA000D15901 /* DWCaptureSessionManager.m in Sources */, C9D2C8662A320AA000D15901 /* DWDataMigrationManager.m in Sources */, 7502A4872AE401EF00ACDDD3 /* UsernameVotingViewController.swift in Sources */, - C9D2C86A2A320AA000D15901 /* (null) in Sources */, + C9D2C86A2A320AA000D15901 /* BuildFile in Sources */, C943B51F2A40A54600AF23C5 /* DWSuccessInvitationView.m in Sources */, C9D2C86B2A320AA000D15901 /* AmountPreviewView.swift in Sources */, 75CDD7812C0898E400F433D2 /* Shape.swift in Sources */, @@ -10026,6 +10571,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C8702A320AA000D15901 /* AmountObject.swift in Sources */, C9D2C8712A320AA000D15901 /* DerivationPathKeysViewController.swift in Sources */, 8A7607606A594C6E99A2FAC7 /* DerivationPathKeysView.swift in Sources */, + 514AEAD62FB34F8600F9B54C /* OrderPreviewView.swift in Sources */, 758A8F182C4D35A80012FA41 /* NSLayoutConstraint+DashWallet.swift in Sources */, C9D2C8722A320AA000D15901 /* DSTransaction+DashWallet.m in Sources */, C943B58D2A40ED6F00AF23C5 /* DWCreateUsernameViewController.m in Sources */, @@ -10068,7 +10614,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C8972A320AA000D15901 /* RatesProvider.swift in Sources */, C9D2C8982A320AA000D15901 /* CBAuth.swift in Sources */, C943B51C2A40A54600AF23C5 /* SuccessInvitationViewController.swift in Sources */, - C9D2C8992A320AA000D15901 /* (null) in Sources */, + C9D2C8992A320AA000D15901 /* BuildFile in Sources */, C9D2C89A2A320AA000D15901 /* CoinbaseCreateAddressesRequest.swift in Sources */, C943B3382A408CED00AF23C5 /* DWEditProfileAvatarView.m in Sources */, C943B5582A40DA3700AF23C5 /* DWFullScreenModalControllerViewController.m in Sources */, @@ -10078,6 +10624,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C89E2A320AA000D15901 /* AddressUserInfo.swift in Sources */, C943B5062A40A54600AF23C5 /* UICollectionView+DWDPItemDequeue.m in Sources */, C9D2C8A02A320AA000D15901 /* SendCoinsService.swift in Sources */, + C049F721C394463EB97CA8FF /* DSAccount+SpentInputCheck.m in Sources */, C9D2C8A12A320AA000D15901 /* ShortcutAction.swift in Sources */, C956AF112A5B592E002FAB75 /* UIButton+Dash.swift in Sources */, C9D2C8A22A320AA000D15901 /* DWModalChevronView.m in Sources */, @@ -10104,9 +10651,9 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l 757422222DF87B5200CB0175 /* MerchantFiltersView.swift in Sources */, C943B5382A40A65B00AF23C5 /* DWScrollingViewController.m in Sources */, C9D2C8B22A320AA000D15901 /* CoinbaseAccountAddress.swift in Sources */, - 754565C82DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendAPI.swift in Sources */, - 754565CA2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendRepository.swift in Sources */, - 754565CB2DAA52A000DA4E8E /* DashSpend/CTX/CTXSpendEndpoint.swift in Sources */, + 754565C82DAA52A000DA4E8E /* CTXSpendAPI.swift in Sources */, + 754565CA2DAA52A000DA4E8E /* CTXSpendRepository.swift in Sources */, + 754565CB2DAA52A000DA4E8E /* CTXSpendEndpoint.swift in Sources */, C930784E2A6AD59E00906E4B /* ConfirmPaymentModel.swift in Sources */, C9D2C8B42A320AA000D15901 /* CrowdNodeTransferViewController.swift in Sources */, C9D2C8B62A320AA000D15901 /* CBAuthInterop.swift in Sources */, @@ -10123,6 +10670,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C943B4EB2A40A54600AF23C5 /* DWModalUserProfileViewController.m in Sources */, C9D2C8C32A320AA000D15901 /* DWInfoTextCell.m in Sources */, 7569D68A2DE6DB6700768BFF /* ExploreContentsView.swift in Sources */, + 51A5774B2FBB3A6E00FB4097 /* OrderPreviewHostingController.swift in Sources */, C943B50D2A40A54600AF23C5 /* DWConfirmInvitationViewController.m in Sources */, C943B4F62A40A54600AF23C5 /* DWDPContactObject.m in Sources */, C9D2C8C42A320AA000D15901 /* ColorizedText.swift in Sources */, @@ -10156,7 +10704,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C943B50E2A40A54600AF23C5 /* DWConfirmInvitationContentView.m in Sources */, C9D2C8DE2A320AA000D15901 /* GettingStartedViewController.swift in Sources */, C9D2C8DF2A320AA000D15901 /* NetworkUnavailableView.swift in Sources */, - C9D2C8E02A320AA000D15901 /* (null) in Sources */, + C9D2C8E02A320AA000D15901 /* BuildFile in Sources */, 751B61C32ADFF9AE00D1C2EF /* UpholdPortalModel.swift in Sources */, C943B4FC2A40A54600AF23C5 /* DWDPEstablishedContactNotificationObject.m in Sources */, C9D2C8E12A320AA000D15901 /* DWLockScreenViewController.m in Sources */, @@ -10178,6 +10726,7 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C943B4B52A40A54600AF23C5 /* DWContactsFetchedDataSource.m in Sources */, C9D2C8EE2A320AA000D15901 /* WelcomeToCrowdNodeViewController.swift in Sources */, C943B3282A408CED00AF23C5 /* DWAvatarEditSelectorContentView.m in Sources */, + 51A577492FBB3A6000FB4097 /* OrderPreviewViewModel.swift in Sources */, 7573C2E52B01120B00F4C347 /* MasternodeKey.swift in Sources */, C9D2C8F02A320AA000D15901 /* DWProgressView.m in Sources */, C9D2C8F12A320AA000D15901 /* TxDetailCells.swift in Sources */, @@ -10188,13 +10737,15 @@ AA0001012AA9F58E00A1B201 /* MayaPortalView.swift */ = {isa = PBXFileReference; l C9D2C8F42A320AA000D15901 /* DWQuickReceiveViewController.m in Sources */, 75A8C1692AE6A1AC0042256E /* VotingViewModel.swift in Sources */, 1DD7BE9CD62998F12672140D /* RadioButtonRow.swift in Sources */, -AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, + AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, AA0001062AA9F58E00A1B202 /* MayaPortalViewController.swift in Sources */, AA0003022CA0F58E00A1B401 /* MayaAddressValidator.swift in Sources */, AA0002032BA0F58E00A1B301 /* MayaCryptoCurrency.swift in Sources */, AA0002062BA0F58E00A1B302 /* MayaPool.swift in Sources */, + DA38F1C5CD32A52891D02D0E /* MayaConstants.swift in Sources */, AA0002092BA0F58E00A1B303 /* MayaEndpoint.swift in Sources */, AA00020C2BA0F58E00A1B304 /* MayaAPIService.swift in Sources */, + AA0020012FA0000000000015 /* SwapExecutionData.swift in Sources */, AA00020F2BA0F58E00A1B305 /* SelectCoinViewModel.swift in Sources */, AA0002122BA0F58E00A1B306 /* CoinRowView.swift in Sources */, AA0002152BA0F58E00A1B307 /* SelectCoinView.swift in Sources */, @@ -10208,6 +10759,8 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, AA0004022DA0F58E00C1C501 /* AddressSourceView.swift in Sources */, AA0004052DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */, 3A0C6A2A57DFF02865E36200 /* ImportPrivateKeySheet.swift in Sources */, + F8BA4B9BBB650F7F50AD936F /* MayaMenuStyles.swift in Sources */, + C124DF94278D4238FAE0975D /* OrderPreviewFeeRow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10333,7 +10886,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, /* Begin XCBuildConfiguration section */ 2A4663072279DC2F0027533B /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BC46CEE26088DC2B21D22315 /* Pods-DashWalletScreenshotsUITests.debug.xcconfig */; + baseConfigurationReference = 3B889D25725F4A7EBFD60324 /* Pods-DashWalletScreenshotsUITests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; @@ -10343,7 +10896,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10370,7 +10923,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; 2A4663082279DC2F0027533B /* Testnet */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C1AFBC4E154DF42D982613E4 /* Pods-DashWalletScreenshotsUITests.testnet.xcconfig */; + baseConfigurationReference = 516C4047DB43CFFBB8ED541B /* Pods-DashWalletScreenshotsUITests.testnet.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; @@ -10380,7 +10933,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10406,7 +10959,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; 2A4663092279DC2F0027533B /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 718A7E2BCD93948084E43A8F /* Pods-DashWalletScreenshotsUITests.release.xcconfig */; + baseConfigurationReference = 180C7A2E60359C7F8FB7B918 /* Pods-DashWalletScreenshotsUITests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; @@ -10416,7 +10969,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10442,7 +10995,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; 2A46630A2279DC2F0027533B /* Testflight */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B1A681489B9516DC10084A61 /* Pods-DashWalletScreenshotsUITests.testflight.xcconfig */; + baseConfigurationReference = 226154FFBC684D1FDFCA35E4 /* Pods-DashWalletScreenshotsUITests.testflight.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ANALYZER_NONNULL = YES; @@ -10452,7 +11005,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10607,13 +11160,13 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; 75D5F3F7191EC270004AB296 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 86CC430C535DAEBF900E5914 /* Pods-dashwallet.debug.xcconfig */; + baseConfigurationReference = 726054552E79221F6B9D97AE /* Pods-dashwallet.debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; @@ -10738,13 +11291,13 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; 75D5F3F8191EC270004AB296 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1C3070D5E872E975F8473A68 /* Pods-dashwallet.release.xcconfig */; + baseConfigurationReference = F203E5FB195609E2B3F255C7 /* Pods-dashwallet.release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -10877,11 +11430,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; 75D5F3FA191EC270004AB296 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 77215C3978EE089C06A41979 /* Pods-DashWalletTests.debug.xcconfig */; + baseConfigurationReference = 41D6F206BA803FDFA21B31F5 /* Pods-DashWalletTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/dashwallet.app/dashwallet"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -10912,11 +11465,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; 75D5F3FB191EC270004AB296 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D8BD81810F2B0C11C86E7A5E /* Pods-DashWalletTests.release.xcconfig */; + baseConfigurationReference = 1AF20341A5E313C9D309BF39 /* Pods-DashWalletTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/dashwallet.app/dashwallet"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -10947,11 +11500,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; BA913BFB1BD57E4E005A7C0E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0995737D764092DF77CB88AC /* Pods-WatchApp.debug.xcconfig */; + baseConfigurationReference = 0CE6B29178211061196A0936 /* Pods-WatchApp.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; IBSC_MODULE = WatchApp_Extension; @@ -10969,11 +11522,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; BA913BFC1BD57E4E005A7C0E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4944819B9E17D4CC7DBFF293 /* Pods-WatchApp.release.xcconfig */; + baseConfigurationReference = E83D8CEAFF69B6D666C02116 /* Pods-WatchApp.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; IBSC_MODULE = WatchApp_Extension; @@ -10991,9 +11544,9 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; BA913BFD1BD57E4E005A7C0E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2E17397DF35152CAFBB79068 /* Pods-WatchApp Extension.debug.xcconfig */; + baseConfigurationReference = 3BA20785E162F1AF2F455D9C /* Pods-WatchApp Extension.debug.xcconfig */; buildSettings = { - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = ""; @@ -11018,9 +11571,9 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; BA913BFE1BD57E4E005A7C0E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E57EEDA37189E949FBAAA627 /* Pods-WatchApp Extension.release.xcconfig */; + baseConfigurationReference = 4457C976B1DB0CF402FF3C5F /* Pods-WatchApp Extension.release.xcconfig */; buildSettings = { - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = ""; @@ -11045,10 +11598,10 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; BAE12BF31B2DEE7F00895CC5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EDE78DB35CE393ADB4F3C5DD /* Pods-TodayExtension.debug.xcconfig */; + baseConfigurationReference = 3B0E86557754B86DD3A389CC /* Pods-TodayExtension.debug.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = dashwallet/dashwalletTodayExtension.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; @@ -11073,11 +11626,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; BAE12BF51B2DEE7F00895CC5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D96AAF71E3E67F7AD309BCC4 /* Pods-TodayExtension.release.xcconfig */; + baseConfigurationReference = 97E3D9AE002847AABFDA5537 /* Pods-TodayExtension.release.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = dashwallet/dashwalletTodayExtension.entitlements; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; @@ -11103,7 +11656,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; C9D2C94F2A320AA000D15901 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A47B6DF71195E41A5B7B5F99 /* Pods-dashpay.debug.xcconfig */; + baseConfigurationReference = 62613C0D937CA301FBBF2356 /* Pods-dashpay.debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -11111,7 +11664,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7; CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; @@ -11226,6 +11779,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.dashpaytnt; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG PIGGYCARDS_ENABLED"; SWIFT_OBJC_BRIDGING_HEADER = "DashWallet/dashwallet-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "dashwallet-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -11238,7 +11792,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; C9D2C9502A320AA000D15901 /* Testnet */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 67217DE34817FB304EDEA8D4 /* Pods-dashpay.testnet.xcconfig */; + baseConfigurationReference = 34B8A8461A3EDA41E40156CE /* Pods-dashpay.testnet.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -11246,7 +11800,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7; CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -11372,7 +11926,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; C9D2C9512A320AA000D15901 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 28AA9F2D282325F800A4F3AA /* Pods-dashpay.release.xcconfig */; + baseConfigurationReference = 7513056584D6F9A1B5E8E79B /* Pods-dashpay.release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -11380,7 +11934,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7; CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -11516,7 +12070,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; C9D2C9522A320AA000D15901 /* Testflight */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8391BA0D088FF516D63DE9A1 /* Pods-dashpay.testflight.xcconfig */; + baseConfigurationReference = 8571065059CF5168F45C86E4 /* Pods-dashpay.testflight.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -11524,7 +12078,7 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7; CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -11722,13 +12276,13 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; CEE253D31E26A9BC00A25B15 /* Testflight */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6DC4B39C45C824674E05DB22 /* Pods-dashwallet.testflight.xcconfig */; + baseConfigurationReference = 1E7370138411E3E74B42F4CC /* Pods-dashwallet.testflight.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -11861,11 +12415,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; CEE253D41E26A9BC00A25B15 /* Testflight */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6A44C167AF8F881176AFB256 /* Pods-TodayExtension.testflight.xcconfig */; + baseConfigurationReference = 393F2EE6876F067BC90A103C /* Pods-TodayExtension.testflight.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = dashwallet/dashwalletTodayExtension.entitlements; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; @@ -11891,11 +12445,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; CEE253D51E26A9BC00A25B15 /* Testflight */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 62C1F7A3ABEE7CF3BBB270C4 /* Pods-DashWalletTests.testflight.xcconfig */; + baseConfigurationReference = F5121ED6CEEEAEDBE95414EE /* Pods-DashWalletTests.testflight.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/dashwallet.app/dashwallet"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -11926,11 +12480,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; CEE253D81E26A9BC00A25B15 /* Testflight */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 04D182469170A3A2843F51CA /* Pods-WatchApp.testflight.xcconfig */; + baseConfigurationReference = 351EC86529F9D2F4DC5CCE1F /* Pods-WatchApp.testflight.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; IBSC_MODULE = WatchApp_Extension; @@ -11948,9 +12502,9 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; CEE253D91E26A9BC00A25B15 /* Testflight */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FC016B99DFFB0CBAC3D19290 /* Pods-WatchApp Extension.testflight.xcconfig */; + baseConfigurationReference = EE15E7C0EEFF1E6930A90DA6 /* Pods-WatchApp Extension.testflight.xcconfig */; buildSettings = { - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = ""; @@ -12043,13 +12597,13 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; FB9CC19B1F8511620079309F /* Testnet */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C09FD41E863456F8BF96B2DB /* Pods-dashwallet.testnet.xcconfig */; + baseConfigurationReference = 2537FCF252FB799EB4103150 /* Pods-dashwallet.testnet.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -12172,10 +12726,10 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; FB9CC19C1F8511620079309F /* Testnet */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 74F4F603C555A521F738DC9E /* Pods-TodayExtension.testnet.xcconfig */; + baseConfigurationReference = AA29A8515217AC4232F794C2 /* Pods-TodayExtension.testnet.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = dashwallet/dashwalletTodayExtension.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; @@ -12200,11 +12754,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; FB9CC19D1F8511620079309F /* Testnet */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 281D63D6AC9610BCF4765D07 /* Pods-DashWalletTests.testnet.xcconfig */; + baseConfigurationReference = 5CBC0F417B9E8DE631F9DE2A /* Pods-DashWalletTests.testnet.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/dashwallet.app/dashwallet"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -12235,11 +12789,11 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; FB9CC1A01F8511620079309F /* Testnet */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AF918A810315A7BE335BFFA6 /* Pods-WatchApp.testnet.xcconfig */; + baseConfigurationReference = 38ED8068CF6060EE8343E195 /* Pods-WatchApp.testnet.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; EXCLUDED_ARCHS = ""; IBSC_MODULE = WatchApp_Extension; @@ -12257,9 +12811,9 @@ AA0001032AA9F58E00A1B201 /* MayaPortalView.swift in Sources */, }; FB9CC1A11F8511620079309F /* Testnet */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4D37A3F73905BC239A9D127D /* Pods-WatchApp Extension.testnet.xcconfig */; + baseConfigurationReference = E2A832899A499C24C7A43E47 /* Pods-WatchApp Extension.testnet.xcconfig */; buildSettings = { - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 44RJ69WHFF; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = ""; diff --git a/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme b/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme index dc4c775c9..b2b6d169e 100644 --- a/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme +++ b/DashWallet.xcodeproj/xcshareddata/xcschemes/dashwallet.xcscheme @@ -73,7 +73,7 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Toast/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Toast/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/Contents.json new file mode 100644 index 000000000..82c61a505 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toast-error@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toast-error@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toast-error@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x1.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x1.png new file mode 100644 index 000000000..567a06767 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x2.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x2.png new file mode 100644 index 000000000..5fb373589 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x3.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x3.png new file mode 100644 index 000000000..a2674581f Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-error.imageset/toast-error@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/Contents.json new file mode 100644 index 000000000..26bd7cfd2 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toast-info@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toast-info@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toast-info@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x1.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x1.png new file mode 100644 index 000000000..6cad68d76 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x2.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x2.png new file mode 100644 index 000000000..dca191ccf Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x3.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x3.png new file mode 100644 index 000000000..735d906d3 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-info.imageset/toast-info@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/Contents.json new file mode 100644 index 000000000..3866e805b --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toast-loading@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toast-loading@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toast-loading@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x1.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x1.png new file mode 100644 index 000000000..151ef577e Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x2.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x2.png new file mode 100644 index 000000000..7530f515a Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x3.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x3.png new file mode 100644 index 000000000..8f847e037 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-loading.imageset/toast-loading@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/Contents.json new file mode 100644 index 000000000..986b3954f --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toast-success@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toast-success@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toast-success@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x1.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x1.png new file mode 100644 index 000000000..fd1f73475 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x2.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x2.png new file mode 100644 index 000000000..b633ed89d Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x3.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x3.png new file mode 100644 index 000000000..fd1ef24f0 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-success.imageset/toast-success@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/Contents.json new file mode 100644 index 000000000..60e5d98a7 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toast-warning-yellow@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toast-warning-yellow@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toast-warning-yellow@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x1.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x1.png new file mode 100644 index 000000000..9f7a0626f Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x2.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x2.png new file mode 100644 index 000000000..7cbfdc3a2 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x3.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x3.png new file mode 100644 index 000000000..5d221fb6d Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning-yellow.imageset/toast-warning-yellow@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/Contents.json new file mode 100644 index 000000000..675ff31cd --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toast-warning@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toast-warning@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toast-warning@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x1.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x1.png new file mode 100644 index 000000000..b342330ed Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x2.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x2.png new file mode 100644 index 000000000..ac12d0bf2 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x3.png b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x3.png new file mode 100644 index 000000000..4ace13b4a Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/Toast/toast-warning.imageset/toast-warning@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Voting/Contents.json b/DashWallet/Resources/AppAssets.xcassets/Voting/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/Voting/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/Contents.json new file mode 100644 index 000000000..28869adb0 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "arrow-down@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "arrow-down@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "arrow-down@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x1.png b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x1.png new file mode 100644 index 000000000..bd96d45b0 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x2.png b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x2.png new file mode 100644 index 000000000..1f5785a00 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x3.png b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x3.png new file mode 100644 index 000000000..2b602afc1 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/arrow-down.imageset/arrow-down@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/Contents.json new file mode 100644 index 000000000..7fe960559 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "chevron-down-currency-select@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chevron-down-currency-select@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chevron-down-currency-select@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x1.png b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x1.png new file mode 100644 index 000000000..7de6369ff Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x2.png b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x2.png new file mode 100644 index 000000000..eb4bdf214 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x3.png b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x3.png new file mode 100644 index 000000000..0a9060a20 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/chevron-down-currency-select.imageset/chevron-down-currency-select@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/Contents.json new file mode 100644 index 000000000..48c4aefd0 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "downarrow-icon@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "downarrow-icon@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "downarrow-icon@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x1.png b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x1.png new file mode 100644 index 000000000..f88242b71 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x2.png b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x2.png new file mode 100644 index 000000000..84e82c443 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x3.png b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x3.png new file mode 100644 index 000000000..a039a2a7f Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/downarrow-icon.imageset/downarrow-icon@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/Contents.json new file mode 100644 index 000000000..f1819a861 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "eye_closed-icon@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "eye_closed-icon@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "eye_closed-icon@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x1.png b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x1.png new file mode 100644 index 000000000..18cc9cb2e Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x2.png b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x2.png new file mode 100644 index 000000000..593d6ffaa Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x3.png b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x3.png new file mode 100644 index 000000000..8db019e79 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/Contents.json new file mode 100644 index 000000000..3eef26e1f --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "eye_opened-icon@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "eye_opened-icon@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "eye_opened-icon@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x1.png b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x1.png new file mode 100644 index 000000000..e2efe598c Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x2.png b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x2.png new file mode 100644 index 000000000..94b6ddc95 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x3.png b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x3.png new file mode 100644 index 000000000..9f506e2d3 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/Contents.json new file mode 100644 index 000000000..3cf0c2121 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "icon-gift_card-piggy_cards@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon-gift_card-piggy_cards@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon-gift_card-piggy_cards@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x1.png b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x1.png new file mode 100644 index 000000000..9d9672cef Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x2.png b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x2.png new file mode 100644 index 000000000..6812561af Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x3.png b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x3.png new file mode 100644 index 000000000..7f1e01d52 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/Contents.json new file mode 100644 index 000000000..991ad6793 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "info-icon@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "info-icon@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "info-icon@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x1.png b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x1.png new file mode 100644 index 000000000..3b3afa4f8 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x2.png b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x2.png new file mode 100644 index 000000000..712e16064 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x3.png b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x3.png new file mode 100644 index 000000000..5a8633e97 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/info-icon.imageset/info-icon@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/minus.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/Contents.json new file mode 100644 index 000000000..020695da6 --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "minus-icon@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "minus-icon@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "minus-icon@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x1.png b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x1.png new file mode 100644 index 000000000..20a8ca749 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x2.png b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x2.png new file mode 100644 index 000000000..1b2bfa32f Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x3.png b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x3.png new file mode 100644 index 000000000..7e57760cb Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/plus.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/Contents.json new file mode 100644 index 000000000..4ce65394b --- /dev/null +++ b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "plus-icon@x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "plus-icon@x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "plus-icon@x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x1.png b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x1.png new file mode 100644 index 000000000..62d63a71b Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x1.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x2.png b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x2.png new file mode 100644 index 000000000..65cf9c178 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x2.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x3.png b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x3.png new file mode 100644 index 000000000..36ae65822 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x3.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Menu/menu_security.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/Contents.json similarity index 67% rename from DashWallet/Resources/AppAssets.xcassets/Menu/menu_security.imageset/Contents.json rename to DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/Contents.json index 1a937e9dc..e3d5f72b8 100644 --- a/DashWallet/Resources/AppAssets.xcassets/Menu/menu_security.imageset/Contents.json +++ b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/Contents.json @@ -1,56 +1,59 @@ { "images" : [ { + "filename" : "search-icon.png", "idiom" : "universal", - "filename" : "menu_security.png", "scale" : "1x" }, { - "idiom" : "universal", - "filename" : "menu_security_dark.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], + "filename" : "search-icon-dark.png", + "idiom" : "universal", "scale" : "1x" }, { + "filename" : "search-icon@2x.png", "idiom" : "universal", - "filename" : "menu_security@2x.png", "scale" : "2x" }, { - "idiom" : "universal", - "filename" : "menu_security_dark@2x.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], + "filename" : "search-icon-dark@2x.png", + "idiom" : "universal", "scale" : "2x" }, { + "filename" : "search-icon@3x.png", "idiom" : "universal", - "filename" : "menu_security@3x.png", "scale" : "3x" }, { - "idiom" : "universal", - "filename" : "menu_security_dark@3x.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], + "filename" : "search-icon-dark@3x.png", + "idiom" : "universal", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" } -} \ No newline at end of file +} diff --git a/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark.png b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark.png new file mode 100644 index 000000000..2a9c93cec Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark@2x.png b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark@2x.png new file mode 100644 index 000000000..8dc443756 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark@2x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark@3x.png b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark@3x.png new file mode 100644 index 000000000..017b188ca Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon-dark@3x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon.png b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon.png new file mode 100644 index 000000000..3c2585aca Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon@2x.png b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon@2x.png new file mode 100644 index 000000000..753520ef3 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon@2x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon@3x.png b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon@3x.png new file mode 100644 index 000000000..c68a244b9 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/search-icon.imageset/search-icon@3x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/Menu/menu_buySellDash.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/Contents.json similarity index 68% rename from DashWallet/Resources/AppAssets.xcassets/Menu/menu_buySellDash.imageset/Contents.json rename to DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/Contents.json index 3df119e03..92ebd6996 100644 --- a/DashWallet/Resources/AppAssets.xcassets/Menu/menu_buySellDash.imageset/Contents.json +++ b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/Contents.json @@ -1,56 +1,59 @@ { "images" : [ { + "filename" : "xmark-icon.png", "idiom" : "universal", - "filename" : "menu_buySellDash.png", "scale" : "1x" }, { - "idiom" : "universal", - "filename" : "menu_buySellDash_dark.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], + "filename" : "xmark-icon-dark.png", + "idiom" : "universal", "scale" : "1x" }, { + "filename" : "xmark-icon@2x.png", "idiom" : "universal", - "filename" : "menu_buySellDash@2x.png", "scale" : "2x" }, { - "idiom" : "universal", - "filename" : "menu_buySellDash_dark@2x.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], + "filename" : "xmark-icon-dark@2x.png", + "idiom" : "universal", "scale" : "2x" }, { + "filename" : "xmark-icon@3x.png", "idiom" : "universal", - "filename" : "menu_buySellDash@3x.png", "scale" : "3x" }, { - "idiom" : "universal", - "filename" : "menu_buySellDash_dark@3x.png", "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], + "filename" : "xmark-icon-dark@3x.png", + "idiom" : "universal", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" } -} \ No newline at end of file +} diff --git a/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark.png b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark.png new file mode 100644 index 000000000..ba9c29eac Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark@2x.png b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark@2x.png new file mode 100644 index 000000000..7d893fd14 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark@2x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark@3x.png b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark@3x.png new file mode 100644 index 000000000..83faa49ab Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon-dark@3x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon.png b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon.png new file mode 100644 index 000000000..d1a678a3d Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon@2x.png b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon@2x.png new file mode 100644 index 000000000..101653474 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon@2x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon@3x.png b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon@3x.png new file mode 100644 index 000000000..f92f6bffc Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/xmark-icon.imageset/xmark-icon@3x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/Contents.json b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/Contents.json index 7818979b8..cf490ea71 100644 --- a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/Contents.json +++ b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "zen_ledger.png", + "filename" : "zenledger.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "zen_ledger@2x.png", + "filename" : "zenledger@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "zen_ledger@3x.png", + "filename" : "zenledger@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger.png b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger.png deleted file mode 100644 index 5d9761e18..000000000 Binary files a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger.png and /dev/null differ diff --git a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger@2x.png b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger@2x.png deleted file mode 100644 index d713d5489..000000000 Binary files a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger@2x.png and /dev/null differ diff --git a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger@3x.png b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger@3x.png deleted file mode 100644 index 4d99036da..000000000 Binary files a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zen_ledger@3x.png and /dev/null differ diff --git a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger.png b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger.png new file mode 100644 index 000000000..2fbca27f5 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger@2x.png b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger@2x.png new file mode 100644 index 000000000..d8be6fa7f Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger@2x.png differ diff --git a/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger@3x.png b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger@3x.png new file mode 100644 index 000000000..3b1c61770 Binary files /dev/null and b/DashWallet/Resources/AppAssets.xcassets/zenledger.imageset/zenledger@3x.png differ diff --git a/DashWallet/Sources/Application/Tools.swift b/DashWallet/Sources/Application/Tools.swift index f3e570f72..bd55c31b3 100644 --- a/DashWallet/Sources/Application/Tools.swift +++ b/DashWallet/Sources/Application/Tools.swift @@ -23,7 +23,7 @@ private var _cachedFormatters: [String: NumberFormatter] = [:] private var _decimalFormatter: NumberFormatter! private var _fiatFormatter: NumberFormatter! private var _dashFormatter: NumberFormatter = { - let maximumFractionDigits = 8 + let maximumFractionDigits = 5 var dashFormat = NumberFormatter.cryptoFormatter(currencyCode: DASH, exponent: maximumFractionDigits) dashFormat.locale = Locale.current diff --git a/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift b/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift index ad8bfb26e..8778d2490 100644 --- a/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift +++ b/DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift @@ -138,90 +138,162 @@ class ExploreDatabaseConnection { } private func insertTestMerchant() { - print("🎯 ExploreDatabaseConnection: Adding test merchant...") + print("🎯 ExploreDatabaseConnection: Adding test merchants...") guard let db = self.db else { print("🎯 ExploreDatabaseConnection: Database not ready yet") return } - // Add PiggyCards test merchant - addPiggyCardsTestMerchant(db: db) + addPiggyCardsTestMerchants(db: db) } - private func addPiggyCardsTestMerchant(db: Connection) { - do { - let testMerchantId = "2e393eee-4508-47fe-954d-66209333fc96" - - // Check if merchant already exists - let checkQuery = "SELECT COUNT(*) FROM merchant WHERE merchantId = '\(testMerchantId)'" - let count = try db.scalar(checkQuery) as? Int64 ?? 0 - - if count > 0 { - print("🎯 PiggyCards test merchant already exists, skipping") - return - } + private struct PiggyCardsTestMerchant { + let merchantId: String + let name: String + let sourceId: String + let merchantSavings: Int + let merchantDenomType: String + let providerSavings: Int + let providerDenomType: String + let logo: String + let website: String + let territory: String? + let city: String? + } - print("🎯 Adding PiggyCards test merchant...") + // Matches Android PiggyCardsTestMerchants data + private let piggyCardsTestMerchants: [PiggyCardsTestMerchant] = [ + PiggyCardsTestMerchant( + merchantId: "2e393eee-4508-47fe-954d-66209333fc96", + name: "Piggy Cards Test Merchant", + sourceId: "177", + merchantSavings: -250, + merchantDenomType: "Fixed", + providerSavings: 100, + providerDenomType: "fixed", + logo: "https://piggy.cards/image/catalog/piggycards/logo2023_mobile.png", + website: "https://piggy.cards", + territory: "MA", + city: "Boston" + ), + PiggyCardsTestMerchant( + merchantId: "2e393fff-4508-47fe-954d-66209333fc96", + name: "Piggy Cards Flexible Test Merchant", + sourceId: "177", + merchantSavings: -250, + merchantDenomType: "min-max", + providerSavings: -250, + providerDenomType: "min-max", + logo: "https://piggy.cards/image/catalog/piggycards/logo2023_mobile.png", + website: "https://piggy.cards", + territory: "MA", + city: "Boston" + ), + PiggyCardsTestMerchant( + merchantId: "2e393aaa-4508-47fe-954d-66209333fc96", + name: "Home Depot [Flexible]", + sourceId: "74", + merchantSavings: 100, + merchantDenomType: "min-max", + providerSavings: -50, + providerDenomType: "min-max", + logo: "https://piggy.cards/image/catalog/piggycards/Home_Depot_Copy.jpg", + website: "https://www.homedepot.com", + territory: nil, + city: nil + ), + PiggyCardsTestMerchant( + merchantId: "2e393ddd-4508-47fe-954d-66209333fc96", + name: "Apple [Flexible]", + sourceId: "13", + merchantSavings: 100, + merchantDenomType: "min-max", + providerSavings: 100, + providerDenomType: "min-max", + logo: "https://piggy.cards/image/catalog/incenti/8aaa3d5d-logo.png", + website: "https://www.apple.com", + territory: nil, + city: nil + ), + PiggyCardsTestMerchant( + merchantId: "2e393ccc-4508-47fe-954d-66209333fc96", + name: "Dominos [Flexible]", + sourceId: "45", + merchantSavings: 100, + merchantDenomType: "min-max", + providerSavings: 150, + providerDenomType: "min-max", + logo: "https://piggy.cards/image/catalog/incenti/68ea431c-logo.png", + website: "https://www.dominos.com", + territory: nil, + city: nil + ), + ] + + private func addPiggyCardsTestMerchants(db: Connection) { + do { + let allIds = piggyCardsTestMerchants.map { "'\($0.merchantId)'" }.joined(separator: ", ") - // Wrap all FTS trigger operations in a transaction for atomicity try db.transaction { - // Drop FTS triggers temporarily + // Drop FTS triggers to allow merchant table modifications try db.run("DROP TRIGGER IF EXISTS room_fts_content_sync_merchant_fts_AFTER_INSERT") try db.run("DROP TRIGGER IF EXISTS room_fts_content_sync_merchant_fts_AFTER_UPDATE") try db.run("DROP TRIGGER IF EXISTS room_fts_content_sync_merchant_fts_BEFORE_UPDATE") try db.run("DROP TRIGGER IF EXISTS room_fts_content_sync_merchant_fts_BEFORE_DELETE") - // Insert merchant with unique prefix + // Delete existing test merchants (matches Android delete+re-insert pattern) try db.run(""" - INSERT INTO merchant ( - merchantId, name, source, sourceId, logoLocation, active, paymentMethod, - savingsPercentage, denominationsType, type, redeemType, territory, city, - website, addDate, updateDate - ) VALUES ( - '2e393eee-4508-47fe-954d-66209333fc96', - 'Piggy Cards Test Merchant', - 'PiggyCards', - '177', - 'https://piggy.cards/image/catalog/piggycards/logo2023_mobile.png', - 1, - 'gift card', - 1000, - 'Fixed', - 'online', - 'online', - 'MA', - 'Boston', - 'https://piggy.cards', - datetime('now'), - datetime('now') + DELETE FROM merchant_fts + WHERE docid IN ( + SELECT rowid + FROM merchant + WHERE source = 'PiggyCards' AND merchantId IN (\(allIds)) ) """) - - let insertedRowId = db.lastInsertRowid - - // Insert gift_card_providers record try db.run(""" - INSERT INTO gift_card_providers ( - merchantId, provider, sourceId, savingsPercentage, - denominationsType, active, redeemType - ) VALUES ( - '2e393eee-4508-47fe-954d-66209333fc96', - 'PiggyCards', - '177', - 10, - 'fixed', - 1, - 'online' - ) + DELETE FROM gift_card_providers + WHERE provider = 'PiggyCards' AND merchantId IN (\(allIds)) """) - - // Update FTS index try db.run(""" - INSERT INTO merchant_fts(docid, name) - VALUES (\(insertedRowId), 'Piggy Cards Test Merchant') + DELETE FROM merchant + WHERE source = 'PiggyCards' AND merchantId IN (\(allIds)) """) + // Insert all test merchants + for m in piggyCardsTestMerchants { + let territory = "'\(m.territory ?? "")'" + let city = "'\(m.city ?? "")'" + + try db.run(""" + INSERT INTO merchant ( + merchantId, name, source, sourceId, logoLocation, active, paymentMethod, + savingsPercentage, denominationsType, type, redeemType, territory, city, + website, addDate, updateDate + ) VALUES ( + '\(m.merchantId)', '\(m.name)', 'PiggyCards', '\(m.sourceId)', + '\(m.logo)', 1, 'gift card', \(m.merchantSavings), + '\(m.merchantDenomType)', 'online', 'online', + \(territory), \(city), '\(m.website)', + datetime('now'), datetime('now') + ) + """) + + let rowId = db.lastInsertRowid + + try db.run(""" + INSERT INTO gift_card_providers ( + merchantId, provider, sourceId, savingsPercentage, + denominationsType, active, redeemType + ) VALUES ( + '\(m.merchantId)', 'PiggyCards', '\(m.sourceId)', + \(m.providerSavings), '\(m.providerDenomType)', 1, 'online' + ) + """) + + try db.run("INSERT INTO merchant_fts(docid, name) VALUES (\(rowId), '\(m.name)')") + } + // Recreate FTS triggers try db.run(""" CREATE TRIGGER room_fts_content_sync_merchant_fts_BEFORE_UPDATE @@ -229,21 +301,18 @@ class ExploreDatabaseConnection { DELETE FROM merchant_fts WHERE docid=OLD.rowid; END """) - try db.run(""" CREATE TRIGGER room_fts_content_sync_merchant_fts_BEFORE_DELETE BEFORE DELETE ON merchant BEGIN DELETE FROM merchant_fts WHERE docid=OLD.rowid; END """) - try db.run(""" CREATE TRIGGER room_fts_content_sync_merchant_fts_AFTER_UPDATE AFTER UPDATE ON merchant BEGIN INSERT INTO merchant_fts(docid, name) VALUES (NEW.rowid, NEW.name); END """) - try db.run(""" CREATE TRIGGER room_fts_content_sync_merchant_fts_AFTER_INSERT AFTER INSERT ON merchant BEGIN @@ -252,10 +321,9 @@ class ExploreDatabaseConnection { """) } - print("✅ PiggyCards test merchant added successfully") - + print("✅ PiggyCards test merchants added successfully (\(piggyCardsTestMerchants.count) merchants)") } catch { - print("🎯 Error adding PiggyCards test merchant: \(error)") + print("🎯 Error adding PiggyCards test merchants: \(error)") } } #endif diff --git a/DashWallet/Sources/Models/Explore Dash/Model/DashSpend/PiggyCardsConstants.swift b/DashWallet/Sources/Models/Explore Dash/Model/DashSpend/PiggyCardsConstants.swift index 62603bde4..b009c96e5 100644 --- a/DashWallet/Sources/Models/Explore Dash/Model/DashSpend/PiggyCardsConstants.swift +++ b/DashWallet/Sources/Models/Explore Dash/Model/DashSpend/PiggyCardsConstants.swift @@ -26,4 +26,5 @@ class PiggyCardsConstants { static let tokenExpirationSeconds = 3600 static let serviceFeePercent = 1.5 // 1.5% service fee deducted from discount static let orderPollingDelayMs = 250 // Delay before first status check + static let maxOrderAmount: Decimal = 2500 } diff --git a/DashWallet/Sources/Models/Explore Dash/Model/Entites/CTXSpendModels.swift b/DashWallet/Sources/Models/Explore Dash/Model/Entites/CTXSpendModels.swift index d646f0861..7fefe75e0 100644 --- a/DashWallet/Sources/Models/Explore Dash/Model/Entites/CTXSpendModels.swift +++ b/DashWallet/Sources/Models/Explore Dash/Model/Entites/CTXSpendModels.swift @@ -106,7 +106,7 @@ struct GiftCardResponse: Codable { } } -struct MerchantResponse: Codable { +struct MerchantResponse: Decodable { let id: String let name: String let logoUrl: String @@ -126,10 +126,84 @@ struct MerchantResponse: Codable { let mapPinUrl: String let type: String let redeemType: String - let info: MerchantInfo + let info: MerchantInfo? let cardImageUrl: String let currency: String + enum CodingKeys: String, CodingKey { + case id + case name + case logoUrl + case enabled + case savingsPercentage + case userDiscount + case denominationsType + case denominationType + case denominations + case denominationValues + case cachedLocationCount + case locationCount + case mapPinUrl + case type + case redeemType + case info + case cardImageUrl + case currency + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(String.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + logoUrl = try container.decode(String.self, forKey: .logoUrl) + enabled = try container.decode(Bool.self, forKey: .enabled) + + savingsPercentage = try container.decodeIfPresent(Int.self, forKey: .savingsPercentage) + userDiscount = try container.decodeIfPresent(Int.self, forKey: .userDiscount) + + let primaryDenominationType = try container.decodeIfPresent(String.self, forKey: .denominationsType) + let legacyDenominationType = try container.decodeIfPresent(String.self, forKey: .denominationType) + denominationsType = primaryDenominationType ?? legacyDenominationType ?? DenominationType.Fixed.rawValue + + denominations = Self.decodeDenominations(from: container) + + cachedLocationCount = try container.decodeIfPresent(Int.self, forKey: .cachedLocationCount) + locationCount = try container.decodeIfPresent(Int.self, forKey: .locationCount) + + mapPinUrl = try container.decode(String.self, forKey: .mapPinUrl) + type = try container.decode(String.self, forKey: .type) + redeemType = try container.decode(String.self, forKey: .redeemType) + info = try container.decodeIfPresent(MerchantInfo.self, forKey: .info) + cardImageUrl = try container.decode(String.self, forKey: .cardImageUrl) + currency = try container.decode(String.self, forKey: .currency) + } + + private static func decodeDenominations(from container: KeyedDecodingContainer) -> [String] { + if let stringValues = try? container.decode([String].self, forKey: .denominations), !stringValues.isEmpty { + return stringValues + } + if let doubleValues = try? container.decode([Double].self, forKey: .denominations), !doubleValues.isEmpty { + return doubleValues.map { NSDecimalNumber(value: $0).stringValue } + } + if let intValues = try? container.decode([Int].self, forKey: .denominations), !intValues.isEmpty { + return intValues.map(String.init) + } + + // Some responses use denominationValues instead of denominations. + if let stringValues = try? container.decode([String].self, forKey: .denominationValues), !stringValues.isEmpty { + return stringValues + } + if let doubleValues = try? container.decode([Double].self, forKey: .denominationValues), !doubleValues.isEmpty { + return doubleValues.map { NSDecimalNumber(value: $0).stringValue } + } + if let intValues = try? container.decode([Int].self, forKey: .denominationValues), !intValues.isEmpty { + return intValues.map(String.init) + } + + return [] + } + // Computed property to get discount value from either field var discount: Int { return savingsPercentage ?? userDiscount ?? 0 diff --git a/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/CTX/CTXSpendRepository.swift b/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/CTX/CTXSpendRepository.swift index f90849fad..aad24b5ce 100644 --- a/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/CTX/CTXSpendRepository.swift +++ b/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/CTX/CTXSpendRepository.swift @@ -213,6 +213,12 @@ class CTXSpendRepository: CTXSpendTokenProvider, DashSpendRepository { case 401, 403: throw DashSpendError.unauthorized case 404: + #if DEBUG + let payloadSummary = sanitizedDebugPayloadSummary(from: response.data) + DSLogger.log("CTXSpend getMerchant 404 for merchantId: \(merchantId), payload: \(payloadSummary)") + #else + DSLogger.log("CTXSpend getMerchant 404 for merchantId: \(merchantId)") + #endif throw DashSpendError.invalidMerchant case 500...599: throw DashSpendError.networkError @@ -226,7 +232,7 @@ class CTXSpendRepository: CTXSpendTokenProvider, DashSpendRepository { throw DashSpendError.networkError } } - + func getGiftCardByTxid(txid: String) async throws -> GiftCardResponse { do { let baseURL = CTXConstants.baseURI @@ -246,6 +252,27 @@ class CTXSpendRepository: CTXSpendTokenProvider, DashSpendRepository { return DashSpendError.networkError } + + private func sanitizedDebugPayloadSummary(from data: Data) -> String { + guard !data.isEmpty else { + return "" + } + + guard let jsonObject = try? JSONSerialization.jsonObject(with: data) else { + return "" + } + + if let dictionary = jsonObject as? [String: Any] { + let keys = dictionary.keys.sorted().joined(separator: ", ") + return "{keys: [\(keys)], size: \(data.count) bytes}" + } + + if let array = jsonObject as? [Any] { + return "[items: \(array.count), size: \(data.count) bytes]" + } + + return "" + } } // MARK: - KeychainService diff --git a/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/DashSpendError.swift b/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/DashSpendError.swift index 081442e33..8c8ee833f 100644 --- a/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/DashSpendError.swift +++ b/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/DashSpendError.swift @@ -32,6 +32,7 @@ enum DashSpendError: Error, LocalizedError { case customError(String) case unknown case paymentProcessingError(String) + case previousSwapPending var errorDescription: String? { switch self { @@ -67,6 +68,11 @@ enum DashSpendError: Error, LocalizedError { return NSLocalizedString("An unknown error occurred. Please try again later.", comment: "DashSpend") case .paymentProcessingError(let details): return String(format: NSLocalizedString("Payment processing error: %@", comment: "DashSpend"), details) + case .previousSwapPending: + return NSLocalizedString( + "A previous Dash conversion is still confirming. Please wait for it to confirm (about 2–5 minutes) before starting a new one.", + comment: "DashSpend/Maya" + ) } } } diff --git a/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift b/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift index fa46a29e9..43bfd1d0b 100644 --- a/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift +++ b/DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift @@ -181,9 +181,38 @@ class PiggyCardsRepository: DashSpendRepository { // MARK: - Gift Card Methods + struct OrderLineItem { + let denomination: Double + let quantity: Int + } + + private struct SelectedBasketItem { + let card: PiggyCardsGiftcard + let quantity: Int + let denomination: Double + } + /// Create an order for a gift card purchase /// This is the main flow for PiggyCards purchases func orderGiftCard(merchantId: String, fiatAmount: Double, fiatCurrency: String = "USD", cryptoCurrency: String = "DASH") async throws -> GiftCardInfo { + let lineItems = [OrderLineItem(denomination: fiatAmount, quantity: 1)] + return try await orderGiftCards( + merchantId: merchantId, + lineItems: lineItems, + fiatCurrency: fiatCurrency, + cryptoCurrency: cryptoCurrency + ) + } + + func orderGiftCards( + merchantId: String, + lineItems: [OrderLineItem], + fiatCurrency: String = "USD", + cryptoCurrency: String = "DASH" + ) async throws -> GiftCardInfo { + guard !lineItems.isEmpty else { + throw DashSpendError.invalidAmount + } // Step 1: Get cached gift cards or fetch them guard let giftCards = PiggyCardsCache.shared.getGiftCards(forMerchant: merchantId) else { @@ -193,31 +222,59 @@ class PiggyCardsRepository: DashSpendRepository { for (index, card) in giftCards.enumerated() { } - // Step 2: Select the appropriate gift card - guard let selectedCard = PiggyCardsCache.shared.selectGiftCard(from: giftCards, forAmount: fiatAmount) else { - for card in giftCards { - let normalizedType = card.priceType.lowercased() - if normalizedType == "fixed" { - } else if normalizedType == "range" { + // Step 2: Select the appropriate gift cards for each line item + var selectedOrders: [PiggyCardsOrder] = [] + var selectedBasketItems: [SelectedBasketItem] = [] + + for lineItem in lineItems where lineItem.quantity > 0 { + guard let selectedCard = PiggyCardsCache.shared.selectGiftCard( + from: giftCards, + forAmount: lineItem.denomination + ) else { + for card in giftCards { + let normalizedType = card.priceType.lowercased() + if normalizedType == "fixed" { + } else if normalizedType == "range" { + } } + throw DashSpendError.invalidAmount + } + + if lineItem.quantity > selectedCard.quantity { + throw DashSpendError.invalidAmount } + + selectedOrders.append( + PiggyCardsOrder( + productId: selectedCard.id, + quantity: lineItem.quantity, + denomination: lineItem.denomination, + currency: fiatCurrency + ) + ) + + selectedBasketItems.append( + SelectedBasketItem( + card: selectedCard, + quantity: lineItem.quantity, + denomination: lineItem.denomination + ) + ) + } + + guard !selectedOrders.isEmpty else { throw DashSpendError.invalidAmount } + let summaryDiscountPercentage = basketSummaryDiscount(from: selectedBasketItems) + // Step 3: Get user email guard let email = userEmail else { throw DashSpendError.unauthorized } // Step 4: Create order request - let order = PiggyCardsOrder( - productId: selectedCard.id, - quantity: 1, - denomination: fiatAmount, - currency: fiatCurrency - ) - // Match Android's hardcoded values exactly // Android uses "2025-07-01" as a fixed date, not actual registration date let userMetadata = PiggyCardsUserMetadata( @@ -233,7 +290,7 @@ class PiggyCardsRepository: DashSpendRepository { ) let orderRequest = PiggyCardsOrderRequest( - orders: [order], + orders: selectedOrders, recipientEmail: email, user: user ) @@ -250,7 +307,12 @@ class PiggyCardsRepository: DashSpendRepository { do { let orderResponse: PiggyCardsOrderResponse = try await PiggyCardsAPI.shared.request(.createOrder(orderRequest)) - return try await processOrderResponse(orderResponse, selectedCard: selectedCard, giftCards: giftCards, fiatCurrency: fiatCurrency) + return try await processOrderResponse( + orderResponse, + summaryDiscountPercentage: summaryDiscountPercentage, + giftCards: giftCards, + fiatCurrency: fiatCurrency + ) } catch let error as HTTPClientError { // Log the raw error response for debugging if case .statusCode(let response) = error { @@ -266,7 +328,7 @@ class PiggyCardsRepository: DashSpendRepository { } private func processOrderResponse(_ orderResponse: PiggyCardsOrderResponse, - selectedCard: PiggyCardsGiftcard, + summaryDiscountPercentage: Double, giftCards: [PiggyCardsGiftcard], fiatCurrency: String) async throws -> GiftCardInfo { @@ -292,11 +354,26 @@ class PiggyCardsRepository: DashSpendRepository { paymentAddress: paymentInfo.address, amount: paymentInfo.amount, merchantName: giftCards.first?.name ?? "Unknown", - discountPercentage: calculateDisplayDiscount(selectedCard.discountPercentage), + discountPercentage: calculateDisplayDiscount(summaryDiscountPercentage), exchangeRate: exchangeRate, status: orderStatus.data.status ) } + + private func basketSummaryDiscount(from items: [SelectedBasketItem]) -> Double { + let totalFaceValue = items.reduce(0.0) { partialResult, item in + partialResult + (item.denomination * Double(item.quantity)) + } + + guard totalFaceValue > 0 else { return 0 } + + let weightedDiscountSum = items.reduce(0.0) { partialResult, item in + let lineTotal = item.denomination * Double(item.quantity) + return partialResult + (item.card.discountPercentage * lineTotal) + } + + return weightedDiscountSum / totalFaceValue + } /// Get order status to retrieve gift card details func getOrderStatus(orderId: String) async throws -> PiggyCardsOrderStatusResponse { @@ -422,4 +499,3 @@ class PiggyCardsRepository: DashSpendRepository { throw DashSpendError.unknown } } - diff --git a/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift b/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift index a948c46e6..be248a5f0 100644 --- a/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift +++ b/DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift @@ -101,6 +101,15 @@ public class ExploreDatabaseSyncManager { let timeInterval = timeIntervalMillesecond/1000 let installedVersion = wSelf.exploreDatabaseLastVersion + let localDatabaseExists = wSelf.hasLocalExploreDatabase() + + // If local DB is missing (e.g. removed due to schema mismatch), force download + // regardless of saved version timestamp to avoid falling back to in-memory DB. + if !localDatabaseExists { + DSLogger.log("ExploreDash: local explore.db missing, forcing cloud database download") + wSelf.downloadDatabase(metadata: metadata) + return + } guard timeInterval > installedVersion else { wSelf.syncState = .synced(Date()) @@ -179,6 +188,11 @@ extension ExploreDatabaseSyncManager { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return paths[0] } + + private func hasLocalExploreDatabase() -> Bool { + let localDbPath = getDocumentsDirectory().appendingPathComponent(kExploreDashDatabaseName).path + return FileManager.default.fileExists(atPath: localDbPath) + } } private let kExploreDatabaseLastSyncTimestampKey = "kExploreDatabaseLastSyncTimestampKey" diff --git a/DashWallet/Sources/Models/Maya/MayaAPIService.swift b/DashWallet/Sources/Models/Maya/MayaAPIService.swift index 7c36f5856..d68cc9ad1 100644 --- a/DashWallet/Sources/Models/Maya/MayaAPIService.swift +++ b/DashWallet/Sources/Models/Maya/MayaAPIService.swift @@ -22,14 +22,37 @@ import Foundation final class MayaAPIService: HTTPClient { static let shared = MayaAPIService() + // MARK: - Pools and Prices (Midgard) + func fetchPools() async throws -> [MayaPool] { try await request(.getPools) } + // MARK: - Inbound Addresses (mayanode) + func fetchInboundAddresses() async throws -> [MayaInboundAddress] { try await request(.getInboundAddresses) } + // MARK: - Quote and Swap (mayanode) + + func fetchQuote(dashSatoshis: Int64, toAsset: String, destination: String) async throws -> MayaSwapQuote { + try await request( + .quoteSwap( + fromAsset: "DASH.DASH", + toAsset: toAsset, + amount: dashSatoshis, + destination: destination + ) + ) + } + + func fetchSwapTransactionInfo(txid: String) async throws -> MayaSwapTransactionInfo { + try await request(.getSwapTransactionInfo(txid: txid)) + } + + // MARK: - Address Validation + /// Validates a destination address by requesting a swap quote from the Maya API. /// Returns nil if the address is valid, or an error string if invalid. func validateAddress(destination: String, toAsset: String) async -> String? { @@ -44,11 +67,10 @@ final class MayaAPIService: HTTPClient { ) return quote.error } catch { - // Non-200 responses may contain an error body with the validation message - if case HTTPClientError.statusCode(let response) = error { - if let body = try? JSONDecoder().decode(MayaSwapQuote.self, from: response.data) { - return body.error - } + // Non-200 responses may still carry a structured error body with the validation message. + if case HTTPClientError.statusCode(let response) = error, + let body = try? JSONDecoder().decode(MayaSwapQuote.self, from: response.data) { + return body.error } DSLogger.log("Maya: Address validation request failed: \(error)") return NSLocalizedString("Address validation unavailable — please check your connection", comment: "Maya") @@ -56,11 +78,181 @@ final class MayaAPIService: HTTPClient { } } +// MARK: - Inbound Address Model + struct MayaInboundAddress: Decodable { let chain: String let halted: Bool + let address: String? + let chainLpActionsPaused: Bool? + let chainTradingPaused: Bool? + let dustThreshold: String? + let gasRate: String? + let gasRateUnits: String? + let globalTradingPaused: Bool? + let outboundFee: String? + let outboundTxSize: String? + let pubKey: String? + + enum CodingKeys: String, CodingKey { + case chain + case halted + case address + case chainLpActionsPaused = "chain_lp_actions_paused" + case chainTradingPaused = "chain_trading_paused" + case dustThreshold = "dust_threshold" + case gasRate = "gas_rate" + case gasRateUnits = "gas_rate_units" + case globalTradingPaused = "global_trading_paused" + case outboundFee = "outbound_fee" + case outboundTxSize = "outbound_tx_size" + case pubKey = "pub_key" + } } +// MARK: - Quote Models + struct MayaSwapQuote: Decodable { let error: String? + let expectedAmountOut: String? + let dustThreshold: String? + let expiry: Int64? + let fees: MayaSwapFees? + let inboundAddress: String? + let inboundConfirmationBlocks: Int? + let inboundConfirmationSeconds: Int? + let memo: String? + let notes: String? + let outboundDelayBlocks: Int? + let outboundDelaySeconds: Int? + let recommendedMinAmountIn: String? + let slippageBps: Int? + let warning: String? + let routeId: String? + let routeProviders: [String]? + let executionNetwork: String? + + enum CodingKeys: String, CodingKey { + case error + case expectedAmountOut = "expected_amount_out" + case dustThreshold = "dust_threshold" + case expiry + case fees + case inboundAddress = "inbound_address" + case inboundConfirmationBlocks = "inbound_confirmation_blocks" + case inboundConfirmationSeconds = "inbound_confirmation_seconds" + case memo + case notes + case outboundDelayBlocks = "outbound_delay_blocks" + case outboundDelaySeconds = "outbound_delay_seconds" + case recommendedMinAmountIn = "recommended_min_amount_in" + case slippageBps = "slippage_bps" + case warning + case routeId + case routeProviders + case executionNetwork + } +} + +struct MayaSwapFees: Decodable { + let affiliate: String? + let asset: String? + let liquidity: String? + let outbound: String? + let slippageBps: Int? + let total: String? + let totalBps: Int? + + enum CodingKeys: String, CodingKey { + case affiliate + case asset + case liquidity + case outbound + case slippageBps = "slippage_bps" + case total + case totalBps = "total_bps" + } +} + +// MARK: - Transaction Tracking Models + +struct MayaSwapTransactionInfo: Decodable { + let observedTx: MayaObservedTx? + let keysignMetric: MayaKeysignMetric? + let error: String? + + enum CodingKeys: String, CodingKey { + case observedTx = "observed_tx" + case keysignMetric = "keysign_metric" + case error + } +} + +struct MayaObservedTx: Decodable { + let transaction: MayaObservedTransaction? + let status: String? + let outHashes: [String]? + let blockHeight: Int? + let signers: [String]? + let observedPubKey: String? + let finaliseHeight: Int? + + enum CodingKeys: String, CodingKey { + case transaction = "tx" + case status + case outHashes = "out_hashes" + case blockHeight = "block_height" + case signers + case observedPubKey = "observed_pub_key" + case finaliseHeight = "finalise_height" + } +} + +struct MayaObservedTransaction: Decodable { + let id: String? + let chain: String? + let fromAddress: String? + let toAddress: String? + let coins: [MayaCoinAmount]? + let gas: [MayaCoinAmount]? + let memo: String? + + enum CodingKeys: String, CodingKey { + case id + case chain + case fromAddress = "from_address" + case toAddress = "to_address" + case coins + case gas + case memo + } +} + +struct MayaCoinAmount: Decodable { + let asset: String? + let amount: String? +} + +struct MayaKeysignMetric: Decodable { + let txId: String? + // The API may return node TSS times as either Double or Int depending on the node version. + let nodeTssTimes: [String: Double]? + + enum CodingKeys: String, CodingKey { + case txId = "tx_id" + case nodeTssTimes = "node_tss_times" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + txId = try container.decodeIfPresent(String.self, forKey: .txId) + + if let asDouble = try? container.decodeIfPresent([String: Double].self, forKey: .nodeTssTimes) { + nodeTssTimes = asDouble + } else if let asInt = try? container.decodeIfPresent([String: Int].self, forKey: .nodeTssTimes) { + nodeTssTimes = asInt.mapValues { Double($0) } + } else { + nodeTssTimes = nil + } + } } diff --git a/DashWallet/Sources/Models/Maya/MayaCoinIconLoader.swift b/DashWallet/Sources/Models/Maya/MayaCoinIconLoader.swift new file mode 100644 index 000000000..e37ba2a75 --- /dev/null +++ b/DashWallet/Sources/Models/Maya/MayaCoinIconLoader.swift @@ -0,0 +1,78 @@ +// +// MayaCoinIconLoader.swift +// DashWallet +// +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// Loads remote coin icons with a two-level cache: memory (`NSCache`) + disk (`URLCache`). +/// +/// Icons are fetched from the jsupa/crypto-icons repository using the lowercased coin code, +/// matching the Android implementation. Disk-cached entries survive app restarts and are +/// served without re-downloading until the OS purges the cache. +/// +/// This actor is responsible only for remote loading and caching. +/// Local asset fallback is handled by `MayaCoinIconView`. +actor MayaCoinIconLoader { + static let shared = MayaCoinIconLoader() + + private static let baseURL = "https://raw.githubusercontent.com/jsupa/crypto-icons/main/icons/" + + private let memoryCache = NSCache() + private let session: URLSession + + private init() { + let diskCache = URLCache( + memoryCapacity: 0, + diskCapacity: 100 * 1024 * 1024, + diskPath: "maya_coin_icons" + ) + let config = URLSessionConfiguration.default + config.urlCache = diskCache + config.requestCachePolicy = .returnCacheDataElseLoad + config.timeoutIntervalForRequest = 10 + session = URLSession(configuration: config) + + memoryCache.countLimit = 200 + } + + /// Returns the icon for `code`, or `nil` if unavailable. + /// Memory cache is checked first; on miss the image is downloaded and cached. + func loadIcon(for code: String) async -> UIImage? { + let key = code.lowercased() as NSString + + if let cached = memoryCache.object(forKey: key) { + return cached + } + + guard let url = URL(string: Self.baseURL + "\(code.lowercased()).png") else { + return nil + } + + do { + let (data, response) = try await session.data(from: url) + guard (response as? HTTPURLResponse)?.statusCode == 200, + let image = UIImage(data: data) else { + return nil + } + memoryCache.setObject(image, forKey: key) + return image + } catch { + return nil + } + } +} diff --git a/DashWallet/Sources/Models/Maya/MayaConstants.swift b/DashWallet/Sources/Models/Maya/MayaConstants.swift new file mode 100644 index 000000000..c27e18ad5 --- /dev/null +++ b/DashWallet/Sources/Models/Maya/MayaConstants.swift @@ -0,0 +1,35 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Centralized constants for the Maya feature. +enum MayaConstants { + /// Maya's official support/community channel — used as the fallback on the failed-conversion + /// screen when there is no on-chain transaction to show (failure before broadcast). + static let supportURL = URL(string: "https://discord.com/invite/mayaprotocol")! + + /// Maya fee documentation, opened from the "Learn more" action on the fee-info sheet. + static let feesDocsURL = URL(string: "https://docs.mayaprotocol.com/deep-dive/how-it-works/fees")! + + /// Deep link to an inbound transaction on the MayaScan block explorer. + /// Used on the failed-conversion screen so the user can see what happened to their swap + /// (including the automatic refund Maya issues on failure). + static func mayaScanTransactionURL(txHash: String) -> URL { + URL(string: "https://www.mayascan.org/tx/\(txHash)")! + } +} diff --git a/DashWallet/Sources/Models/Maya/MayaCryptoCurrency.swift b/DashWallet/Sources/Models/Maya/MayaCryptoCurrency.swift index c7a20b947..c6f70ec0d 100644 --- a/DashWallet/Sources/Models/Maya/MayaCryptoCurrency.swift +++ b/DashWallet/Sources/Models/Maya/MayaCryptoCurrency.swift @@ -20,33 +20,225 @@ import Foundation struct MayaCryptoCurrency: Identifiable, Hashable { + /// Stable unique identifier used by `Identifiable` (e.g. `"btc"`, `"usdc_arb"`). let id: String + /// Ticker symbol displayed in the UI (e.g. `"BTC"`, `"USDC"`). let code: String + /// Human-readable name (e.g. `"Bitcoin"`, `"USD Coin (Arbitrum)"`). let name: String + /// Maya pool asset string used in API calls (e.g. `"BTC.BTC"`, `"ETH.USDC-0XA0B8..."`). let mayaAsset: String + /// Maya chain identifier used for local address validation (e.g. `"BTC"`, `"ARB"`). let chain: String + /// Name of the local image asset. `"convert.crypto"` is the generic placeholder, + /// used when no dedicated local icon exists — `MayaCoinIconView` falls back to + /// remote loading in that case. let iconAssetName: String - /// All Maya-supported coins (excluding DASH which is the source currency) + // MARK: - Supported Coins + + /// All Maya-supported destination coins, grouped by chain. + /// DASH is excluded — it is always the source currency. static let supportedCoins: [MayaCryptoCurrency] = [ - MayaCryptoCurrency(id: "btc", code: "BTC", name: "Bitcoin", mayaAsset: "BTC.BTC", chain: "BTC", iconAssetName: "maya.coin.btc"), - MayaCryptoCurrency(id: "eth", code: "ETH", name: "Ethereum", mayaAsset: "ETH.ETH", chain: "ETH", iconAssetName: "maya.coin.eth"), - MayaCryptoCurrency(id: "pepe", code: "PEPE", name: "PEPE", mayaAsset: "ETH.PEPE-0X6982508145454CE325DDBE47A25D4EC3D2311933", chain: "ETH", iconAssetName: "maya.coin.pepe"), - MayaCryptoCurrency(id: "usdc", code: "USDC", name: "USD Coin", mayaAsset: "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48", chain: "ETH", iconAssetName: "maya.coin.usdc"), - MayaCryptoCurrency(id: "usdt", code: "USDT", name: "Tether", mayaAsset: "ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7", chain: "ETH", iconAssetName: "maya.coin.usdt"), - MayaCryptoCurrency(id: "wsteth", code: "WSTETH", name: "Wrapped stETH", mayaAsset: "ETH.WSTETH-0X7F39C581F595B53C5CB19BD0B3F8DA6C935E2CA0", chain: "ETH", iconAssetName: "maya.coin.wsteth"), - MayaCryptoCurrency(id: "arb", code: "ARB", name: "Arbitrum", mayaAsset: "ARB.ARB-0X912CE59144191C1204E64559FE8253A0E49E6548", chain: "ARB", iconAssetName: "maya.coin.arb"), - MayaCryptoCurrency(id: "eth_arb", code: "ETH", name: "Ethereum (Arbitrum)", mayaAsset: "ARB.ETH", chain: "ARB", iconAssetName: "maya.coin.eth"), - MayaCryptoCurrency(id: "dai", code: "DAI", name: "Dai", mayaAsset: "ARB.DAI-0XDA10009CBD5D07DD0CECC66161FC93D7C9000DA1", chain: "ARB", iconAssetName: "maya.coin.dai"), - MayaCryptoCurrency(id: "gld", code: "GLD", name: "Goldario", mayaAsset: "ARB.GLD-0XAFD091F140C21770F4E5D53D26B2859AE97555AA", chain: "ARB", iconAssetName: "maya.coin.gld"), - MayaCryptoCurrency(id: "leo", code: "LEO", name: "LEO", mayaAsset: "ARB.LEO-0X93864D81175095DD93360FFA2A529B8642F76A6E", chain: "ARB", iconAssetName: "maya.coin.leo"), - MayaCryptoCurrency(id: "link", code: "LINK", name: "ChainLink", mayaAsset: "ARB.LINK-0XF97F4DF75117A78C1A5A0DBB814AF92458539FB4", chain: "ARB", iconAssetName: "maya.coin.link"), - MayaCryptoCurrency(id: "pepe_arb", code: "PEPE", name: "PEPE (Arbitrum)", mayaAsset: "ARB.PEPE-0X25D887CE7A35172C62FEBFD67A1856F20FAEBB00", chain: "ARB", iconAssetName: "maya.coin.pepe"), - MayaCryptoCurrency(id: "tgt", code: "TGT", name: "THORWallet", mayaAsset: "ARB.TGT-0X429FED88F10285E61B12BDF00848315FBDFCC341", chain: "ARB", iconAssetName: "maya.coin.tgt"), - MayaCryptoCurrency(id: "usdc_arb", code: "USDC", name: "USD Coin (Arbitrum)", mayaAsset: "ARB.USDC-0XAF88D065E77C8CC2239327C5EDB3A432268E5831", chain: "ARB", iconAssetName: "maya.coin.usdc"), - MayaCryptoCurrency(id: "wbtc", code: "WBTC", name: "Wrapped Bitcoin", mayaAsset: "ARB.WBTC-0X2F2A2543B76A4166549F7AAB2E75BEF0AEFC5B0F", chain: "ARB", iconAssetName: "maya.coin.wbtc"), - MayaCryptoCurrency(id: "wsteth_arb", code: "WSTETH", name: "Wrapped stETH (ARB)", mayaAsset: "ARB.WSTETH-0X5979D7B546E38E414F7E9822514BE443A4800529", chain: "ARB", iconAssetName: "maya.coin.wsteth"), - MayaCryptoCurrency(id: "kuji", code: "KUJI", name: "Kujira", mayaAsset: "KUJI.KUJI", chain: "KUJI", iconAssetName: "maya.coin.kuji"), - MayaCryptoCurrency(id: "rune", code: "RUNE", name: "Rune", mayaAsset: "THOR.RUNE", chain: "THOR", iconAssetName: "maya.coin.rune"), + // BTC + MayaCryptoCurrency( + id: "btc", code: "BTC", name: "Bitcoin", mayaAsset: "BTC.BTC", + chain: "BTC", iconAssetName: "maya.coin.btc" + ), + + // ADA + MayaCryptoCurrency( + id: "ada", code: "ADA", name: "Cardano", mayaAsset: "ADA.ADA", + chain: "ADA", iconAssetName: "convert.crypto" + ), + + // ETH chain + MayaCryptoCurrency( + id: "eth", code: "ETH", name: "Ethereum", mayaAsset: "ETH.ETH", + chain: "ETH", iconAssetName: "maya.coin.eth" + ), + MayaCryptoCurrency( + id: "moca", code: "MOCA", name: "Mocaverse", + mayaAsset: "ETH.MOCA-0X53312F85BBA24C8CB99CFFC13BF82420157230D3", + chain: "ETH", iconAssetName: "convert.crypto" + ), + MayaCryptoCurrency( + id: "pepe", code: "PEPE", name: "PEPE", + mayaAsset: "ETH.PEPE-0X6982508145454CE325DDBE47A25D4EC3D2311933", + chain: "ETH", iconAssetName: "maya.coin.pepe" + ), + MayaCryptoCurrency( + id: "usdc", code: "USDC", name: "USD Coin", + mayaAsset: "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48", + chain: "ETH", iconAssetName: "maya.coin.usdc" + ), + MayaCryptoCurrency( + id: "usdt", code: "USDT", name: "Tether", + mayaAsset: "ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7", + chain: "ETH", iconAssetName: "maya.coin.usdt" + ), + MayaCryptoCurrency( + id: "wsteth", code: "WSTETH", name: "Wrapped stETH", + mayaAsset: "ETH.WSTETH-0X7F39C581F595B53C5CB19BD0B3F8DA6C935E2CA0", + chain: "ETH", iconAssetName: "maya.coin.wsteth" + ), + + // ARB (Arbitrum One) + MayaCryptoCurrency( + id: "arb", code: "ARB", name: "Arbitrum", + mayaAsset: "ARB.ARB-0X912CE59144191C1204E64559FE8253A0E49E6548", + chain: "ARB", iconAssetName: "maya.coin.arb" + ), + MayaCryptoCurrency( + id: "eth_arb", code: "ETH", name: "Ethereum (Arbitrum)", mayaAsset: "ARB.ETH", + chain: "ARB", iconAssetName: "maya.coin.eth" + ), + MayaCryptoCurrency( + id: "dai", code: "DAI", name: "Dai", + mayaAsset: "ARB.DAI-0XDA10009CBD5D07DD0CECC66161FC93D7C9000DA1", + chain: "ARB", iconAssetName: "maya.coin.dai" + ), + MayaCryptoCurrency( + id: "gld", code: "GLD", name: "Goldario", + mayaAsset: "ARB.GLD-0XAFD091F140C21770F4E5D53D26B2859AE97555AA", + chain: "ARB", iconAssetName: "maya.coin.gld" + ), + MayaCryptoCurrency( + id: "leo", code: "LEO", name: "LEO", + mayaAsset: "ARB.LEO-0X93864D81175095DD93360FFA2A529B8642F76A6E", + chain: "ARB", iconAssetName: "maya.coin.leo" + ), + MayaCryptoCurrency( + id: "link", code: "LINK", name: "ChainLink", + mayaAsset: "ARB.LINK-0XF97F4DF75117A78C1A5A0DBB814AF92458539FB4", + chain: "ARB", iconAssetName: "maya.coin.link" + ), + MayaCryptoCurrency( + id: "pepe_arb", code: "PEPE", name: "PEPE (Arbitrum)", + mayaAsset: "ARB.PEPE-0X25D887CE7A35172C62FEBFD67A1856F20FAEBB00", + chain: "ARB", iconAssetName: "maya.coin.pepe" + ), + MayaCryptoCurrency( + id: "tgt", code: "TGT", name: "THORWallet", + mayaAsset: "ARB.TGT-0X429FED88F10285E61B12BDF00848315FBDFCC341", + chain: "ARB", iconAssetName: "maya.coin.tgt" + ), + MayaCryptoCurrency( + id: "usdc_arb", code: "USDC", name: "USD Coin (Arbitrum)", + mayaAsset: "ARB.USDC-0XAF88D065E77C8CC2239327C5EDB3A432268E5831", + chain: "ARB", iconAssetName: "maya.coin.usdc" + ), + MayaCryptoCurrency( + id: "usdt_arb", code: "USDT", name: "Tether", + mayaAsset: "ARB.USDT-0XFD086BC7CD5C481DCC9C85EBE478A1C0B69FCBB9", + chain: "ARB", iconAssetName: "maya.coin.usdt" + ), + MayaCryptoCurrency( + id: "wbtc", code: "WBTC", name: "Wrapped Bitcoin", + mayaAsset: "ARB.WBTC-0X2F2A2543B76A4166549F7AAB2E75BEF0AEFC5B0F", + chain: "ARB", iconAssetName: "maya.coin.wbtc" + ), + MayaCryptoCurrency( + id: "wsteth_arb", code: "WSTETH", name: "Wrapped stETH", + mayaAsset: "ARB.WSTETH-0X5979D7B546E38E414F7E9822514BE443A4800529", + chain: "ARB", iconAssetName: "maya.coin.wsteth" + ), + MayaCryptoCurrency( + id: "yum", code: "YUM", name: "YUM", + mayaAsset: "ARB.YUM-0X9F41B34F42058A7B74672055A5FAE22C4B113FD1", + chain: "ARB", iconAssetName: "convert.crypto" + ), + + // Other chains + MayaCryptoCurrency( + id: "kuji", code: "KUJI", name: "Kujira", mayaAsset: "KUJI.KUJI", + chain: "KUJI", iconAssetName: "maya.coin.kuji" + ), + MayaCryptoCurrency( + id: "rune", code: "RUNE", name: "Rune", mayaAsset: "THOR.RUNE", + chain: "THOR", iconAssetName: "maya.coin.rune" + ), + MayaCryptoCurrency( + id: "sol", code: "SOL", name: "Solana", mayaAsset: "SOL.SOL", + chain: "SOL", iconAssetName: "convert.crypto" + ), + MayaCryptoCurrency( + id: "near", code: "NEAR", name: "NEAR Protocol", mayaAsset: "NEAR.NEAR", + chain: "NEAR", iconAssetName: "convert.crypto" + ), + MayaCryptoCurrency( + id: "trx", code: "TRX", name: "Tron", mayaAsset: "TRON.TRX", + chain: "TRON", iconAssetName: "convert.crypto" + ), + MayaCryptoCurrency( + id: "zec", code: "ZEC", name: "Zcash", mayaAsset: "ZEC.ZEC", + chain: "ZEC", iconAssetName: "convert.crypto" + ), + MayaCryptoCurrency( + id: "xrd", code: "XRD", name: "Radix", mayaAsset: "XRD.XRD", + chain: "XRD", iconAssetName: "convert.crypto" + ), ] + + // MARK: - Factory + + /// Returns the known `MayaCryptoCurrency` for a Maya asset string, or synthesises + /// an unknown-coin record as a safe fallback. Returns `nil` for `DASH.DASH`. + static func coin(for mayaAsset: String) -> MayaCryptoCurrency? { + let normalizedAsset = mayaAsset.uppercased() + + guard normalizedAsset != "DASH.DASH" else { return nil } + + if let knownCoin = supportedCoins.first(where: { $0.mayaAsset.uppercased() == normalizedAsset }) { + return knownCoin + } + + // Synthesise a minimal record for assets not yet in the static list. + let parts = normalizedAsset.split(separator: ".", maxSplits: 1).map(String.init) + guard parts.count == 2 else { return nil } + + let chain = parts[0] + let assetComponent = parts[1] + let code = assetComponent.split(separator: "-", maxSplits: 1).first.map(String.init) ?? assetComponent + + // Generate a network-aware display name so ambiguous symbols (e.g. two USDC pools on + // different chains) remain distinguishable without hardcoding suffixes in the UI layer. + let chainLabel = Self.chainDisplayName(chain) + let synthesizedName = chainLabel.isEmpty ? code : "\(code) (\(chainLabel))" + + return MayaCryptoCurrency( + id: normalizedAsset + .lowercased() + .replacingOccurrences(of: ".", with: "_") + .replacingOccurrences(of: "-", with: "_"), + code: code, + name: synthesizedName, + mayaAsset: normalizedAsset, + chain: chain, + iconAssetName: "convert.crypto" + ) + } + + // MARK: - Chain display helpers + + /// Maps a Maya chain identifier to a human-readable label used in synthesised coin names. + /// Returns the raw chain string as-is for chains not in the table. + static func chainDisplayName(_ chain: String) -> String { + switch chain.uppercased() { + case "ARB": return "Arbitrum" + case "ETH": return "Ethereum" + case "BTC": return "Bitcoin" + case "SOL": return "Solana" + case "NEAR": return "NEAR" + case "TRON": return "Tron" + case "ZEC": return "Zcash" + case "XRD": return "Radix" + case "KUJI": return "Kujira" + case "THOR": return "THORChain" + case "ADA": return "Cardano" + case "MAYA": return "Maya" + case "DASH": return "" // source chain — never synthesised + default: return chain + } + } } diff --git a/DashWallet/Sources/Models/Maya/MayaEndpoint.swift b/DashWallet/Sources/Models/Maya/MayaEndpoint.swift index fcd2f2dd1..87544dd79 100644 --- a/DashWallet/Sources/Models/Maya/MayaEndpoint.swift +++ b/DashWallet/Sources/Models/Maya/MayaEndpoint.swift @@ -19,20 +19,27 @@ import Moya +// Pool/price data comes from Midgard (analytics layer). +// All swap operations — inbound addresses, quotes, tx lookup — go to mayanode (protocol layer). +private enum MayaHost { + static let midgard = "https://midgard.mayachain.info/v2/" + static let mayanode = "https://mayanode.mayachain.info/mayachain/" +} + enum MayaEndpoint { case getPools case getInboundAddresses case quoteSwap(fromAsset: String, toAsset: String, amount: Int64, destination: String) + case getSwapTransactionInfo(txid: String) } extension MayaEndpoint: TargetType { - var baseURL: URL { switch self { case .getPools: - return URL(string: "https://midgard.mayachain.info/v2/")! - case .getInboundAddresses, .quoteSwap: - return URL(string: "https://mayanode.mayachain.info/mayachain/")! + return URL(string: MayaHost.midgard)! + case .getInboundAddresses, .quoteSwap, .getSwapTransactionInfo: + return URL(string: MayaHost.mayanode)! } } @@ -44,6 +51,8 @@ extension MayaEndpoint: TargetType { return "inbound_addresses" case .quoteSwap: return "quote/swap" + case .getSwapTransactionInfo(let txid): + return "tx/\(txid)" } } @@ -53,7 +62,7 @@ extension MayaEndpoint: TargetType { var task: Moya.Task { switch self { - case .quoteSwap(let fromAsset, let toAsset, let amount, let destination): + case let .quoteSwap(fromAsset, toAsset, amount, destination): return .requestParameters( parameters: [ "from_asset": fromAsset, @@ -63,12 +72,12 @@ extension MayaEndpoint: TargetType { ], encoding: URLEncoding.queryString ) - default: + case .getPools, .getInboundAddresses, .getSwapTransactionInfo: return .requestPlain } } var headers: [String: String]? { - [:] + nil } } diff --git a/DashWallet/Sources/Models/Swap/SwapExecutionData.swift b/DashWallet/Sources/Models/Swap/SwapExecutionData.swift new file mode 100644 index 000000000..e32b91574 --- /dev/null +++ b/DashWallet/Sources/Models/Swap/SwapExecutionData.swift @@ -0,0 +1,26 @@ +// +// SwapExecutionData.swift +// DashWallet +// +// 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 + +struct SwapExecutionData { + let vaultAddress: String + let memo: String + let executionNetwork: String +} diff --git a/DashWallet/Sources/Models/Transactions/DSAccount+SpentInputCheck.h b/DashWallet/Sources/Models/Transactions/DSAccount+SpentInputCheck.h new file mode 100644 index 000000000..b95ff036c --- /dev/null +++ b/DashWallet/Sources/Models/Transactions/DSAccount+SpentInputCheck.h @@ -0,0 +1,52 @@ +// +// Created by Roman Chornyi +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Swift-callable wrapper around DSAccount's -isSpent: method. +/// +/// DSAccount maintains two sets during updateBalance: +/// - spentOutputs — correctly updated for ALL registered transactions +/// - utxos — NOT updated when a tx is marked pending +/// +/// A Maya swap transaction is always marked pending because its OP_RETURN +/// output has amount=0 < TX_MIN_OUTPUT_AMOUNT (546 duffs). That causes the +/// reconciliation step (which removes spent entries from utxos) to be skipped. +/// The result: the just-spent input stays in utxos and gets re-selected for +/// the next coin-selection call, producing a double-spend. +/// +/// -isSpent: reads spentOutputs, which IS correct. This category exposes it +/// with plain scalar arguments so Swift code does not need to construct an +/// NSValue manually (requires @encode, unavailable in Swift). +@interface DSAccount (SpentInputCheck) + +/// Returns YES if the output identified by (txHash, index) is already listed +/// in the account's spentOutputs set. +- (BOOL)isInputSpent:(UInt256)txHash atIndex:(uint32_t)index; + +/// Returns YES if the account has an UNCONFIRMED transaction that carries an +/// OP_RETURN output (script first byte 0x6a). This app only builds OP_RETURN +/// outputs for Maya swaps, so this is a wallet-level "a swap is still +/// confirming" check — used to block other spends until the swap confirms. +- (BOOL)hasUnconfirmedSwapTransaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashWallet/Sources/Models/Transactions/DSAccount+SpentInputCheck.m b/DashWallet/Sources/Models/Transactions/DSAccount+SpentInputCheck.m new file mode 100644 index 000000000..f6ef30d8b --- /dev/null +++ b/DashWallet/Sources/Models/Transactions/DSAccount+SpentInputCheck.m @@ -0,0 +1,45 @@ +// +// Created by Roman Chornyi +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSAccount+SpentInputCheck.h" +#import +#import +#import + +@implementation DSAccount (SpentInputCheck) + +- (BOOL)isInputSpent:(UInt256)txHash atIndex:(uint32_t)index { + DSUTXO utxo = {.hash = txHash, .n = (unsigned long)index}; + return [self isSpent:dsutxo_obj(utxo)]; +} + +- (BOOL)hasUnconfirmedSwapTransaction { + for (DSTransaction *tx in self.allTransactions) { + if (tx.confirmed) { + continue; + } + for (DSTransactionOutput *output in tx.outputs) { + NSData *script = output.outScript; + if (script.length > 0 && ((const uint8_t *)script.bytes)[0] == 0x6a) { + return YES; + } + } + } + return NO; +} + +@end diff --git a/DashWallet/Sources/Models/Transactions/SendCoinsService.swift b/DashWallet/Sources/Models/Transactions/SendCoinsService.swift index f4fc23534..4b18865e4 100644 --- a/DashWallet/Sources/Models/Transactions/SendCoinsService.swift +++ b/DashWallet/Sources/Models/Transactions/SendCoinsService.swift @@ -18,12 +18,49 @@ import Combine public final class SendCoinsService: NSObject { + private enum PaymentAuthenticationResult { + case success + case cancelled + case failed + } + private let transactionManager: DSTransactionManager = DWEnvironment.sharedInstance().currentChainManager.transactionManager // Payment processing private var paymentProcessor: DWPaymentProcessor? private var pendingPaymentContinuation: CheckedContinuation? + @MainActor + private func authenticateForPayment() async -> PaymentAuthenticationResult { + await withCheckedContinuation { continuation in + DSAuthenticationManager.sharedInstance().authenticate( + withPrompt: nil, + usingBiometricAuthentication: DWGlobalOptions.sharedInstance().biometricAuthEnabled, + alertIfLockout: true + ) { authenticatedOrSuccess, _, cancelled in + if cancelled { + continuation.resume(returning: .cancelled) + } else if authenticatedOrSuccess { + continuation.resume(returning: .success) + } else { + continuation.resume(returning: .failed) + } + } + } + } + + @MainActor + private func ensureAuthenticatedForPayment() async throws { + switch await authenticateForPayment() { + case .success: + return + case .cancelled: + throw DashSpendError.paymentProcessingError("Authentication cancelled") + case .failed: + throw DashSpendError.paymentProcessingError("Authentication failed") + } + } + func sendCoins(address: String, amount: UInt64, inputSelector: SingleInputAddressSelector? = nil, adjustAmountDownwards: Bool = false) async throws -> DSTransaction { @@ -75,6 +112,21 @@ public final class SendCoinsService: NSObject { } } + // Guard against stale UTXOs: DSAccount.updateBalance skips reconciliation + // (removing spent entries from utxos) when a transaction is "pending". + // spentOutputs IS always correct; utxos may lag after a Maya swap because + // the OP_RETURN output (amount=0 < TX_MIN_OUTPUT_AMOUNT) marks the whole + // swap tx as pending, bypassing the reconciliation step. Reject here to + // prevent broadcasting a double-spend. + for input in transaction.inputs { + if account.isInputSpent(input.inputHash, at: input.index) { + throw DashSpendError.paymentProcessingError( + "Input \(input.index) is already spent by a pending transaction. " + + "Wait for the previous transaction to confirm before sending again." + ) + } + } + let authenticated = await authenticate() if !authenticated { @@ -84,8 +136,12 @@ public final class SendCoinsService: NSObject { // Sign the transaction after authentication account.sign(transaction) - // Register the transaction - account.register(transaction, saveImmediately: false) + // saveImmediately:true: if the app crashes between register and publish the + // transaction is already in Core Data and will be reloaded on next launch. + // NOTE: this does NOT affect UTXO state — updateBalance runs identically + // for both true and false; the standard send path uses false + a post-broadcast + // saveInitial call, but the net Core Data outcome is the same. + account.register(transaction, saveImmediately: true) // Publish the transaction try await transactionManager.publishTransaction(transaction) @@ -93,10 +149,168 @@ public final class SendCoinsService: NSObject { return transaction } + /// Sends a MAYA swap transaction where: + /// - VOUT0 pays DASH to the MAYA vault address + /// - VOUT1 stores MAYA memo in OP_RETURN + /// - VOUT2 is change back to the wallet (if any) + /// + /// DSTransactionSortType.none is used so the wallet builder preserves the + /// vault→memo→change insertion order without BIP69 sorting or shuffling, + /// matching the Android sendRequest.sortByBIP69 = false / shuffleOutputs = false + /// requirement from MayaBlockchainApi.kt. + func sendMayaSwap(vaultAddress: String, dashAmount: UInt64, memo: String) async throws -> DSTransaction { + let memoByteCount = memo.utf8.count + DSLogger.log("sendMayaSwap memo=\(memo) bytes=\(memoByteCount)") + guard memoByteCount <= 80 else { + throw DashSpendError.paymentProcessingError("Swap memo (\(memoByteCount) bytes) exceeds the 80-byte OP_RETURN limit. Try a simpler destination asset.") + } + + let chain = DWEnvironment.sharedInstance().currentChain + let account = DWEnvironment.sharedInstance().currentAccount + let transaction = DSTransaction(on: chain) + + let vaultScript = NSData.scriptPubKey(forAddress: vaultAddress, for: chain) + let memoScript = opReturnScript(for: memo) + + _ = account.update( + transaction, + forAmounts: [NSNumber(value: dashAmount), NSNumber(value: 0)], + toOutputScripts: [vaultScript, memoScript], + withFee: true, + sortType: DSTransactionSortType.none // preserve vault→memo→change order + ) + + guard transaction.outputs.count >= 2 else { + logOutputs(transaction, label: "build-failed") + throw DashSpendError.paymentProcessingError("Failed to build MAYA swap outputs") + } + + // Validate VOUT0: must be the vault payment output + guard transaction.outputs[0].address == vaultAddress else { + logOutputs(transaction, label: "order-invalid") + throw DashSpendError.paymentProcessingError("MAYA swap output ordering is invalid") + } + + // Validate VOUT1: must be the OP_RETURN memo output (amount 0, script starts with 0x6a) + let memoOutput = transaction.outputs[1] + guard memoOutput.amount == 0, + let firstByte = memoOutput.outScript.first, firstByte == 0x6a else { + logOutputs(transaction, label: "memo-invalid") + throw DashSpendError.paymentProcessingError("MAYA memo output is invalid") + } + + // Pre-sign diagnostics: log full tx structure, fee, and VIN0 relationship + // (mirrors Android's log.info("maya swap transaction: {}", sendRequest.tx)) + let txSize = transaction.size + let estimatedFee = chain.fee(forTxSize: UInt(txSize)) + let feePerByte = txSize > 0 ? estimatedFee / UInt64(txSize) : 0 + + // Real wire size of the OP_RETURN output: transaction.size uses TX_OUTPUT_SIZE=34 + // for every output, but OP_RETURN is 8 (value) + varint(scriptLen) + scriptLen. + let memoScriptLen = memoOutput.outScript.count + let varintLen = memoScriptLen < 253 ? 1 : 3 + let realOpReturnSize = 8 + varintLen + memoScriptLen + let realEstimatedSize = Int(txSize) - 34 + realOpReturnSize + + // FEE FIX: account.update uses TX_OUTPUT_SIZE=34 for every output, but the + // OP_RETURN output is realOpReturnSize bytes. The builder therefore under-pays + // the fee by extraBytes duffs. Close the gap by reducing the change output (VOUT2) + // so the effective fee rate clears TX_FEE_PER_B against the real wire size. + // DSTransactionOutput.amount is readonly in the public header but readwrite in its + // class extension, so KVC setValue(_:forKey:) reaches the private setter safely. + var correctedFee = estimatedFee + let extraBytes = realEstimatedSize > Int(txSize) ? realEstimatedSize - Int(txSize) : 0 + if extraBytes > 0 { + let requiredFee = chain.fee(forTxSize: UInt(realEstimatedSize) + 1) // +1 byte safety margin + if requiredFee > estimatedFee, transaction.outputs.count >= 3 { + let feeDelta = requiredFee - estimatedFee + let changeOutput = transaction.outputs[2] + guard changeOutput.amount >= feeDelta + chain.minOutputAmount else { + throw DashSpendError.paymentProcessingError( + "Change output (\(changeOutput.amount) duffs) too small to absorb fee correction (\(feeDelta) duffs)" + ) + } + let correctedChangeAmount = changeOutput.amount - feeDelta + changeOutput.setValue(NSNumber(value: correctedChangeAmount), forKey: "amount") + correctedFee = requiredFee + DSLogger.log("sendMayaSwap FEE FIX: extraBytes=\(extraBytes) feeDelta=\(feeDelta) requiredFee=\(requiredFee) correctedChange=\(correctedChangeAmount)") + } + } + + DSLogger.log("sendMayaSwap pre-sign: inputs=\(transaction.inputs.count) outputs=\(transaction.outputs.count) size=\(txSize) estimatedFee=\(estimatedFee) feePerByte=\(feePerByte)") + logOutputs(transaction, label: "pre-sign") + logVin0(transaction, chain: chain) + + // Guard: check fee rate against the REAL wire size, not the builder's undercount. + // Before this fix, feePerByte was measured against txSize (= under-counted estimate), + // giving a false pass even when the relay fee was below the 1 duff/byte minimum. + let correctedFeePerByte = realEstimatedSize > 0 ? correctedFee / UInt64(realEstimatedSize) : 0 + guard correctedFeePerByte >= TX_FEE_PER_B else { + throw DashSpendError.paymentProcessingError( + "Transaction fee too low: \(correctedFeePerByte) duff/byte against real size \(realEstimatedSize) (min \(TX_FEE_PER_B))" + ) + } + + // Guard against stale UTXOs caused by DSAccount.updateBalance treating the + // Maya swap tx as "pending" (OP_RETURN output amount=0 < TX_MIN_OUTPUT_AMOUNT, + // i.e. 546 duffs). The pending path calls `continue` before the reconciliation + // step that removes spent entries from `utxos`, so the just-spent input stays + // visible to the next coin-selection call and gets re-selected — producing a + // double-spend. `spentOutputs` IS always updated correctly (before the continue); + // `-isInputSpent:atIndex:` reads that set and catches stale re-use early. + for input in transaction.inputs { + if account.isInputSpent(input.inputHash, at: input.index) { + throw DashSpendError.previousSwapPending + } + } + + let authenticated = await authenticate() + if !authenticated { + throw DashSpendError.paymentProcessingError("Authentication cancelled") + } + + account.sign(transaction) + DSLogger.log("sendMayaSwap post-sign txid=\(transaction.txHashHexString)") + // saveImmediately:true: crash-safe — tx is in Core Data before broadcast. + // Does NOT affect UTXO state; updateBalance runs the same either way. + account.register(transaction, saveImmediately: true) + try await transactionManager.publishTransaction(transaction) + DSLogger.log("sendMayaSwap published txid=\(transaction.txHashHexString)") + return transaction + } + + private func logOutputs(_ tx: DSTransaction, label: String) { + DSLogger.log("sendMayaSwap [\(label)] \(tx.outputs.count) output(s):") + for (i, out) in tx.outputs.enumerated() { + let scriptHex = out.outScript.map { String(format: "%02x", $0) }.joined() + let isOpReturn = out.outScript.first == 0x6a + if isOpReturn { + let payload = out.outScript.dropFirst(2) + let memoStr = String(bytes: payload, encoding: .utf8) ?? scriptHex + DSLogger.log(" [\(i)] amount=\(out.amount) OP_RETURN memo=\(memoStr)") + } else { + DSLogger.log(" [\(i)] amount=\(out.amount) addr=\(out.address ?? "(none)") script=\(scriptHex.prefix(40))") + } + } + } + + private func logVin0(_ tx: DSTransaction, chain: DSChain) { + guard let vin0 = tx.inputs.first else { return } + var hash = vin0.inputHash + let hashData = withUnsafeBytes(of: &hash) { Data($0) } + let hashHex = hashData.reversed().map { String(format: "%02x", $0) }.joined() + DSLogger.log("sendMayaSwap VIN0: prevout=\(hashHex):\(vin0.index)") + if let parentTx = chain.transaction(forHash: vin0.inputHash), + Int(vin0.index) < parentTx.outputs.count { + let vin0Out = parentTx.outputs[Int(vin0.index)] + DSLogger.log("sendMayaSwap VIN0 script: addr=\(vin0Out.address ?? "(none)")") + } + } + // MARK: - BIP70 func payWithDashUrl(url paymentUrlString: String) async throws -> DSTransaction { - // Create payment input from the URL + // Validate URL and payment request before prompting auth. guard let paymentUrl = URL(string: paymentUrlString) else { throw DashSpendError.paymentProcessingError("Invalid payment URL") } @@ -109,6 +323,9 @@ public final class SendCoinsService: NSObject { guard paymentInput.request?.r != nil else { throw DashSpendError.paymentProcessingError("Invalid payment request") } + + // Keep CTX (BIP70) behavior consistent with direct send flow. + try await ensureAuthenticatedForPayment() return try await fetchAndProcessPaymentRequest(paymentInput: paymentInput) } @@ -144,6 +361,41 @@ public final class SendCoinsService: NSObject { continuation.resume(throwing: DashSpendError.paymentProcessingError("No transaction returned")) } } + + private func authenticate() async -> Bool { + @MainActor func performAuthentication() async -> Bool { + await withCheckedContinuation { continuation in + DSAuthenticationManager.sharedInstance().authenticate( + withPrompt: nil, + usingBiometricAuthentication: DWGlobalOptions.sharedInstance().biometricAuthEnabled, + alertIfLockout: true + ) { authenticatedOrSuccess, _, cancelled in + continuation.resume(returning: authenticatedOrSuccess && !cancelled) + } + } + } + return await performAuthentication() + } + + private func opReturnScript(for memo: String) -> Data { + let payload = Data(memo.utf8) + var script = Data([0x6a]) // OP_RETURN + + let count = payload.count + if count <= 0x4b { + script.append(UInt8(count)) + } else if count <= 0xff { + script.append(0x4c) // OP_PUSHDATA1 + script.append(UInt8(count)) + } else { + script.append(0x4d) // OP_PUSHDATA2 + script.append(UInt8(count & 0xff)) + script.append(UInt8((count >> 8) & 0xff)) + } + + script.append(payload) + return script + } } // MARK: - DWPaymentProcessorDelegate diff --git a/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard b/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard index ec4a9a638..3198c6f14 100644 --- a/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard +++ b/DashWallet/Sources/UI/Buy Sell/BuySellPortal.storyboard @@ -586,9 +586,6 @@ - - - diff --git a/DashWallet/Sources/UI/Buy Sell/BuySellPortalScreen.swift b/DashWallet/Sources/UI/Buy Sell/BuySellPortalScreen.swift new file mode 100644 index 000000000..6cfa9dd6e --- /dev/null +++ b/DashWallet/Sources/UI/Buy Sell/BuySellPortalScreen.swift @@ -0,0 +1,174 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +private struct MenuCardStyle: ViewModifier { + func body(content: Content) -> some View { + content + .padding(6) + .background(Color.secondaryBackground) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) + .padding(.horizontal, 20) + } +} + +struct BuySellPortalScreen: View { + @Environment(\.colorScheme) + private var colorScheme + @ObservedObject var model: BuySellPortalModel + private let onBack: () -> Void + private let onTopper: () -> Void + private let onCoinbase: () -> Void + private let onUphold: () -> Void + + init(model: BuySellPortalModel, + onBack: @escaping () -> Void, + onTopper: @escaping () -> Void, + onCoinbase: @escaping () -> Void, + onUphold: @escaping () -> Void) { + self.model = model + self.onBack = onBack + self.onTopper = onTopper + self.onCoinbase = onCoinbase + self.onUphold = onUphold + } + + private var coinbaseItem: ServiceItem? { + model.items.first { $0.service == .coinbase } + } + + private var upholdItem: ServiceItem? { + model.items.first { $0.service == .uphold } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavBarBack(onBack: onBack) + + TopIntro( + title: NSLocalizedString("Buy & Sell Dash", comment: "Buy Sell Dash"), + subtitle: NSLocalizedString("Select a service to buy, sell, convert and transfer Dash", comment: "Buy Sell Dash") + ) + + VStack(spacing: 20) { + // Topper + VStack(spacing: 0) { + MenuItem( + title: NSLocalizedString("Topper", comment: "Buy Sell Dash"), + subtitle: NSLocalizedString("Buy Dash · No account needed", comment: "Buy Sell Dash"), + icon: .custom("service-topper", maxHeight: 30), + action: onTopper + ) + .frame(minHeight: 56) + + HStack(spacing: 6) { + Image("service-uphold") + .resizable() + .scaledToFit() + .frame(width: 18, height: 18) + Text(NSLocalizedString("Powered by Uphold", comment: "Buy Sell Dash")) + .font(.caption) + .foregroundColor(.secondaryText) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background(colorScheme == .dark ? Color.whiteAlpha5 : Color.primaryBackground) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + .modifier(MenuCardStyle()) + + // Coinbase + VStack(spacing: 0) { + MenuItem( + title: NSLocalizedString("Coinbase", comment: "Buy Sell Dash"), + subtitle: coinbaseSubtitle, + icon: .custom("service-coinbase", maxHeight: 30), + trailingView: serviceBalanceView(item: coinbaseItem), + action: onCoinbase + ) + .frame(minHeight: 56) + } + .modifier(MenuCardStyle()) + + // Uphold + VStack(spacing: 0) { + MenuItem( + title: NSLocalizedString("Uphold", comment: "Buy Sell Dash"), + subtitle: upholdSubtitle, + icon: .custom("service-uphold", maxHeight: 30), + trailingView: serviceBalanceView(item: upholdItem), + action: onUphold + ) + .frame(minHeight: 56) + } + .modifier(MenuCardStyle()) + } + + Spacer() + } + .background(Color.primaryBackground) + .navigationBarHidden(true) + } + + // MARK: - Helpers + + private func serviceSubtitle(for item: ServiceItem?) -> String? { + guard let item else { + return NSLocalizedString("Link your account", comment: "Buy Sell Dash") + } + switch item.status { + case .syncing: + return NSLocalizedString("Syncing...", comment: "Buy Sell Dash") + case .authorized: + return nil + default: + return NSLocalizedString("Link your account", comment: "Buy Sell Dash") + } + } + + private var coinbaseSubtitle: String? { serviceSubtitle(for: coinbaseItem) } + private var upholdSubtitle: String? { serviceSubtitle(for: upholdItem) } + + private func serviceBalanceView(item: ServiceItem?) -> AnyView? { + guard let item, item.status == .authorized, let formattedAmount = item.dashBalanceFormatted else { + return nil + } + + return AnyView( + VStack(alignment: .trailing, spacing: 2) { + HStack(spacing: 4) { + Text(formattedAmount) + .font(.footnote) + .fontWeight(.medium) + Image("dash-logo-black") + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 12) + } + .foregroundColor(.primaryText) + if let fiat = item.fiatBalanceFormatted, (item.dashBalance ?? 0) > 0 { + Text(fiat) + .font(.caption) + .foregroundColor(.secondaryText) + } + } + ) + } +} diff --git a/DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift b/DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift index b75c08af1..ba2057550 100644 --- a/DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift +++ b/DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift @@ -19,8 +19,35 @@ import SwiftUI +// MARK: - MenuItemButtonStyle + +private struct MenuItemButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.97 : 1.0) + .opacity(configuration.isPressed ? 0.85 : 1.0) + .animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed) + } +} + +// MARK: - MenuCardStyle + +private struct MenuCardStyle: ViewModifier { + func body(content: Content) -> some View { + content + .padding(6) + .background(Color.secondaryBackground) + .clipShape(.rect(cornerRadius: 20)) + .shadow(color: .shadow, radius: 10, x: 0, y: 5) + } +} + +// MARK: - BuySellPortalView + struct BuySellPortalView: View { let showCoinbase: Bool + @ObservedObject var model: BuySellPortalModel + var onBack: () -> Void var onUphold: () -> Void var onCoinbase: () -> Void var onTopper: () -> Void @@ -28,118 +55,143 @@ struct BuySellPortalView: View { var body: some View { ScrollView { - VStack(alignment: .leading, spacing: 20) { - // Title and subtitle - VStack(alignment: .leading, spacing: 4) { - Text(NSLocalizedString("Buy & sell Dash", comment: "Buy Sell Portal")) - .font(.system(size: 28, weight: .bold)) - .foregroundColor(.primaryText) - - Text(NSLocalizedString("Select a service to buy, sell, convert and transfer Dash", comment: "Buy Sell Portal")) - .font(.system(size: 15, weight: .regular)) - .foregroundColor(.secondaryText) - } - .padding(.trailing, 40) + VStack(spacing: 0) { + NavigationBar(leading: { NavigationBarElement.back.button { onBack() } }) - // Card 1: Uphold + Coinbase - VStack(spacing: 2) { - serviceRow( - icon: "shortcut_uphold", - name: Service.uphold.title, - subtitle: Service.uphold.subtitle, - action: onUphold - ) + VStack(alignment: .leading, spacing: 20) { + topIntro + topperCard + upholdCard if showCoinbase { - serviceRow( - icon: "shortcut_coinbase", - name: Service.coinbase.title, - subtitle: Service.coinbase.subtitle, - action: onCoinbase - ) - } - } - .padding(6) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) - - // Card 2: Topper + "Powered by Uphold" badge - VStack(spacing: 0) { - serviceRow( - icon: "shortcut_topper", - name: Service.topper.title, - subtitle: Service.topper.subtitle, - action: onTopper - ) - - HStack(spacing: 6) { - Image("uphold_logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 16) - - Text(NSLocalizedString("Powered by Uphold", comment: "Buy Sell Portal")) - .font(.system(size: 12, weight: .regular)) - .foregroundColor(.primaryText) + coinbaseCard } - .frame(maxWidth: .infinity) - .padding(.vertical, 5) - .background(Color(.sRGB, red: 176.0 / 255.0, green: 182.0 / 255.0, blue: 188.0 / 255.0, opacity: 0.1)) - .cornerRadius(8) - } - .padding(6) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) - - // Card 3: Maya - VStack(spacing: 0) { - serviceRow( - icon: Service.maya.icon, - name: Service.maya.title, - subtitle: Service.maya.subtitle, - action: onMaya - ) + mayaCard } - .padding(6) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) + .padding(.top, 10) + .padding(.horizontal, 20) } - .padding(.horizontal, 20) - .padding(.top, 10) } - .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.primaryBackground.ignoresSafeArea()) } - @ViewBuilder - private func serviceRow(icon: String, name: String, subtitle: String, action: @escaping () -> Void) -> some View { - Button(action: action) { - HStack(spacing: 16) { - Image(icon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 26, height: 26) - - VStack(alignment: .leading, spacing: 2) { - Text(name) - .font(.system(size: 15, weight: .medium)) - .foregroundColor(.primaryText) - - Text(subtitle) - .font(.system(size: 15, weight: .regular)) - .foregroundColor(.secondaryText) - } + // MARK: - Cards - Spacer() - } - .padding(.horizontal, 10) - .padding(.vertical, 12) - .frame(minHeight: 56) + private var topperCard: some View { + VStack(spacing: 2) { + menuItem(for: .topper, action: onTopper) + poweredByUpholdBadge + } + .modifier(MenuCardStyle()) + } + + private var upholdCard: some View { + VStack(spacing: 2) { + menuItem(for: .uphold, action: onUphold) + } + .modifier(MenuCardStyle()) + } + + private var coinbaseCard: some View { + VStack(spacing: 2) { + menuItem(for: .coinbase, action: onCoinbase) + } + .modifier(MenuCardStyle()) + } + + private var mayaCard: some View { + VStack(spacing: 2) { + menuItem(for: .maya, action: onMaya) + } + .modifier(MenuCardStyle()) + } + + private var poweredByUpholdBadge: some View { + HStack(spacing: 6) { + Image("uphold_logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12, height: 16) + + Text(NSLocalizedString("Powered by Uphold", comment: "Buy Sell Portal")) + .font(.caption1) + .foregroundColor(.primaryText) } - .buttonStyle(.plain) - .cornerRadius(10) + .frame(maxWidth: .infinity) + .padding(.vertical, 5) + .background(Color.gray300Alpha10) + .clipShape(.rect(cornerRadius: 14)) + } + + // MARK: - Helpers + + private func menuItem(for service: Service, action: @escaping () -> Void) -> some View { + let item = model.items.first { $0.service == service } + + // Show balance when an account-based service is connected. + // For 0 balance we still show the amount (not the subtitle) so the + // "connected" state is clear — MenuItem hides the fiat line for 0. + let isConnected = item?.status == .authorized + && (service == .uphold || service == .coinbase) + + let dashAmount: Int64? = isConnected + ? Int64(item?.dashBalance ?? 0) + : nil + + let subtitle: String? = isConnected + ? nil + : service.subtitle + + // Apply MenuItemButtonStyle to MenuItem's inner Button so the press + // animation works without double-wrapping taps. + return MenuItem( + title: service.title, + subtitle: subtitle, + icon: .custom(service.icon), + dashAmount: dashAmount, + showDashAmountDirection: false, + overrideFiatAmount: isConnected ? item?.fiatBalanceFormatted : nil, + action: action + ) + .buttonStyle(MenuItemButtonStyle()) + } + + // MARK: - Subviews + + private var topIntro: some View { + TopIntro( + title: NSLocalizedString("Buy & Sell Dash", comment: "Buy Sell Portal"), + subtitle: NSLocalizedString("Select a service to buy, sell, convert and transfer Dash", comment: "Buy Sell Portal") + ) } } + +#if DEBUG +private extension BuySellPortalModel { + static var preview: BuySellPortalModel { BuySellPortalModel() } +} + +#Preview("BuySell Portal - Coinbase") { + BuySellPortalView( + showCoinbase: true, + model: .preview, + onBack: {}, + onUphold: {}, + onCoinbase: {}, + onTopper: {}, + onMaya: {} + ) +} + +#Preview("BuySell Portal - No Coinbase") { + BuySellPortalView( + showCoinbase: false, + model: .preview, + onBack: {}, + onUphold: {}, + onCoinbase: {}, + onTopper: {}, + onMaya: {} + ) +} +#endif diff --git a/DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift b/DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift index 3c84a38ff..b4f86a09b 100644 --- a/DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift +++ b/DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift @@ -24,15 +24,14 @@ import Combine // MARK: - BuySellPortalViewController @objc -final class BuySellPortalViewController: UIViewController, NavigationBarDisplayable, NavigationStackControllable { - - var isBackButtonHidden: Bool { false } - +final class BuySellPortalViewController: UIViewController, NavigationBarDisplayable { + var isNavigationBarHidden: Bool { true } + private var model = BuySellPortalModel() private let topperViewModel = TopperViewModel.shared private var locationRequested = false - + @objc var showCloseButton = false - + func shouldPopViewController() -> Bool { if navigationController?.viewControllers.first == self { dismiss(animated: true) @@ -40,14 +39,18 @@ final class BuySellPortalViewController: UIViewController, NavigationBarDisplaya } return true } - + + func closeAction() { + dismiss(animated: true) + } + @objc func upholdAction() { let vc = IntegrationViewController.controller(model: UpholdPortalModel()) vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } - + @objc func coinbaseAction() { Task { @@ -55,29 +58,29 @@ final class BuySellPortalViewController: UIViewController, NavigationBarDisplaya requestLocation() return } - + if DWLocationManager.shared.currentLocation == nil { self.locationRequested = true return } - + if await isGeoblocked() { informGeoblocked() return } - + navigateToCoinbase() } } - + func navigateToCoinbase() { if Coinbase.shared.isAuthorized { let vc = IntegrationViewController.controller(model: CoinbaseEntryPointModel()) vc.userSignedOutBlock = { [weak self] isNeedToShowSignOutError in guard let self else { return } - + self.navigationController!.popToViewController(self, animated: true) - + if isNeedToShowSignOutError { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now().advanced(by: .milliseconds(500))) { self.showAlert(with: NSLocalizedString("Error", comment: ""), @@ -94,7 +97,7 @@ final class BuySellPortalViewController: UIViewController, NavigationBarDisplaya navigationController?.pushViewController(vc, animated: true) } } - + @objc func topperAction() { let walletName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "Dash Wallet" @@ -104,7 +107,7 @@ final class BuySellPortalViewController: UIViewController, NavigationBarDisplaya present(safariViewController, animated: true) } } - + func mayaAction() { // UIHostingController hides the UIKit navigation bar when pushed directly, // so a thin UIViewController wrapper is required for BaseNavigationController @@ -113,12 +116,15 @@ final class BuySellPortalViewController: UIViewController, NavigationBarDisplaya vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } - + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + model.refreshData() + } + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.dw_secondaryBackground() - title = nil navigationItem.largeTitleDisplayMode = .never @@ -128,19 +134,28 @@ final class BuySellPortalViewController: UIViewController, NavigationBarDisplaya private func setupSwiftUIView() { let portalView = BuySellPortalView( showCoinbase: CoinbaseDataSource.shouldShow(), + model: model, + onBack: { [weak self] in + guard let self else { return } + if self.showCloseButton { + self.closeAction() + } else { + self.navigationController?.popViewController(animated: true) + } + }, onUphold: { [weak self] in self?.upholdAction() }, onCoinbase: { [weak self] in self?.coinbaseAction() }, onTopper: { [weak self] in self?.topperAction() }, onMaya: { [weak self] in self?.mayaAction() } ) - + let hostingController = UIHostingController(rootView: portalView) hostingController.view.backgroundColor = .clear - + addChild(hostingController) view.addSubview(hostingController.view) hostingController.didMove(toParent: self) - + hostingController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), @@ -149,17 +164,17 @@ final class BuySellPortalViewController: UIViewController, NavigationBarDisplaya hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } - + @objc class func controller() -> BuySellPortalViewController { BuySellPortalViewController() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) DWLocationManager.shared.add(observer: self) } - + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) DWLocationManager.shared.remove(observer: self) @@ -196,11 +211,16 @@ extension BuySellPortalViewController: DWLocationObserver { } private func requestLocation() { + let geoblockMessage = NSLocalizedString( + "Due to regulatory constraints, we need to verify that you are not in the UK. We only check your location when you enter the Coinbase features.", + comment: "Geoblock" + ) + showModalDialog( style: .regular, icon: .system("location.fill"), heading: NSLocalizedString("Location", comment: ""), - textBlock1: NSLocalizedString("Due to regulatory constraints, we need to verify that you are not in the UK. We only check your location when you enter the Coinbase features.", comment: "Geoblock"), + textBlock1: geoblockMessage, positiveButtonText: NSLocalizedString("Continue", comment: ""), positiveButtonAction: { [weak self] in self?.locationRequested = true diff --git a/DashWallet/Sources/UI/Buy Sell/Model/BuySellPortalModel.swift b/DashWallet/Sources/UI/Buy Sell/Model/BuySellPortalModel.swift index 8a7ce9656..d95284902 100644 --- a/DashWallet/Sources/UI/Buy Sell/Model/BuySellPortalModel.swift +++ b/DashWallet/Sources/UI/Buy Sell/Model/BuySellPortalModel.swift @@ -56,10 +56,10 @@ extension Service { var icon: String { switch self { - case .coinbase: return "portal.coinbase" - case .uphold: return "portal.uphold" - case .topper: return "portal.topper" - case .maya: return "portal.maya" + case .coinbase: return "menu-coinbase" + case .uphold: return "menu-uphold" + case .topper: return "menu-topper" + case .maya: return "menu-maya" } } @@ -81,25 +81,13 @@ extension Service { } } -// MARK: - BuySellPortalModelDelegate - -protocol BuySellPortalModelDelegate: AnyObject { - func serviceItemsDidChange(); -} - // MARK: - BuySellPortalModel -class BuySellPortalModel: NetworkReachabilityHandling { +class BuySellPortalModel: ObservableObject, NetworkReachabilityHandling { var networkStatusDidChange: ((NetworkStatus) -> ())? internal var reachabilityObserver: Any! - weak var delegate: BuySellPortalModelDelegate? - - var items: [ServiceItem] = [] { - didSet { - delegate?.serviceItemsDidChange() - } - } + @Published var items: [ServiceItem] = [] var services: [Service] = Service.allCases private var upholdDashCard: DWUpholdCardObject? @@ -109,8 +97,9 @@ class BuySellPortalModel: NetworkReachabilityHandling { init() { serviceItemDataProvider = ServiceDataProviderImpl() serviceItemDataProvider.listenForData { [weak self] items in - self?.items = items - self?.delegate?.serviceItemsDidChange() + DispatchQueue.main.async { + self?.items = items + } } networkStatusDidChange = { [weak self] _ in @@ -127,5 +116,3 @@ class BuySellPortalModel: NetworkReachabilityHandling { stopNetworkMonitoring() } } - - diff --git a/DashWallet/Sources/UI/Explore Dash/ExploreMenuScreen.swift b/DashWallet/Sources/UI/Explore Dash/ExploreMenuScreen.swift new file mode 100644 index 000000000..7e8fc4ae4 --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/ExploreMenuScreen.swift @@ -0,0 +1,211 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import UIKit + +struct ExploreMenuScreen: View { + private let vc: UINavigationController + private let showBackButton: Bool + private let onShowSendPayment: () -> Void + private let onShowReceivePayment: () -> Void + private let onShowGiftCard: (Data) -> Void + + @State private var showSyncingAlert = false + + private var isTestnet: Bool { + DWEnvironment.sharedInstance().currentChain.isTestnet() + } + + init(vc: UINavigationController, + showBackButton: Bool = true, + onShowSendPayment: @escaping () -> Void, + onShowReceivePayment: @escaping () -> Void, + onShowGiftCard: @escaping (Data) -> Void) { + self.vc = vc + self.showBackButton = showBackButton + self.onShowSendPayment = onShowSendPayment + self.onShowReceivePayment = onShowReceivePayment + self.onShowGiftCard = onShowGiftCard + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + if showBackButton { + NavBarBack { + vc.popViewController(animated: true) + } + } + + TopIntro( + title: NSLocalizedString("Explore Dash", comment: ""), + subtitle: NSLocalizedString("Find merchants that accept Dash, where to buy it and how to earn income with it.", comment: "") + ) + .padding(.leading, 20) + .padding(.trailing, 60) + .padding(.top, 10) + .padding(.bottom, 20) + + // Menu list + VStack(spacing: 2) { + if isTestnet { + MenuItem( + title: NSLocalizedString("Get Test Dash", comment: ""), + subtitle: NSLocalizedString("Test Dash is free and can be obtained from what is called a faucet.", comment: ""), + icon: .custom("dashCurrency", maxHeight: 30), + action: { getTestDashAction() } + ) + .frame(minHeight: 56) + } + + MenuItem( + title: NSLocalizedString("Where to Spend?", comment: ""), + subtitle: NSLocalizedString("Find merchants that accept Dash payments", comment: ""), + icon: .custom("image-menu-merchant", maxHeight: 30), + action: { showWhereToSpend() } + ) + .frame(minHeight: 56) + + MenuItem( + title: NSLocalizedString("ATMs", comment: ""), + subtitle: NSLocalizedString("Find ATMs where you can buy or sell Dash", comment: ""), + icon: .custom("image-menu-atm", maxHeight: 30), + action: { showAtms() } + ) + .frame(minHeight: 56) + + MenuItem( + title: NSLocalizedString("Staking", comment: ""), + subtitleView: AnyView(StakingSubtitle()), + icon: .custom("image-menu-staking", maxHeight: 30), + iconAlignment: .top, + action: { showStaking() } + ) + .frame(minHeight: 56) + } + .padding(6) + .background(Color.secondaryBackground) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) + .padding(.horizontal, 20) + + Spacer() + } + .background(Color.primaryBackground) + .navigationBarHidden(true) + .alert(NSLocalizedString("The chain is syncing…", comment: ""), isPresented: $showSyncingAlert) { + Button(NSLocalizedString("Go to CrowdNode website", comment: "")) { + UIApplication.shared.open(CrowdNodeObjcWrapper.crowdNodeWebsiteUrl(), options: [:], completionHandler: nil) + } + Button(NSLocalizedString("Close", comment: ""), role: .cancel) { } + } message: { + Text(NSLocalizedString("Wait until the chain is fully synced, so we can review your transaction history. Visit CrowdNode website to log in or sign up.", comment: "")) + } + } + + @ViewBuilder + private func StakingSubtitle() -> some View { + VStack(alignment: .leading, spacing: 11) { + Text(NSLocalizedString("Easily stake Dash and earn passive income with a few simple clicks", comment: "")) + .font(.footnote) + .foregroundColor(.tertiaryText) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + + CrowdNodeAPYBadge() + .frame(height: 24) + .fixedSize(horizontal: true, vertical: false) + } + .padding(.top, 2) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func getTestDashAction() { + let account = DWEnvironment.sharedInstance().currentAccount + if let paymentAddress = account.receiveAddress { + UIPasteboard.general.string = paymentAddress + } + if let url = URL(string: "http://faucet.testnet.networks.dash.org/") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + private func showWhereToSpend() { + if UserDefaults.standard.bool(forKey: kMerchantTypesShown) != true { + let hostingController = UIHostingController(rootView: MerchantTypesDialog { + UserDefaults.standard.setValue(true, forKey: kMerchantTypesShown) + showMerchants() + }) + hostingController.setDetent(640) + vc.present(hostingController, animated: true) + } else { + showMerchants() + } + } + + private func showMerchants() { + let merchantVC = MerchantListViewController() + merchantVC.payWithDashHandler = { onShowSendPayment() } + merchantVC.onGiftCardPurchased = { txId in + vc.popToRootViewController(animated: false) + onShowGiftCard(txId) + } + vc.pushViewController(merchantVC, animated: true) + } + + private func showAtms() { + let atmVC = AtmListViewController() + atmVC.payWithDashHandler = { onShowReceivePayment() } + atmVC.sellDashHandler = { onShowSendPayment() } + vc.pushViewController(atmVC, animated: true) + } + + private func showStaking() { + if SyncingActivityMonitor.shared.state == .syncDone { + let stakingVC = CrowdNodeModelObjcWrapper.getRootVC() + vc.pushViewController(stakingVC, animated: true) + } else { + showSyncingAlert = true + } + } +} + +struct CrowdNodeAPYBadge: View { + private static let systemGreen = Color(red: 98 / 255, green: 182 / 255, blue: 125 / 255) + + private var apy: String { + let apyValue = CrowdNode.shared.crowdnodeAPY + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .percent + numberFormatter.minimumFractionDigits = 0 + numberFormatter.maximumFractionDigits = 2 + numberFormatter.multiplier = 1 + return numberFormatter.string(from: NSNumber(value: apyValue)) ?? "" + } + + var body: some View { + HStack(spacing: 4) { + Text(String.localizedStringWithFormat(NSLocalizedString("Current APY = %@", comment: "CrowdNode"), apy)) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(Self.systemGreen) + } + .padding(.horizontal, 8) + .frame(height: 24) + .background(Self.systemGreen.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/ExploreViewController.swift b/DashWallet/Sources/UI/Explore Dash/ExploreViewController.swift index 662b39529..c145eb205 100644 --- a/DashWallet/Sources/UI/Explore Dash/ExploreViewController.swift +++ b/DashWallet/Sources/UI/Explore Dash/ExploreViewController.swift @@ -18,7 +18,7 @@ import UIKit import SwiftUI -private let kMerchantTypesShown = "merchantTypesInfoDialogShownKey" +let kMerchantTypesShown = "merchantTypesInfoDialogShownKey" private let kExploreHeaderViewHeight: CGFloat = 351.0 // MARK: - DWExploreTestnetViewControllerDelegate @@ -34,9 +34,9 @@ protocol ExploreViewControllerDelegate: AnyObject { @objc(DWExploreViewController) class ExploreViewController: UIViewController, NavigationFullscreenable { - + @objc weak var delegate: ExploreViewControllerDelegate? - + var requiresNoNavigationBar: Bool { return true } diff --git a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsView.swift b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsView.swift index a165d1c8b..7ab354e56 100644 --- a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsView.swift +++ b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsView.swift @@ -570,3 +570,139 @@ struct POIDetailsView: View { .frame(height: 1/UIScreen.main.scale) } } + +// MARK: - Previews + +#if DEBUG + +private extension ExplorePointOfUse { + static func previewMock( + id: Int64 = 1, + name: String = "Merchant", + category: Category, + active: Bool = true, + city: String? = nil, + territory: String? = nil, + address1: String? = nil, + address2: String? = nil, + address3: String? = nil, + address4: String? = nil, + latitude: Double? = nil, + longitude: Double? = nil, + website: String? = nil, + phone: String? = nil, + logoLocation: String? = nil, + coverImage: String? = nil, + source: String? = nil + ) -> ExplorePointOfUse { + ExplorePointOfUse( + id: id, + name: name, + category: category, + active: active, + city: city, + territory: territory, + address1: address1, + address2: address2, + address3: address3, + address4: address4, + latitude: latitude, + longitude: longitude, + website: website, + phone: phone, + logoLocation: logoLocation, + coverImage: coverImage, + source: source + ) + } +} + +#Preview("Merchant — Gift Card (single provider)") { + let merchant = ExplorePointOfUse.previewMock( + name: "Amazon", + category: .merchant( + ExplorePointOfUse.Merchant( + merchantId: "amazon-ctx", + paymentMethod: .giftCard, + type: .online, + deeplink: nil, + savingsBasisPoints: 1000, + denominationsType: "Fixed", + denominations: [], + redeemType: "online", + giftCardProviders: [ + ExplorePointOfUse.Merchant.GiftCardProviderInfo( + providerId: "ctx", + savingsPercentage: 1000, + denominationsType: "Fixed", + sourceId: "84793fe2-603d-465c-8899-6c90f6e11b63" + ) + ] + ) + ) + ) + ScrollView { + POIDetailsView(merchant: merchant) + } + .background(Color.primaryBackground) +} + +#if PIGGYCARDS_ENABLED +#Preview("Merchant — Gift Card (multi-provider)") { + let merchant = ExplorePointOfUse.previewMock( + name: "Domino's", + category: .merchant( + ExplorePointOfUse.Merchant( + merchantId: "dominos-multi", + paymentMethod: .giftCard, + type: .online, + deeplink: nil, + savingsBasisPoints: 1000, + denominationsType: "Fixed", + denominations: [], + redeemType: "online", + giftCardProviders: [ + ExplorePointOfUse.Merchant.GiftCardProviderInfo( + providerId: "ctx", + savingsPercentage: 1000, + denominationsType: "Fixed", + sourceId: nil + ), + ExplorePointOfUse.Merchant.GiftCardProviderInfo( + providerId: "piggycards", + savingsPercentage: 800, + denominationsType: "Fixed", + sourceId: "45" + ) + ] + ) + ) + ) + ScrollView { + POIDetailsView(merchant: merchant) + } + .background(Color.primaryBackground) +} +#endif + +#Preview("ATM — Buy & Sell") { + let merchant = ExplorePointOfUse.previewMock( + id: 42, + name: "Coinsource", + category: .atm(ExplorePointOfUse.Atm(manufacturer: "Coinsource", type: .buySell)), + city: "Phoenix", + territory: "AZ", + address1: "2800 E Camelback Rd", + latitude: 33.5092, + longitude: -112.0186, + website: "https://coinsource.net", + phone: "6025551234", + source: "Coinsource" + ) + ScrollView { + POIDetailsView(merchant: merchant) + } + .background(Color.primaryBackground) +} + +#endif diff --git a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsViewModel.swift index 71f31bc84..d2033f03f 100644 --- a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsViewModel.swift +++ b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsViewModel.swift @@ -79,12 +79,14 @@ class POIDetailsViewModel: ObservableObject, SyncingActivityMonitorObserver, Net guard let provider = provider, let repository = repositories[provider] else { return } repository.isUserSignedInPublisher + .receive(on: DispatchQueue.main) .sink { [weak self] isSignedIn in self?.isUserSignedIn = isSignedIn } .store(in: &cancellableBag) repository.userEmailPublisher + .receive(on: DispatchQueue.main) .sink { [weak self] email in self?.userEmail = email } @@ -228,10 +230,14 @@ class POIDetailsViewModel: ObservableObject, SyncingActivityMonitorObserver, Net do { let merchantResponse = try await repository.getMerchant(merchantId: m.merchantId) - merchantEnabled = merchantResponse.enabled + await MainActor.run { + self.merchantEnabled = merchantResponse.enabled + } } catch { // On error, default to enabled to avoid blocking legitimate purchases - merchantEnabled = true + await MainActor.run { + self.merchantEnabled = true + } } } diff --git a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift index 13ef18716..365997542 100644 --- a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift +++ b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift @@ -18,6 +18,10 @@ import CoreLocation import UIKit +#if DEBUG +import SwiftUI +#endif + private let kReuseIdentifier = "PointOfUseItemCell" // MARK: - PointOfUseItemCell @@ -99,3 +103,88 @@ extension PointOfUseItemCell { ]) } } + +// MARK: - Previews + +#if DEBUG + +private extension ExplorePointOfUse { + static func previewMock( + name: String = "Merchant", + logoLocation: String? = nil + ) -> ExplorePointOfUse { + ExplorePointOfUse( + id: 1, + name: name, + category: .merchant( + Merchant( + merchantId: "preview-id", + paymentMethod: .dash, + type: .onlineAndPhysical, + deeplink: nil, + savingsBasisPoints: 0, + denominationsType: nil, + denominations: [], + redeemType: nil, + giftCardProviders: [] + ) + ), + active: true, + city: nil, + territory: nil, + address1: nil, + address2: nil, + address3: nil, + address4: nil, + latitude: nil, + longitude: nil, + website: nil, + phone: nil, + logoLocation: logoLocation, + coverImage: nil, + source: nil + ) + } +} + +private struct PointOfUseItemCellRepresentable: UIViewRepresentable { + let pointOfUse: ExplorePointOfUse + + func makeUIView(context: Context) -> UIView { + let cell = PointOfUseItemCell(style: .default, reuseIdentifier: nil) + cell.update(with: pointOfUse) + return cell.contentView + } + + func updateUIView(_ uiView: UIView, context: Context) {} +} + +#Preview("With logo") { + VStack { + PointOfUseItemCellRepresentable( + pointOfUse: .previewMock( + name: "Test Merchant", + logoLocation: "https://piggy.cards/image/catalog/piggycards/logo2023_mobile.png" + ) + ) + .frame(width: 375, height: 60) + } + .background(Color(.systemBackground)) + .padding() +} + +#Preview("Empty logo") { + VStack { + PointOfUseItemCellRepresentable( + pointOfUse: .previewMock( + name: "No Logo Merchant", + logoLocation: nil + ) + ) + .frame(width: 375, height: 60) + } + .background(Color(.systemBackground)) + .padding() +} + +#endif diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendAmountView.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendAmountView.swift new file mode 100644 index 000000000..2b8ed6c3f --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendAmountView.swift @@ -0,0 +1,38 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DashSpendAmountView: View { + let currencySymbol: String + let amount: String + + var body: some View { + Text("\(currencySymbol) \(amount)") + .font(.largeTitle) + .lineLimit(1) + .minimumScaleFactor(0.5) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.horizontal, 40) + } +} + +#Preview { + DashSpendAmountView(currencySymbol: "$", amount: "42.50") + .frame(height: 80) + .background(Color.gray.opacity(0.1)) +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendDenominationRow.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendDenominationRow.swift new file mode 100644 index 000000000..26a356de4 --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendDenominationRow.swift @@ -0,0 +1,70 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DashSpendDenominationRow: View { + let denomination: Decimal + @Binding var count: Int + var inventoryLimit: Int? = nil + + private let fiatFormatter: NumberFormatter = { + let formatter = NumberFormatter.fiatFormatter(currencyCode: kDefaultCurrencyCode) + formatter.minimumFractionDigits = 0 + return formatter + }() + + var body: some View { + HStack { + HStack(spacing: 12) { + Icon(name: .custom("icon-gift_card-piggy_cards", maxHeight: 26)) + + Text(fiatFormatter.string(from: NSDecimalNumber(decimal: denomination)) ?? "") + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primaryText) + } + .frame(maxWidth: .infinity, alignment: .leading) + + DashStepper(count: $count, maxCount: inventoryLimit) + } + } +} + +#Preview { + VStack(spacing: 10) { + DashSpendDenominationRowPreview(denomination: 5) + DashSpendDenominationRowPreview(denomination: 25) + DashSpendDenominationRowPreview(denomination: 100) + } + .padding(20) + .background(Color.secondaryBackground) + .cornerRadius(20) + .padding(20) +} + +private struct DashSpendDenominationRowPreview: View { + let denomination: Decimal + @State private var count = 0 + + var body: some View { + DashSpendDenominationRow( + denomination: denomination, + count: $count + ) + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFixedContent.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFixedContent.swift new file mode 100644 index 000000000..777ccf3d9 --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFixedContent.swift @@ -0,0 +1,127 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DashSpendFixedContent: View { + @ObservedObject var viewModel: DashSpendPayViewModel + @Binding var quantities: [Decimal: Int] + let onAction: () -> Void + + private var total: String { + let formatter = NumberFormatter() + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + let sum = quantities.reduce(Decimal(0)) { $0 + $1.key * Decimal($1.value) } + return formatter.string(from: NSDecimalNumber(decimal: sum)) ?? "0" + } + + private var denominations: [Decimal] { + viewModel.denominations.map { Decimal($0) } + } + + private var actionEnabled: Bool { + !quantities.isEmpty && + viewModel.error == nil && + !viewModel.isLoading && + !viewModel.isProcessingPayment + } + + var body: some View { + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 20) { + DashSpendPayIntro( + merchantIconUrl: viewModel.merchantIconUrl, + merchantTitle: viewModel.merchantTitle, + isMixing: viewModel.isMixing, + dashBalance: viewModel.isMixing ? viewModel.coinJoinBalance : viewModel.walletBalance + ) + + DashSpendAmountView( + currencySymbol: viewModel.currencySymbol, + amount: total + ) + .frame(height: 85) + } + .padding(.horizontal, 20) + .padding(.bottom, 20) + .padding(.top, 10) + + DashSpendMultiplePanel( + denominations: denominations, + quantities: $quantities, + actionEnabled: actionEnabled, + inProgress: viewModel.isProcessingPayment, + error: viewModel.error, + showCost: !quantities.isEmpty && viewModel.error == nil, + costMessage: viewModel.costMessage, + inventoryLimits: viewModel.denominationInventory.reduce(into: [:]) { $0[Decimal($1.key)] = $1.value }, + onContinue: onAction, + onReset: { quantities.removeAll() } + ) + .onChange(of: quantities) { newQuantities in + let total = newQuantities.reduce(Decimal(0)) { $0 + $1.key * Decimal($1.value) } + viewModel.updateTotalAmount(total, quantities: newQuantities) + } + } + } +} + +#Preview { + DashSpendFixedContentPreview() + .background(Color.primaryBackground) +} + +private struct DashSpendFixedContentPreview: View { + @State private var quantities: [Decimal: Int] = [:] + + var body: some View { + DashSpendFixedContent( + viewModel: FixedPreviewViewModel(), + quantities: $quantities, + onAction: {} + ) + } +} + +private class FixedPreviewViewModel: DashSpendPayViewModel { + override var isMixing: Bool { false } + override func subscribeToUpdates() {} + override func unsubscribeFromAll() {} + + init() { + let merchant = ExplorePointOfUse( + id: 2, name: "Domino's", + category: .merchant(ExplorePointOfUse.Merchant( + merchantId: "dominos-123", paymentMethod: .giftCard, type: .online, + deeplink: nil, savingsBasisPoints: 500, denominationsType: "fixed", + denominations: [5, 25, 50, 100], redeemType: "online", + giftCardProviders: [ExplorePointOfUse.Merchant.GiftCardProviderInfo( + providerId: "ctx", savingsPercentage: 500, + denominationsType: "fixed", sourceId: nil + )] + )), + active: true, city: nil, territory: nil, + address1: nil, address2: nil, address3: nil, address4: nil, + latitude: nil, longitude: nil, website: nil, phone: nil, + logoLocation: nil, coverImage: nil, source: "ctx" + ) + super.init(merchant: merchant, provider: .ctx) + merchantTitle = "Domino's" + denominations = [5, 25, 50] + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFlexibleContent.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFlexibleContent.swift new file mode 100644 index 000000000..37660fa63 --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFlexibleContent.swift @@ -0,0 +1,293 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +enum DashSpendMode: CaseIterable, Hashable { + case single + case multiple + + var localizedTitle: String { + switch self { + case .single: return NSLocalizedString("Single", comment: "DashSpend") + case .multiple: return NSLocalizedString("Multiple", comment: "DashSpend") + } + } +} + +struct DashSpendFlexibleContent: View { + @ObservedObject var viewModel: DashSpendPayViewModel + @Binding var quantities: [Decimal: Int] + let onAction: () -> Void + + @State private var segmentSelection: DashSpendMode = .single + @State private var goingForward: Bool = true + private let multipleMaxAmount: Decimal = PiggyCardsConstants.maxOrderAmount + + private var multipleTotal: String { + let formatter = NumberFormatter() + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + return formatter.string(from: NSDecimalNumber(decimal: multipleTotalAmount)) ?? "0" + } + + private var multipleTotalAmount: Decimal { + quantities.reduce(Decimal(0)) { $0 + $1.key * Decimal($1.value) } + } + + private var multipleShowCost: Bool { + guard !quantities.isEmpty, viewModel.error == nil else { return false } + guard viewModel.hasValidLimits else { return false } + return multipleTotalAmount >= viewModel.minimumAmount && multipleTotalAmount <= multipleMaxAmount + } + + private var multipleActionEnabled: Bool { + !quantities.isEmpty && + viewModel.error == nil && + !viewModel.isLoading && + !viewModel.isProcessingPayment && + multipleTotalAmount >= viewModel.minimumAmount && + multipleTotalAmount <= multipleMaxAmount + } + + private var multipleDenominations: [Decimal] { + let minimum = max(Decimal(5), viewModel.minimumAmount) + let maximum = min(Decimal(500), viewModel.maximumAmount) + guard minimum > 0, maximum >= minimum else { return [] } + return uniqueInOrder(formulaCandidates(minimum: minimum, maximum: maximum)) + } + + private func formulaCandidates(minimum: Decimal, maximum: Decimal) -> [Decimal] { + [ + roundedToCents(minimum), + roundedToCents(minimum * 2), + roundedToCents(minimum * 4), + roundedToCents(maximum / 2), + roundedToCents(maximum) + ].map { min(max($0, minimum), maximum) } + } + + private func uniqueInOrder(_ values: [Decimal]) -> [Decimal] { + var uniqueValues: [Decimal] = [] + var seen: Set = [] + + for value in values { + let rounded = roundedToCents(value) + guard !seen.contains(rounded) else { continue } + seen.insert(rounded) + uniqueValues.append(rounded) + } + return uniqueValues + } + + private func roundedToCents(_ value: Decimal) -> Decimal { + var source = value + var result = Decimal() + NSDecimalRound(&result, &source, 2, .plain) + return result + } + + var body: some View { + if viewModel.supportsMultipleMode { + multiModeBody + } else { + singleModeBody + } + } + + // CTX: simple single-panel layout, no multiple mode state involved + private var singleModeBody: some View { + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 20) { + DashSpendPayIntro( + merchantIconUrl: viewModel.merchantIconUrl, + merchantTitle: viewModel.merchantTitle, + isMixing: viewModel.isMixing, + dashBalance: viewModel.isMixing ? viewModel.coinJoinBalance : viewModel.walletBalance + ) + DashSpendAmountView( + currencySymbol: viewModel.currencySymbol, + amount: viewModel.input + ) + .frame(height: 85) + } + .padding(.top, 10) + .padding(.horizontal, 20) + + DashSpendSinglePanel( + isLoading: viewModel.isLoading, + hasValidLimits: viewModel.hasValidLimits, + isFixedDenomination: false, + denominations: [], + selectedDenomination: nil, + actionEnabled: viewModel.error == nil && !viewModel.isLoading, + inProgress: viewModel.isProcessingPayment, + input: $viewModel.input, + showLimits: viewModel.showLimits, + error: viewModel.error, + minimumLimitMessage: viewModel.minimumLimitMessage, + maximumLimitMessage: viewModel.maximumLimitMessage, + amount: viewModel.amount, + minimumAmount: viewModel.minimumAmount, + maximumAmount: viewModel.maximumAmount, + showCost: viewModel.showCost, + costMessage: viewModel.costMessage, + onDenominationSelected: { _ in }, + onAction: onAction + ) +// .ignoresSafeArea(.all, edges: .bottom) + } + } + + // PiggyCards: SegmentedControl + Single/Multiple panels with animation + private var multiModeBody: some View { + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 20) { + DashSpendPayIntro( + merchantIconUrl: viewModel.merchantIconUrl, + merchantTitle: viewModel.merchantTitle, + isMixing: viewModel.isMixing, + dashBalance: viewModel.isMixing ? viewModel.coinJoinBalance : viewModel.walletBalance + ) + SegmentedControl( + options: DashSpendMode.allCases, + selection: Binding( + get: { segmentSelection }, + set: { newValue in + guard newValue != segmentSelection else { return } + goingForward = newValue == .multiple + withAnimation(.easeInOut(duration: 0.3)) { + segmentSelection = newValue + } + if newValue == .single { + // Restore single-mode amount-driven validation after returning + // from multiple mode where total is derived from quantities. + viewModel.updateTotalAmount(viewModel.input.decimal() ?? 0) + } + } + ), + label: \.localizedTitle + ) + DashSpendAmountView( + currencySymbol: viewModel.currencySymbol, + amount: segmentSelection == .multiple ? multipleTotal : viewModel.input + ) + .frame(height: 85) + } + .padding(.top, 10) + .padding(.horizontal, 20) + + if segmentSelection == .single { + DashSpendSinglePanel( + isLoading: viewModel.isLoading, + hasValidLimits: viewModel.hasValidLimits, + isFixedDenomination: false, + denominations: [], + selectedDenomination: nil, + actionEnabled: viewModel.error == nil && !viewModel.isLoading, + inProgress: viewModel.isProcessingPayment, + input: $viewModel.input, + showLimits: viewModel.showLimits, + error: viewModel.error, + minimumLimitMessage: viewModel.minimumLimitMessage, + maximumLimitMessage: viewModel.maximumLimitMessage, + amount: viewModel.amount, + minimumAmount: viewModel.minimumAmount, + maximumAmount: viewModel.maximumAmount, + showCost: viewModel.showCost, + costMessage: viewModel.costMessage, + onDenominationSelected: { _ in }, + onAction: onAction + ) + .transition(.asymmetric( + insertion: .opacity.combined(with: .offset(x: goingForward ? 50 : -50)), + removal: .opacity.combined(with: .offset(x: goingForward ? -50 : 50)) + )) + } else { + DashSpendMultiplePanel( + denominations: multipleDenominations, + quantities: $quantities, + actionEnabled: multipleActionEnabled, + inProgress: viewModel.isProcessingPayment, + error: viewModel.error, + showCost: multipleShowCost, + costMessage: viewModel.costMessage, + inventoryLimits: [:], + onContinue: onAction, + onReset: { quantities.removeAll() } + ) + .transition(.asymmetric( + insertion: .opacity.combined(with: .offset(x: goingForward ? 50 : -50)), + removal: .opacity.combined(with: .offset(x: goingForward ? -50 : 50)) + )) + .onChange(of: quantities) { newQuantities in + let total = newQuantities.reduce(Decimal(0)) { $0 + $1.key * Decimal($1.value) } + viewModel.updateTotalAmount(total, quantities: newQuantities) + } + .onAppear { + viewModel.updateTotalAmount(multipleTotalAmount, quantities: quantities) + } + } + } + } +} + +#Preview("Single / Keyboard") { + DashSpendFlexibleContentPreview() + .background(Color.primaryBackground) +} + +private struct DashSpendFlexibleContentPreview: View { + @State private var quantities: [Decimal: Int] = [:] + + var body: some View { + DashSpendFlexibleContent( + viewModel: FlexiblePreviewViewModel(), + quantities: $quantities, + onAction: {} + ) + } +} + +private class FlexiblePreviewViewModel: DashSpendPayViewModel { + override var isMixing: Bool { false } + override func subscribeToUpdates() {} + override func unsubscribeFromAll() {} + + init() { + let merchant = ExplorePointOfUse( + id: 1, name: "Amazon", + category: .merchant(ExplorePointOfUse.Merchant( + merchantId: "amazon-123", paymentMethod: .giftCard, type: .online, + deeplink: nil, savingsBasisPoints: 1000, denominationsType: "range", + denominations: [], redeemType: "online", + giftCardProviders: [ExplorePointOfUse.Merchant.GiftCardProviderInfo( + providerId: "ctx", savingsPercentage: 1000, + denominationsType: "range", sourceId: nil + )] + )), + active: true, city: nil, territory: nil, + address1: nil, address2: nil, address3: nil, address4: nil, + latitude: nil, longitude: nil, website: nil, phone: nil, + logoLocation: nil, coverImage: nil, source: "ctx" + ) + super.init(merchant: merchant, provider: .ctx) + merchantTitle = "Amazon" + minimumAmount = 5 + maximumAmount = 100 + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift new file mode 100644 index 000000000..0c6ebec7f --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift @@ -0,0 +1,169 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DashSpendMultiplePanel: View { + let denominations: [Decimal] + @Binding var quantities: [Decimal: Int] + let actionEnabled: Bool + let inProgress: Bool + let error: (any Error)? + let showCost: Bool + let costMessage: String + let inventoryLimits: [Decimal: Int] + let onContinue: () -> Void + let onReset: () -> Void + + private var inventoryWarning: String? { + for (denomination, count) in quantities where count > 0 { + if let limit = inventoryLimits[denomination], count >= limit { + return String( + format: NSLocalizedString("Merchant has only %d cards available", comment: "DashSpend"), + limit + ) + } + } + return nil + } + + var body: some View { + VStack(spacing: 0) { + ZStack(alignment: .bottom) { + DenominationCard( + denominations: denominations, + quantities: $quantities, + inventoryLimits: inventoryLimits, + onReset: onReset + ) + + if let warning = inventoryWarning { + Text(warning) + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Color(red: 10/255, green: 11/255, blue: 13/255).opacity(0.7)) + .cornerRadius(10) + .offset(y: 20) + } + } + + Spacer() + + VStack(spacing: 16) { + if let error = error { + Text(error.localizedDescription) + .font(.caption) + .foregroundColor(Color.systemRed) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) + .padding(.bottom, 4) + } else if showCost { + Text(costMessage) + .font(.subhead) + .foregroundColor(Color.primaryText) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.bottom, 4) + } + + DashButton( + text: NSLocalizedString("Continue", comment: "DashSpend"), + isEnabled: actionEnabled, + isLoading: inProgress, + action: onContinue + ) + .padding(.horizontal, 40) + } + } + .padding(.horizontal, 20) + } +} + +private struct DenominationCard: View { + let denominations: [Decimal] + @Binding var quantities: [Decimal: Int] + let inventoryLimits: [Decimal: Int] + let onReset: () -> Void + + var body: some View { + VStack(spacing: 10) { + ForEach(denominations, id: \.self) { denomination in + DashSpendDenominationRow( + denomination: denomination, + count: Binding( + get: { quantities[denomination] ?? 0 }, + set: { newValue in + if newValue > 0 { + quantities[denomination] = newValue + } else { + quantities.removeValue(forKey: denomination) + } + } + ), + inventoryLimit: inventoryLimits[denomination] + ) + } + + if !quantities.isEmpty { + HStack { + Spacer() + DashButton( + text: NSLocalizedString("Reset", comment: "DashSpend"), + size: .extraSmall, + action: onReset + ) + .overrideForegroundColor(.primaryText) + .overrideBackgroundColor(.gray300Alpha10) + .frame(maxWidth: 126, alignment: .trailing) + } + } + } + .padding(20) + .background(Color.secondaryBackground) + .cornerRadius(20) + } +} + +#Preview { + DashSpendMultiplePanelPreview() +} + +private struct DashSpendMultiplePanelPreview: View { + @State private var quantities: [Decimal: Int] = [:] + + var body: some View { + ZStack { + Color.primaryBackground.ignoresSafeArea() + + DashSpendMultiplePanel( + denominations: [5, 10, 50, 100], + quantities: $quantities, + actionEnabled: !quantities.isEmpty, + inProgress: false, + error: quantities.isEmpty ? DashSpendError.customError("You can buy up to $2,500 in gift cards per order") : nil, + showCost: !quantities.isEmpty, + costMessage: "You are buying a $150 gift cards for $135.00 (10% discount)", + inventoryLimits: [50: 3], + onContinue: {}, + onReset: { quantities.removeAll() } + ) + } + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift new file mode 100644 index 000000000..18d47bb3e --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift @@ -0,0 +1,137 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import SwiftUI +import SDWebImageSwiftUI + +struct DashSpendPayIntro: View { + let merchantIconUrl: String + let merchantTitle: String + let isMixing: Bool + let dashBalance: UInt64 + + @State private var balanceHidden: Bool = true + + private enum Layout { + static let vStackSpacing: CGFloat = 0 + static let merchantHStackSpacing: CGFloat = 5 + static let balanceHStackSpacing: CGFloat = 4 + static let merchantIconSize: CGFloat = 18 + static let eyeCircleSize: CGFloat = 28 + static let eyeIconSize: CGFloat = 14 + } + + private var balanceLabel: String { + (isMixing ? NSLocalizedString("Mixed balance", comment: "") : NSLocalizedString("Balance", comment: "")) + ":" + } + + private var merchantIcon: some View { + WebImage(url: URL(string: merchantIconUrl)) + .resizable() + .indicator(.activity) + .transition(.fade) + .scaledToFit() + .clipShape(Circle()) + .frame(width: Layout.merchantIconSize, height: Layout.merchantIconSize) + } + + private var eyeIcon: some View { + ZStack { + Circle() + .fill(Color.gray300Alpha10) + .frame(width: Layout.eyeCircleSize, height: Layout.eyeCircleSize) + + Icon(name: .custom("eye_opened-icon", maxHeight: Layout.eyeIconSize)) + .foregroundColor(.primaryText) + .opacity(balanceHidden ? 1 : 0) + + Icon(name: .custom("eye_closed-icon", maxHeight: Layout.eyeIconSize)) + .foregroundColor(.primaryText) + .opacity(balanceHidden ? 0 : 1) + } + .compositingGroup() + } + + var body: some View { + VStack(alignment: .leading, spacing: Layout.vStackSpacing) { + Text(NSLocalizedString("Buy gift card", comment: "DashSpend")) + .font(.title1) + .fontWeight(.bold) + + HStack(alignment: .center, spacing: Layout.merchantHStackSpacing) { + Text(NSLocalizedString("at", comment: "DashSpend")) + merchantIcon + Text(merchantTitle) + } + .font(.subhead) + + HStack(spacing: Layout.balanceHStackSpacing) { + Text(balanceLabel) + + if balanceHidden { + Text("***********") + } else { + DashAmount(amount: Int64(dashBalance), font: .subheadline, showDirection: false) + Text("~").font(.subheadline) + formattedFiatText(from: dashBalance) + } + + Button(action: { withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() } }) { + eyeIcon + } + } + .font(.subheadline) + .foregroundColor(.secondaryText) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + @ViewBuilder + private func formattedFiatText(from dashAmount: UInt64) -> some View { + let text = try? CurrencyExchanger.shared.convertDash(amount: dashAmount.dashAmount, to: App.fiatCurrency).formattedFiatAmount + + Text(text ?? NSLocalizedString("Not available", comment: "")) + .font(.subheadline) + .foregroundColor(.secondaryText) + } +} + +#Preview("DashSpendPayIntro") { + VStack(spacing: 20) { + Text("DashSpendPayIntro").font(.caption).foregroundColor(.secondary) + DashSpendPayIntro( + merchantIconUrl: "", + merchantTitle: "Amazon", + isMixing: false, + dashBalance: 555566 + ) + Divider() + Text("SendIntro (reference)").font(.caption).foregroundColor(.secondary) + SendIntro( + title: "Buy gift card", + preposition: "at", + destination: "Amazon", + dashBalance: nil, + balanceLabel: "Balance:", + avatarView: { + Circle().fill(Color.orange).frame(width: 18, height: 18) + } + ) + } + .padding() +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift new file mode 100644 index 000000000..92b77a9c9 --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift @@ -0,0 +1,237 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +extension View { + func bottomPanelStyle() -> some View { + self + .padding(.horizontal, 20) + .background(Color.secondaryBackground) +// .padding(.bottom, 30) + .cornerRadius(20) + } +} + +struct DashSpendSinglePanel: View { + let isLoading: Bool + let hasValidLimits: Bool + let isFixedDenomination: Bool + let denominations: [Int] + let selectedDenomination: Int? + let actionEnabled: Bool + let inProgress: Bool + @Binding var input: String + let showLimits: Bool + let error: (any Error)? + let minimumLimitMessage: String + let maximumLimitMessage: String + let amount: Decimal + let minimumAmount: Decimal + let maximumAmount: Decimal + let showCost: Bool + let costMessage: String + let onDenominationSelected: (Int) -> Void + let onAction: () -> Void + + var body: some View { + if isFixedDenomination { + if isLoading && denominations.isEmpty { + loadingView + } else { + denominationsView + } + } else { + keyboardView + } + } + + @ViewBuilder + private var costMessageView: some View { + if showCost { + Text(costMessage) + .font(.subhead) + .foregroundColor(Color.primaryText) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 12) + } + } + + @ViewBuilder + private var limitsView: some View { + if let error = error { + Text(error.localizedDescription) + .font(.caption) + .foregroundColor(Color.systemRed) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 12) + .frame(maxWidth: .infinity) + } else if hasValidLimits { + HStack { + Text(minimumLimitMessage) + .font(.caption) + .foregroundColor(amount > 0 && amount < minimumAmount ? Color.systemRed : Color.tertiaryText) + .frame(maxWidth: .infinity, alignment: .leading) + Text(maximumLimitMessage) + .font(.caption) + .foregroundColor(amount > maximumAmount ? Color.systemRed : Color.tertiaryText) + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.top, 12) + } + } + + private var loadingView: some View { + VStack(spacing: 20) { + Text(NSLocalizedString("Loading gift card options...", comment: "DashSpend")) + .font(.subhead) + .foregroundColor(.secondaryText) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var denominationsView: some View { + MerchantDenominations( + denominations: denominations, + selectedDenomination: selectedDenomination, + actionEnabled: selectedDenomination != nil && actionEnabled, + onDenominationSelected: onDenominationSelected, + actionHandler: onAction + ) + .frame(maxWidth: .infinity) + .padding(.top, 20) + .bottomPanelStyle() + } + + private var keyboardView: some View { + VStack(spacing: 0) { + VStack(spacing: 20) { + limitsView + + costMessageView + } + .padding(.horizontal, 20) + + Spacer(minLength: 0) + + NumericKeyboardView( + value: $input, + showDecimalSeparator: true, + actionButtonText: NSLocalizedString("Continue", comment: ""), + actionEnabled: actionEnabled && !showLimits && hasValidLimits, + inProgress: inProgress, + actionHandler: onAction + ) + .frame(maxWidth: .infinity) + .bottomPanelStyle() + .background(Color.secondaryBackground, ignoresSafeAreaEdges: .bottom) + } + + } +} + +#Preview("Keyboard") { + DashSpendSinglePanelKeyboardPreview() +} + +#Preview("Fixed denominations") { + DashSpendSinglePanelDenominationsPreview() +} + +#Preview("Loading") { + DashSpendSinglePanel( + isLoading: true, + hasValidLimits: false, + isFixedDenomination: false, + denominations: [], + selectedDenomination: nil, + actionEnabled: false, + inProgress: false, + input: .constant(""), + showLimits: false, + error: nil, + minimumLimitMessage: "", + maximumLimitMessage: "", + amount: 0, + minimumAmount: 0, + maximumAmount: 0, + showCost: false, + costMessage: "", + onDenominationSelected: { _ in }, + onAction: {} + ) +} + +private struct DashSpendSinglePanelKeyboardPreview: View { + @State private var input = "0" + + var body: some View { + DashSpendSinglePanel( + isLoading: false, + hasValidLimits: true, + isFixedDenomination: false, + denominations: [], + selectedDenomination: nil, + actionEnabled: true, + inProgress: false, + input: $input, + showLimits: true, + error: nil, + minimumLimitMessage: "Min $5", + maximumLimitMessage: "Max $100", + amount: 0, + minimumAmount: 5, + maximumAmount: 100, + showCost: true, + costMessage: "You are buying a $50 gift card for $45.00 (10% discount)", + onDenominationSelected: { _ in }, + onAction: {} + ) + .background(Color.primaryBackground) + } +} + +private struct DashSpendSinglePanelDenominationsPreview: View { + @State private var selected: Int? = nil + + var body: some View { + DashSpendSinglePanel( + isLoading: false, + hasValidLimits: true, + isFixedDenomination: true, + denominations: [5, 10, 25, 50, 100], + selectedDenomination: selected, + actionEnabled: selected != nil, + inProgress: false, + input: .constant(""), + showLimits: false, + error: nil, + minimumLimitMessage: "", + maximumLimitMessage: "", + amount: 0, + minimumAmount: 0, + maximumAmount: 0, + showCost: false, + costMessage: "", + onDenominationSelected: { selected = $0 }, + onAction: {} + ) + .background(Color.primaryBackground) + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendConfirmationDialog.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendConfirmationDialog.swift index 2b9a36c83..bb9d318d2 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendConfirmationDialog.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendConfirmationDialog.swift @@ -1,4 +1,4 @@ -// +// // Created by Andrei Ashikhmin // Copyright © 2025 Dash Core Group. All rights reserved. // @@ -19,143 +19,205 @@ import SwiftUI import SDWebImageSwiftUI struct DashSpendConfirmationDialog: View { - let amount: String let merchantName: String let merchantIconUrl: String let originalPrice: Decimal let discount: Decimal + let quantities: [Decimal: Int]? let onConfirm: () -> Void let onCancel: () -> Void - - @Environment(\.presentationMode) private var presentationMode - + @Binding var contentHeight: CGFloat + private let fiatFormatter = NumberFormatter.fiatFormatter(currencyCode: kDefaultCurrencyCode) - - var body: some View { - VStack(spacing: 40) { - HStack { - Text(fiatFormatter.currencySymbol + amount) - .font(.system(size: 32, weight: .medium)) - .foregroundColor(.primaryText) + private var youPayAmount: Decimal { originalPrice * (1 - discount) } + + private var formattedPayAmount: String { + let hasCents = youPayAmount != Decimal(Int(truncating: NSDecimalNumber(decimal: youPayAmount))) + let formatter = NumberFormatter.fiatFormatter(currencyCode: kDefaultCurrencyCode) + if !hasCents { + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 0 + } + return formatter.string(from: NSDecimalNumber(decimal: youPayAmount))?.strippingCurrencySymbol(formatter) ?? "" + } + + private var quantityLines: [String] { + guard let quantities = quantities else { return [] } + return quantities + .filter { $0.value > 0 } + .sorted { $0.key < $1.key } + .map { denomination, count in + let amount = fiatFormatter.string(from: NSDecimalNumber(decimal: denomination)) ?? "$\(denomination)" + return "\(count) x \(amount)" } - - // Details - VStack(spacing: 0) { - HStack(spacing: 8) { - Text(NSLocalizedString("From", comment: "DashSpend")) - .font(.subhead) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - Image("image.explore.dash.wts.dash") - .resizable() - .frame(width: 24, height: 24) - - Text(NSLocalizedString("Dash Wallet", comment: "DashSpend")) - .font(.subhead) - .foregroundColor(.primaryText) - } - .padding(.horizontal, 12) - .frame(height: 50) - - HStack(spacing: 8) { - Text(NSLocalizedString("To", comment: "DashSpend")) - .font(.subhead) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - WebImage(url: URL(string: merchantIconUrl)) - .resizable() - .indicator(.activity) - .transition(.fade(duration: 0.3)) - .scaledToFit() - .frame(width: 24, height: 24) - .clipShape(Circle()) - - Text(merchantName) - .font(.subhead) - .foregroundColor(.primaryText) - } - .padding(.horizontal, 12) - .frame(height: 50) - - HStack { - Text(NSLocalizedString("Gift card total", comment: "DashSpend")) - .font(.subhead) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - Text(fiatFormatter.string(from: NSDecimalNumber(decimal: originalPrice)) ?? "") - .font(.subhead) - .foregroundColor(.primaryText) - } - .padding(.horizontal, 12) - .frame(height: 50) - - HStack { - Text(NSLocalizedString("Discount", comment: "DashSpend")) - .font(.subhead) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - Text(PercentageFormatter.format(percent: NSDecimalNumber(decimal: discount * 100).doubleValue)) - .font(.subhead) - .foregroundColor(.primaryText) + } + + var body: some View { + VStack(spacing: 0) { + Capsule() + .fill(Color(red: 0.69, green: 0.71, blue: 0.74).opacity(0.5)) + .frame(width: 36, height: 5) + .padding(.top, 6) + .padding(.bottom, 13) + + Text(NSLocalizedString("Confirm", comment: "DashSpend")) + .font(.calloutMedium) + .foregroundColor(.primaryText) + .frame(height: 44) + + VStack(spacing: 20) { + DashSpendAmountView( + currencySymbol: fiatFormatter.currencySymbol, + amount: formattedPayAmount + ) + .frame(height: 85) + + VStack(alignment: .leading, spacing: 2) { + detailsRow(title: NSLocalizedString("From", comment: "DashSpend")) { + HStack(spacing: 8) { + Image("image.explore.dash.wts.dash") + .resizable() + .frame(width: 20, height: 20) + .clipShape(RoundedRectangle(cornerRadius: 7)) + Text(NSLocalizedString("Dash Wallet", comment: "DashSpend")) + .font(.subhead) + .foregroundColor(.primaryText) + } + } + + detailsRow(title: NSLocalizedString("To", comment: "DashSpend")) { + HStack(spacing: 8) { + WebImage(url: URL(string: merchantIconUrl)) + .resizable() + .indicator(.activity) + .transition(.fade(duration: 0.3)) + .scaledToFit() + .frame(width: 20, height: 20) + .clipShape(RoundedRectangle(cornerRadius: 7)) + Text(merchantName) + .font(.subhead) + .foregroundColor(.primaryText) + } + } + + detailsRow(title: NSLocalizedString("Gift card", comment: "DashSpend")) { + Text(fiatFormatter.string(from: NSDecimalNumber(decimal: originalPrice)) ?? "") + .font(.subhead) + .foregroundColor(.primaryText) + } + + if !quantityLines.isEmpty { + detailsRow(title: NSLocalizedString("Quantity", comment: "DashSpend")) { + VStack(alignment: .trailing, spacing: 2) { + ForEach(quantityLines, id: \.self) { line in + Text(line) + .font(.subhead) + .foregroundColor(.primaryText) + } + } + } + } + + detailsRow(title: NSLocalizedString("Discount", comment: "DashSpend")) { + Text(PercentageFormatter.format(percent: NSDecimalNumber(decimal: discount * 100).doubleValue)) + .font(.subhead) + .foregroundColor(.primaryText) + } + + detailsRow(title: NSLocalizedString("You pay", comment: "DashSpend")) { + Text(fiatFormatter.string(from: NSDecimalNumber(decimal: youPayAmount)) ?? "") + .font(.subhead) + .foregroundColor(.primaryText) + } } - .padding(.horizontal, 12) - .frame(height: 50) - - HStack { - Text(NSLocalizedString("You pay", comment: "DashSpend")) - .font(.subhead) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - Text(fiatFormatter.string(from: NSDecimalNumber(decimal: originalPrice * (1 - discount))) ?? "") - .font(.subhead) - .foregroundColor(.primaryText) + .padding(6) + .background(Color.secondaryBackground) + .cornerRadius(20) + .shadow(color: Color(red: 0.72, green: 0.76, blue: 0.8).opacity(0.1), radius: 20, x: 0, y: 5) + + HStack(spacing: 20) { + DashButton( + text: NSLocalizedString("Cancel", comment: "DashSpend"), + action: onCancel + ) + .overrideForegroundColor(.primaryText) + .overrideBackgroundColor(.gray300Alpha10) + + DashButton( + text: NSLocalizedString("Confirm", comment: "DashSpend"), + action: onConfirm + ) } - .padding(.horizontal, 12) - .frame(height: 50) + .padding(.horizontal, 10) } - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: Color.shadow, radius: 10, x: 0, y: 5) - - HStack(spacing: 20) { - Button(action: onCancel) { - Text(NSLocalizedString("Cancel", comment: "DashSpend")) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.primaryText) - .frame(maxWidth: .infinity) - .frame(height: 46) - } - .background(Color(UIColor.systemGray5)) - .cornerRadius(12) - - Button(action: onConfirm) { - Text(NSLocalizedString("Confirm", comment: "DashSpend")) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - } - .frame(maxWidth: .infinity) - .frame(height: 46) - .background(Color.dashBlue) - .cornerRadius(12) + .padding(.horizontal, 20) + .padding(.bottom, 20) + } + .background( + GeometryReader { proxy in + Color.clear + .onAppear { contentHeight = proxy.size.height } + .onChange(of: proxy.size.height) { contentHeight = $0 } + } + ) + } + + private func detailsRow(title: String, @ViewBuilder value: () -> some View) -> some View { + HStack(alignment: .top) { + Text(title) + .font(.subhead) + .fontWeight(.medium) + .foregroundColor(.tertiaryText) + + Spacer() + + value() + } + .padding(.horizontal, 14) + .frame(minHeight: 46) + } +} + +private extension String { + func strippingCurrencySymbol(_ formatter: NumberFormatter) -> String { + replacingOccurrences(of: formatter.currencySymbol, with: "").trimmingCharacters(in: .whitespaces) + } +} + +#Preview { + DashSpendConfirmationDialogPreview() +} + +private struct DashSpendConfirmationDialogPreview: View { + @State private var isPresented = true + @State private var contentHeight: CGFloat = 0 + + var body: some View { + VStack { + Text("Tap to open") + .onTapGesture { isPresented = true } + } + .sheet(isPresented: $isPresented) { + let content = DashSpendConfirmationDialog( + merchantName: "Amazon", + merchantIconUrl: "", + originalPrice: 75.70, + discount: 0.10, + quantities: [50: 1, 25: 2], + onConfirm: {}, + onCancel: {}, + contentHeight: $contentHeight + ) + + if #available(iOS 18.0, *) { + content + .presentationBackground(Color.primaryBackground) + .presentationDetents([.height(contentHeight > 0 ? contentHeight : 550)]) + .presentationCornerRadius(32) + .presentationDragIndicator(.hidden) + } else { + content } } - .padding(.top, 15) - .padding(.horizontal, 20) - .edgesIgnoringSafeArea(.bottom) } } diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift index b7b3ec290..fbd05a28b 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift @@ -1,4 +1,4 @@ -// +// // Created by Andrei Ashikhmin // Copyright © 2025 Dash Core Group. All rights reserved. // @@ -30,9 +30,17 @@ struct DashSpendPayScreen: View { @State private var showCustomErrorDialog = false @State private var errorMessage = "" @State private var errorTitle = "" + @State private var quantities: [Decimal: Int] = [:] + @State private var confirmationQuantities: [Decimal: Int] = [:] + @State private var confirmationOriginalPrice: Decimal = 0 let onPurchaseSuccess: ((Data) -> Void)? - - init(merchant: ExplorePointOfUse, provider: GiftCardProvider = .ctx, justAuthenticated: Bool = false, onPurchaseSuccess: ((Data) -> Void)? = nil) { + + init( + merchant: ExplorePointOfUse, + provider: GiftCardProvider = .ctx, + justAuthenticated: Bool = false, + onPurchaseSuccess: ((Data) -> Void)? = nil + ) { self.merchant = merchant self.provider = provider self._viewModel = .init(wrappedValue: DashSpendPayViewModel(merchant: merchant, provider: provider)) @@ -40,210 +48,61 @@ struct DashSpendPayScreen: View { self.showConfirmToast = false self.onPurchaseSuccess = onPurchaseSuccess } - + + fileprivate init(previewViewModel: DashSpendPayViewModel) { + self.merchant = ExplorePointOfUse( + id: 0, + name: "", + category: .unknown, + active: true, + city: nil, + territory: nil, + address1: nil, + address2: nil, + address3: nil, + address4: nil, + latitude: nil, + longitude: nil, + website: nil, + phone: nil, + logoLocation: nil, + coverImage: nil, + source: nil + ) + self.provider = .ctx + self._viewModel = .init(wrappedValue: previewViewModel) + self.justAuthenticated = false + self.showConfirmToast = false + self.onPurchaseSuccess = nil + } + var body: some View { ZStack(alignment: .bottom) { VStack(spacing: 0) { - HStack { - Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Image(systemName: "chevron.left") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(.primaryText) - .padding(10) - } - - Spacer() + NavBarBack { + presentationMode.wrappedValue.dismiss() } - .padding(.top, 5) - .padding(.horizontal, 10) - - SendIntro( - title: NSLocalizedString("Buy gift card", comment: "DashSpend"), - preposition: NSLocalizedString("at", comment: "DashSpend"), - destination: viewModel.merchantTitle, - dashBalance: viewModel.isMixing ? viewModel.coinJoinBalance : viewModel.walletBalance, - balanceLabel: (viewModel.isMixing ? NSLocalizedString("Mixed balance", comment: "") : NSLocalizedString("Balance", comment: "")) + ":", - avatarView: { - WebImage(url: URL(string: viewModel.merchantIconUrl)) - .resizable() - .indicator(.activity) - .transition(.fade(duration: 0.3)) - .scaledToFit() - .clipShape(Circle()) - } - ).padding(.horizontal, 20) - - Spacer() - - Text(viewModel.currencySymbol + viewModel.input) - .font(.largeTitle) - .foregroundColor(.primaryText) - .lineLimit(1) - .minimumScaleFactor(0.5) - - HStack { - if viewModel.showLimits { - Text(viewModel.minimumLimitMessage) - .font(.subhead) - .foregroundColor(Color.primaryText) - .padding(.leading, 20) - Spacer() - } - - if let error = viewModel.error { - Text(error.localizedDescription) - .font(.subhead) - .foregroundColor(Color.systemRed) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .padding(.horizontal, 10) - } else if viewModel.showCost { - Text(viewModel.costMessage) - .font(.subhead) - .foregroundColor(Color.primaryText) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .padding(.horizontal, 10) - } - - if viewModel.showLimits { - Spacer() - Text(viewModel.maximumLimitMessage) - .font(.subhead) - .foregroundColor(Color.primaryText) - .padding(.trailing, 20) - } - } - .frame(maxWidth: .infinity) - .frame(height: 20) - .padding(.top, 20) - .padding(.bottom, 10) - - Spacer() - - // Show loading state while fetching merchant info to prevent UI flicker - if viewModel.isLoading && !viewModel.hasValidLimits && viewModel.denominations.isEmpty { - VStack(spacing: 20) { - Text(NSLocalizedString("Loading gift card options...", comment: "DashSpend")) - .font(.subhead) - .foregroundColor(.secondaryText) - } - .frame(maxWidth: .infinity) - .frame(height: 320) - .padding(.horizontal, 20) - .padding(.bottom, 30) - .background(Color.secondaryBackground) - .cornerRadius(20) - } else if viewModel.isFixedDenomination { - MerchantDenominations( - denominations: viewModel.denominations, - selectedDenomination: viewModel.selectedDenomination, - actionEnabled: viewModel.selectedDenomination != nil && viewModel.error == nil && !viewModel.isLoading, - onDenominationSelected: { denom in - viewModel.selectedDenomination = denom - }, - actionHandler: { - if !viewModel.isUserSignedIn { - showSignInError() - return - } - - showConfirmationDialog = true - } + if viewModel.isFixedDenomination { + DashSpendFixedContent( + viewModel: viewModel, + quantities: $quantities, + onAction: handlePayAction ) - .frame(maxWidth: .infinity) - .padding(.top, 20) - .padding(.horizontal, 20) - .padding(.bottom, 30) - .background(Color.secondaryBackground) - .cornerRadius(20) } else { - NumericKeyboardView( - value: $viewModel.input, - showDecimalSeparator: true, - actionButtonText: NSLocalizedString("Preview", comment: ""), - actionEnabled: viewModel.error == nil && !viewModel.showLimits && !viewModel.isLoading && viewModel.hasValidLimits, - inProgress: viewModel.isProcessingPayment, - actionHandler: { - if !viewModel.isUserSignedIn { - showSignInError() - return - } - - showConfirmationDialog = true - } + DashSpendFlexibleContent( + viewModel: viewModel, + quantities: $quantities, + onAction: handlePayAction ) - .frame(maxWidth: .infinity) - .frame(height: 320) - .padding(.horizontal, 20) - .padding(.bottom, 30) - .background(Color.secondaryBackground) - .cornerRadius(20) } } - - if justAuthenticated { - ToastView( - text: NSLocalizedString("Logged in to DashSpend account", comment: "DashSpend"), - icon: .system("checkmark.circle") - ) - .frame(height: 20) - .padding(.bottom, 30) - } - - if showConfirmToast { - ToastView( - text: NSLocalizedString("Gift card purchase successful", comment: "DashSpend") - ) - .frame(height: 20) - .padding(.bottom, 30) - } - - if showErrorDialog { - ModalDialog( - style: .error, - icon: .system("exclamationmark.triangle.fill"), - heading: errorTitle, - textBlock1: errorMessage, - positiveButtonText: NSLocalizedString("OK", comment: ""), - positiveButtonAction: { - showErrorDialog = false - } - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(0.7)) - .edgesIgnoringSafeArea(.all) - } - - if showCustomErrorDialog { - ModalDialog( - style: .error, - icon: .system("exclamationmark.triangle.fill"), - heading: errorTitle, - textBlock1: errorMessage, - positiveButtonText: NSLocalizedString("Close", comment: ""), - positiveButtonAction: { - showCustomErrorDialog = false - }, - negativeButtonText: viewModel.contactSupportButtonText, - negativeButtonAction: { - showCustomErrorDialog = false - viewModel.contactSupport() - } - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(0.7)) - .edgesIgnoringSafeArea(.all) - } + overlays } - .ignoresSafeArea(.all, edges: .bottom) .background(Color.primaryBackground) .onAppear { viewModel.subscribeToUpdates() - + if justAuthenticated { DispatchQueue.main.asyncAfter(deadline: .now() + 3) { justAuthenticated = false @@ -259,39 +118,99 @@ struct DashSpendPayScreen: View { } } .sheet(isPresented: $showConfirmationDialog) { - let dialog = BottomSheet( - title: NSLocalizedString("Confirm", comment: "DashSpend"), - showBackButton: Binding.constant(false) - ) { - DashSpendConfirmationDialog( - amount: viewModel.input, - merchantName: viewModel.merchantTitle, - merchantIconUrl: viewModel.merchantIconUrl, - originalPrice: viewModel.amount, - discount: viewModel.savingsFraction, - onConfirm: { - showConfirmationDialog = false - purchaseGiftCard() - }, - onCancel: { - showConfirmationDialog = false - } - ) - }.background(Color.primaryBackground) - - if #available(iOS 16.0, *) { - dialog.presentationDetents([.height(500)]) - } else { - dialog - } + DashSpendPayConfirmationSheet( + merchantName: viewModel.merchantTitle, + merchantIconUrl: viewModel.merchantIconUrl, + originalPrice: confirmationOriginalPrice, + discount: viewModel.displaySavingsFraction, + quantities: confirmationQuantities.isEmpty ? nil : confirmationQuantities, + onConfirm: { + showConfirmationDialog = false + purchaseGiftCard() + }, + onCancel: { + showConfirmationDialog = false + } + ) } } - - private func purchaseGiftCard() { + @ViewBuilder + private var overlays: some View { + if justAuthenticated { + ToastView( + text: NSLocalizedString("Logged in to DashSpend account", comment: "DashSpend"), + icon: .system("checkmark.circle") + ) + .frame(height: 20) + .padding(.bottom, 30) + } + + if showConfirmToast { + ToastView(text: NSLocalizedString("Gift card purchase successful", comment: "DashSpend")) + .frame(height: 20) + .padding(.bottom, 30) + } + + if showErrorDialog { + ModalDialog( + style: .error, + icon: .system("exclamationmark.triangle.fill"), + heading: errorTitle, + textBlock1: errorMessage, + positiveButtonText: NSLocalizedString("OK", comment: ""), + positiveButtonAction: { showErrorDialog = false } + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black.opacity(0.7)) + .edgesIgnoringSafeArea(.all) + } + + if showCustomErrorDialog { + ModalDialog( + style: .error, + icon: .system("exclamationmark.triangle.fill"), + heading: errorTitle, + textBlock1: errorMessage, + positiveButtonText: NSLocalizedString("Close", comment: ""), + positiveButtonAction: { showCustomErrorDialog = false }, + negativeButtonText: viewModel.contactSupportButtonText, + negativeButtonAction: { + showCustomErrorDialog = false + viewModel.contactSupport() + } + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black.opacity(0.7)) + .edgesIgnoringSafeArea(.all) + } + } + + private func handlePayAction() { + if !viewModel.isUserSignedIn { showSignInError(); return } + let snapshotQuantities = quantities.filter { $0.value > 0 } + let snapshotTotal = snapshotQuantities.reduce(Decimal(0)) { $0 + $1.key * Decimal($1.value) } + let snapshotSingleAmount = viewModel.input.decimal() ?? viewModel.amount + + confirmationQuantities = snapshotQuantities + confirmationOriginalPrice = snapshotQuantities.isEmpty ? snapshotSingleAmount : snapshotTotal + + // Keep viewModel amount in sync before presenting the dialog to avoid delayed UI updates. + if snapshotQuantities.isEmpty { + viewModel.updateTotalAmount(snapshotSingleAmount) + } else { + viewModel.updateTotalAmount(snapshotTotal, quantities: snapshotQuantities) + } + + DispatchQueue.main.async { + showConfirmationDialog = true + } + } + + private func purchaseGiftCard() { Task { do { - let txId = try await viewModel.purchaseGiftCardAndPay() + let txId = try await viewModel.purchaseGiftCardAndPay(selectedQuantities: confirmationQuantities) showConfirmationDialog = false presentationMode.wrappedValue.dismiss() onPurchaseSuccess?(txId) @@ -299,27 +218,168 @@ struct DashSpendPayScreen: View { showConfirmationDialog = false errorTitle = NSLocalizedString("Purchase Failed", comment: "DashSpend") errorMessage = error.errorDescription ?? NSLocalizedString("Error", comment: "") - + if case .customError = error { showCustomErrorDialog = true } else { showErrorDialog = true } - + } catch { showConfirmationDialog = false errorTitle = !error.localizedDescription.isEmpty ? error.localizedDescription : NSLocalizedString("Error", comment: "") errorMessage = error.localizedDescription showErrorDialog = true - + DSLogger.log("Gift card purchase failed with error: \(error)") } } } - + private func showSignInError() { errorTitle = NSLocalizedString("Sign in required", comment: "Alert title") errorMessage = NSLocalizedString("You need to sign in to DashSpend to purchase gift cards.", comment: "DashSpend") showErrorDialog = true } } + +private struct DashSpendPayConfirmationSheet: View { + let merchantName: String + let merchantIconUrl: String + let originalPrice: Decimal + let discount: Decimal + let quantities: [Decimal: Int]? + let onConfirm: () -> Void + let onCancel: () -> Void + @State private var contentHeight: CGFloat = 0 + + @ViewBuilder + var body: some View { + let dialog = DashSpendConfirmationDialog( + merchantName: merchantName, + merchantIconUrl: merchantIconUrl, + originalPrice: originalPrice, + discount: discount, + quantities: quantities, + onConfirm: onConfirm, + onCancel: onCancel, + contentHeight: $contentHeight + ) + + if #available(iOS 16.4, *) { + dialog + .presentationBackground(Color.primaryBackground) + .presentationDetents([.height(550)]) + .presentationCornerRadius(32) + .presentationDragIndicator(.hidden) + } else { + dialog + } + } +} + +#Preview("Flexible amount") { + DashSpendPayScreen(previewViewModel: DashSpendPayPreviewViewModel()) +} + +#Preview("Fixed amount") { + DashSpendPayScreen(previewViewModel: DashSpendPayFixedPreviewViewModel()) +} + +private class DashSpendPayFixedPreviewViewModel: DashSpendPayViewModel { + override var isMixing: Bool { false } + override func subscribeToUpdates() {} + override func unsubscribeFromAll() {} + + init() { + let merchant = ExplorePointOfUse( + id: 2, + name: "Domino's", + category: .merchant( + ExplorePointOfUse.Merchant( + merchantId: "dominos-123", + paymentMethod: .giftCard, + type: .online, + deeplink: nil, + savingsBasisPoints: 500, + denominationsType: "fixed", + denominations: [], + redeemType: "online", + giftCardProviders: [ + ExplorePointOfUse.Merchant.GiftCardProviderInfo( + providerId: "ctx", savingsPercentage: 500, + denominationsType: "fixed", sourceId: nil + ) + ] + ) + ), + active: true, + city: nil, + territory: nil, + address1: nil, + address2: nil, + address3: nil, + address4: nil, + latitude: nil, + longitude: nil, + website: nil, + phone: nil, + logoLocation: nil, + coverImage: nil, + source: "ctx" + ) + super.init(merchant: merchant, provider: .ctx) + merchantTitle = "Domino's" + isFixedDenomination = true + denominations = [5, 25, 50, 100] + } +} + +private class DashSpendPayPreviewViewModel: DashSpendPayViewModel { + override var isMixing: Bool { false } + override func subscribeToUpdates() {} + override func unsubscribeFromAll() {} + + init() { + let merchant = ExplorePointOfUse( + id: 1, + name: "Amazon", + category: .merchant( + ExplorePointOfUse.Merchant( + merchantId: "amazon-123", + paymentMethod: .giftCard, + type: .online, + deeplink: nil, + savingsBasisPoints: 1000, + denominationsType: "range", + denominations: [], + redeemType: "online", + giftCardProviders: [ + ExplorePointOfUse.Merchant.GiftCardProviderInfo( + providerId: "ctx", savingsPercentage: 1000, + denominationsType: "range", sourceId: nil + ) + ] + ) + ), + active: true, + city: nil, + territory: nil, + address1: nil, + address2: nil, + address3: nil, + address4: nil, + latitude: nil, + longitude: nil, + website: nil, + phone: nil, + logoLocation: nil, + coverImage: nil, + source: "ctx" + ) + super.init(merchant: merchant, provider: .ctx) + merchantTitle = "Amazon" + minimumAmount = 5 + maximumAmount = 100 + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift index eae1c4b97..a7f49d1e3 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift @@ -20,15 +20,20 @@ import Combine private let defaultCurrency = kDefaultCurrencyCode +private struct GiftCardOrderMetadata: Codable { + let orderId: String + let cardAmounts: [String]? +} + @MainActor class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHandling { private var cancellableBag = Set() private let fiatFormatter = NumberFormatter.fiatFormatter(currencyCode: defaultCurrency) private let ctxSpendRepository = CTXSpendRepository.shared private let provider: GiftCardProvider - private let customIconProvider = CustomIconMetadataProvider.shared - private let txMetadataDao = TransactionMetadataDAOImpl.shared - private let sendCoinsService = SendCoinsService() + private lazy var customIconProvider = CustomIconMetadataProvider.shared + private lazy var txMetadataDao = TransactionMetadataDAOImpl.shared + private lazy var sendCoinsService = SendCoinsService() private let repository: [GiftCardProvider: any DashSpendRepository] = { var dict: [GiftCardProvider: any DashSpendRepository] = [ @@ -47,6 +52,7 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand private var merchantId: String = "" private var merchantUrl: String? = nil private var sourceId: String? + private let providerConfiguredIsFixed: Bool? private(set) var amount: Decimal = 0 private(set) var savingsFraction: Decimal = 0.0 @Published private(set) var isLoading = false @@ -67,8 +73,11 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand @Published var error: Error? = nil @Published var isFixedDenomination: Bool = false @Published var denominations: [Int] = [] + /// Maps each denomination to its available inventory count + @Published var denominationInventory: [Int: Int] = [:] /// Maps each denomination to its specific discount percentage (after service fee) private var denominationDiscounts: [Int: Decimal] = [:] + @Published private var basketSavingsFraction: Decimal? = nil @Published var selectedDenomination: Int? = nil { didSet { if let denom = selectedDenomination { @@ -82,6 +91,16 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand } } } + var displaySavingsFraction: Decimal { + basketSavingsFraction ?? savingsFraction + } + + func updateTotalAmount(_ total: Decimal, quantities: [Decimal: Int]? = nil) { + amount = total + basketSavingsFraction = basketDiscountFraction(from: quantities) + checkAmountForErrors() + } + @Published var input: String = "0" { didSet { // Replace the initial "0" when entering a new digit @@ -108,10 +127,11 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand var costMessage: String { let originalPrice = fiatFormatter.string(for: amount) ?? "0.00" - let discountedPrice = amount * (1 - savingsFraction) + let savingsToDisplay = displaySavingsFraction + let discountedPrice = amount * (1 - savingsToDisplay) let formattedDiscountedPrice = fiatFormatter.string(for: discountedPrice) ?? "0.00" - let discountPercent = NSDecimalNumber(decimal: savingsFraction * 100).doubleValue + let discountPercent = NSDecimalNumber(decimal: savingsToDisplay * 100).doubleValue let discountText = PercentageFormatter.format(percent: discountPercent, includePercent: false) return String(format: @@ -134,6 +154,24 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand var minimumLimitMessage: String { String.localizedStringWithFormat(NSLocalizedString("Min: %@", comment: "DashSpend"), fiatFormatter.string(for: minimumAmount) ?? "0.0" ) } var maximumLimitMessage: String { String.localizedStringWithFormat(NSLocalizedString("Max: %@", comment: "DashSpend"), fiatFormatter.string(for: maximumAmount) ?? "0.0" ) } var isMixing: Bool { CoinJoinService.shared.mixingState.isInProgress } + + var supportsMultipleMode: Bool { + #if PIGGYCARDS_ENABLED + return provider == .piggyCards + #else + return false + #endif + } + + var maxOrderLimitMessage: String? { + #if PIGGYCARDS_ENABLED + if provider == .piggyCards { + let formatted = fiatFormatter.string(for: PiggyCardsConstants.maxOrderAmount) ?? "$2,500" + return String(format: NSLocalizedString("You can buy up to %@ in gift cards per order", comment: "DashSpend"), formatted) + } + #endif + return nil + } init(merchant: ExplorePointOfUse, provider: GiftCardProvider = .ctx) { self.provider = provider @@ -146,7 +184,7 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand } // Get the sourceId and denomination type for the current provider from giftCardProviders - var providerIsFixed = false // Local variable to store denomination type + var providerIsFixed: Bool? = nil // Local variable to store denomination type var providerSavingsFraction = Decimal(merchant.merchant?.toSavingsFraction() ?? 0.0) if let giftCardProviders = merchant.merchant?.giftCardProviders { @@ -176,16 +214,24 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand // Store initial savingsFraction savingsFraction = providerSavingsFraction - - // Only use merchant-level denominations as fallback if we don't have provider-specific info - if merchant.merchant?.denominations != nil { - self.denominations = merchant.merchant?.denominations.compactMap { Int($0) } ?? [] + // Seed initial UI state from local merchant data while remote details are loading. + if let merchantData = merchant.merchant { + let localDenominations = merchantData.denominations + let localDenominationType = merchantData.denominationsType + + if providerIsFixed == true { + self.denominations = localDenominations + } else if localDenominationType == DenominationType.Range.rawValue, localDenominations.count >= 2 { + self.minimumAmount = Decimal(localDenominations[0]) + self.maximumAmount = Decimal(localDenominations[1]) + } } + self.providerConfiguredIsFixed = providerIsFixed super.init() // Set the denomination type after super.init() - self.isFixedDenomination = providerIsFixed + self.isFixedDenomination = providerIsFixed ?? false // Log debug info after super.init() @@ -204,6 +250,7 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand .store(in: &cancellableBag) repository[provider]?.isUserSignedInPublisher + .receive(on: DispatchQueue.main) .sink { [weak self] isSignedIn in self?.isUserSignedIn = isSignedIn } @@ -226,12 +273,12 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand } } - func purchaseGiftCardAndPay() async throws -> Data { + func purchaseGiftCardAndPay(selectedQuantities: [Decimal: Int] = [:]) async throws -> Data { isProcessingPayment = true defer { isProcessingPayment = false } let transaction: DSTransaction - let giftCardId: String + let giftCardNote: String switch provider { case .ctx: @@ -242,7 +289,7 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand throw DashSpendError.paymentProcessingError("No payment URL received") } - giftCardId = response.paymentId + giftCardNote = response.paymentId // CTX uses BIP70 payment request URLs transaction = try await sendCoinsService.payWithDashUrl(url: url) @@ -255,11 +302,10 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand throw DashSpendError.unauthorized } - let fiatAmountDouble = Double(truncating: amount as NSDecimalNumber) - - let giftCardInfo = try await piggyCardsRepo.orderGiftCard( + let lineItems = buildPiggyOrderLineItems(selectedQuantities: selectedQuantities) + let giftCardInfo = try await piggyCardsRepo.orderGiftCards( merchantId: merchantId, - fiatAmount: fiatAmountDouble, + lineItems: lineItems, fiatCurrency: "USD", cryptoCurrency: "DASH" ) @@ -278,14 +324,14 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand ) - giftCardId = giftCardInfo.orderId + giftCardNote = buildPiggyOrderNote(orderId: giftCardInfo.orderId, selectedQuantities: selectedQuantities) #endif } // Payment successful - save gift card information markGiftCardTransaction(txId: transaction.txHashData, provider: provider.displayName) customIconProvider.updateIcon(txId: transaction.txHashData, iconUrl: merchantIconUrl) - saveGiftCardDummy(txHashData: transaction.txHashData, giftCardId: giftCardId) + saveGiftCardDummy(txHashData: transaction.txHashData, giftCardNote: giftCardNote) return transaction.txHashData } @@ -329,9 +375,10 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand walletBalance = DWEnvironment.sharedInstance().currentWallet.balance } - private func checkAmountForErrors() { - // Check network availability first - guard networkStatus == .online else { + var isNetworkAvailable: Bool { networkStatus == .online } + + func checkAmountForErrors() { + guard isNetworkAvailable else { error = SendAmountError.networkUnavailable return } @@ -343,6 +390,15 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand return } + #if PIGGYCARDS_ENABLED + if provider == .piggyCards && amount > 0 { + guard amount <= PiggyCardsConstants.maxOrderAmount else { + error = DashSpendError.purchaseLimitExceeded + return + } + } + #endif + guard !canShowInsufficientFunds else { error = isMixing ? SendAmountError.insufficientMixedFunds : SendAmountError.insufficientFunds return @@ -361,11 +417,19 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand } // MARK: - CTX Integration - + private func updateMerchantInfo() async { - guard !merchantId.isEmpty, repository[provider]?.isUserSignedIn == true else { - return - } + guard !merchantId.isEmpty else { return } + + // CTX getMerchant is a public endpoint — fetch limits even when not signed in. + // PiggyCards requires authentication, so skip if not signed in. + let requiresAuth: Bool + #if PIGGYCARDS_ENABLED + requiresAuth = provider == .piggyCards + #else + requiresAuth = false + #endif + if requiresAuth && repository[provider]?.isUserSignedIn != true { return } // Set loading state @@ -378,23 +442,27 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand case .ctx: let merchantInfo = try await ctxSpendRepository.getMerchant(merchantId: merchantId) - // Update merchant details from API (API is source of truth when signed in) - // Use discount property for backwards compatibility (handles both savingsPercentage and userDiscount) - savingsFraction = Decimal(merchantInfo.discount) / Decimal(10000) - - // Set denomination type from API response - if merchantInfo.denominationType == .Range { - isFixedDenomination = false - minimumAmount = Decimal(merchantInfo.minimumCardPurchase) - maximumAmount = Decimal(merchantInfo.maximumCardPurchase) - // Clear fixed denomination values - denominations = [] - } else { - isFixedDenomination = true - denominations = merchantInfo.denominations.compactMap { Int($0) } - // Clear flexible amount values - minimumAmount = 0 - maximumAmount = 0 + let newSavings = Decimal(merchantInfo.discount) / Decimal(10000) + let isRange = merchantInfo.denominationType == .Range + let apiMin = Decimal(merchantInfo.minimumCardPurchase) + let apiMax = Decimal(merchantInfo.maximumCardPurchase) + let newMin = isRange ? (apiMin > 0 ? apiMin : minimumAmount) : Decimal(0) + let newMax = isRange ? (apiMax > 0 ? apiMax : maximumAmount) : Decimal(0) + let newDenominations = isRange ? [] : merchantInfo.denominations.compactMap { parseWholeDollarDenomination($0) } + + await MainActor.run { + self.savingsFraction = newSavings + self.isFixedDenomination = !isRange + self.minimumAmount = newMin + self.maximumAmount = newMax + self.denominations = newDenominations + self.basketSavingsFraction = nil + + if isRange { + self.selectedDenomination = nil + } else if let selected = self.selectedDenomination, !newDenominations.contains(selected) { + self.selectedDenomination = nil + } } #if PIGGYCARDS_ENABLED @@ -425,6 +493,7 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand // Each fixed denomination card is a separate entry in the API response var allFixedDenominations: Set = [] var denomDiscounts: [Int: Decimal] = [:] // Track discount per denomination + var denomInventory: [Int: Int] = [:] // Track inventory per denomination var hasRangeCard = false var rangeMin: Double = 0 var rangeMax: Double = 0 @@ -441,17 +510,19 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand if normalizedPriceType == PiggyCardsPriceType.fixed.rawValue { // Fixed type: collect the denomination and its specific discount - if let fixedValue = Int(card.denomination.trimmingCharacters(in: .whitespaces)) { + if let fixedValue = parseWholeDollarDenomination(card.denomination) { allFixedDenominations.insert(fixedValue) denomDiscounts[fixedValue] = cardDiscount + denomInventory[fixedValue] = card.quantity } } else if normalizedPriceType == PiggyCardsPriceType.option.rawValue { // Option type: parse comma-separated values (all share same discount) let denominationValues = card.denomination.split(separator: ",") - .compactMap { Int($0.trimmingCharacters(in: .whitespaces)) } + .compactMap { parseWholeDollarDenomination(String($0)) } for value in denominationValues { allFixedDenominations.insert(value) denomDiscounts[value] = cardDiscount + denomInventory[value] = card.quantity } } else if normalizedPriceType == PiggyCardsPriceType.range.rawValue { // Range type: track min/max for flexible amounts @@ -469,37 +540,65 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand } } - // Store denomination-specific discounts - denominationDiscounts = denomDiscounts - - // Determine UI mode: fixed denominations take precedence if available - if !allFixedDenominations.isEmpty { - // Show fixed denomination buttons - isFixedDenomination = true - denominations = allFixedDenominations.sorted() - // Clear flexible amount values - minimumAmount = 0 - maximumAmount = 0 - // Set initial savingsFraction to 0 until user selects a denomination - savingsFraction = 0 + // Compute final state on background, then publish on MainActor. + // Signed-in API response is the source of truth for PiggyCards denomination mode. + var finalFixed = false + var finalMin = Decimal(0) + var finalMax = Decimal(0) + var finalDenominations: [Int] = [] + var finalSavings = Decimal(0) + + let hasFixedCards = !allFixedDenominations.isEmpty + let preferredFixedInMixedSet = providerConfiguredIsFixed + + if hasRangeCard && hasFixedCards { + // Some sources (e.g. test data) can return mixed types for the same sourceId. + // In that case, honor merchant/provider configuration from the local database. + if preferredFixedInMixedSet == true { + finalFixed = true + finalDenominations = allFixedDenominations.sorted() + finalSavings = denomDiscounts.values.max() ?? 0 + } else { + finalFixed = false + finalMin = Decimal(rangeMin > 0 ? rangeMin : 10) + finalMax = Decimal(rangeMax > 0 ? rangeMax : 500) + finalSavings = max(0, Decimal(rangeDiscount) - serviceFee) / 100 + } } else if hasRangeCard { - // Only range cards available - show keyboard input - isFixedDenomination = false - minimumAmount = Decimal(rangeMin > 0 ? rangeMin : 10) - maximumAmount = Decimal(rangeMax > 0 ? rangeMax : 500) - denominations = [] - // Apply range card discount - savingsFraction = max(0, Decimal(rangeDiscount) - serviceFee) / 100 - } else if let firstCard = giftCards.first { - // Fallback: use first card's denomination - if let fixedValue = Int(firstCard.denomination.trimmingCharacters(in: .whitespaces)) { - isFixedDenomination = true - denominations = [fixedValue] - minimumAmount = 0 - maximumAmount = 0 - let cardDiscount = max(0, Decimal(firstCard.discountPercentage) - serviceFee) / 100 - denominationDiscounts[fixedValue] = cardDiscount - savingsFraction = cardDiscount + finalFixed = false + finalMin = Decimal(rangeMin > 0 ? rangeMin : 10) + finalMax = Decimal(rangeMax > 0 ? rangeMax : 500) + finalSavings = max(0, Decimal(rangeDiscount) - serviceFee) / 100 + } else if hasFixedCards { + finalFixed = true + finalDenominations = allFixedDenominations.sorted() + finalSavings = denomDiscounts.values.max() ?? 0 + } else if let firstCard = giftCards.first, + let fixedValue = parseWholeDollarDenomination(firstCard.denomination) { + finalFixed = true + finalDenominations = [fixedValue] + finalSavings = max(0, Decimal(firstCard.discountPercentage) - serviceFee) / 100 + } + + await MainActor.run { + DSLogger.log( + "DashSpend: PiggyCards mode=\(finalFixed ? "fixed" : "flexible"), " + + "range=[\(finalMin), \(finalMax)], fixedCount=\(finalDenominations.count)" + ) + self.denominationDiscounts = finalFixed ? denomDiscounts : [:] + self.denominationInventory = finalFixed ? denomInventory : [:] + self.isFixedDenomination = finalFixed + self.minimumAmount = finalMin + self.maximumAmount = finalMax + self.denominations = finalDenominations + self.savingsFraction = finalSavings + self.basketSavingsFraction = nil + if finalFixed { + if let selected = self.selectedDenomination, !finalDenominations.contains(selected) { + self.selectedDenomination = nil + } + } else { + self.selectedDenomination = nil } } #endif @@ -513,8 +612,17 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand self.checkAmountForErrors() } } catch { + DSLogger.log("DashSpend updateMerchantInfo failed for provider \(provider.displayName): \(error.localizedDescription)") + await MainActor.run { self.isLoading = false + if let dashError = error as? DashSpendError { + self.error = dashError + } else { + self.error = DashSpendError.customError( + NSLocalizedString("Unable to load merchant details. Please try again.", comment: "DashSpend") + ) + } } } } @@ -552,15 +660,15 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand } } - private func saveGiftCardDummy(txHashData: Data, giftCardId: String) { - DSLogger.log("Gift card saved - txId: \(txHashData.hexEncodedString()), giftCardId: \(giftCardId)") + private func saveGiftCardDummy(txHashData: Data, giftCardNote: String) { + DSLogger.log("Gift card saved - txId: \(txHashData.hexEncodedString())") let giftCard = GiftCard( txId: txHashData, merchantName: merchantTitle, merchantUrl: merchantUrl, price: amount, - note: giftCardId, // Store payment ID in note field temporarily + note: giftCardNote, // Store payment/order metadata in note field temporarily provider: provider.displayName // Store provider (CTX or PiggyCards) ) @@ -590,4 +698,108 @@ class DashSpendPayViewModel: NSObject, ObservableObject, NetworkReachabilityHand // Re-check errors when network status changes checkAmountForErrors() } + + private func buildPiggyOrderLineItems(selectedQuantities: [Decimal: Int]) -> [PiggyCardsRepository.OrderLineItem] { + let positiveSelections = selectedQuantities + .filter { $0.value > 0 } + .map { PiggyCardsRepository.OrderLineItem(denomination: decimalToDouble($0.key), quantity: $0.value) } + + if !positiveSelections.isEmpty { + return positiveSelections + } + + return [PiggyCardsRepository.OrderLineItem(denomination: decimalToDouble(amount), quantity: 1)] + } + + private func buildPiggyOrderNote(orderId: String, selectedQuantities: [Decimal: Int]) -> String { + var expandedAmounts = selectedQuantities + .filter { $0.value > 0 } + .sorted { $0.key < $1.key } + .flatMap { entry in Array(repeating: entry.key.description, count: entry.value) } + + if expandedAmounts.isEmpty { + expandedAmounts = [amount.description] + } + + let metadata = GiftCardOrderMetadata( + orderId: orderId, + cardAmounts: expandedAmounts + ) + + guard + let encoded = try? JSONEncoder().encode(metadata), + let serialized = String(data: encoded, encoding: .utf8) + else { + return orderId + } + + return serialized + } + + private func decimalToDouble(_ value: Decimal) -> Double { + Double(truncating: value as NSDecimalNumber) + } + + private func basketDiscountFraction(from quantities: [Decimal: Int]?) -> Decimal? { + #if PIGGYCARDS_ENABLED + guard + provider == .piggyCards, + isFixedDenomination, + let quantities, + !quantities.isEmpty + else { return nil } + + var originalTotal = Decimal(0) + var discountedTotal = Decimal(0) + + for (denomination, quantity) in quantities where quantity > 0 { + let discount = discountForDenomination(denomination) + let lineOriginal = denomination * Decimal(quantity) + let lineDiscounted = lineOriginal * (1 - discount) + originalTotal += lineOriginal + discountedTotal += lineDiscounted + } + + guard originalTotal > 0 else { return nil } + return 1 - (discountedTotal / originalTotal) + #else + return nil + #endif + } + + private func discountForDenomination(_ denomination: Decimal) -> Decimal { + guard let key = wholeDollarDenominationKey(from: denomination), + let mappedDiscount = denominationDiscounts[key] else { + return savingsFraction + } + + return mappedDiscount + } + + private func wholeDollarDenominationKey(from denomination: Decimal) -> Int? { + let asDouble = NSDecimalNumber(decimal: denomination).doubleValue + let rounded = Int(asDouble.rounded()) + guard abs(asDouble - Double(rounded)) < 0.0001 else { return nil } + return rounded + } + + private func parseWholeDollarDenomination(_ raw: String) -> Int? { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + + // Accept values like "5", "5.0", "5.00". + if let intValue = Int(trimmed) { + return intValue + } + + if let decimalValue = Decimal(string: trimmed, locale: Locale(identifier: "en_US_POSIX")) { + let asDouble = NSDecimalNumber(decimal: decimalValue).doubleValue + let rounded = Int(asDouble.rounded()) + if abs(asDouble - Double(rounded)) < 0.0001 { + return rounded + } + } + + return nil + } } diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendUserAuthScreen.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendUserAuthScreen.swift index 077fe7536..48d5728c5 100644 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendUserAuthScreen.swift +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendUserAuthScreen.swift @@ -190,10 +190,9 @@ struct DashSpendUserAuthScreen: View { EmptyView() } } - .background(Color.secondaryBackground) + .background(Color.secondaryBackground.ignoresSafeArea(edges: .top)) .navigationBarHidden(true) .navigationBarBackButtonHidden(true) - .edgesIgnoringSafeArea(.top) .onAppear { viewModel.screenType = authType diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsHowToUseSection.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsHowToUseSection.swift new file mode 100644 index 000000000..0fba2392f --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsHowToUseSection.swift @@ -0,0 +1,57 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct GiftCardDetailsHowToUseSection: View { + var body: some View { + VStack(alignment: .leading, spacing: 30) { + Text(NSLocalizedString("How to use your gift card", comment: "DashSpend")) + .font(.subhead) + .fontWeight(.medium) + .foregroundColor(.tertiaryText) + .frame(maxWidth: .infinity, alignment: .leading) + + FeatureSingleItem( + iconName: .custom("dp_user_generic"), + title: NSLocalizedString("Self-checkout", comment: "DashSpend"), + description: NSLocalizedString("Request assistance and show the barcode on your screen for scanning.", comment: "DashSpend") + ) + + FeatureSingleItem( + iconName: .custom("image.dashspend.shop"), + title: NSLocalizedString("In store", comment: "DashSpend"), + description: NSLocalizedString("Tell the cashier that you'd like to pay with a gift card and share the card number and pin.", comment: "DashSpend") + ) + + FeatureSingleItem( + iconName: .custom("image.dashspend.online"), + title: NSLocalizedString("Online", comment: "DashSpend"), + description: NSLocalizedString("In the payment section of your checkout, select \"gift card\" and enter your card number and pin.", comment: "DashSpend") + ) + } + .padding(20) + .background(Color.secondaryBackground) + .cornerRadius(20) + } +} + +#Preview { + GiftCardDetailsHowToUseSection() + .padding() + .background(Color.primaryBackground) +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsInfoCard.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsInfoCard.swift new file mode 100644 index 000000000..de2d6d95b --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsInfoCard.swift @@ -0,0 +1,248 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct GiftCardDetailsInfoCard: View { + let formattedPrice: String + let card: GiftCardDetailsCardItem? + let isLoadingCardDetails: Bool + let hasBeenPollingForLongTime: Bool + let loadingError: Error? + let onOpenClaimLink: (String) -> Void + let onCopy: (String) -> Void + + private var isClaimLink: Bool { card?.isClaimLink ?? false } + + var body: some View { + VStack(spacing: 2) { + barcodeSection + detailsRowsSection + } + .background(Color.secondaryBackground) + .cornerRadius(20) + } + + @ViewBuilder + private var barcodeSection: some View { + if isClaimLink { + EmptyView() + } else if let barcodeImage = card?.barcodeImage { + Image(uiImage: barcodeImage) + .resizable() + .frame(width: 200, height: 80) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + } else { + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(Color(UIColor.systemGray6)) + .frame(height: 72) + + if isLoadingCardDetails { + if hasBeenPollingForLongTime { + Text(NSLocalizedString("As soon as your code is generated, it will be displayed here", comment: "DashSpend")) + .font(.footnote) + .foregroundColor(.tertiaryText) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + } else { + SwiftUI.ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(0.8) + } + } else if loadingError != nil { + Text(NSLocalizedString("Failed to load barcode", comment: "DashSpend")) + .font(.footnote) + .foregroundColor(.systemRed) + } + } + .padding(.horizontal, 6) + .padding(.top, 6) + } + } + + @ViewBuilder + private var detailsRowsSection: some View { + if isClaimLink { + VStack(spacing: 20) { + textValueRow( + title: NSLocalizedString("Original Price", comment: "DashSpend"), + value: formattedPrice + ) + + if let claimLink = card?.cardNumber { + Button(action: { + onOpenClaimLink(claimLink) + }) { + Text(NSLocalizedString("See card details", comment: "DashSpend")) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.dashBlue) + .frame(maxWidth: .infinity, alignment: .center) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.horizontal, 14) + .padding(.top, 20) + .padding(.bottom, 20) + } else { + VStack(spacing: 2) { + textValueRow( + title: NSLocalizedString("Original Price", comment: "DashSpend"), + value: formattedPrice + ) + + if let cardNumber = card?.cardNumber { + cardValueRow( + title: NSLocalizedString("Card number", comment: "DashSpend"), + value: cardNumber, + onCopy: { + onCopy(cardNumber) + } + ) + } + + if let cardPin = card?.cardPin { + cardValueRow( + title: NSLocalizedString("Card PIN", comment: "DashSpend"), + value: cardPin, + onCopy: { + onCopy(cardPin) + } + ) + } + + multilineValueRow( + title: NSLocalizedString("Cashier instructions", comment: "DashSpend"), + value: NSLocalizedString("Tell the cashier that you'd like to pay with a gift card and share the card number and pin.", comment: "DashSpend") + ) + } + .padding(.horizontal, 14) + } + } + + private func textValueRow(title: String, value: String) -> some View { + HStack(alignment: .top, spacing: 0) { + Text(title) + .font(.subheadMedium) + .foregroundColor(.tertiaryText) + + Spacer() + + Text(value) + .font(.subhead) + .foregroundColor(.primaryText) + } + .padding(.vertical, 12) + } + + private func cardValueRow(title: String, value: String, onCopy: @escaping () -> Void) -> some View { + HStack(alignment: .center) { + Text(title) + .font(.subheadMedium) + .foregroundColor(.tertiaryText) + + Spacer() + + HStack(spacing: 6) { + Text(value) + .font(.subheadline) + .foregroundColor(.primaryText) + + Button(action: onCopy) { + Icon(name: .custom("icon_copy_outline", maxHeight: 14)) + .tint(.primaryText) + } + } + } + .frame(height: 42, alignment: .center) + } + + private func multilineValueRow(title: String, value: String) -> some View { + VStack(alignment: .leading, spacing: 1) { + Text(title) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.tertiaryText) + + Text(value) + .font(.subheadline) + .foregroundColor(.primaryText) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.vertical, 12) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +#Preview("Barcode + PIN") { + GiftCardDetailsInfoCard( + formattedPrice: "$75.00", + card: GiftCardDetailsCardItem( + id: "preview-barcode", + formattedPrice: "$75.00", + cardNumber: "1234 5678 9012", + cardPin: "7890", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + isClaimLink: false + ), + isLoadingCardDetails: false, + hasBeenPollingForLongTime: false, + loadingError: nil, + onOpenClaimLink: { _ in }, + onCopy: { _ in } + ) + .padding() + .background(Color.primaryBackground) +} + +#Preview("Claim Link") { + GiftCardDetailsInfoCard( + formattedPrice: "$50.00", + card: GiftCardDetailsCardItem( + id: "preview-claim", + formattedPrice: "$50.00", + cardNumber: "https://giftcards.example.com/claim/ABC123", + cardPin: nil, + barcodeImage: nil, + isClaimLink: true + ), + isLoadingCardDetails: false, + hasBeenPollingForLongTime: false, + loadingError: nil, + onOpenClaimLink: { _ in }, + onCopy: { _ in } + ) + .padding() + .background(Color.primaryBackground) +} + +#Preview("Loading") { + GiftCardDetailsInfoCard( + formattedPrice: "$100.00", + card: nil, + isLoadingCardDetails: true, + hasBeenPollingForLongTime: true, + loadingError: nil, + onOpenClaimLink: { _ in }, + onCopy: { _ in } + ) + .padding() + .background(Color.primaryBackground) +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift new file mode 100644 index 000000000..4a674938f --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift @@ -0,0 +1,88 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct GiftCardDetailsMerchantHeader: View { + let merchantIcon: UIImage? + let merchantName: String + let purchaseDateText: String? + + var body: some View { + HStack(spacing: 15) { + ZStack(alignment: .bottomTrailing) { + if let icon = merchantIcon { + Image(uiImage: icon) + .resizable() + .transition(.opacity) + .scaledToFit() + .frame(width: 50, height: 50) + .clipShape(Circle()) + } else { + Image("image.explore.dash.wts.payment.gift-card") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + } + + if merchantIcon != nil { + Image("image.explore.dash.wts.payment.gift-card") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .frame(width: 23, height: 23) + .background(Circle().fill(Color.secondaryBackground)) + .offset(x: 2, y: 2) + } + } + + VStack(alignment: .leading, spacing: 1) { + Text(merchantName) + .font(.subhead) + .fontWeight(.medium) + .foregroundColor(.primaryText) + + if let purchaseDateText = purchaseDateText { + Text(purchaseDateText) + .font(.footnote) + .foregroundColor(.tertiaryText) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +#Preview("With Icon") { + GiftCardDetailsMerchantHeader( + merchantIcon: UIImage(systemName: "cart.fill"), + merchantName: "Amazon", + purchaseDateText: "May 04, 2026 at 5:35 PM" + ) + .padding() + .background(Color.primaryBackground) +} + +#Preview("No Icon") { + GiftCardDetailsMerchantHeader( + merchantIcon: nil, + merchantName: "Target", + purchaseDateText: "May 04, 2026 at 3:10 PM" + ) + .padding() + .background(Color.primaryBackground) +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsPoweredBySection.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsPoweredBySection.swift new file mode 100644 index 000000000..61bf82fdd --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsPoweredBySection.swift @@ -0,0 +1,55 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct GiftCardDetailsPoweredBySection: View { + let provider: String? + + var body: some View { + VStack(spacing: 5) { + Text(NSLocalizedString("Powered by", comment: "DashSpend")) + .font(.caption) + .foregroundColor(.tertiaryText) + + if provider == "PiggyCards" { + Image("piggycards.logo") + .resizable() + .scaledToFit() + .frame(height: 22) + } else { + Image("ctx.logo") + .resizable() + .scaledToFit() + .frame(width: 43, height: 13) + } + } + .frame(maxWidth: .infinity) + } +} + +#Preview("CTX") { + GiftCardDetailsPoweredBySection(provider: "CTX") + .padding() + .background(Color.primaryBackground) +} + +#Preview("PiggyCards") { + GiftCardDetailsPoweredBySection(provider: "PiggyCards") + .padding() + .background(Color.primaryBackground) +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift new file mode 100644 index 000000000..5cb9f9bcc --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift @@ -0,0 +1,142 @@ +// +// Created by Codex +// + +import SwiftUI + +struct GiftCardPurchaseSelectionSheet: View { + let merchantIcon: UIImage? + let merchantName: String + let provider: String? + let cards: [GiftCardDetailsCardItem] + let isLoadingCardDetails: Bool + let hasBeenPollingForLongTime: Bool + + let onSelectCard: (Int) -> Void + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 20) { + GiftCardDetailsMerchantHeader( + merchantIcon: merchantIcon, + merchantName: merchantName, + purchaseDateText: nil + ) + + contentSection + + GiftCardDetailsPoweredBySection(provider: provider) + } + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 30) + } + .background(Color.primaryBackground) + } + + @ViewBuilder + private var contentSection: some View { + if cards.isEmpty { + loadingCard + } else { + cardsList + } + } + + private var loadingCard: some View { + VStack { + if isLoadingCardDetails { + SwiftUI.ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(0.9) + } else if hasBeenPollingForLongTime { + Text(NSLocalizedString("As soon as your code is generated, it will be displayed here", comment: "DashSpend")) + .font(.footnote) + .foregroundColor(.tertiaryText) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + } else { + Text(NSLocalizedString("Gift card is being prepared", comment: "DashSpend")) + .font(.subheadline) + .foregroundColor(.tertiaryText) + } + } + .frame(maxWidth: .infinity) + .frame(height: 120) + .background(Color.secondaryBackground) + .cornerRadius(20) + } + + private var cardsList: some View { + VStack(spacing: 10) { + ForEach(Array(cards.enumerated()), id: \.offset) { index, card in + Button(action: { + onSelectCard(index) + }) { + HStack(spacing: 20) { + HStack(spacing: 12) { + if cards.count > 2 { + Text("\(index + 1)") + .font(.caption1.weight(.medium)) + .padding(.horizontal, 4) + .background(Color.gray50) + .clipShape(.rect(cornerRadius: 5)) + } + + Icon( + name: .custom( + provider == "PiggyCards" ? "icon-gift_card-piggy_cards" : "ctx.logo", + maxHeight: 23 + ) + ) + + Text(card.formattedPrice) + .font(.title3) + .fontWeight(.semibold) + .foregroundColor(.primaryText) + } + .frame(maxWidth: .infinity, alignment: .leading) + + Icon(name: .custom("list-chevron-right", maxHeight: 10)) + .padding(.trailing, 14) + } + .frame(height: 42) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + .padding(20) + .background(Color.secondaryBackground) + .cornerRadius(20) + } +} + +#Preview("Multiple") { + GiftCardPurchaseSelectionSheet( + merchantIcon: UIImage(systemName: "cart.fill"), + merchantName: "Amazon", + provider: "PiggyCards", + cards: [ + GiftCardDetailsCardItem( + id: "preview-multi-1", + formattedPrice: "$50.00", + cardNumber: "1111 2222 3333", + cardPin: "1234", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + isClaimLink: false + ), + GiftCardDetailsCardItem( + id: "preview-multi-2", + formattedPrice: "$50.00", + cardNumber: "4444 5555 6666", + cardPin: "9876", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + isClaimLink: false + ) + ], + isLoadingCardDetails: false, + hasBeenPollingForLongTime: false, + onSelectCard: { _ in } + ) +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsView.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsView.swift new file mode 100644 index 000000000..db0004c83 --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsView.swift @@ -0,0 +1,227 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct GiftCardDetailsView: View { + @StateObject private var viewModel: GiftCardDetailsViewModel + @State private var showHowToUse = false + @State private var originalBrightness: CGFloat = -1 + @Binding var backNavigationRequested: Bool + var onShowBackButton: (Bool) -> Void + var onOpenTransaction: ((DSTransaction) -> Void)? + private let selectedCardIndex: Int + private let isPreview: Bool + private let shouldObserve: Bool + + init( + txId: Data, + selectedCardIndex: Int = 0, + backNavigationRequested: Binding, + onShowBackButton: @escaping (Bool) -> Void, + onOpenTransaction: ((DSTransaction) -> Void)? = nil + ) { + _viewModel = StateObject(wrappedValue: GiftCardDetailsViewModel(txId: txId)) + _backNavigationRequested = backNavigationRequested + self.onShowBackButton = onShowBackButton + self.onOpenTransaction = onOpenTransaction + self.selectedCardIndex = selectedCardIndex + self.isPreview = false + self.shouldObserve = true + } + + init( + viewModel: GiftCardDetailsViewModel, + selectedCardIndex: Int = 0, + backNavigationRequested: Binding, + onShowBackButton: @escaping (Bool) -> Void, + onOpenTransaction: ((DSTransaction) -> Void)? = nil + ) { + _viewModel = StateObject(wrappedValue: viewModel) + _backNavigationRequested = backNavigationRequested + self.onShowBackButton = onShowBackButton + self.onOpenTransaction = onOpenTransaction + self.selectedCardIndex = selectedCardIndex + self.isPreview = false + self.shouldObserve = false + } + + fileprivate init(previewViewModel: GiftCardDetailsViewModel) { + _viewModel = StateObject(wrappedValue: previewViewModel) + _backNavigationRequested = .constant(false) + self.onShowBackButton = { _ in } + self.onOpenTransaction = nil + self.selectedCardIndex = 0 + self.isPreview = true + self.shouldObserve = false + } + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 20) { + merchantHeaderSection + giftCardInfoSection + transactionDetailsSection + howToUseSection + poweredBySection + } + } + .padding(.horizontal, 20) + .background(Color.primaryBackground) + .onAppear { + guard !isPreview else { return } + if shouldObserve { + viewModel.startObserving() + } + setMaxBrightness(true) + } + .onDisappear { + guard !isPreview else { return } + if shouldObserve { + viewModel.stopObserving() + } + setMaxBrightness(false) + } + } + + private var merchantHeaderSection: some View { + GiftCardDetailsMerchantHeader( + merchantIcon: viewModel.uiState.merchantIcon, + merchantName: viewModel.uiState.merchantName, + purchaseDateText: purchaseDateText + ) + } + + private var giftCardInfoSection: some View { + GiftCardDetailsInfoCard( + formattedPrice: selectedCard?.formattedPrice ?? viewModel.uiState.formattedPrice, + card: selectedCard, + isLoadingCardDetails: viewModel.uiState.isLoadingCardDetails, + hasBeenPollingForLongTime: viewModel.uiState.hasBeenPollingForLongTime, + loadingError: viewModel.uiState.loadingError, + onOpenClaimLink: openClaimLink, + onCopy: copyToPasteboard + ) + } + + private var selectedCard: GiftCardDetailsCardItem? { + if viewModel.uiState.cards.indices.contains(selectedCardIndex) { + return viewModel.uiState.cards[selectedCardIndex] + } + return viewModel.uiState.cards.first + } + + private var transactionDetailsSection: some View { + Button(action: { + guard let transaction = viewModel.uiState.transaction else { return } + onOpenTransaction?(transaction) + }) { + HStack { + Text(NSLocalizedString("View transaction details", comment: "DashSpend")) + .font(.subheadMedium) + .foregroundColor(.primaryText) + + Spacer() + + Image("greyarrow") + .resizable() + .scaledToFit() + .frame(width: 12, height: 12) + } + .padding(6) + .padding(.horizontal, 14) + .frame(height: 46) + .background(Color.secondaryBackground) + .cornerRadius(20) + } + } + + @ViewBuilder + private var howToUseSection: some View { + Group { + if !showHowToUse { + Button(action: { + withAnimation { + showHowToUse = true + } + }) { + Text(NSLocalizedString("See how to use this gift card", comment: "DashSpend")) + .font(.subheadMedium) + .foregroundColor(.dashBlue) + } + .padding(.horizontal, 16) + } else { + GiftCardDetailsHowToUseSection() + } + } + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + } + + private var poweredBySection: some View { + GiftCardDetailsPoweredBySection(provider: viewModel.uiState.provider) + } + + private var purchaseDateText: String? { + guard let date = viewModel.uiState.purchaseDate else { return nil } + return dateFormatter.string(from: date) + } + + private func openClaimLink(_ claimLink: String) { + guard let url = URL(string: claimLink) else { return } + UIApplication.shared.open(url) + } + + private func copyToPasteboard(_ value: String) { + UIPasteboard.general.string = value + } + + private func setMaxBrightness(_ enable: Bool) { + if enable { + // Save original brightness + if originalBrightness < 0 { + originalBrightness = UIScreen.main.brightness + } + + UIScreen.main.brightness = 1.0 + } else { + // Restore original brightness + if originalBrightness >= 0 { + UIScreen.main.brightness = originalBrightness + originalBrightness = -1 + } + } + } + + private var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM dd, yyyy 'at' h:mm a" + return formatter + } +} + +#Preview("Barcode + PIN") { + GiftCardDetailsView(previewViewModel: .previewBarcodeCard()) +} + +#Preview("Claim Link") { + GiftCardDetailsView(previewViewModel: .previewClaimLinkCard()) +} + +#Preview("Loading State") { + GiftCardDetailsView(previewViewModel: .previewLoadingCard()) +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsViewModel.swift new file mode 100644 index 000000000..5c08b827a --- /dev/null +++ b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsViewModel.swift @@ -0,0 +1,724 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import UIKit + +struct GiftCardDetailsCardItem: Identifiable { + let id: String + let formattedPrice: String + let cardNumber: String? + let cardPin: String? + let barcodeImage: UIImage? + let isClaimLink: Bool +} + +struct GiftCardDetailsUIState { + var merchantName: String = "" + var merchantUrl: String? = nil + var formattedPrice: String = "$0.00" + var cardNumber: String? = nil + var cardPin: String? = nil + var barcodeImage: UIImage? = nil + var merchantIcon: UIImage? = nil + var purchaseDate: Date? = nil + var isLoadingCardDetails: Bool = false + var loadingError: Error? = nil + var transaction: DSTransaction? = nil + var isClaimLink: Bool = false + var hasBeenPollingForLongTime: Bool = false + var provider: String? = nil + var cards: [GiftCardDetailsCardItem] = [] +} + +private struct GiftCardOrderMetadata: Codable { + let orderId: String + let cardAmounts: [String]? + let cards: [GiftCardPayloadEntry]? +} + +private struct GiftCardPayloadEntry: Codable { + let formattedPrice: String + let cardNumber: String? + let cardPin: String? + let barcodeValue: String? + let barcodeFormat: String? + let isClaimLink: Bool +} + +@MainActor +class GiftCardDetailsViewModel: ObservableObject { + private var cancellableBag = Set() + private lazy var ctxSpendRepository = CTXSpendRepository.shared + private lazy var piggyCardsRepository = PiggyCardsRepository.shared + private lazy var giftCardsDAO = GiftCardsDAOImpl.shared + private lazy var customIconDAO = IconBitmapDAOImpl.shared + private lazy var txMetadataDAO = TransactionMetadataDAOImpl.shared + private var tickerTimer: Timer? + private var retryCount = 0 + private let maxRetries = 40 + private let longPollingThreshold = 27 + + let txId: Data + @Published private(set) var uiState = GiftCardDetailsUIState() + + private let currencyFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = "USD" + return formatter + }() + + init(txId: Data, shouldLoadTransaction: Bool = true) { + self.txId = txId + if shouldLoadTransaction { + loadTransaction() + } + } + + func startObserving() { + loadExistingMetadata() + + Task { + await loadGiftCard() + } + + giftCardsDAO.giftCardTxIdPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] publishedTxId in + guard let self = self, + let publishedTxId = publishedTxId, + publishedTxId == self.txId else { return } + + Task { + await self.loadGiftCard() + } + } + .store(in: &cancellableBag) + + self.txMetadataDAO.$lastChange + .receive(on: DispatchQueue.main) + .sink { [weak self] change in + guard let self = self, let change = change else { return } + + Task { + switch change { + case .created(let metadata), .updated(let metadata, _): + guard metadata.txHash == self.txId else { return } + await self.loadIcon(metadata: metadata) + default: + break + } + } + } + .store(in: &cancellableBag) + } + + func stopObserving() { + cancellableBag.removeAll() + stopTicker() + } + + private func loadTransaction() { + Task.detached { [weak self] in + guard let self = self else { return } + + let transaction = DWEnvironment.sharedInstance().currentWallet.allTransactions.first { transaction in + transaction.txHashData == self.txId + } + + await MainActor.run { + if let tx = transaction { + self.uiState.purchaseDate = Date(timeIntervalSince1970: TimeInterval(tx.timestamp)) + self.uiState.transaction = tx + } + } + } + } + + private func loadGiftCard() async { + guard let card = await giftCardsDAO.get(byTxId: txId) else { return } + + let formattedPrice = currencyFormatter.string(from: card.price as NSDecimalNumber) ?? "$0.00" + let displayCards = buildDisplayCards(from: card, fallbackFormattedPrice: formattedPrice) + let isClaimLink = displayCards.first?.isClaimLink ?? (card.number?.starts(with: "http") ?? false) + let barcodeImage = displayCards.first?.barcodeImage ?? imageFromBarcode(value: card.barcodeValue, format: card.barcodeFormat) + + await MainActor.run { + self.uiState.merchantName = card.merchantName + self.uiState.merchantUrl = card.merchantUrl + self.uiState.formattedPrice = formattedPrice + self.uiState.cardNumber = card.number + self.uiState.cardPin = card.pin + self.uiState.provider = card.provider + self.uiState.isClaimLink = isClaimLink + self.uiState.barcodeImage = barcodeImage + self.uiState.cards = displayCards + + if shouldStartPolling(for: card, hasDisplayCards: !displayCards.isEmpty) { + self.startTicker() + } else { + self.stopTicker() + } + } + } + + private func shouldStartPolling(for card: GiftCard, hasDisplayCards: Bool) -> Bool { + guard card.note != nil else { return false } + + if card.provider == "PiggyCards" { + if let note = card.note, decodeOrderMetadata(from: note).cards?.isEmpty == false { + return false + } + return !hasDisplayCards + } + + return card.number == nil + } + + private func startTicker() { + guard tickerTimer == nil else { return } + + uiState.isLoadingCardDetails = true + uiState.loadingError = nil + + Task { + await fetchGiftCardInfo() + } + + tickerTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [weak self] _ in + Task { [weak self] in + await self?.fetchGiftCardInfo() + } + } + } + + private func stopTicker() { + tickerTimer?.invalidate() + tickerTimer = nil + uiState.isLoadingCardDetails = false + uiState.hasBeenPollingForLongTime = false + retryCount = 0 + } + + private func fetchGiftCardInfo() async { + guard let giftCard = await giftCardsDAO.get(byTxId: txId), + giftCard.note != nil else { + stopTicker() + return + } + + if retryCount >= longPollingThreshold { + await MainActor.run { + self.uiState.hasBeenPollingForLongTime = true + } + } + + let providerName = giftCard.provider ?? "nil (defaulting to CTX)" + let txIdHex = txId.map { String(format: "%02x", $0) }.joined() + DSLogger.log("DashSpend: Fetching gift card - Provider: \(providerName), TxId: \(txIdHex)") + + if giftCard.provider == "PiggyCards" { + await fetchPiggyCardsGiftCardInfo() + } else { + await fetchCTXGiftCardInfo() + } + } + + private func fetchCTXGiftCardInfo() async { + guard let giftCard = await giftCardsDAO.get(byTxId: txId), + giftCard.note != nil, + ctxSpendRepository.isUserSignedIn else { + stopTicker() + return + } + + do { + let base58TxId = ((txId as NSData).reverse() as NSData).base58String() + DSLogger.log("DashSpend: Calling CTX API - Base58TxId: \(base58TxId)") + let response = try await ctxSpendRepository.getGiftCardByTxid(txid: base58TxId) + + switch response.status { + case "fulfilled": + if let cardNumber = response.cardNumber, !cardNumber.isEmpty { + await giftCardsDAO.updateCardDetails( + txId: txId, + number: cardNumber, + pin: response.cardPin + ) + + if !cardNumber.isEmpty { + let cleanNumber = cardNumber.replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "-", with: "") + await giftCardsDAO.updateBarcode( + txId: txId, + value: cleanNumber, + format: "CODE128" + ) + } + stopTicker() + DSLogger.log("DashSpend: Gift card details fetched successfully") + } else if let redeemUrl = response.redeemUrl, !redeemUrl.isEmpty { + await giftCardsDAO.updateCardDetails( + txId: txId, + number: redeemUrl, + pin: nil + ) + stopTicker() + DSLogger.log("DashSpend: Gift card redeem URL fetched successfully") + } + + case "rejected": + await MainActor.run { + self.uiState.loadingError = DashSpendError.customError( + NSLocalizedString("Gift card purchase was rejected", comment: "") + ) + } + stopTicker() + + default: + break + } + } catch { + retryCount += 1 + if retryCount >= maxRetries { + await MainActor.run { + self.uiState.loadingError = error + } + stopTicker() + } + DSLogger.log("DashSpend: Failed to fetch gift card info: \(error)") + } + } + + private func fetchPiggyCardsGiftCardInfo() async { + guard let giftCard = await giftCardsDAO.get(byTxId: txId), + let note = giftCard.note, + piggyCardsRepository.isUserSignedIn else { + stopTicker() + return + } + + let metadata = decodeOrderMetadata(from: note) + + do { + DSLogger.log("DashSpend: Calling PiggyCards API - OrderId: \(metadata.orderId)") + let orderStatus = try await piggyCardsRepository.getOrderStatus(orderId: metadata.orderId) + + switch orderStatus.data.status.lowercased() { + case "complete", "completed": + let payload = await buildPayloadEntries( + cards: orderStatus.data.cards, + fallbackFormattedPrice: uiState.formattedPrice, + expectedAmounts: metadata.cardAmounts + ) + + guard !payload.isEmpty else { + await MainActor.run { + self.uiState.loadingError = DashSpendError.customError( + NSLocalizedString("Gift card details are unavailable. Please contact support.", comment: "") + ) + } + stopTicker() + return + } + + let completedMetadata = GiftCardOrderMetadata( + orderId: metadata.orderId, + cardAmounts: metadata.cardAmounts, + cards: payload + ) + let serializedNote = encodeOrderMetadata(completedMetadata) ?? note + let firstEntry = payload.first + + let updatedCard = GiftCard( + txId: giftCard.txId, + merchantName: giftCard.merchantName, + merchantUrl: giftCard.merchantUrl, + price: giftCard.price, + number: firstEntry?.cardNumber, + pin: firstEntry?.cardPin, + barcodeValue: firstEntry?.barcodeValue, + barcodeFormat: firstEntry?.barcodeFormat, + note: serializedNote, + provider: giftCard.provider + ) + + await giftCardsDAO.update(dto: updatedCard) + stopTicker() + + case "failed", "rejected", "cancelled": + await MainActor.run { + self.uiState.loadingError = DashSpendError.customError( + NSLocalizedString("Gift card purchase was rejected", comment: "") + ) + } + stopTicker() + + default: + break + } + } catch { + retryCount += 1 + if retryCount >= maxRetries { + await MainActor.run { + self.uiState.loadingError = error + } + stopTicker() + } + } + } + + private func buildPayloadEntries( + cards: [PiggyCardsOrderGiftCard], + fallbackFormattedPrice: String, + expectedAmounts: [String]? + ) async -> [GiftCardPayloadEntry] { + var entries: [GiftCardPayloadEntry] = [] + + for (index, card) in cards.enumerated() { + let formattedPrice = formattedPriceForCard( + amountString: expectedAmounts?[safe: index], + fallbackFormattedPrice: fallbackFormattedPrice + ) + + if let claimCode = card.claimCode, !claimCode.isEmpty { + let cleanCode = claimCode.replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "-", with: "") + + var barcodeValue = cleanCode + var barcodeFormat = "CODE_128" + + if let barcodeLink = card.barcodeLink, !barcodeLink.isEmpty { + if let parsed = parseBarcodePayload(from: barcodeLink) { + barcodeValue = parsed.value + barcodeFormat = parsed.format + } else if let result = await BarcodeScanner.downloadAndScan(from: barcodeLink) { + barcodeValue = result.value.replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "-", with: "") + barcodeFormat = result.format.rawValue + } + } + + entries.append( + GiftCardPayloadEntry( + formattedPrice: formattedPrice, + cardNumber: claimCode, + cardPin: card.claimPin, + barcodeValue: barcodeValue, + barcodeFormat: barcodeFormat, + isClaimLink: false + ) + ) + } else if let claimLink = card.claimLink, !claimLink.isEmpty { + entries.append( + GiftCardPayloadEntry( + formattedPrice: formattedPrice, + cardNumber: claimLink, + cardPin: nil, + barcodeValue: nil, + barcodeFormat: nil, + isClaimLink: true + ) + ) + } + } + + return entries + } + + private func decodeOrderMetadata(from note: String) -> GiftCardOrderMetadata { + guard let data = note.data(using: .utf8), + let decoded = try? JSONDecoder().decode(GiftCardOrderMetadata.self, from: data) else { + return GiftCardOrderMetadata(orderId: note, cardAmounts: nil, cards: nil) + } + + return decoded + } + + private func encodeOrderMetadata(_ metadata: GiftCardOrderMetadata) -> String? { + guard let data = try? JSONEncoder().encode(metadata) else { return nil } + return String(data: data, encoding: .utf8) + } + + private func buildDisplayCards(from card: GiftCard, fallbackFormattedPrice: String) -> [GiftCardDetailsCardItem] { + if let note = card.note, let payload = decodeOrderMetadata(from: note).cards, !payload.isEmpty { + return payload.enumerated().map { index, entry in + GiftCardDetailsCardItem( + id: "\(index)-\(entry.cardNumber ?? "")", + formattedPrice: entry.formattedPrice, + cardNumber: entry.cardNumber, + cardPin: entry.cardPin, + barcodeImage: imageFromBarcode(value: entry.barcodeValue, format: entry.barcodeFormat), + isClaimLink: entry.isClaimLink + ) + } + } + + if card.number != nil || card.pin != nil || card.barcodeValue != nil { + return [ + GiftCardDetailsCardItem( + id: "legacy-0", + formattedPrice: fallbackFormattedPrice, + cardNumber: card.number, + cardPin: card.pin, + barcodeImage: imageFromBarcode(value: card.barcodeValue, format: card.barcodeFormat), + isClaimLink: card.number?.starts(with: "http") ?? false + ) + ] + } + + return [] + } + + private func formattedPriceForCard(amountString: String?, fallbackFormattedPrice: String) -> String { + guard let amountString, + let amount = Decimal(string: amountString) else { + return fallbackFormattedPrice + } + + return currencyFormatter.string(from: amount as NSDecimalNumber) ?? fallbackFormattedPrice + } + + private func parseBarcodePayload(from link: String) -> (value: String, format: String)? { + guard let components = URLComponents(string: link), + let queryItems = components.queryItems, + !queryItems.isEmpty else { return nil } + + let loweredItems = queryItems.reduce(into: [String: String]()) { dict, item in + guard let value = item.value, !value.isEmpty else { return } + dict[item.name.lowercased()] = value + } + + let valueKeys = ["text", "data", "code", "barcode"] + guard let rawValue = valueKeys.compactMap({ loweredItems[$0] }).first, !rawValue.isEmpty else { + return nil + } + + let formatCandidate = loweredItems["format"] ?? loweredItems["type"] ?? loweredItems["symbology"] + let normalizedValue = rawValue + .replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "-", with: "") + return (normalizedValue, normalizeBarcodeFormat(formatCandidate)) + } + + private func normalizeBarcodeFormat(_ rawFormat: String?) -> String { + let normalized = (rawFormat ?? "CODE_128") + .trimmingCharacters(in: .whitespacesAndNewlines) + .uppercased() + .replacingOccurrences(of: "-", with: "_") + + switch normalized { + case "CODE128", "CODE_128": return "CODE_128" + case "QRCODE", "QR_CODE", "QR": return "QR_CODE" + case "PDF417", "PDF_417": return "PDF_417" + case "AZTEC", "AZTEC_CODE": return "AZTEC" + case "DATAMATRIX", "DATA_MATRIX": return "DATA_MATRIX" + default: return "CODE_128" + } + } + + private func loadExistingMetadata() { + Task { + if let metadata = txMetadataDAO.get(by: txId) { + await self.loadIcon(metadata: metadata) + } + } + } + + private func loadIcon(metadata: TransactionMetadata) async { + if let customIconId = metadata.customIconId, + let iconBitmap = await self.customIconDAO.getBitmap(id: customIconId) { + guard let image = UIImage(data: iconBitmap.imageData) else { + DSLogger.log("Failed to create image from data for tx icon: \(metadata.txHash.hexEncodedString())") + return + } + + self.uiState.merchantIcon = image + } + } + + private func imageFromBarcode(value: String?, format: String?) -> UIImage? { + guard let value, !value.isEmpty else { return nil } + let normalizedFormat = normalizeBarcodeFormat(format) + + let filterName: String + let transform: CGAffineTransform + switch normalizedFormat { + case "QR_CODE": + filterName = "CIQRCodeGenerator" + transform = CGAffineTransform(scaleX: 6.0, y: 6.0) + case "PDF_417": + filterName = "CIPDF417BarcodeGenerator" + transform = CGAffineTransform(scaleX: 3.0, y: 3.0) + case "AZTEC": + filterName = "CIAztecCodeGenerator" + transform = CGAffineTransform(scaleX: 6.0, y: 6.0) + case "DATA_MATRIX": + filterName = "CIDataMatrixCodeGenerator" + transform = CGAffineTransform(scaleX: 8.0, y: 8.0) + default: + filterName = "CICode128BarcodeGenerator" + transform = CGAffineTransform(scaleX: 3.0, y: 5.0) + } + + guard let filter = CIFilter(name: filterName) else { return nil } + filter.setValue(value.data(using: .utf8), forKey: "inputMessage") + if normalizedFormat == "QR_CODE" { + filter.setValue("M", forKey: "inputCorrectionLevel") + } + + guard let outputImage = filter.outputImage else { return nil } + let transformedImage = outputImage.transformed(by: transform) + let context = CIContext() + guard let cgImage = context.createCGImage(transformedImage, from: transformedImage.extent) else { return nil } + + return UIImage(cgImage: cgImage) + } +} + +private extension Array { + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } +} + +extension GiftCardDetailsViewModel { + static func previewBarcodeCard() -> GiftCardDetailsViewModel { + let viewModel = GiftCardDetailsViewModel(txId: Data(), shouldLoadTransaction: false) + viewModel.uiState = GiftCardDetailsUIState( + merchantName: "Amazon", + merchantUrl: "https://www.amazon.com", + formattedPrice: "$75.00", + cardNumber: "1234 5678 9012", + cardPin: "7890", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + merchantIcon: UIImage(systemName: "cart.fill"), + purchaseDate: Date(timeIntervalSince1970: 1_713_484_800), + isLoadingCardDetails: false, + loadingError: nil, + transaction: nil, + isClaimLink: false, + hasBeenPollingForLongTime: false, + provider: "CTX", + cards: [ + GiftCardDetailsCardItem( + id: "preview-1", + formattedPrice: "$75.00", + cardNumber: "1234 5678 9012", + cardPin: "7890", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + isClaimLink: false + ) + ] + ) + return viewModel + } + + static func previewClaimLinkCard() -> GiftCardDetailsViewModel { + let viewModel = GiftCardDetailsViewModel(txId: Data(), shouldLoadTransaction: false) + viewModel.uiState = GiftCardDetailsUIState( + merchantName: "Steam", + merchantUrl: "https://store.steampowered.com", + formattedPrice: "$50.00", + cardNumber: "https://giftcards.example.com/claim/ABC123", + cardPin: nil, + barcodeImage: nil, + merchantIcon: UIImage(systemName: "gamecontroller.fill"), + purchaseDate: Date(timeIntervalSince1970: 1_713_571_200), + isLoadingCardDetails: false, + loadingError: nil, + transaction: nil, + isClaimLink: true, + hasBeenPollingForLongTime: false, + provider: "PiggyCards", + cards: [ + GiftCardDetailsCardItem( + id: "preview-claim-1", + formattedPrice: "$50.00", + cardNumber: "https://giftcards.example.com/claim/ABC123", + cardPin: nil, + barcodeImage: nil, + isClaimLink: true + ) + ] + ) + return viewModel + } + + static func previewLoadingCard() -> GiftCardDetailsViewModel { + let viewModel = GiftCardDetailsViewModel(txId: Data(), shouldLoadTransaction: false) + viewModel.uiState = GiftCardDetailsUIState( + merchantName: "Target", + merchantUrl: "https://www.target.com", + formattedPrice: "$100.00", + cardNumber: nil, + cardPin: nil, + barcodeImage: nil, + merchantIcon: UIImage(systemName: "bag.fill"), + purchaseDate: Date(timeIntervalSince1970: 1_713_657_600), + isLoadingCardDetails: true, + loadingError: nil, + transaction: nil, + isClaimLink: false, + hasBeenPollingForLongTime: true, + provider: "CTX", + cards: [] + ) + return viewModel + } + + static func previewMultipleCards() -> GiftCardDetailsViewModel { + let viewModel = GiftCardDetailsViewModel(txId: Data(), shouldLoadTransaction: false) + viewModel.uiState = GiftCardDetailsUIState( + merchantName: "Amazon", + merchantUrl: "https://www.amazon.com", + formattedPrice: "$125.00", + cardNumber: "1111 2222 3333", + cardPin: "1234", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + merchantIcon: UIImage(systemName: "cart.fill"), + purchaseDate: Date(timeIntervalSince1970: 1_713_484_800), + isLoadingCardDetails: false, + loadingError: nil, + transaction: nil, + isClaimLink: false, + hasBeenPollingForLongTime: false, + provider: "PiggyCards", + cards: [ + GiftCardDetailsCardItem( + id: "preview-multi-1", + formattedPrice: "$25.00", + cardNumber: "1111 2222 3333", + cardPin: "1234", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + isClaimLink: false + ), + GiftCardDetailsCardItem( + id: "preview-multi-2", + formattedPrice: "$100.00", + cardNumber: "4444 5555 6666", + cardPin: "9876", + barcodeImage: UIImage(systemName: "barcode.viewfinder"), + isClaimLink: false + ) + ] + ) + return viewModel + } +} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift deleted file mode 100644 index ecf11efdf..000000000 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift +++ /dev/null @@ -1,404 +0,0 @@ -// -// Created by Andrei Ashikhmin -// Copyright © 2025 Dash Core Group. All rights reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI -import SDWebImageSwiftUI -import CoreImage.CIFilterBuiltins - -struct GiftCardDetailsView: View { - @StateObject private var viewModel: GiftCardDetailsViewModel - @State private var showHowToUse = false - @State private var navigationController: UINavigationController? = nil - @State private var originalBrightness: CGFloat = -1 - @State private var openTransaction: Bool = false - @Binding var backNavigationRequested: Bool - var onShowBackButton: (Bool) -> Void - - init(txId: Data, backNavigationRequested: Binding, onShowBackButton: @escaping (Bool) -> Void) { - _viewModel = StateObject(wrappedValue: GiftCardDetailsViewModel(txId: txId)) - _backNavigationRequested = backNavigationRequested - self.onShowBackButton = onShowBackButton - } - - var body: some View { - ScrollView { - VStack(spacing: 0) { - // Merchant header - HStack(spacing: 15) { - ZStack(alignment: .bottomTrailing) { - if let icon = viewModel.uiState.merchantIcon { - Image(uiImage: icon) - .resizable() - .transition(.fade(duration: 0.3)) - .scaledToFit() - .frame(width: 48, height: 48) - .clipShape(Circle()) - } else { - Image("image.explore.dash.wts.payment.gift-card") - .resizable() - .scaledToFit() - .frame(width: 48, height: 48) - } - - // Secondary icon - if viewModel.uiState.merchantIcon != nil { - Image("image.explore.dash.wts.payment.gift-card") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .frame(width: 23, height: 23) - .background(Circle().fill(Color.secondaryBackground)) - .offset(x: 2, y: 2) - } - } - - VStack(alignment: .leading, spacing: 4) { - Text(viewModel.uiState.merchantName) - .font(.subhead) - .fontWeight(.medium) - .foregroundColor(.primaryText) - - if let date = viewModel.uiState.purchaseDate { - Text(date, formatter: dateFormatter) - .font(.footnote) - .foregroundColor(.tertiaryText) - } - } - - Spacer() - } - .padding(.horizontal, 20) - .padding(.top, 20) - - // Gift card info container - VStack(spacing: 0) { - // Barcode/Claim Link section - if viewModel.uiState.isClaimLink { - // Case 3: Claim link button - // No barcode section for claim links - } else if let barcodeImage = viewModel.uiState.barcodeImage { - // Case 2: Barcode available - Image(uiImage: barcodeImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: .infinity) - .frame(height: 100) - .padding(.horizontal, 20) - .padding(.vertical, 20) - } else { - // Case 1: Loading or waiting for barcode - ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(Color(UIColor.systemGray6)) - .frame(height: 72) - - if viewModel.uiState.isLoadingCardDetails { - if viewModel.uiState.hasBeenPollingForLongTime { - // After 60 seconds, show message - Text(NSLocalizedString("As soon as your code is generated, it will be displayed here", comment: "DashSpend")) - .font(.footnote) - .foregroundColor(.tertiaryText) - .multilineTextAlignment(.center) - .padding(.horizontal, 50) - } else { - // First 60 seconds, show spinner - SwiftUI.ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .scaleEffect(0.8) - } - } else if viewModel.uiState.loadingError != nil { - Text(NSLocalizedString("Failed to load barcode", comment: "DashSpend")) - .font(.footnote) - .foregroundColor(.systemRed) - } - } - .padding(.horizontal, 6) - .padding(.top, 6) - } - - // Original purchase value - HStack { - Text(NSLocalizedString("Original purchase", comment: "DashSpend")) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - Text(viewModel.uiState.formattedPrice) - .font(.subheadline) - .foregroundColor(.primaryText) - } - .padding(.horizontal, 20) - .padding(.top, 20) - - // Card number and PIN or Claim Link - if viewModel.uiState.isClaimLink { - // Case 3: Show claim link as centered text - if let claimLink = viewModel.uiState.cardNumber { - Button(action: { - if let url = URL(string: claimLink) { - UIApplication.shared.open(url) - } - }) { - Text(NSLocalizedString("View your gift card details", comment: "DashSpend")) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.dashBlue) - .frame(maxWidth: .infinity, alignment: .center) - } - .buttonStyle(PlainButtonStyle()) - .padding(.horizontal, 20) - .padding(.top, 20) - .padding(.bottom, 20) - } - } else if viewModel.uiState.isLoadingCardDetails { - // Still loading card details - if !viewModel.uiState.hasBeenPollingForLongTime { - // Don't show spinner here if we're already showing it above - EmptyView() - } - } else if let error = viewModel.uiState.loadingError { - VStack(spacing: 10) { - Text(error.localizedDescription) - .font(.caption) - .foregroundColor(.systemRed) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - } - .padding(.vertical, 20) - } else { - // Case 2: Show card number and PIN - VStack(spacing: 20) { - // Card number - if let cardNumber = viewModel.uiState.cardNumber, !viewModel.uiState.isClaimLink { - HStack { - Text(NSLocalizedString("Card number", comment: "DashSpend")) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - HStack(spacing: 6) { - Text(cardNumber) - .font(.subheadline) - .foregroundColor(.primaryText) - - Button(action: { - UIPasteboard.general.string = cardNumber - // TODO: Show toast - }) { - Image("icon_copy_outline") - .resizable() - .scaledToFit() - .tint(.primaryText) - .frame(width: 14, height: 14) - } - } - } - .padding(.horizontal, 20) - } - - // Card PIN - if let cardPin = viewModel.uiState.cardPin { - HStack { - Text(NSLocalizedString("Card PIN", comment: "DashSpend")) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - - Spacer() - - HStack(spacing: 6) { - Text(cardPin) - .font(.subheadline) - .foregroundColor(.primaryText) - - Button(action: { - UIPasteboard.general.string = cardPin - // TODO: Show toast - }) { - Image("icon_copy_outline") - .resizable() - .scaledToFit() - .tint(.primaryText) - .frame(width: 14, height: 14) - } - } - } - .padding(.horizontal, 20) - } - } - .padding(.top, 20) - .padding(.bottom, 20) - } - } - .background(Color.secondaryBackground) - .cornerRadius(12) - .padding(.horizontal, 20) - .padding(.top, 20) - - Button(action: { - openTransaction = true - onShowBackButton(true) - }) { - HStack { - Text(NSLocalizedString("View transaction details", comment: "DashSpend")) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.primaryText) - - Spacer() - - Image("greyarrow") - .resizable() - .scaledToFit() - .frame(width: 12, height: 12) - } - .padding(.horizontal, 15) - .padding(.vertical, 20) - .background(Color.secondaryBackground) - .cornerRadius(12) - } - .padding(.horizontal, 20) - .padding(.top, 20) - - // How to use button - if !showHowToUse { - Button(action: { - withAnimation { - showHowToUse = true - } - }) { - Text(NSLocalizedString("See how to use this gift card", comment: "DashSpend")) - .font(.calloutMedium) - .foregroundColor(.dashBlue) - } - .padding(.top, 30) - } else { - // How to use expanded content - VStack(alignment: .leading, spacing: 30) { - Text(NSLocalizedString("How to use your gift card", comment: "DashSpend")) - .font(.subhead) - .fontWeight(.medium) - .foregroundColor(.tertiaryText) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 20) - - // Self-checkout - FeatureSingleItem( - iconName: .custom("dp_user_generic"), - title: NSLocalizedString("Self-checkout", comment: "DashSpend"), - description: NSLocalizedString("Request assistance and show the barcode on your screen for scanning.", comment: "DashSpend") - ) - - // In store - FeatureSingleItem( - iconName: .custom("image.dashspend.shop"), - title: NSLocalizedString("In store", comment: "DashSpend"), - description: NSLocalizedString("Tell the cashier that you'd like to pay with a gift card and share the card number and pin.", comment: "DashSpend") - ) - - // Online - FeatureSingleItem( - iconName: .custom("image.dashspend.online"), - title: NSLocalizedString("Online", comment: "DashSpend"), - description: NSLocalizedString("In the payment section of your checkout, select \"gift card\" and enter your card number and pin.", comment: "DashSpend") - ) - } - .padding(.horizontal, 20) - .padding(.bottom, 35) - .background(Color.secondaryBackground) - .cornerRadius(12) - .padding(.horizontal, 20) - .padding(.top, 20) - } - - // Powered by Provider - VStack(spacing: 8) { - Text(NSLocalizedString("Powered by", comment: "DashSpend")) - .font(.caption) - .foregroundColor(.tertiaryText) - - if viewModel.uiState.provider == "PiggyCards" { - Image("piggycards.logo") - .resizable() - .scaledToFit() - .frame(height: 22) - } else { - // Default to CTX logo - Image("ctx.logo") - .resizable() - .scaledToFit() - .frame(width: 49, height: 18) - } - } - .padding(.top, 30) - .padding(.bottom, 40) - } - - if let transaction = viewModel.uiState.transaction { - NavigationLink( - destination: - TXDetailVCWrapper( - transaction: transaction, - navigateBack: $backNavigationRequested, - onDismissed: { - onShowBackButton(false) - } - ).navigationBarHidden(true), - isActive: $openTransaction - ) { - SwiftUI.EmptyView() - }.opacity(0) - } - } - .background(Color.primaryBackground) - .onAppear { - viewModel.startObserving() - setMaxBrightness(true) - } - .onDisappear { - viewModel.stopObserving() - setMaxBrightness(false) - } - } - - private func setMaxBrightness(_ enable: Bool) { - if enable { - // Save original brightness - if originalBrightness < 0 { - originalBrightness = UIScreen.main.brightness - } - - UIScreen.main.brightness = 1.0 - } else { - // Restore original brightness - if originalBrightness >= 0 { - UIScreen.main.brightness = originalBrightness - originalBrightness = -1 - } - } - } - - private var dateFormatter: DateFormatter { - let formatter = DateFormatter() - formatter.dateFormat = "MMMM dd, yyyy 'at' h:mm a" - return formatter - } -} diff --git a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift b/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift deleted file mode 100644 index 472ef3ff3..000000000 --- a/DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift +++ /dev/null @@ -1,403 +0,0 @@ -// -// Created by Andrei Ashikhmin -// Copyright © 2025 Dash Core Group. All rights reserved. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://opensource.org/licenses/MIT -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Combine -import UIKit - -struct GiftCardDetailsUIState { - var merchantName: String = "" - var merchantUrl: String? = nil - var formattedPrice: String = "$0.00" - var cardNumber: String? = nil - var cardPin: String? = nil - var barcodeImage: UIImage? = nil - var merchantIcon: UIImage? = nil - var purchaseDate: Date? = nil - var isLoadingCardDetails: Bool = false - var loadingError: Error? = nil - var transaction: DSTransaction? = nil - var isClaimLink: Bool = false - var hasBeenPollingForLongTime: Bool = false - var provider: String? = nil -} - -@MainActor -class GiftCardDetailsViewModel: ObservableObject { - private var cancellableBag = Set() - private let ctxSpendRepository = CTXSpendRepository.shared - private let piggyCardsRepository = PiggyCardsRepository.shared - private let giftCardsDAO = GiftCardsDAOImpl.shared - private let customIconDAO = IconBitmapDAOImpl.shared - private let txMetadataDAO = TransactionMetadataDAOImpl.shared - private var tickerTimer: Timer? - private var retryCount = 0 - private let maxRetries = 40 // 60 seconds / 1.5 sec interval = 40 retries - private let longPollingThreshold = 27 // Show "code is being generated" message after ~40s (27 * 1.5s) - - let txId: Data - @Published private(set) var uiState = GiftCardDetailsUIState() - - private let currencyFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.currencyCode = "USD" - return formatter - }() - - init(txId: Data) { - self.txId = txId - loadTransaction() - } - - func startObserving() { - loadExistingMetadata() - - // Load the initial gift card data - Task { - await loadGiftCard() - } - - // Observe changes to gift card txId - giftCardsDAO.giftCardTxIdPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] publishedTxId in - guard let self = self, - let publishedTxId = publishedTxId, - publishedTxId == self.txId else { return } - - // The gift card for our txId has changed, fetch it - Task { - await self.loadGiftCard() - } - } - .store(in: &cancellableBag) - - self.txMetadataDAO.$lastChange - .receive(on: DispatchQueue.main) - .sink { [weak self] change in - guard let self = self, let change = change else { return } - - Task { - switch change { - case .created(let metadata), .updated(let metadata, _): - await self.loadIcon(metadata: metadata) - default: - break - } - } - } - .store(in: &cancellableBag) - } - - func stopObserving() { - cancellableBag.removeAll() - stopTicker() - } - - private func loadTransaction() { - Task.detached { [weak self] in - guard let self = self else { return } - - let transaction = DWEnvironment.sharedInstance().currentWallet.allTransactions.first { transaction in - return transaction.txHashData == self.txId - } - - await MainActor.run { - if let tx = transaction { - self.uiState.purchaseDate = Date(timeIntervalSince1970: TimeInterval(tx.timestamp)) - self.uiState.transaction = tx - } - } - } - } - - private func loadGiftCard() async { - guard let card = await giftCardsDAO.get(byTxId: txId) else { return } - - await MainActor.run { - self.uiState.merchantName = card.merchantName - self.uiState.merchantUrl = card.merchantUrl - self.uiState.formattedPrice = self.currencyFormatter.string(from: card.price as NSDecimalNumber) ?? "$0.00" - self.uiState.cardNumber = card.number - self.uiState.cardPin = card.pin - self.uiState.provider = card.provider - - // Check if this is a claim link (URL in number field) - if let number = card.number, number.starts(with: "http") { - self.uiState.isClaimLink = true - } - - // Generate barcode if we have the value - if let barcodeValue = card.barcodeValue { - self.generateBarcode(from: barcodeValue, format: card.barcodeFormat ?? "CODE128") - } - - // If we don't have card details yet but have a note (payment ID), start ticker - if card.number == nil && card.note != nil { - self.startTicker() - } else { - self.stopTicker() - } - } - } - - private func startTicker() { - guard tickerTimer == nil else { return } - - uiState.isLoadingCardDetails = true - uiState.loadingError = nil - - Task { - await fetchGiftCardInfo() - } - - // Set up timer for periodic fetches - tickerTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [weak self] _ in - Task { [weak self] in - await self?.fetchGiftCardInfo() - } - } - } - - private func stopTicker() { - tickerTimer?.invalidate() - tickerTimer = nil - uiState.isLoadingCardDetails = false - uiState.hasBeenPollingForLongTime = false - retryCount = 0 - } - - private func fetchGiftCardInfo() async { - guard let giftCard = await giftCardsDAO.get(byTxId: txId), - let _ = giftCard.note else { - stopTicker() - return - } - - // Check if we've been polling for more than 60 seconds - if retryCount >= longPollingThreshold { - await MainActor.run { - self.uiState.hasBeenPollingForLongTime = true - } - } - - // Log provider decision for debugging upgrade issues - let providerName = giftCard.provider ?? "nil (defaulting to CTX)" - let txIdHex = txId.map { String(format: "%02x", $0) }.joined() - DSLogger.log("DashSpend: Fetching gift card - Provider: \(providerName), TxId: \(txIdHex)") - - // Check provider and call appropriate API - if giftCard.provider == "PiggyCards" { - await fetchPiggyCardsGiftCardInfo() - } else { - // Default to CTX for backward compatibility - await fetchCTXGiftCardInfo() - } - } - - private func fetchCTXGiftCardInfo() async { - guard let giftCard = await giftCardsDAO.get(byTxId: txId), - let _ = giftCard.note, - ctxSpendRepository.isUserSignedIn else { - stopTicker() - return - } - - do { - let base58TxId = ((txId as NSData).reverse() as NSData).base58String() - DSLogger.log("DashSpend: Calling CTX API - Base58TxId: \(base58TxId)") - let response = try await ctxSpendRepository.getGiftCardByTxid(txid: base58TxId) - - switch response.status { - case "fulfilled": - if let cardNumber = response.cardNumber, !cardNumber.isEmpty { - // Update gift card with received details - await giftCardsDAO.updateCardDetails( - txId: txId, - number: cardNumber, - pin: response.cardPin - ) - - // Save barcode - if !cardNumber.isEmpty { - let cleanNumber = cardNumber.replacingOccurrences(of: " ", with: "") - .replacingOccurrences(of: "-", with: "") - await giftCardsDAO.updateBarcode( - txId: txId, - value: cleanNumber, - format: "CODE128" - ) - } - stopTicker() - DSLogger.log("DashSpend: Gift card details fetched successfully") - } - - case "rejected": - await MainActor.run { - self.uiState.loadingError = DashSpendError.customError( - NSLocalizedString("Gift card purchase was rejected", comment: "") - ) - } - stopTicker() - - default: - // Keep polling for other statuses - break - } - } catch { - retryCount += 1 - if retryCount >= maxRetries { - await MainActor.run { - self.uiState.loadingError = error - } - stopTicker() - } - DSLogger.log("DashSpend: Failed to fetch gift card info: \(error)") - } - } - - private func fetchPiggyCardsGiftCardInfo() async { - guard let giftCard = await giftCardsDAO.get(byTxId: txId), - let orderId = giftCard.note, - piggyCardsRepository.isUserSignedIn else { - stopTicker() - return - } - - do { - DSLogger.log("DashSpend: Calling PiggyCards API - OrderId: \(orderId)") - let orderStatus = try await piggyCardsRepository.getOrderStatus(orderId: orderId) - - switch orderStatus.data.status.lowercased() { - case "complete", "completed": - // Get the card details from the order - let cards = orderStatus.data.cards - if let firstCard = cards.first { - - // Update gift card with received details - if let claimCode = firstCard.claimCode, !claimCode.isEmpty { - await giftCardsDAO.updateCardDetails( - txId: txId, - number: claimCode, - pin: firstCard.claimPin - ) - - // Download and scan barcode from URL if available (matching Android) - if let barcodeLink = firstCard.barcodeLink, !barcodeLink.isEmpty { - if let result = await BarcodeScanner.downloadAndScan(from: barcodeLink) { - // Clean the barcode value (remove spaces and dashes) - let cleanValue = result.value.replacingOccurrences(of: " ", with: "") - .replacingOccurrences(of: "-", with: "") - await giftCardsDAO.updateBarcode( - txId: txId, - value: cleanValue, - format: result.format.rawValue - ) - } else { - // Fallback: Generate barcode from claimCode - let cleanCode = claimCode.replacingOccurrences(of: " ", with: "") - .replacingOccurrences(of: "-", with: "") - await giftCardsDAO.updateBarcode( - txId: txId, - value: cleanCode, - format: "CODE_128" - ) - } - } else { - // Fallback: Generate barcode from claimCode (legacy behavior) - let cleanCode = claimCode.replacingOccurrences(of: " ", with: "") - .replacingOccurrences(of: "-", with: "") - await giftCardsDAO.updateBarcode( - txId: txId, - value: cleanCode, - format: "CODE_128" - ) - } - stopTicker() - } else if let claimLink = firstCard.claimLink, !claimLink.isEmpty { - // Link-based redemption - await giftCardsDAO.updateCardDetails( - txId: txId, - number: claimLink, // Store URL as the card number - pin: nil // No PIN for link-based cards - ) - stopTicker() - } - } - - case "failed", "rejected", "cancelled": - await MainActor.run { - self.uiState.loadingError = DashSpendError.customError( - NSLocalizedString("Gift card purchase was rejected", comment: "") - ) - } - stopTicker() - - default: - // Keep polling for other statuses - break - } - } catch { - retryCount += 1 - if retryCount >= maxRetries { - await MainActor.run { - self.uiState.loadingError = error - } - stopTicker() - } - } - } - - private func loadExistingMetadata() { - Task { - if let metadata = txMetadataDAO.get(by: txId) { - await self.loadIcon(metadata: metadata) - } - } - } - - private func loadIcon(metadata: TransactionMetadata) async { - if let customIconId = metadata.customIconId, - let iconBitmap = await self.customIconDAO.getBitmap(id: customIconId) { - guard let image = UIImage(data: iconBitmap.imageData) else { - DSLogger.log("Failed to create image from data for tx icon: \(metadata.txHash.hexEncodedString())") - return - } - - self.uiState.merchantIcon = image - } - } - - private func generateBarcode(from string: String, format: String) { - guard let filter = CIFilter(name: "CICode128BarcodeGenerator") else { return } - - let data = string.data(using: .ascii) - filter.setValue(data, forKey: "inputMessage") - - guard let outputImage = filter.outputImage else { return } - - let scaleX = 3.0 - let scaleY = 5.0 - let transformedImage = outputImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY)) - - let context = CIContext() - if let cgImage = context.createCGImage(transformedImage, from: transformedImage.extent) { - uiState.barcodeImage = UIImage(cgImage: cgImage) - } - } -} diff --git a/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift b/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift index 389889a2e..7ab5e367a 100644 --- a/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift +++ b/DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift @@ -19,7 +19,7 @@ import UIKit import SafariServices import SwiftUI -extension HomeViewController: DWLocalCurrencyViewControllerDelegate, ExploreViewControllerDelegate { +extension HomeViewController: DWLocalCurrencyViewControllerDelegate { func performAction(for action: ShortcutAction, sender: UIView?) { switch action.type { case .secureWallet: @@ -132,10 +132,22 @@ extension HomeViewController: DWLocalCurrencyViewControllerDelegate, ExploreView } private func showExploreDash() { - let controller = ExploreViewController() - controller.delegate = self - let navigationController = BaseNavigationController(rootViewController: controller) - present(navigationController, animated: true, completion: nil) + guard let navController = navigationController else { return } + let screen = ExploreMenuScreen( + vc: navController, + onShowSendPayment: { [weak self] in + self?.delegate?.showPaymentsController(withActivePage: PaymentsViewControllerState.pay.rawValue) + }, + onShowReceivePayment: { [weak self] in + self?.delegate?.showPaymentsController(withActivePage: PaymentsViewControllerState.receive.rawValue) + }, + onShowGiftCard: { [weak self] txId in + self?.showGiftCardDetails(txId: txId) + } + ) + let hostingController = UIHostingController(rootView: screen) + hostingController.hidesBottomBarWhenPushed = true + navController.pushViewController(hostingController, animated: true) } private func showWhereToSpend() { @@ -170,7 +182,8 @@ extension HomeViewController: DWLocalCurrencyViewControllerDelegate, ExploreView private func showSendToContact() { #if DASHPAY - let controller = DWContactsViewController() + let controller = DWContactsViewController(payModel: payModel, dataProvider: dataProvider) + controller.intent = .payToSelector controller.payDelegate = self let navigationController = BaseNavigationController(rootViewController: controller) present(navigationController, animated: true, completion: nil) @@ -298,17 +311,15 @@ extension HomeViewController: DWLocalCurrencyViewControllerDelegate, ExploreView controller.navigationController?.dismiss(animated: true, completion: nil) } - // MARK: - DWExploreTestnetViewControllerDelegate - - func exploreViewControllerShowSendPayment(_ controller: ExploreViewController) { - delegate?.showPaymentsController(withActivePage: PaymentsViewControllerState.pay.rawValue) - } - - func exploreViewControllerShowReceivePayment(_ controller: ExploreViewController) { - delegate?.showPaymentsController(withActivePage: PaymentsViewControllerState.receive.rawValue) - } - - func exploreViewControllerShowGiftCard(_ controller: ExploreViewController, txId: Data) { - showGiftCardDetails(txId: txId) +} +#if DASHPAY +// MARK: DWContactsViewControllerPayDelegate +extension HomeViewController: DWContactsViewControllerPayDelegate { + func contactsViewController(_ controller: DWContactsViewController, payTo item: DWDPBasicUserItem) { + dismiss(animated: true) { [weak self] in + self?.performPay(toUser: item) + } } } +#endif + diff --git a/DashWallet/Sources/UI/Home/HomeViewController.swift b/DashWallet/Sources/UI/Home/HomeViewController.swift index a0a9b2881..eaf7cf718 100644 --- a/DashWallet/Sources/UI/Home/HomeViewController.swift +++ b/DashWallet/Sources/UI/Home/HomeViewController.swift @@ -218,7 +218,26 @@ class HomeViewController: DWBasePayViewController, NavigationBarDisplayable { let hostingController = UIHostingController(rootView: GiftCardDetailsSheet(txId: txId).background(Color.primaryBackground) ) - + hostingController.modalPresentationStyle = .pageSheet + + if #available(iOS 16.4, *) { + if let sheet = hostingController.sheetPresentationController { + let compactId = UISheetPresentationController.Detent.Identifier("giftCardCompact") + let compactDetent = UISheetPresentationController.Detent.custom(identifier: compactId) { context in + // Keep initial height adaptive across devices while staying close to the + // compact GiftCard selection layout used in HomeView's SwiftUI sheet. + let estimated = context.maximumDetentValue * 0.58 + return min(max(estimated, 380), 540) + } + + sheet.detents = [compactDetent, .large()] + sheet.selectedDetentIdentifier = compactId + sheet.preferredCornerRadius = 32 + sheet.prefersGrabberVisible = false + sheet.prefersScrollingExpandsWhenScrolledToEdge = false + } + } + present(hostingController, animated: true, completion: nil) } diff --git a/DashWallet/Sources/UI/Home/Views/HomeView.swift b/DashWallet/Sources/UI/Home/Views/HomeView.swift index 2b443183a..ea118cca0 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeView.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeView.swift @@ -463,24 +463,202 @@ struct HomeViewContent: View { } struct GiftCardDetailsSheet: View { - var txId: Data + @Environment(\.dismiss) private var dismiss + + let txId: Data @State private var showBackButton: Bool = false @State private var backNavigationRequested: Bool = false + @State private var selectedCardIndex: Int? = nil + @State private var txDetailRoute: TxDetailRoute? = nil + @State private var prefersLargeDetent: Bool = false + @State private var compactSheetHeight: CGFloat = 0 + @StateObject private var viewModel: GiftCardDetailsViewModel + + init(txId: Data) { + self.txId = txId + _viewModel = StateObject(wrappedValue: GiftCardDetailsViewModel(txId: txId)) + } var body: some View { - BottomSheet(showBackButton: $showBackButton, onBackButtonPressed: { - backNavigationRequested = true + let dialog = BottomSheet(showBackButton: $showBackButton, onBackButtonPressed: { + handleBackNavigation() }) { - GiftCardDetailsView( - txId: txId, - backNavigationRequested: $backNavigationRequested, - onShowBackButton: { show in - showBackButton = show - } - ) + if let txDetailRoute { + TXDetailVCWrapper( + tx: txDetailRoute.tx, + navigateBack: $backNavigationRequested, + onDismissed: { + showBackButton = shouldShowBackButton + } + ) + } else if let selectedCardIndex { + GiftCardDetailsView( + viewModel: viewModel, + selectedCardIndex: selectedCardIndex, + backNavigationRequested: $backNavigationRequested, + onShowBackButton: { show in + showBackButton = show + }, + onOpenTransaction: { transaction in + txDetailRoute = TxDetailRoute( + tx: Transaction(transaction: transaction), + selectedCardIndex: selectedCardIndex + ) + showBackButton = true + prefersLargeDetent = true + } + ) + } else { + GiftCardPurchaseSelectionSheet( + merchantIcon: viewModel.uiState.merchantIcon, + merchantName: viewModel.uiState.merchantName, + provider: viewModel.uiState.provider, + cards: viewModel.uiState.cards, + isLoadingCardDetails: viewModel.uiState.isLoadingCardDetails, + hasBeenPollingForLongTime: viewModel.uiState.hasBeenPollingForLongTime, + onSelectCard: { index in + selectedCardIndex = index + showBackButton = true + prefersLargeDetent = true + } + ) + } + } + + Group { + if #available(iOS 16.4, *) { + dialog + .presentationBackground(Color.primaryBackground) + .presentationDetents( + [.height(effectiveCompactSheetHeight), .large], + selection: sheetDetentSelection + ) + .presentationCornerRadius(32) + .presentationDragIndicator(.hidden) + } else { + dialog + } } .background(Color.primaryBackground) + .onAppear { + viewModel.startObserving() + cacheCompactSheetHeightIfNeeded() + showBackButton = shouldShowBackButton + prefersLargeDetent = selectedCardIndex != nil || txDetailRoute != nil + } + .onDisappear { + viewModel.stopObserving() + } + .onChange(of: selectedCardIndex) { _ in + showBackButton = shouldShowBackButton + if selectedCardIndex != nil || txDetailRoute != nil { + prefersLargeDetent = true + } + } + .onChange(of: txDetailRoute?.id) { _ in + showBackButton = shouldShowBackButton + if selectedCardIndex != nil || txDetailRoute != nil { + prefersLargeDetent = true + } + } + .onChange(of: viewModel.uiState.cards.count) { cardsCount in + if cardsCount > 0 { + cacheCompactSheetHeightIfNeeded(force: true) + } else { + cacheCompactSheetHeightIfNeeded() + } + if selectedCardIndex == nil, txDetailRoute == nil, cardsCount == 1 { + selectedCardIndex = 0 + prefersLargeDetent = true + } + showBackButton = shouldShowBackButton + } + .onChange(of: backNavigationRequested) { requested in + guard requested else { return } + backNavigationRequested = false + handleBackNavigation() + } + } + + @available(iOS 16.4, *) + private var sheetDetentSelection: Binding { + Binding( + get: { + prefersLargeDetent ? .large : .height(effectiveCompactSheetHeight) + }, + set: { newDetent in + prefersLargeDetent = newDetent == .large + } + ) + } + + private var effectiveCompactSheetHeight: CGFloat { + compactSheetHeight > 0 ? compactSheetHeight : calculatedSelectionSheetHeight + } + + private var calculatedSelectionSheetHeight: CGFloat { + // Merchant header + cards/loading + provider + paddings + bottom safe area. + let baseHeight: CGFloat = 250 + if viewModel.uiState.cards.isEmpty { + return baseHeight + 120 + } + + let rowHeight: CGFloat = 64 + let dividerHeight: CGFloat = 1 + let cardsCount = CGFloat(viewModel.uiState.cards.count) + let listHeight = cardsCount * rowHeight + max(0, cardsCount - 1) * dividerHeight + let cappedListHeight = min(listHeight, 5 * rowHeight + 4 * dividerHeight) + + return baseHeight + cappedListHeight } + + private func cacheCompactSheetHeightIfNeeded(force: Bool = false) { + let candidate = calculatedSelectionSheetHeight + guard candidate > 0 else { return } + if force { + compactSheetHeight = candidate + return + } + guard compactSheetHeight <= 0 else { return } + compactSheetHeight = candidate + } + + private var shouldShowBackButton: Bool { + txDetailRoute != nil + } + + private func handleBackNavigation() { + if let txDetailRoute { + selectedCardIndex = txDetailRoute.selectedCardIndex + self.txDetailRoute = nil + prefersLargeDetent = true + return + } + + if selectedCardIndex != nil { + if viewModel.uiState.cards.count > 1 { + selectedCardIndex = nil + // Two-step transition avoids the UIKit/SwiftUI detent jump-to-top glitch: + // 1) switch content to selection, 2) collapse detent on next runloop tick. + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.2)) { + prefersLargeDetent = false + } + } + } else { + dismiss() + } + return + } + + dismiss() + } +} + +private struct TxDetailRoute: Identifiable { + let id = UUID() + let tx: Transaction + let selectedCardIndex: Int? } struct TransactionDetailsSheet: View { diff --git a/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift b/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift index 76b8535b4..12d4466c4 100644 --- a/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift +++ b/DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift @@ -49,6 +49,8 @@ protocol ShortcutsViewDelegate: AnyObject { // MARK: - ShortcutsView class ShortcutsView: UIView { + private static let verticalPadding: CGFloat = 8.0 + private var cancellableBag = Set() private let viewModel: HomeViewModel private var lastLayoutWidth: CGFloat = 0 @@ -140,7 +142,7 @@ class ShortcutsView: UIView { var cellSize = cellSize(for: contentSizeCategory, viewWidth: width) cellSize.height = ceil(cellSize.height) // This fixes the autolayout issue when the size of the cell is higher than the collection view itself - collectionViewHeightConstraint.constant = cellSize.height + collectionViewHeightConstraint.constant = cellSize.height + ShortcutsView.verticalPadding * 2 setNeedsUpdateConstraints() if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { @@ -212,7 +214,7 @@ extension ShortcutsView: UICollectionViewDataSource, UICollectionViewDelegate, U let cellCount = CGFloat(collectionView.numberOfItems(inSection: section)) var inset = (collectionView.bounds.size.width - (cellCount * cellWidth) - ((cellCount - 1) * cellSpacing)) * 0.5 inset = max(inset, 0.0) - return UIEdgeInsets(top: 0.0, left: inset, bottom: 0.0, right: inset) + return UIEdgeInsets(top: ShortcutsView.verticalPadding, left: inset, bottom: ShortcutsView.verticalPadding, right: inset) } } diff --git a/DashWallet/Sources/UI/Main/MainTabbarController.swift b/DashWallet/Sources/UI/Main/MainTabbarController.swift index 07c160bb6..21d3609df 100644 --- a/DashWallet/Sources/UI/Main/MainTabbarController.swift +++ b/DashWallet/Sources/UI/Main/MainTabbarController.swift @@ -17,6 +17,7 @@ import UIKit import Combine +import SwiftUI // MARK: - MainTabbarTabs @@ -29,10 +30,6 @@ private enum MainTabbarTabs: Int, CaseIterable { } extension MainTabbarTabs { - var isEmpty: Bool { - self == .payment - } - var icon: UIImage { let name: String @@ -42,7 +39,7 @@ extension MainTabbarTabs { case .contacts: name = "tabbar_contacts_icon" case .payment: - return UIImage() + name = "tabbar_pay_button" case .explore: name = "tabbar_discover_icon" case .more: @@ -51,7 +48,7 @@ extension MainTabbarTabs { return UIImage(named: name)!.withRenderingMode(.alwaysOriginal) } - + var selectedIcon: UIImage { let name: String @@ -61,7 +58,7 @@ extension MainTabbarTabs { case .contacts: name = "tabbar_contacts_selected" case .payment: - return UIImage() + name = "tabbar_pay_button" case .explore: name = "tabbar_discover_selected" case .more: @@ -81,21 +78,19 @@ class MainTabbarController: UITabBarController { private var cancellableBag = Set() private var ratesFetchErrorShown = false private var ratesVolatileWarningShown = false + private var paymentIsOpened = false weak var homeController: HomeViewController? weak var menuNavigationController: MainMenuViewController? - + #if DASHPAY weak var contactsNavigationController: DWRootContactsViewController? - weak var exploreNavigationController: ExploreViewController? #endif // TODO: Refactor this and send notification about wiped wallet instead of chaining the delegate @objc weak var wipeDelegate: DWWipeDelegate? - private var paymentButton: PaymentButton! - @objc var isDemoMode = false @@ -105,7 +100,7 @@ class MainTabbarController: UITabBarController { // TODO: Move it out from here and initialize the model inside home view controller @objc var homeModel: DWHomeProtocol! - + #if DASHPAY // TODO: MOCK_DASHPAY remove when not mocked private var blockchainIdentity: DSBlockchainIdentity? { @@ -146,35 +141,15 @@ class MainTabbarController: UITabBarController { fatalError("init(coder:) has not been implemented") } - // MARK: Actions - - @objc - private func paymentButtonAction() { - showPaymentsController(withActivePage: .none) - } - // MARK: Life Cycle - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - tabBar.addSubview(paymentButton) - } - override func viewDidLoad() { super.viewDidLoad() delegate = self - configureHierarchy() + tabBar.barTintColor = .dw_background() setupRatesErrorHandling() } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // Add Payment Button again to make sure it's at the top - tabBar.addSubview(paymentButton) - } } // MARK: - Private @@ -212,13 +187,15 @@ extension MainTabbarController { } #endif - // Payment - item = UITabBarItem(title: "", image: UIImage(), tag: 2) + // Payment (tapping this tab opens the payment modal instead of switching tabs) + let paymentImage = Self.makePaymentTabImage() + item = UITabBarItem(title: nil, image: paymentImage, selectedImage: paymentImage) item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) + item.accessibilityIdentifier = "tabbar_payments_button" - let vc = EmptyController() - vc.tabBarItem = item - viewControllers.append(vc) + let paymentVC = EmptyController() + paymentVC.tabBarItem = item + viewControllers.append(paymentVC) #if DASHPAY if identity != nil { @@ -226,10 +203,18 @@ extension MainTabbarController { item = UITabBarItem(title: nil, image: MainTabbarTabs.explore.icon, selectedImage: MainTabbarTabs.explore.selectedIcon) item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) - let exploreVC = ExploreViewController() - exploreVC.delegate = self - exploreNavigationController = exploreVC - nvc = BaseNavigationController(rootViewController: exploreVC) + nvc = BaseNavigationController(rootViewController: EmptyController()) + let exploreScreen = ExploreMenuScreen( + vc: nvc, + showBackButton: false, + onShowSendPayment: { [weak self] in self?.showPaymentsController(withActivePage: PaymentsViewControllerState.pay) }, + onShowReceivePayment: { [weak self] in self?.showPaymentsController(withActivePage: PaymentsViewControllerState.receive) }, + onShowGiftCard: { [weak self] txId in + self?.selectedIndex = MainTabbarTabs.home.rawValue + self?.homeController?.showGiftCardDetails(txId: txId) + } + ) + nvc.viewControllers = [UIHostingController(rootView: exploreScreen)] nvc.tabBarItem = item viewControllers.append(nvc) } @@ -256,25 +241,29 @@ extension MainTabbarController { self.viewControllers = viewControllers } - private func configureHierarchy() { - paymentButton = PaymentButton() - paymentButton.translatesAutoresizingMaskIntoConstraints = false - paymentButton.addTarget(self, action: #selector(paymentButtonAction), for: .touchUpInside) - tabBar.addSubview(paymentButton) + /// Creates a tab bar image with a blue circle background and the payment icon centered on top. + private static func makePaymentTabImage() -> UIImage { + let size: CGFloat = 47 + let rect = CGRect(x: 0, y: 0, width: size, height: size) - NSLayoutConstraint.activate([ - paymentButton.centerXAnchor.constraint(equalTo: tabBar.centerXAnchor), - paymentButton.topAnchor.constraint(equalTo: tabBar.topAnchor, constant: UIDevice.hasHomeIndicator ? 4 : 1), + let renderer = UIGraphicsImageRenderer(size: rect.size) + let image = renderer.image { context in + // Draw blue circle background + UIColor.dw_dashBlue().setFill() + UIBezierPath(ovalIn: rect).fill() - paymentButton.widthAnchor.constraint(equalToConstant: PaymentButton.kCenterCircleSize), - paymentButton.heightAnchor.constraint(equalToConstant: PaymentButton.kCenterCircleSize), - ]) + // Draw icon centered + if let icon = UIImage(named: "tabbar_pay_button") { + let iconSize = CGSize(width: 22, height: 22) + let iconOrigin = CGPoint(x: (size - iconSize.width) / 2, y: (size - iconSize.height) / 2) + icon.draw(in: CGRect(origin: iconOrigin, size: iconSize)) + } + } - tabBar.barTintColor = .dw_background() + return image.withRenderingMode(.alwaysOriginal) } private func closePayments(completion: (() -> Void)? = nil) { - paymentButton.isOpened = false guard let top = selectedViewController?.topController(), top != selectedViewController @@ -378,8 +367,6 @@ extension MainTabbarController: PaymentsViewControllerDelegate { } func paymentsViewControllerWantsToImportPrivateKey(_ controller: PaymentsViewController) { - paymentButton.isOpened = false - controller.dismiss(animated: true) { self.performScanQRCodeAction() } @@ -398,8 +385,6 @@ extension MainTabbarController: HomeViewControllerDelegate { } func showPaymentsController(withActivePage pageIndex: PaymentsViewControllerState) { - paymentButton.isOpened = true - let receiveModel = DWReceiveModel() let payModel = DWPayModel() @@ -427,25 +412,15 @@ extension MainTabbarController: HomeViewControllerDelegate { extension MainTabbarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { - !(viewController is EmptyController) + if viewController is EmptyController { + // Intercept the payment tab tap — show the payment modal instead of switching tabs + showPaymentsController(withActivePage: .none) + return false + } + return true } } -// MARK: DWExploreTestnetViewControllerDelegate - -extension MainTabbarController: ExploreViewControllerDelegate { - func exploreViewControllerShowGiftCard(_ controller: ExploreViewController, txId: Data) { - homeController?.showGiftCardDetails(txId: txId) - } - - func exploreViewControllerShowSendPayment(_ controller: ExploreViewController) { - showPaymentsController(withActivePage: PaymentsViewControllerState.pay) - } - - func exploreViewControllerShowReceivePayment(_ controller: ExploreViewController) { - showPaymentsController(withActivePage: PaymentsViewControllerState.receive) - } -} // MARK: - EmptyController diff --git a/DashWallet/Sources/UI/Maya/AddressSourceView.swift b/DashWallet/Sources/UI/Maya/AddressSourceView.swift deleted file mode 100644 index 56c0b433e..000000000 --- a/DashWallet/Sources/UI/Maya/AddressSourceView.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// AddressSourceView.swift -// DashWallet -// -// 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 - -// MARK: - AddressSourceType - -enum AddressSourceType: Identifiable { - case uphold - case coinbase - - var id: String { - switch self { - case .uphold: return "uphold" - case .coinbase: return "coinbase" - } - } - - var title: String { - switch self { - case .uphold: return "Uphold" - case .coinbase: return "Coinbase" - } - } -} - -// MARK: - AddressSourceState - -enum AddressSourceState { - case loggedOut - case loading - case available(String) - case notAvailable -} - -// MARK: - AddressSourceView - -/// A row in the "Paste address from" menu showing an exchange service or clipboard -/// with its address or login action. -struct AddressSourceView: View { - let sourceType: AddressSourceType - let state: AddressSourceState - let onTap: () -> Void - - private var isLoggedOut: Bool { - if case .loggedOut = state { return true } - return false - } - - private var isLoading: Bool { - if case .loading = state { return true } - return false - } - - var body: some View { - Button(action: onTap, label: { - HStack(spacing: 16) { - icon - .frame(width: 26, height: 26) - - VStack(alignment: .leading, spacing: 1) { - HStack { - Text(sourceType.title) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.primaryText) - - Spacer() - - if isLoggedOut { - Text(NSLocalizedString("Log In", comment: "Maya")) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.dashBlue) - } - } - - subtitle - } - - Spacer() - - if isLoading { - loadingIndicator - } - } - .padding(.horizontal, 10) - .padding(.vertical, 12) - .contentShape(Rectangle()) - }) - .buttonStyle(.plain) - .disabled(isDisabled) - } - - // MARK: - Subviews - - private var loadingIndicator: some View { - SwiftUI.ProgressView() - } - - @ViewBuilder - private var icon: some View { - switch sourceType { - case .uphold: - Image("maya.uphold.logo") - .resizable() - .aspectRatio(contentMode: .fit) - case .coinbase: - Image("maya.coinbase.logo") - .resizable() - .aspectRatio(contentMode: .fit) - } - } - - @ViewBuilder - private var subtitle: some View { - switch state { - case .available(let address): - Text(address) - .font(.system(size: 14)) - .foregroundColor(.secondaryText) - .lineLimit(1) - .truncationMode(.middle) - case .notAvailable: - Text(NSLocalizedString("Not available", comment: "Maya")) - .font(.system(size: 14)) - .foregroundColor(.tertiaryText) - case .loading: - Text(NSLocalizedString("Loading...", comment: "Maya")) - .font(.system(size: 14)) - .foregroundColor(.tertiaryText) - case .loggedOut: - EmptyView() - } - } - - private var isDisabled: Bool { - switch state { - case .notAvailable, .loading: - return true - default: - return false - } - } -} - diff --git a/DashWallet/Sources/UI/Maya/CoinRowView.swift b/DashWallet/Sources/UI/Maya/CoinRowView.swift deleted file mode 100644 index 09278374c..000000000 --- a/DashWallet/Sources/UI/Maya/CoinRowView.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// CoinRowView.swift -// DashWallet -// -// 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 CoinRowView: View { - let item: CoinDisplayItem - - var body: some View { - HStack(spacing: 12) { - coinIcon - - VStack(alignment: .leading, spacing: 1) { - Text(item.coin.name) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.primaryText) - - Text(item.coin.code) - .font(.system(size: 12, weight: .regular)) - .foregroundColor(.secondaryText) - } - - Spacer() - - if item.isHalted { - Text(NSLocalizedString("halted", comment: "Maya")) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.secondaryText) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.gray400.opacity(0.15)) - .cornerRadius(6) - } else if let price = item.fiatPrice { - Text(price) - .font(.system(size: 12, weight: .regular)) - .foregroundColor(.secondaryText) - } - } - .padding(.horizontal, 16) - .padding(.vertical, 10) - .opacity(item.isHalted ? 0.5 : 1.0) - .saturation(item.isHalted ? 0.0 : 1.0) - } - - private var coinIcon: some View { - Image(item.coin.iconAssetName) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 26, height: 26) - .clipShape(RoundedRectangle(cornerRadius: 6)) - } -} diff --git a/DashWallet/Sources/UI/Maya/Convert/Components/DashBalanceView.swift b/DashWallet/Sources/UI/Maya/Convert/Components/DashBalanceView.swift new file mode 100644 index 000000000..3490f8202 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/Components/DashBalanceView.swift @@ -0,0 +1,67 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - DashBalanceView + +/// Trailing balance for the convert source row: a symbol-free Dash amount followed by the Dash +/// symbol, with the fiat value beneath. Both strings come from `MayaConvertViewModel` +/// (`dashBalanceFormatted` / `dashBalanceFiat`); pass `fiat: nil` to hide the fiat line. +struct DashBalanceView: View { + /// Symbol-free formatted balance, e.g. "1.5". + let balance: String + /// Formatted fiat value; `nil` hides the line (e.g. zero balance). + var fiat: String? + + var body: some View { + VStack(alignment: .trailing, spacing: 0) { + HStack(spacing: 6) { + Text(balance) + .font(.subhead) + .lineLimit(1) + + dashSymbol + } + .foregroundStyle(Color.primaryText) + + if let fiat { + Text(fiat) + .font(.caption) + .foregroundColor(.primaryText) + } + } + } + + private var dashSymbol: some View { + Image("icon_dash_currency") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12, height: 10) + } +} + +#if DEBUG +#Preview { + VStack(spacing: 16) { + DashBalanceView(balance: "1.5", fiat: "$ 150.00") + DashBalanceView(balance: "0", fiat: nil) + } + .padding() + .background(Color.secondaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/Convert/Components/DashPickerView.swift b/DashWallet/Sources/UI/Maya/Convert/Components/DashPickerView.swift new file mode 100644 index 000000000..f4faa88ff --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/Components/DashPickerView.swift @@ -0,0 +1,66 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +private enum Layout { + static let hPadding: CGFloat = 6 + static let vPadding: CGFloat = 3 + static let cornerRadius: CGFloat = 6 +} + +struct DashPickerView: View { + + let options: [Option] + let title: (Option) -> String + @Binding var selected: Option + + var body: some View { + VStack(alignment: .center, spacing: 0) { + ForEach(options, id: \.self) { option in + Button { + selected = option + } label: { + pickerOption(option) + } + .buttonStyle(.plain) + } + } + } + + private func pickerOption(_ option: Option) -> some View { + Text(title(option)) + .font(.caption2) + .foregroundColor(selected == option ? .primaryText : Color.tertiaryText) + .padding(.horizontal, Layout.hPadding) + .padding(.vertical, Layout.vPadding) + .background(selected == option ? Color.black1000Alpha5 : Color.clear) + .clipShape(.rect(cornerRadius: Layout.cornerRadius)) + } +} + +#if DEBUG +#Preview { + DashPickerView( + options: ["US$", "DASH", "BTC"], + title: { $0 }, + selected: .constant("DASH") + ) + .padding() + .background(Color.primaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/Convert/Components/EnterAmountView.swift b/DashWallet/Sources/UI/Maya/Convert/Components/EnterAmountView.swift new file mode 100644 index 000000000..eb17aee7d --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/Components/EnterAmountView.swift @@ -0,0 +1,119 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - CurrencyOption + +enum CurrencyOption: Hashable { + case fiat(String) + case dash + case coin(String) + + var isFiat: Bool { + if case .fiat = self { return true } + return false + } + + var displayName: String { + switch self { + case .fiat(let code): return code + case .dash: return "DASH" + case .coin(let code): return code + } + } + + var symbol: String? { + switch self { + case .fiat(let code): + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = code + return formatter.currencySymbol + case .dash: return nil + case .coin(let code): return code + } + } +} + +// MARK: - EnterAmountView + +struct EnterAmountView: View { + + @Binding var value: String + @Binding var selectedCurrency: CurrencyOption + var options: [CurrencyOption] + var onMax: (() -> Void)? + var onCurrencyTap: (() -> Void)? + + var body: some View { + HStack(alignment: .center, spacing: 40) { + maxButton + amountView + currencyPickerView + } + } + + private var maxButton: some View { + Button { + onMax?() + } label: { + ZStack { + Circle() + .fill(Color.blueAlpha5) + .frame(width: 40, height: 40) + + Text(NSLocalizedString("Max", comment: "")) + .font(.caption2) + .foregroundStyle(Color.blue) + } + } + } + + private var amountView: some View { + MayaAmountView( + amount: value, + symbol: selectedCurrency.symbol, + showDashLogo: selectedCurrency == .dash, + showCurrencyButton: selectedCurrency.isFiat, + onCurrencyTap: onCurrencyTap + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var currencyPickerView: some View { + DashPickerView( + options: options, + title: { $0.displayName }, + selected: $selectedCurrency + ) + } + +} + +#if DEBUG +#Preview { + EnterAmountView( + value: .constant("12.5"), + selectedCurrency: .constant(.dash), + options: [.fiat("USD"), .dash, .coin("BTC")] + ) + .frame(height: 110) + .background(.red.opacity(0.3)) + .padding(20) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/Convert/Components/MayaAmountView.swift b/DashWallet/Sources/UI/Maya/Convert/Components/MayaAmountView.swift new file mode 100644 index 000000000..df5c0215c --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/Components/MayaAmountView.swift @@ -0,0 +1,200 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - MayaAmountView + +struct MayaAmountView: View { + + let amount: String + var symbol: String? = nil + var secondaryText: String? = nil + var topText: String? = nil + var bottomText: String? = nil + var showDashLogo: Bool = false + var showCurrencyButton: Bool = false + var onCurrencyTap: (() -> Void)? = nil + + var body: some View { + VStack(spacing: 2) { + if let top = topText { + Text(top) + .font(.caption1) + .foregroundStyle(Color.tertiaryText) + } + + VStack(spacing: 0) { + primaryRow + + if let secondary = secondaryText { + Text(secondary) + .font(.subhead) + .foregroundStyle(Color.tertiaryText) + } + } + + if let bottom = bottomText { + Text(bottom) + .font(.caption1) + .foregroundStyle(Color.tertiaryText) + } + } + .frame(maxWidth: .infinity) + } + + private var primaryRow: some View { + HStack(spacing: 10) { + // Symbol + amount + dash logo scale down together as one unit (scaleToFitWidth), + // unlike minimumScaleFactor which would only shrink each Text independently. + amountView + .foregroundStyle(Color.primaryText) +// .scaleToFitWidth() +// .layoutPriority(1) + + if showCurrencyButton { + Button { + onCurrencyTap?() + } label: { + Image("chevron-down-currency-select") + .frame(width: 10, height: 5) + } + .buttonStyle(.plain) + .fixedSize() // never compress the currency button + } + } + .frame(maxWidth: .infinity) + } + + /// The numeric string as shown to the user. Pure display transform: + /// - empty → "0" + /// - bare-decimal input gets a leading zero: ".34" → "0.34", "." → "0." (same for ",") + /// Does NOT trim, reformat, or cap precision — that happens upstream in `sanitize`. + private var displayAmount: String { + guard !amount.isEmpty else { return "0" } + if let first = amount.first, first == "." || first == "," { + return "0" + amount + } + return amount + } + + private var amountView: some View { + HStack(spacing: 4) { + if let sym = symbol, !sym.isEmpty { + Text(sym) + .font(.largeTitle) + } + + Text(displayAmount) + .font(.largeTitle) + + if showDashLogo { + Image("enter-amount-dash") + } + } + } +} + +#if DEBUG +#Preview("Normal amounts") { + VStack(spacing: 20) { + MayaAmountView( + amount: "1.5", + secondaryText: "$ 150.00", + topText: "Enter amount", + showDashLogo: true + ) + MayaAmountView( + amount: "100", + symbol: "$", + secondaryText: "Ð 1.0", + showCurrencyButton: true + ) + } + .padding(20) +} + +#Preview("Edge cases — scaling") { + VStack(spacing: 20) { + // Long Dash amount: icon + text must both shrink + MayaAmountView( + amount: "99999.99999999", + topText: "Dash — very long", + showDashLogo: true + ) + // Long fiat — currency button must stay visible + MayaAmountView( + amount: "123456.78", + symbol: "$", + topText: "Fiat — long with button", + showCurrencyButton: true + ) + // Long crypto + MayaAmountView( + amount: "0.00012345", + symbol: "BTC", + topText: "Crypto — small value" + ) + } + .padding(20) +} + +#Preview("Edge cases — precision") { + VStack(spacing: 20) { + // Fiat: 2 dp is the max; "0.1344255" sanitizes to "0.13" + MayaAmountView( + amount: "0.13", + symbol: "$", + topText: "Fiat 0.13 (max 2 dp)", + showCurrencyButton: true + ) + // Leading-zero normalized: "01" → "1" + MayaAmountView( + amount: "1", + topText: "Normalized from 01 → 1", + showDashLogo: true + ) + // In-progress decimal "0." — preserved + MayaAmountView( + amount: "0.", + symbol: "$", + topText: "In-progress: 0.", + showCurrencyButton: true + ) + } + .padding(20) +} + +#Preview("Leading zero — bare decimal") { + VStack(spacing: 20) { + // Bare decimal: ".34" renders as "0.34" + MayaAmountView( + amount: ".34", + topText: "Bare decimal: .34 → 0.34", + showDashLogo: true + ) + // In-progress: just the decimal key tapped — "." renders as "0." + MayaAmountView( + amount: ".", + symbol: "$", + topText: "In-progress: . → 0.", + showCurrencyButton: true + ) + } + .padding(20) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/Convert/Components/MayaConvertCardRow.swift b/DashWallet/Sources/UI/Maya/Convert/Components/MayaConvertCardRow.swift new file mode 100644 index 000000000..42beeba0e --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/Components/MayaConvertCardRow.swift @@ -0,0 +1,60 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - Card row height preference + +/// Identifies the two cards of the conversion card stack so the parent can position the arrow +/// badge between them. +enum MayaConvertRowSlot { + case top + case bottom +} + +/// Reports each row's measured height up to `MayaConvertView`, which centers the arrow badge +/// between the two cards. Kept here next to the row that writes it; the parent owns the read. +struct MayaConvertRowHeightKey: PreferenceKey { + static var defaultValue: [MayaConvertRowSlot: CGFloat] = [:] + + static func reduce(value: inout [MayaConvertRowSlot: CGFloat], nextValue: () -> [MayaConvertRowSlot: CGFloat]) { + value.merge(nextValue()) { _, new in new } + } +} + +// MARK: - MayaConvertCardRow + +/// One card in the conversion stack: wraps arbitrary content with the shared, non-interactive +/// card chrome and reports its height via `MayaConvertRowHeightKey` for its `slot`. +struct MayaConvertCardRow: View { + let slot: MayaConvertRowSlot + @ViewBuilder var content: () -> Content + + var body: some View { + content() + .allowsHitTesting(false) // rows are display-only + .padding(6) + .background(Color.secondaryBackground) + .clipShape(.rect(cornerRadius: 20)) + .background( + GeometryReader { proxy in + Color.clear + .preference(key: MayaConvertRowHeightKey.self, value: [slot: proxy.size.height]) + } + ) + } +} diff --git a/DashWallet/Sources/UI/Maya/Convert/MayaConvertAmount.swift b/DashWallet/Sources/UI/Maya/Convert/MayaConvertAmount.swift new file mode 100644 index 000000000..be6d2b4bd --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/MayaConvertAmount.swift @@ -0,0 +1,103 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Which currency the user is currently entering; the other two are derived. +enum CurrencyInputType { + case dash, fiat, crypto +} + +/// Mirrors Android's `Amount` model from ConvertViewViewModel. +/// Keeps dash, fiat, and selected-crypto amounts in sync via exchange rates. +/// The `anchor` indicates which value was directly user-entered; the other two are derived. +struct MayaConvertAmount { + private(set) var dash: Decimal = 0 + private(set) var fiat: Decimal = 0 + private(set) var crypto: Decimal = 0 + private(set) var anchor: CurrencyInputType = .dash + + /// 1 DASH = x local fiat (e.g. 35 means 1 DASH = $35) + var dashFiatRate: Decimal = 1 + + /// 1 crypto token = x local fiat (e.g. 65000 means 1 BTC = $65000). Zero until pool data loads. + var cryptoFiatRate: Decimal = 0 + + /// 1 DASH = x crypto (derived: dashFiatRate / cryptoFiatRate) + var cryptoDashRate: Decimal { + guard !dashFiatRate.isZero, !cryptoFiatRate.isZero else { return 0 } + return dashFiatRate / cryptoFiatRate + } + + /// DASH amount expressed as satoshis (duffs) for use in Maya API requests. + var dashSatoshis: Int64 { + guard dash > 0 else { return 0 } + let satoshis = dash * Decimal(100_000_000) // 1 DASH = 1e8 duffs + var rounded = Decimal() + var val = satoshis + NSDecimalRound(&rounded, &val, 0, .plain) + return NSDecimalNumber(decimal: rounded).int64Value + } + + var anchoredValue: Decimal { + switch anchor { + case .dash: return dash + case .fiat: return fiat + case .crypto: return crypto + } + } + + mutating func setDash(_ value: Decimal) { + dash = value + anchor = .dash + recalculate() + } + + mutating func setFiat(_ value: Decimal) { + fiat = value + anchor = .fiat + recalculate() + } + + mutating func setCrypto(_ value: Decimal) { + crypto = value + anchor = .crypto + recalculate() + } + + /// Updates exchange rates and recalculates derived values from the current anchor. + mutating func updateRates(dashFiatRate: Decimal, cryptoFiatRate: Decimal) { + self.dashFiatRate = dashFiatRate + self.cryptoFiatRate = cryptoFiatRate + recalculate() + } + + private mutating func recalculate() { + switch anchor { + case .dash: + fiat = dash * dashFiatRate + crypto = cryptoDashRate.isZero ? 0 : dash * cryptoDashRate + case .fiat: + dash = dashFiatRate.isZero ? 0 : fiat / dashFiatRate + crypto = cryptoFiatRate.isZero ? 0 : fiat / cryptoFiatRate + case .crypto: + // dash = crypto / (cryptoDashRate) = crypto * (cryptoFiatRate / dashFiatRate) + dash = cryptoDashRate.isZero ? 0 : crypto / cryptoDashRate + fiat = crypto * cryptoFiatRate + } + } +} diff --git a/DashWallet/Sources/UI/Maya/Convert/MayaConvertHostingController.swift b/DashWallet/Sources/UI/Maya/Convert/MayaConvertHostingController.swift new file mode 100644 index 000000000..bb9077aa6 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/MayaConvertHostingController.swift @@ -0,0 +1,75 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import UIKit + +class MayaConvertHostingController: UIViewController, NavigationBarDisplayable { + var isNavigationBarHidden: Bool { true } + + private let coin: MayaCryptoCurrency + private let address: String + private lazy var viewModel = MayaConvertViewModel(coin: coin, address: address) + + init(coin: MayaCryptoCurrency, address: String) { + self.coin = coin + self.address = address + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.dw_secondaryBackground() + + let swiftUIView = MayaConvertView( + viewModel: viewModel, + onBack: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + onContinue: { [weak self] in + self?.showOrderPreview() + } + ) + + let hostingController = UIHostingController(rootView: swiftUIView) + hostingController.view.backgroundColor = .clear + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + private func showOrderPreview() { + guard let previewViewModel = viewModel.makeOrderPreviewViewModel() else { + return + } + let previewController = OrderPreviewHostingController(viewModel: previewViewModel) + navigationController?.pushViewController(previewController, animated: true) + } +} diff --git a/DashWallet/Sources/UI/Maya/Convert/MayaConvertView.swift b/DashWallet/Sources/UI/Maya/Convert/MayaConvertView.swift new file mode 100644 index 000000000..e508d0538 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/MayaConvertView.swift @@ -0,0 +1,253 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - MayaConvertView + +struct MayaConvertView: View { + + private enum Layout { + static let iconSize: CGFloat = 30 + static let padding: CGFloat = 10 + static let hPadding: CGFloat = 20 + static let topPadding: CGFloat = 20 + static let contentSpacing: CGFloat = 20 + static let cardSpacing: CGFloat = 5 + static let topMenuItemHeightFallback: CGFloat = 74 + static let arrowIconHeight: CGFloat = 11 + static let arrowBadgeSize: CGFloat = 30 + static let arrowBadgeCornerRadius: CGFloat = 10 + static let arrowBadgeInset: CGFloat = 5 + } + + @StateObject private var viewModel: MayaConvertViewModel + @StateObject private var reachability = NetworkReachabilityMonitor() + @State private var showLocalCurrency = false + @State private var topMenuItemHeight: CGFloat = Layout.topMenuItemHeightFallback + @State private var bottomMenuItemHeight: CGFloat = Layout.topMenuItemHeightFallback + private let onBack: (() -> Void)? + private let onContinue: () -> Void + + init(viewModel: MayaConvertViewModel, onBack: (() -> Void)? = nil, onContinue: @escaping () -> Void = {}) { + _viewModel = StateObject(wrappedValue: viewModel) + self.onBack = onBack + self.onContinue = onContinue + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(leading: { NavigationBarElement.back.button { onBack?() } }) + TopIntro(title: String(format: NSLocalizedString("Convert Dash to %@", comment: "Maya"), viewModel.coin.name)) + .padding(.horizontal, Layout.hPadding) + .padding(.bottom, 6) + amountSection + if reachability.isOnline { + keyboard + } else { + // Quote/amount entry needs network — replace the keyboard area with + // the offline state, mirroring Coinbase's amount screen. + NetworkUnavailableStateView() + .frame(maxWidth: .infinity, maxHeight: 320) + .padding(.horizontal, Layout.hPadding) + } + } + .sheet(isPresented: $showLocalCurrency) { + let dialog = BottomSheet( + showBackButton: Binding.constant(false) + ) { + LocalCurrencyView { code in + viewModel.selectFiatCurrency(code) + showLocalCurrency = false + } + } + + if #available(iOS 16.0, *) { + dialog.presentationDetents([.large]) + } else { + dialog + } + } + } + + // MARK: - Top Section + + private var amountSection: some View { + VStack(alignment: .leading, spacing: Layout.contentSpacing) { + EnterAmountView( + value: $viewModel.inputValue, + selectedCurrency: $viewModel.selectedCurrency, + options: viewModel.currencyOptions, + onMax: { viewModel.setMax() }, + onCurrencyTap: { showLocalCurrency = true } + ) + .frame(height: 70) + conversionCard + receiveSection + } + .padding(.horizontal, Layout.hPadding) + .padding(.top, Layout.topPadding) + .frame(maxHeight: .infinity, alignment: .top) + .animation(.easeInOut(duration: 0.2), value: viewModel.inputValue.isEmpty) + } + + // MARK: - Conversion Card + + private var conversionCard: some View { + VStack(spacing: Layout.cardSpacing) { + MayaConvertCardRow(slot: .top) { dashSourceRow } + MayaConvertCardRow(slot: .bottom) { coinDestinationRow } + } + .overlay(alignment: .top) { + arrowDivider + .offset(y: arrowCenterY - (Layout.arrowBadgeSize / 2) - Layout.cardSpacing - 3) + } + .onPreferenceChange(MayaConvertRowHeightKey.self) { heights in + if let h = heights[.top], h > 0 { topMenuItemHeight = h } + if let h = heights[.bottom], h > 0 { bottomMenuItemHeight = h } + } + } + + /// Source row: Dash / Dash Wallet with the trailing balance + fiat. Non-interactive + /// (`action: nil`); the card chrome / non-interactivity live in `MayaConvertCardRow`. + private var dashSourceRow: some View { + MenuItem( + title: NSLocalizedString("Dash", comment: "Maya"), + subtitleView: AnyView( + Text(NSLocalizedString("Dash Wallet", comment: "Maya")) + .font(.footnote) + .lineSpacing(4) + .foregroundColor(.primaryText) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + ), + icon: .custom("dashCircleFilled"), + trailingView: AnyView( + DashBalanceView( + balance: viewModel.dashBalanceFormatted, + fiat: viewModel.dashBalance != 0 ? viewModel.dashBalanceFiat : nil + ) + ), + action: nil + ) + } + + /// Destination row: coin name / address with the remote coin icon. The address can be long, + /// so its subtitle keeps the previous unlimited wrapping. + private var coinDestinationRow: some View { + MenuItem( + title: viewModel.coin.name, + subtitleView: AnyView( + Text(viewModel.address) + .font(.footnote) + .lineSpacing(4) + .foregroundColor(.primaryText) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + ), + iconView: AnyView(MayaCoinIconView(coin: viewModel.coin, size: Layout.iconSize, cornerRadius: 7)), + action: nil + ) + } + + private var arrowCenterY: CGFloat { + let topCenterY = topMenuItemHeight / 2 + let bottomCenterY = topMenuItemHeight + Layout.cardSpacing + (bottomMenuItemHeight / 2) + return (topCenterY + bottomCenterY) / 2 + } + + private var arrowDivider: some View { + ZStack { + RoundedRectangle(cornerRadius: Layout.arrowBadgeCornerRadius, style: .continuous) + .fill(Color.secondaryBackground) + .frame(width: Layout.arrowBadgeSize + 5, height: Layout.arrowBadgeSize + 5) + .overlay( + RoundedRectangle(cornerRadius: Layout.arrowBadgeCornerRadius, style: .continuous) + .stroke(Color.primaryBackground, lineWidth: Layout.arrowBadgeInset) + ) + .overlay( + ArrowDownIcon( + size: CGSize(width: 6.6, height: 11), + color: Color(uiColor: UIColor(red: 0, green: 0.55, blue: 0.89, alpha: 1)) + ) + ) + } + .frame(height: Layout.arrowBadgeSize + 5) + .padding(.horizontal, Layout.padding) + } + + // MARK: - Receive Section + + @ViewBuilder + private var receiveSection: some View { + if !viewModel.inputValue.isEmpty { + VStack(alignment: .center, spacing: 0) { + if viewModel.isLoading { + SwiftUI.ProgressView() + } else if let amount = viewModel.receiveAmount { + Text(NSLocalizedString("Receive amount", comment: "Maya")) + .font(.caption1) + .foregroundStyle(Color.tertiaryText) + + Text("~ \(amount)") + .font(.subhead) + .foregroundStyle(Color.primaryText) + } else if let error = viewModel.errorMessage { + Text(error) + .font(.caption1) + .foregroundStyle(Color.systemRed) + } + } + .frame(maxWidth: .infinity, alignment: .center) + .transition(.opacity) + } + } + + // MARK: - Keyboard + + private var keyboard: some View { + NumericKeyboardView( + value: Binding( + get: { viewModel.inputValue }, + set: { viewModel.setInput($0) } + ), + showDecimalSeparator: true, + actionButtonText: NSLocalizedString("Continue", comment: ""), + actionEnabled: viewModel.canOpenOrderPreview, + inProgress: viewModel.isLoading, + actionHandler: onContinue + ) + .frame(maxWidth: .infinity, maxHeight: 320) + .padding(.horizontal, Layout.hPadding) + .background(Color.secondaryBackground) + .clipShape(.rect(cornerRadius: 20)) + .background(Color.secondaryBackground, ignoresSafeAreaEdges: .bottom) + } +} + +#if DEBUG +#Preview { + MayaConvertView( + viewModel: MayaConvertViewModel( + coin: MayaCryptoCurrency.supportedCoins[0], + address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" + ), + onBack: {} + ) + .background(Color.primaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/Convert/MayaConvertViewModel.swift b/DashWallet/Sources/UI/Maya/Convert/MayaConvertViewModel.swift new file mode 100644 index 000000000..f6d8afe67 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/Convert/MayaConvertViewModel.swift @@ -0,0 +1,416 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +@MainActor +final class MayaConvertViewModel: ObservableObject { + let coin: MayaCryptoCurrency + let address: String + + @Published var inputValue: String = "" + @Published var selectedCurrency: CurrencyOption + @Published private(set) var currentFiatCurrency: String + @Published var receiveAmount: String? + @Published var isLoading: Bool = false + @Published var errorMessage: String? + + // MARK: - Private Types + + private enum ValidationResult { + case empty + case insufficientBalance + case exchangeRateUnavailable + case valid(dashSatoshis: Int64) + } + + private struct QuoteRequestSnapshot { + let id: Int + let dashSatoshis: Int64 + } + + // MARK: - Private State + + private var amount = MayaConvertAmount() + private var latestQuote: MayaSwapQuote? + // Monotonically increasing; stale responses are discarded when their snapshot id no longer matches. + private var quoteRequestID = 0 + // Suppresses input observation while we programmatically sync the displayed value during a currency switch. + private var isSwitchingCurrency = false + private var cancellables = Set() + + // MARK: - Computed Properties + + var dashBalance: Int64 { + Int64(DWEnvironment.sharedInstance().currentAccount.balance) + } + + /// Symbol-free formatted Dash balance (e.g. "1.5") for the convert source row. + var dashBalanceFormatted: String { + DWEnvironment.sharedInstance().currentAccount.balance.formattedDashAmountWithoutCurrencySymbol + } + + var dashBalanceFiat: String { + let balance = DWEnvironment.sharedInstance().currentAccount.balance + return CurrencyExchanger.shared.fiatAmountString(for: balance.dashAmount) + } + + var currencyOptions: [CurrencyOption] { + [.fiat(currentFiatCurrency), .dash, .coin(coin.code)] + } + + var isActionEnabled: Bool { + guard parseInput(inputValue) != nil else { return false } + return errorMessage == nil + } + + var canOpenOrderPreview: Bool { + guard isActionEnabled && !isLoading else { return false } + return latestQuote != nil + } + + // MARK: - Init + + init(coin: MayaCryptoCurrency, address: String) { + self.coin = coin + self.address = address + let initialFiat = App.fiatCurrency + self.currentFiatCurrency = initialFiat + self.selectedCurrency = .fiat(initialFiat) + initializeRates() + observeInput() + observeCurrencySwitch() + Task { await fetchCryptoRate() } + } + + // MARK: - Public Actions + + /// Sets the input to the wallet's full balance, preserving the active currency display. + func setMax() { + let balance = DWEnvironment.sharedInstance().currentAccount.balance + amount.setDash(balance.dashAmount) + clearQuoteState() + errorMessage = nil + isSwitchingCurrency = true + syncInputValueForCurrency(selectedCurrency) + isSwitchingCurrency = false + // The debounced inputValue subscriber fires next, triggering a fresh quote. + } + + /// Updates the active fiat currency, recalculates amounts, and re-fetches a quote. + func selectFiatCurrency(_ code: String) { + guard code != currentFiatCurrency else { return } + App.shared.fiatCurrency = code + currentFiatCurrency = code + + let dashFiatRate = (try? CurrencyExchanger.shared.rate(for: code)) ?? 1 + amount.updateRates(dashFiatRate: dashFiatRate, cryptoFiatRate: amount.cryptoFiatRate) + + clearQuoteState() + + // When the picker is already showing fiat, update its identity to the new code so + // SwiftUI detects the change (fiat(ALL) ≠ fiat(UAH) because CurrencyOption is Hashable). + if case .fiat = selectedCurrency { + selectedCurrency = .fiat(code) + } + + scheduleQuoteFetch() + Task { await fetchCryptoRate() } + } + + func setInput(_ raw: String) { + inputValue = Self.sanitize(raw, currency: selectedCurrency) + } + + func makeOrderPreviewViewModel() -> OrderPreviewViewModel? { + guard let quote = latestQuote else { return nil } + return OrderPreviewViewModel( + coin: coin, + address: address, + dashSatoshis: amount.dashSatoshis, + fromDashAmount: amount.dash.formattedDashAmountWithoutCurrencySymbol, + fromFiatAmount: MayaInputFormatter.fiat(amount.fiat, currencyCode: currentFiatCurrency), + cryptoFiatRate: amount.cryptoFiatRate, + fiatCurrencyCode: currentFiatCurrency, + initialQuote: quote + ) + } + + // MARK: - Private: Rate Initialisation + + private func initializeRates() { + let dashFiatRate = (try? CurrencyExchanger.shared.rate(for: currentFiatCurrency)) ?? 1 + amount.dashFiatRate = dashFiatRate + } + + private func fetchCryptoRate() async { + do { + let pools = try await MayaAPIService.shared.fetchPools() + guard let pool = pools.first(where: { $0.asset.uppercased() == coin.mayaAsset.uppercased() }), + let cryptoUsdPrice = pool.priceUSD, + cryptoUsdPrice > 0 else { return } + + let fiatCurrency = currentFiatCurrency + let freshDashFiatRate = (try? CurrencyExchanger.shared.rate(for: fiatCurrency)) ?? amount.dashFiatRate + let dashUsdRate = (try? CurrencyExchanger.shared.rate(for: "USD")) ?? freshDashFiatRate + let cryptoFiatRate = Decimal(cryptoUsdPrice) * freshDashFiatRate / dashUsdRate + amount.updateRates(dashFiatRate: freshDashFiatRate, cryptoFiatRate: cryptoFiatRate) + + if case .coin = selectedCurrency { + isSwitchingCurrency = true + syncInputValueForCurrency(selectedCurrency) + isSwitchingCurrency = false + } + + scheduleQuoteFetch() + } catch { + // Crypto input mode remains unavailable until rates can be fetched. + } + } + + // MARK: - Private: Validation + + private func validate() -> ValidationResult { + guard parseInput(inputValue) != nil else { return .empty } + + if case .coin = selectedCurrency, amount.cryptoFiatRate == 0 { + return .exchangeRateUnavailable + } + + let satoshis = amount.dashSatoshis + guard satoshis > 0 else { return .empty } + + let accountBalance = Int64(DWEnvironment.sharedInstance().currentAccount.balance) + if satoshis > accountBalance { return .insufficientBalance } + + return .valid(dashSatoshis: satoshis) + } + + private func checkBalance() { + let satoshis = amount.dashSatoshis + let accountBalance = Int64(DWEnvironment.sharedInstance().currentAccount.balance) + errorMessage = satoshis > accountBalance + ? NSLocalizedString("Insufficient balance", comment: "Maya") + : nil + } + + // MARK: - Private: Quote State + + private func clearQuoteState() { + latestQuote = nil + receiveAmount = nil + } + + private func applySuccessfulQuote(_ quote: MayaSwapQuote) { + guard let raw = quote.expectedAmountOut, let rawValue = Double(raw) else { + latestQuote = nil + errorMessage = nil + receiveAmount = nil + return + } + latestQuote = quote + errorMessage = nil + receiveAmount = "\(coin.code) \(MayaInputFormatter.receiveAmount(rawValue / 1e8))" + } + + private func applyQuoteError(_ apiError: String) { + latestQuote = nil + receiveAmount = nil + errorMessage = apiError.contains("not enough asset to pay for fees") + ? NSLocalizedString("Amount too small to cover fees", comment: "Maya") + : NSLocalizedString("Unable to get a quote", comment: "Maya") + } + + // MARK: - Private: Combine Subscriptions + + private func observeInput() { + $inputValue + .sink { [weak self] value in + guard let self, !self.isSwitchingCurrency else { return } + + self.clearQuoteState() + guard self.parseInput(value) != nil else { + self.errorMessage = nil + return + } + self.updateAmountModel(input: value, currency: self.selectedCurrency) + self.checkBalance() + } + .store(in: &cancellables) + + $inputValue + .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) + .sink { [weak self] _ in self?.scheduleQuoteFetch() } + .store(in: &cancellables) + } + + private func observeCurrencySwitch() { + $selectedCurrency + .dropFirst() + .sink { [weak self] newCurrency in + guard let self else { return } + self.isSwitchingCurrency = true + self.syncInputValueForCurrency(newCurrency) + self.isSwitchingCurrency = false + // The inputValue change above triggers the debounced quote refresh automatically. + } + .store(in: &cancellables) + } + + // MARK: - Private: Quote Fetching + + private func scheduleQuoteFetch() { + switch validate() { + case .empty: + isLoading = false + clearQuoteState() + errorMessage = nil + case .exchangeRateUnavailable: + isLoading = false + clearQuoteState() + errorMessage = NSLocalizedString("Exchange rate not available", comment: "Maya") + case .insufficientBalance: + isLoading = false + clearQuoteState() + errorMessage = NSLocalizedString("Insufficient balance", comment: "Maya") + case .valid(let satoshis): + errorMessage = nil + isLoading = true + quoteRequestID += 1 + let snapshot = QuoteRequestSnapshot(id: quoteRequestID, dashSatoshis: satoshis) + Task { await fetchQuote(snapshot: snapshot) } + } + } + + private func fetchQuote(snapshot: QuoteRequestSnapshot) async { + defer { + if quoteRequestID == snapshot.id { isLoading = false } + } + do { + let quote = try await MayaAPIService.shared.fetchQuote( + dashSatoshis: snapshot.dashSatoshis, + toAsset: coin.mayaAsset, + destination: address + ) + guard quoteRequestID == snapshot.id else { return } + if let apiError = quote.error { + applyQuoteError(apiError) + } else { + applySuccessfulQuote(quote) + } + } catch { + guard quoteRequestID == snapshot.id else { return } + latestQuote = nil + errorMessage = nil + receiveAmount = nil + } + } + + // MARK: - Private: Amount Model + + private func updateAmountModel(input: String, currency: CurrencyOption) { + guard let d = parseInput(input) else { return } + let decimal = Decimal(d) + + switch currency { + case .dash: + amount.setDash(decimal) + case .fiat: + amount.setFiat(decimal) + case .coin: + guard amount.cryptoFiatRate > 0 else { return } + amount.setCrypto(decimal) + } + } + + private func syncInputValueForCurrency(_ currency: CurrencyOption) { + switch currency { + case .dash: + inputValue = amount.dash.isZero ? "" : amount.dash.formattedDashAmountWithoutCurrencySymbol + case .fiat: + guard !amount.fiat.isZero else { inputValue = ""; return } + let d = (amount.fiat as NSDecimalNumber).doubleValue + inputValue = String(format: "%.2f", d) + case .coin: + guard !amount.crypto.isZero, amount.cryptoFiatRate > 0 else { inputValue = ""; return } + let d = (amount.crypto as NSDecimalNumber).doubleValue + inputValue = MayaInputFormatter.trimTrailingZeros(String(format: "%.8f", d)) + } + } + + private func parseInput(_ value: String) -> Double? { + let normalized = value.replacingOccurrences(of: ",", with: ".") + guard let d = Double(normalized), d > 0 else { return nil } + return d + } + + // MARK: - Private: Input Sanitization + + /// Normalizes a raw keyboard string before it reaches the amount model. + /// Rules: + /// - Leading zeros stripped from the integer part: "01" → "1", but "0." and "0.12" stay. + /// - Decimal precision capped: fiat → 2 places, dash/crypto → 8 places. + /// - Empty string and in-progress decimals (e.g. "0.") pass through unchanged. + private static func sanitize(_ raw: String, currency: CurrencyOption) -> String { + guard !raw.isEmpty else { return raw } + + let s = raw.replacingOccurrences(of: ",", with: ".") + let maxDecimals: Int + switch currency { + case .fiat: maxDecimals = 2 + case .dash, .coin: maxDecimals = 8 + } + + if let dotRange = s.range(of: ".") { + let intPart = String(s[s.startIndex.. String { + if s.isEmpty { return "0" } + var result = s + while result.count > 1, result.hasPrefix("0") { result.removeFirst() } + return result + } +} + +private struct MayaInputFormatter { + static func trimTrailingZeros(_ s: String) -> String { + var result = s + while result.hasSuffix("0") { result.removeLast() } + if result.hasSuffix(".") { result.removeLast() } + return result + } + + static func receiveAmount(_ humanValue: Double) -> String { + let s = String(format: humanValue < 0.001 ? "%.8f" : "%.4f", humanValue) + return trimTrailingZeros(s) + } + + static func fiat(_ value: Decimal, currencyCode: String) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currencyCode + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + return formatter.string(from: value as NSDecimalNumber) ?? "\(value)" + } +} diff --git a/DashWallet/Sources/UI/Maya/EnterAddress/Components/AddressSourceView.swift b/DashWallet/Sources/UI/Maya/EnterAddress/Components/AddressSourceView.swift new file mode 100644 index 000000000..8a06bc8d9 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/EnterAddress/Components/AddressSourceView.swift @@ -0,0 +1,186 @@ +// +// AddressSourceView.swift +// DashWallet +// +// 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 + +// MARK: - AddressSourceType + +enum AddressSourceType: Identifiable { + case uphold + case coinbase + case clipboard + + var id: String { + switch self { + case .uphold: return "uphold" + case .coinbase: return "coinbase" + case .clipboard: return "clipboard" + } + } + + var title: String { + switch self { + case .uphold: return "Uphold" + case .coinbase: return "Coinbase" + case .clipboard: return "Clipboard" + } + } +} + +// MARK: - AddressSourceState + +enum AddressSourceState { + case loggedOut + case loading + case available(String) + case notAvailable +} + +// MARK: - AddressSourceView + +/// A row in the "Paste address from" menu showing an exchange service or clipboard +/// with its address or login action. +struct AddressSourceView: View { + let sourceType: AddressSourceType + let state: AddressSourceState + let onTap: () -> Void + + private var isLoggedOut: Bool { + if case .loggedOut = state { return true } + return false + } + + private var isLoading: Bool { + if case .loading = state { return true } + return false + } + + var body: some View { + + Group { + if let subtitleText { + MenuItem( + title: sourceType.title, + subtitleView: AnyView( + Text(subtitleText) + .font(.footnote) + .lineSpacing(4) + .foregroundColor(Color.tertiaryText) + .lineLimit(sourceType == .clipboard ? 2 : nil) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + ), + icon: iconName, + trailingView: trailingView, + action: onTap) + } else { + MenuItem( + title: sourceType.title, + icon: iconName, + trailingView: trailingView, + action: onTap) + } + } + .disabled(isDisabled) + } + + private var iconName: IconName { + switch sourceType { + case .uphold: + .custom("maya.uphold.logo") + case .coinbase: + .custom("maya.coinbase.logo") + case .clipboard: + .custom("masternode-keys") + } + } + + private var subtitleText: String? { + switch state { + case .available(let address): + address + case .notAvailable: + NSLocalizedString("Not available", comment: "Maya") + case .loading: + NSLocalizedString("Loading...", comment: "Maya") + case .loggedOut: + nil + } + } + + private var trailingView: AnyView? { + switch state { + case .loggedOut: + AnyView( + Text(NSLocalizedString("Log In", comment: "Maya")) + .font(.subheadMedium) + .foregroundColor(.dashBlue) + ) + case .loading: + AnyView(SwiftUI.ProgressView()) + case .available, .notAvailable: + nil + } + } + + private var isDisabled: Bool { + switch state { + case .notAvailable, .loading: + return true + default: + return false + } + } +} + +#if DEBUG +#Preview("Logged Out") { + VStack(spacing: 0) { + AddressSourceView(sourceType: .uphold, state: .loggedOut, onTap: {}) + AddressSourceView(sourceType: .coinbase, state: .loggedOut, onTap: {}) + .background(.red.opacity(0.5)) + } + .padding(6) + .background(Color.red.opacity(0.3)) + .cornerRadius(12) + .padding() +} + +#Preview("Loading") { + VStack(spacing: 0) { + AddressSourceView(sourceType: .uphold, state: .loading, onTap: {}) + AddressSourceView(sourceType: .coinbase, state: .loading, onTap: {}) + } + .padding(6) + .background(Color.secondaryBackground) + .cornerRadius(12) + .padding() +} + +#Preview("Available") { + VStack(spacing: 0) { + AddressSourceView(sourceType: .uphold, state: .available("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"), onTap: {}) + AddressSourceView(sourceType: .coinbase, state: .notAvailable, onTap: {}) + } + .padding(6) + .background(Color.secondaryBackground) + .cornerRadius(12) + .padding() +} +#endif diff --git a/DashWallet/Sources/UI/Maya/EnterAddressHostingController.swift b/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressHostingController.swift similarity index 77% rename from DashWallet/Sources/UI/Maya/EnterAddressHostingController.swift rename to DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressHostingController.swift index 8e29a8843..ef59d26c1 100644 --- a/DashWallet/Sources/UI/Maya/EnterAddressHostingController.swift +++ b/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressHostingController.swift @@ -21,13 +21,24 @@ import AuthenticationServices import SwiftUI import UIKit -class EnterAddressHostingController: UIViewController { +class EnterAddressHostingController: UIViewController, NavigationBarDisplayable { + var isNavigationBarHidden: Bool { true } + #if DEBUG + private let isDemoValidationBypassEnabled = true + #else + private let isDemoValidationBypassEnabled = false + #endif var onAddressConfirmed: ((MayaCryptoCurrency, String) -> Void)? private let coin: MayaCryptoCurrency private let viewModel: EnterAddressViewModel private var authSession: ASWebAuthenticationSession? + private lazy var keyboardDismissTapRecognizer: UITapGestureRecognizer = { + let recognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + recognizer.cancelsTouchesInView = false + return recognizer + }() init(coin: MayaCryptoCurrency) { self.coin = coin @@ -42,12 +53,13 @@ class EnterAddressHostingController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = NSLocalizedString("Enter address", comment: "Maya") view.backgroundColor = UIColor.dw_secondaryBackground() - navigationItem.largeTitleDisplayMode = .never let enterAddressView = EnterAddressView( viewModel: viewModel, + onBack: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, onScanQR: { [weak self] in self?.presentQRScanner() }, @@ -78,37 +90,43 @@ class EnterAddressHostingController: UIViewController { hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + + hostingController.view.addGestureRecognizer(keyboardDismissTapRecognizer) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // Returning from a pushed screen (QR scanner, Uphold/Coinbase login) may not re-fire + // SwiftUI's onAppear, so refresh the clipboard card here too. + viewModel.refreshClipboardAddress() } - // MARK: - Address Validation (Maya API) + // MARK: - Address Validation private func validateAndContinue(address: String) { + dismissKeyboard() viewModel.errorMessage = nil Task { - let error = await MayaAPIService.shared.validateAddress( - destination: address, - toAsset: coin.mayaAsset - ) + let error = await MayaAPIService.shared.validateAddress(destination: address, toAsset: coin.mayaAsset) if let error = error { - viewModel.errorMessage = error + if isDemoValidationBypassEnabled { + onAddressConfirmed?(coin, address) + } else { + viewModel.errorMessage = error + } } else { - // Temp success dialog — will be replaced in later stories - let alert = UIAlertController( - title: "SUCCESS", - message: "Address validation passed for \(coin.code):\n\(address)", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in - guard let self else { return } - self.onAddressConfirmed?(self.coin, address) - }) - present(alert, animated: true) + self.onAddressConfirmed?(self.coin, address) } } } + @objc + private func dismissKeyboard() { + view.endEditing(true) + } + // MARK: - QR Scanner private func presentQRScanner() { diff --git a/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressView.swift b/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressView.swift new file mode 100644 index 000000000..a1d058df8 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressView.swift @@ -0,0 +1,198 @@ +// +// EnterAddressView.swift +// DashWallet +// +// 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 +import UIKit + +// MARK: - EnterAddressView + +struct EnterAddressView: View { + @ObservedObject var viewModel: EnterAddressViewModel + @StateObject private var reachability = NetworkReachabilityMonitor() + var onBack: (() -> Void)? + var onScanQR: (() -> Void)? + var onContinue: ((String) -> Void)? + var onLoginUphold: (() -> Void)? + var onLoginCoinbase: (() -> Void)? + + var body: some View { + ZStack { + Color.primaryBackground + .ignoresSafeArea() + + VStack(spacing: 0) { + NavigationBar( + leading: { NavigationBarElement.back.button { onBack?() } }, + central: { Text(NSLocalizedString("Enter address", comment: "Maya")).font(.subheadMedium) } + ) + + // Offline: hide the whole address-entry content (address sources and the + // server-side address validation both need network) and show the offline state, + // matching Convert / Order Preview. Continue stays disabled below. + if reachability.isOnline { + addressContent + } else { + NetworkUnavailableStateView() + .frame(maxWidth: .infinity) + .padding(.horizontal, 20) + .padding(.top, 40) + + Spacer(minLength: 0) + } + + continueButton + .padding(.horizontal, 60) + .padding(.bottom, 16) + } + } + .onChange(of: viewModel.addressText) { _ in + viewModel.onAddressChanged() + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + // UIKit app lifecycle: scenePhase isn't driven here, so refresh on the foreground + // notification — the only moment iOS exposes another app's freshly-copied clipboard. + viewModel.refreshClipboardAddress() + } + .onAppear { + viewModel.loadAddressSources() + // Refresh whenever the screen (re)appears so the clipboard card is never stale. + viewModel.refreshClipboardAddress() + } + } + + // MARK: - Content + + /// The online address-entry content: address field + sources menu + clipboard rows. + private var addressContent: some View { + ScrollView { + VStack(alignment: .leading, spacing: 2) { + addressField + + addressSourcesMenu + + if let clipboardContent = viewModel.clipboardContent { + clipboardContentRow(clipboardContent) + } else if viewModel.hasClipboardCandidate { + clipboardPermissionRow + } + } + .modifier(MayaMenuCardStyle()) + .padding(.horizontal, 20) + .padding(.top, 10) + } + } + + // MARK: - Address Field + + private var addressField: some View { + AddressFieldView( + text: $viewModel.addressText, + label: viewModel.addressLabel, + placeholder: viewModel.placeholderText, + hasError: viewModel.showAddressError, + errorText: viewModel.addressValidationErrorMessage ?? viewModel.errorMessage, + onScanQR: onScanQR + ) + .padding(14) + } + + // MARK: - Address Sources Menu + + private var addressSourcesMenu: some View { + VStack(alignment: .leading, spacing: 2) { + Text(NSLocalizedString("Paste address from", comment: "Maya")) + .font(.footnote) + .foregroundColor(.tertiaryText) + .padding(.vertical, 10) + .padding(.horizontal, 14) + + AddressSourceView( + sourceType: .uphold, + state: viewModel.upholdState, + onTap: { + if case .loggedOut = viewModel.upholdState { + onLoginUphold?() + } else { + viewModel.selectUpholdAddress() + } + } + ) + + AddressSourceView( + sourceType: .coinbase, + state: viewModel.coinbaseState, + onTap: { + if case .loggedOut = viewModel.coinbaseState { + onLoginCoinbase?() + } else { + viewModel.selectCoinbaseAddress() + } + } + ) + } + } + + // MARK: - Clipboard + + private var clipboardPermissionRow: some View { + MenuItem( + title: "Clipboard", + subtitle: NSLocalizedString("Show content in the clipboard", comment: "Maya"), + icon: .custom("masternode-keys"), + action: { viewModel.pasteFromClipboard() } + ) + } + + private func clipboardContentRow(_ content: String) -> some View { + AddressSourceView( + sourceType: .clipboard, + state: .available(content), + onTap: { + viewModel.pasteFromClipboard() + } + ) + } + + // MARK: - Continue Button + + private var continueButton: some View { + DashButton( + // Offline disables Continue because address validation and the quote flow both require network. + text: NSLocalizedString("Continue", comment: ""), + isEnabled: viewModel.isContinueEnabled && reachability.isOnline) { + guard let address = viewModel.attemptContinue() else { return } + onContinue?(address) + } + } +} + +#Preview("Default") { + EnterAddressView(viewModel: EnterAddressViewModel(coin: MayaCryptoCurrency.supportedCoins[0]), onBack: {}) +} + +#Preview("Clipboard Revealed") { + EnterAddressView( + viewModel: { + let viewModel = EnterAddressViewModel(coin: MayaCryptoCurrency.supportedCoins[0]) + viewModel.clipboardContent = "dash:Xq9M8LkYh6sQa7u7pVv3mY8i7fQ5j8m4Kc?amount=0.1" + return viewModel + }(), + onBack: {} + ) +} diff --git a/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressViewModel.swift b/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressViewModel.swift new file mode 100644 index 000000000..301b6ab6e --- /dev/null +++ b/DashWallet/Sources/UI/Maya/EnterAddress/EnterAddressViewModel.swift @@ -0,0 +1,319 @@ +// +// EnterAddressViewModel.swift +// DashWallet +// +// 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 SwiftUI +import UIKit + +@MainActor +class EnterAddressViewModel: ObservableObject { + let coin: MayaCryptoCurrency + + // MARK: - Published State + + @Published var addressText: String = "" + @Published var errorMessage: String? + @Published private(set) var shouldShowAddressValidationError: Bool = false + + @Published var upholdState: AddressSourceState = .loggedOut + @Published var coinbaseState: AddressSourceState = .loggedOut + + @Published var hasClipboardCandidate: Bool = false + @Published var clipboardContent: String? + + // MARK: - Computed Properties + + var addressLabel: String { + String(format: NSLocalizedString("%@ address", comment: "Maya"), coin.code) + } + + var placeholderText: String { + String(format: NSLocalizedString("Long press to paste", comment: "Maya")) + } + + var isContinueEnabled: Bool { + let trimmed = addressText.trimmingCharacters(in: .whitespacesAndNewlines) + return !trimmed.isEmpty && addressValidationErrorMessage == nil && errorMessage == nil + } + + var showAddressError: Bool { + shouldShowAddressValidationError && !isAddressValid + } + + var addressValidationErrorMessage: String? { + guard showAddressError else { return nil } + return String(format: NSLocalizedString("%@ address is not valid", comment: "Maya"), coin.code) + } + + // MARK: - Private + + private let addressProvider = MayaExchangeAddressProvider() + private var upholdAddress: String? + private var coinbaseAddress: String? + /// True once the user has opted in to reading the clipboard (tapped the clipboard row). + /// Until then we only probe pasteboard metadata so the iOS paste banner is never surfaced + /// unexpectedly — this preserves the existing permission flow. + private var hasRevealedClipboard = false + + /// The currency code used for exchange lookups. Exchanges manage their own currency listings. + private var exchangeCurrencyCode: String { coin.code } + + // MARK: - Init + + init(coin: MayaCryptoCurrency) { + self.coin = coin + } + + // MARK: - Source Loading + + func loadAddressSources() { + #if DEBUG + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { return } + #endif + loadAddressSource(.uphold) + loadAddressSource(.coinbase) + } + + // MARK: - Clipboard + + /// Re-reads the clipboard and refreshes the clipboard card. Safe to call repeatedly — + /// invoke it whenever the screen reappears or the app returns to the foreground so the card + /// never shows a stale, previously-copied address. + func refreshClipboardAddress() { + #if DEBUG + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { return } + #endif + + let hasCandidate = UIPasteboard.general.hasStrings || UIPasteboard.general.hasURLs + + // Preserve the permission flow: before the user opts in (taps the clipboard row) we only + // probe pasteboard metadata — never the contents — so the iOS paste banner is not + // surfaced. This just toggles the "Show content in the clipboard" row. + guard hasRevealedClipboard else { + hasClipboardCandidate = hasCandidate + clipboardContent = nil + return + } + + // Opted in: re-read live and gate the card on a valid address for the selected coin. + // Recomputed on every call so a newly-copied address replaces the old one, and a + // cleared / invalid clipboard hides the card. + clipboardContent = validClipboardAddress() + hasClipboardCandidate = clipboardContent != nil + } + + func pasteFromClipboard() { + // First tap on the clipboard row opts the user in and reads the live clipboard. + if !hasRevealedClipboard { + hasRevealedClipboard = true + refreshClipboardAddress() + } + guard let content = clipboardContent else { return } + addressText = extractAddressFromURI(content) + clearValidationError() + } + + // MARK: - Manual Input + + func setAddress(_ address: String) { + addressText = extractAddressFromURI(address) + clearValidationError() + } + + func onAddressChanged() { + clearValidationError() + } + + // MARK: - Address Selection + + func selectUpholdAddress() { selectStoredAddress(.uphold) } + func selectCoinbaseAddress() { selectStoredAddress(.coinbase) } + + // MARK: - Continue Action + + /// Validates the current address text. Returns the trimmed address on success, or `nil` if invalid. + /// Validation only happens here — the error state is NOT set on text changes. + func attemptContinue() -> String? { + let trimmed = addressText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + guard validateCurrentAddress() else { return nil } + return trimmed + } + + // MARK: - Post-Login Callbacks + + func onUpholdLoginCompleted() { handleLoginCompleted(.uphold) } + func onCoinbaseLoginCompleted() { handleLoginCompleted(.coinbase) } + + // MARK: - Private: Validation + + private var isAddressValid: Bool { + let trimmed = addressText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return false } + return MayaAddressValidator.isValid(address: trimmed, for: coin) + } + + @discardableResult + private func validateCurrentAddress() -> Bool { + shouldShowAddressValidationError = true + return isAddressValid + } + + private func clearValidationError() { + shouldShowAddressValidationError = false + errorMessage = nil + } + + // MARK: - Private: Source Kind + + private enum AddressSourceKind { case uphold, coinbase } + + private func loadAddressSource(_ kind: AddressSourceKind) { + guard isAuthorized(kind) else { setSourceState(.loggedOut, for: kind); return } + setSourceState(.loading, for: kind) + Task { + let address = await fetchAddressForLoad(kind) + storeAddress(address, for: kind) + // Re-check authorization in case the session was revoked during the fetch. + setSourceState(resolveSourceState(address: address, isStillAuthorized: isAuthorized(kind)), for: kind) + } + } + + private func handleLoginCompleted(_ kind: AddressSourceKind) { + setSourceState(.loading, for: kind) + Task { + let address = await fetchAddressAfterLogin(kind) + storeAddress(address, for: kind) + setSourceState(resolveSourceState(address: address, isStillAuthorized: isAuthorized(kind)), for: kind) + } + } + + private func selectStoredAddress(_ kind: AddressSourceKind) { + guard let address = storedAddress(for: kind) else { return } + addressText = address + clearValidationError() + } + + private func isAuthorized(_ kind: AddressSourceKind) -> Bool { + switch kind { + case .uphold: return addressProvider.isUpholdAuthorized + case .coinbase: return addressProvider.isCoinbaseAuthorized + } + } + + private func setSourceState(_ state: AddressSourceState, for kind: AddressSourceKind) { + switch kind { + case .uphold: upholdState = state + case .coinbase: coinbaseState = state + } + } + + private func storeAddress(_ address: String?, for kind: AddressSourceKind) { + switch kind { + case .uphold: upholdAddress = address + case .coinbase: coinbaseAddress = address + } + } + + private func storedAddress(for kind: AddressSourceKind) -> String? { + switch kind { + case .uphold: return upholdAddress + case .coinbase: return coinbaseAddress + } + } + + private func fetchAddressForLoad(_ kind: AddressSourceKind) async -> String? { + switch kind { + case .uphold: return await addressProvider.fetchUpholdAddress(for: exchangeCurrencyCode) + case .coinbase: return await addressProvider.fetchCoinbaseAddress(for: exchangeCurrencyCode) + } + } + + private func fetchAddressAfterLogin(_ kind: AddressSourceKind) async -> String? { + switch kind { + case .uphold: return await addressProvider.fetchAndCacheUpholdAddress(for: exchangeCurrencyCode) + case .coinbase: return await addressProvider.createAndCacheCoinbaseAddress(for: exchangeCurrencyCode) + } + } + + // MARK: - Private: Source State Mapping + + /// Maps a fetch result to the appropriate `AddressSourceState`. + /// On failure, re-checks authorization to distinguish "not available" from "session expired". + private func resolveSourceState(address: String?, isStillAuthorized: Bool) -> AddressSourceState { + if let address { return .available(address) } + return isStillAuthorized ? .notAvailable : .loggedOut + } + + // MARK: - Private: Clipboard + + /// Returns the raw clipboard string when it holds a valid address for the selected coin, + /// otherwise nil. Reads the pasteboard contents — only call once the user has opted in. + private func validClipboardAddress() -> String? { + guard let raw = currentClipboardContent() else { return nil } + let address = extractAddressFromURI(raw) + guard MayaAddressValidator.isValid(address: address, for: coin) else { return nil } + return raw + } + + private func currentClipboardContent() -> String? { + let rawContent = UIPasteboard.general.url?.absoluteString ?? UIPasteboard.general.string + guard let rawContent else { return nil } + let trimmed = rawContent.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + + // MARK: - Private: URI Parsing + + /// Strips recognized crypto URI schemes and query parameters, returning the bare address. + /// For example, `bitcoin:1abc...?amount=0.1` becomes `1abc...`. + private func extractAddressFromURI(_ input: String) -> String { + let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) + + // Skip http/https URLs — they're not crypto addresses + guard !trimmed.hasPrefix("http://"), !trimmed.hasPrefix("https://") else { + return trimmed + } + + // Check for scheme:address pattern + guard let colonIndex = trimmed.firstIndex(of: ":") else { + return trimmed + } + + let scheme = trimmed[.. = ["bitcoin", "ethereum", "kujira", "thorchain", "dash"] + guard knownSchemes.contains(scheme.lowercased()) else { + return trimmed + } + + var address = String(trimmed[trimmed.index(after: colonIndex)...]) + + // Strip EIP-681 chain ID (@chainId) + if let atIndex = address.firstIndex(of: "@") { + address = String(address[.. Void)? - var onContinue: ((String) -> Void)? - var onLoginUphold: (() -> Void)? - var onLoginCoinbase: (() -> Void)? - - var body: some View { - ZStack { - Color.primaryBackground - .ignoresSafeArea() - .onTapGesture { dismissKeyboard() } - - VStack(spacing: 0) { - ScrollView { - VStack(alignment: .leading, spacing: 20) { - addressField - .padding(.top, 20) - - if let error = viewModel.addressValidationErrorMessage ?? viewModel.errorMessage { - Text(error) - .font(.system(size: 12)) - .foregroundColor(.systemRed) - .padding(.horizontal, 16) - .padding(.top, -10) - } - - addressSourcesMenu - - if viewModel.hasClipboardContent { - if viewModel.isClipboardRevealed { - clipboardSection - } else { - showClipboardButton - } - } - } - .padding(.horizontal, 20) - } - - continueButton - .padding(.horizontal, 20) - .padding(.bottom, 16) - } - } - .onAppear { - viewModel.loadAddressSources() - } - } - - private func dismissKeyboard() { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } - - // MARK: - Address Field - - private var addressField: some View { - HStack(spacing: 8) { - TextField(viewModel.placeholderText, text: $viewModel.addressText) - .textFieldStyle(PlainTextFieldStyle()) - .font(.system(size: 14)) - .autocapitalization(.none) - .disableAutocorrection(true) - - Button(action: { onScanQR?() }) { - Image("scan-qr.accessory.icon") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - .foregroundColor(.secondaryText) - .frame(minWidth: 44, minHeight: 44) - .contentShape(Rectangle()) - } - .accessibilityLabel(NSLocalizedString("Scan QR code", comment: "Maya")) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(viewModel.showAddressError ? Color.systemRed.opacity(0.05) : Color.gray400.opacity(0.1)) - .cornerRadius(16) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(viewModel.showAddressError ? Color.systemRed : Color.clear, lineWidth: 1) - ) - } - - // MARK: - Address Sources Menu - - private var addressSourcesMenu: some View { - VStack(alignment: .leading, spacing: 2) { - Text(NSLocalizedString("Paste address from", comment: "Maya")) - .font(.system(size: 13)) - .foregroundColor(.secondaryText) - .padding(.horizontal, 10) - .padding(.vertical, 10) - - AddressSourceView( - sourceType: .uphold, - state: viewModel.upholdState, - onTap: { - if case .loggedOut = viewModel.upholdState { - onLoginUphold?() - } else { - viewModel.selectUpholdAddress() - } - } - ) - - AddressSourceView( - sourceType: .coinbase, - state: viewModel.coinbaseState, - onTap: { - if case .loggedOut = viewModel.coinbaseState { - onLoginCoinbase?() - } else { - viewModel.selectCoinbaseAddress() - } - } - ) - } - .padding(6) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) - } - - // MARK: - Clipboard - - private var showClipboardButton: some View { - Button(action: { viewModel.revealClipboard() }) { - HStack(spacing: 8) { - Image(systemName: "doc.on.clipboard") - .font(.system(size: 14)) - .foregroundColor(.dashBlue) - - Text(NSLocalizedString("Show content in the clipboard", comment: "Maya")) - .font(.system(size: 13, weight: .medium)) - .foregroundColor(.dashBlue) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) - } - .buttonStyle(.plain) - } - - private var clipboardSection: some View { - VStack(alignment: .leading, spacing: 10) { - Text(NSLocalizedString("Tap the address from the clipboard to paste it", comment: "Maya")) - .font(.system(size: 13, weight: .regular)) - .foregroundColor(.secondaryText) - - Button(action: { viewModel.pasteFromClipboard() }) { - HStack(spacing: 12) { - Image(systemName: "doc.on.clipboard") - .font(.system(size: 16)) - .foregroundColor(.dashBlue) - .frame(width: 24, height: 24) - - if let content = viewModel.revealedClipboardContent { - Text(content) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.primaryText) - .lineLimit(1) - .truncationMode(.middle) - } - - Spacer() - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - } - .buttonStyle(.plain) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) - } - } - - // MARK: - Continue Button - - private var continueButton: some View { - Button(action: { - let address = viewModel.addressText.trimmingCharacters(in: .whitespacesAndNewlines) - onContinue?(address) - }, label: { - Text(NSLocalizedString("Continue", comment: "")) - .font(.system(size: 15, weight: .semibold)) - .foregroundColor(viewModel.isAddressValid ? .white : Color(UIColor.label.withAlphaComponent(0.4))) - .frame(maxWidth: .infinity) - .padding(.vertical, 14) - .background(viewModel.isAddressValid ? Color.dashBlue : Color(UIColor.systemFill)) - .cornerRadius(12) - }) - .disabled(!viewModel.isAddressValid) - } -} diff --git a/DashWallet/Sources/UI/Maya/EnterAddressViewModel.swift b/DashWallet/Sources/UI/Maya/EnterAddressViewModel.swift deleted file mode 100644 index 70387df42..000000000 --- a/DashWallet/Sources/UI/Maya/EnterAddressViewModel.swift +++ /dev/null @@ -1,249 +0,0 @@ -// -// EnterAddressViewModel.swift -// DashWallet -// -// 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 SwiftUI -import UIKit - -@MainActor -class EnterAddressViewModel: ObservableObject { - let coin: MayaCryptoCurrency - - @Published var addressText: String = "" - @Published var errorMessage: String? - - // MARK: - Address Sources - - @Published var upholdState: AddressSourceState = .loggedOut - @Published var coinbaseState: AddressSourceState = .loggedOut - - // MARK: - Clipboard (two-step: detect → reveal → paste) - - @Published var hasClipboardContent: Bool = false - @Published var revealedClipboardContent: String? - - var isClipboardRevealed: Bool { - revealedClipboardContent != nil - } - - private let addressProvider = MayaExchangeAddressProvider() - private var upholdAddress: String? - private var coinbaseAddress: String? - - var placeholderText: String { - String(format: NSLocalizedString("%@ address", comment: "Maya"), coin.code) - } - - var isAddressValid: Bool { - let trimmed = addressText.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return false } - return MayaAddressValidator.isValid(address: trimmed, for: coin) - } - - var showAddressError: Bool { - let trimmed = addressText.trimmingCharacters(in: .whitespacesAndNewlines) - return !trimmed.isEmpty && !MayaAddressValidator.isValid(address: trimmed, for: coin) - } - - var addressValidationErrorMessage: String? { - guard showAddressError else { return nil } - return String(format: NSLocalizedString("%@ address is not valid", comment: "Maya"), coin.code) - } - - /// The currency code to look up on exchanges. - /// Uses the coin code directly — exchanges manage their own currency listings. - /// If the exchange doesn't support the currency, the UI shows "Not available". - private var exchangeCurrencyCode: String { - coin.code - } - - init(coin: MayaCryptoCurrency) { - self.coin = coin - } - - // MARK: - Load Address Sources - - func loadAddressSources() { - loadUpholdState() - loadCoinbaseState() - checkClipboard() - } - - private func loadUpholdState() { - guard addressProvider.isUpholdAuthorized else { - upholdState = .loggedOut - return - } - - upholdState = .loading - - Task { - let address = await addressProvider.fetchUpholdAddress(for: exchangeCurrencyCode) - if let address = address { - upholdAddress = address - upholdState = .available(address) - } else { - // Re-check authorization: if the session was revoked during the fetch, - // show "Log In" instead of "Not available". - if !addressProvider.isUpholdAuthorized { - upholdState = .loggedOut - } else { - upholdState = .notAvailable - } - } - } - } - - private func loadCoinbaseState() { - guard addressProvider.isCoinbaseAuthorized else { - coinbaseState = .loggedOut - return - } - - coinbaseState = .loading - - Task { - // Uses cached address if available, otherwise creates a new one - let address = await addressProvider.fetchCoinbaseAddress(for: exchangeCurrencyCode) - if let address = address { - coinbaseAddress = address - coinbaseState = .available(address) - } else { - // Re-check authorization: if the session was revoked during the fetch - // (e.g., expired token after device restart), show "Log In" instead of - // "Not available" so the user can re-authenticate. - if !addressProvider.isCoinbaseAuthorized { - coinbaseState = .loggedOut - } else { - coinbaseState = .notAvailable - } - } - } - } - - // MARK: - Address Selection - - func selectUpholdAddress() { - guard let address = upholdAddress else { return } - addressText = address - errorMessage = nil - } - - func selectCoinbaseAddress() { - guard let address = coinbaseAddress else { return } - addressText = address - errorMessage = nil - } - - // MARK: - Clipboard - - func checkClipboard() { - hasClipboardContent = UIPasteboard.general.hasStrings || UIPasteboard.general.hasURLs - } - - func revealClipboard() { - let content = UIPasteboard.general.url?.absoluteString ?? UIPasteboard.general.string - withAnimation(.easeInOut(duration: 0.2)) { - revealedClipboardContent = content - } - } - - func pasteFromClipboard() { - guard let content = revealedClipboardContent else { return } - addressText = extractAddressFromURI(content) - errorMessage = nil - } - - // MARK: - Post-Login Refresh - - func onUpholdLoginCompleted() { - upholdState = .loading - - Task { - // Login triggers a fresh API fetch, replacing any cached address - let address = await addressProvider.fetchAndCacheUpholdAddress(for: exchangeCurrencyCode) - if let address = address { - upholdAddress = address - upholdState = .available(address) - } else { - upholdState = .notAvailable - } - } - } - - func onCoinbaseLoginCompleted() { - coinbaseState = .loading - - Task { - // Login triggers a fresh address creation (POST), replacing any cached address - let address = await addressProvider.createAndCacheCoinbaseAddress(for: exchangeCurrencyCode) - if let address = address { - coinbaseAddress = address - coinbaseState = .available(address) - } else { - coinbaseState = .notAvailable - } - } - } - - // MARK: - QR / Manual - - func setAddress(_ address: String) { - addressText = extractAddressFromURI(address) - errorMessage = nil - } - - // MARK: - Private - - /// Strips recognized crypto URI schemes and query parameters, returning the bare address. - /// For example, `bitcoin:1abc...?amount=0.1` becomes `1abc...`. - private func extractAddressFromURI(_ input: String) -> String { - let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) - - // Skip http/https URLs — they're not crypto addresses - guard !trimmed.hasPrefix("http://"), !trimmed.hasPrefix("https://") else { - return trimmed - } - - // Check for scheme:address pattern - guard let colonIndex = trimmed.firstIndex(of: ":") else { - return trimmed - } - - let scheme = trimmed[.. = ["bitcoin", "ethereum", "kujira", "thorchain", "dash"] - guard knownSchemes.contains(scheme.lowercased()) else { - return trimmed - } - - var address = String(trimmed[trimmed.index(after: colonIndex)...]) - - // Strip EIP-681 chain ID (@chainId) - if let atIndex = address.firstIndex(of: "@") { - address = String(address[.. some View { + content + .padding(6) + .background(Color.secondaryBackground) + .clipShape(.rect(cornerRadius: 20)) + .shadow(color: .shadow, radius: shadowRadius, x: 0, y: 5) + } +} + +struct MayaMenuItemButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.97 : 1.0) + .opacity(configuration.isPressed ? 0.85 : 1.0) + .animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed) + } +} diff --git a/DashWallet/Sources/UI/Maya/MayaPortal/MayaPortalView.swift b/DashWallet/Sources/UI/Maya/MayaPortal/MayaPortalView.swift new file mode 100644 index 000000000..587e76e49 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/MayaPortal/MayaPortalView.swift @@ -0,0 +1,93 @@ +// +// MayaPortalView.swift +// DashWallet +// +// 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 + +// MARK: - MayaPortalView + +struct MayaPortalView: View { + var onBack: () -> Void + var onConvertDash: (() -> Void)? + + var body: some View { + VStack(spacing: 0) { + NavigationBar(leading: { + NavigationBarElement.back.button { onBack() } + }) + + ScrollView { + VStack(spacing: 20) { + introSection + actionsCard + Spacer() + } + .padding(.horizontal, 20) + .padding(.top, 10) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .background(Color.primaryBackground.ignoresSafeArea()) + } + + // MARK: - Sections + + private var introSection: some View { + VStack(spacing: 20) { + logoContainer + descriptionBlock + } + .padding(.bottom, 20) + } + + private var actionsCard: some View { + MenuItem( + title: NSLocalizedString("Convert Dash", comment: "Maya Portal"), + subtitle: NSLocalizedString("From Dash Wallet to any crypto", comment: "Maya Portal"), + icon: .custom("convert.crypto"), + action: { onConvertDash?() } + ) + .modifier(MayaMenuCardStyle()) + } + + // MARK: - Subviews + + private var logoContainer: some View { + Icon(name: .custom("maya-illustration", maxHeight: 90)) + .frame(width: 90, height: 90) + } + + private var descriptionBlock: some View { + VStack(spacing: 6) { + Text(NSLocalizedString("Maya", comment: "Maya Portal")) + .font(.title2) + .foregroundColor(.primaryText) + + Text(NSLocalizedString("Convert Dash from Dash Wallet to any crypto that is supported on Maya and send it to any wallet", comment: "Maya Portal")) + .font(.footnote) + .foregroundColor(.secondaryText) + .multilineTextAlignment(.center) + .lineSpacing(2) + } + .padding(.horizontal, 20) + } +} + +#Preview { + MayaPortalView(onBack: {}, onConvertDash: {}) +} diff --git a/DashWallet/Sources/UI/Maya/MayaPortalViewController.swift b/DashWallet/Sources/UI/Maya/MayaPortal/MayaPortalViewController.swift similarity index 55% rename from DashWallet/Sources/UI/Maya/MayaPortalViewController.swift rename to DashWallet/Sources/UI/Maya/MayaPortal/MayaPortalViewController.swift index f9e834486..15767d702 100644 --- a/DashWallet/Sources/UI/Maya/MayaPortalViewController.swift +++ b/DashWallet/Sources/UI/Maya/MayaPortal/MayaPortalViewController.swift @@ -20,17 +20,36 @@ import SwiftUI import UIKit -class MayaPortalViewController: UIViewController { +// MARK: - Maya Flow Architecture Note +// +// The Maya feature uses one UIViewController per screen rather than a single SwiftUI NavigationStack +// container. This is intentional: EnterAddressHostingController requires UIKit for +// ASWebAuthenticationSession (Uphold OAuth), Coinbase.shared.signIn, and QR scanner presentation, +// all of which need a UIViewController as presentation context. Consolidating into a pure SwiftUI +// NavigationStack would require non-trivial UIViewControllerRepresentable bridges for those flows. +// +// Navigation coordination: MayaPortalViewController orchestrates the full flow +// (portal → select coin → enter address → convert). MayaConvertHostingController independently +// pushes OrderPreviewHostingController because it owns the OrderPreviewViewModel factory. +// Both approaches use NavigationBarDisplayable to hide the UIKit nav bar and render their own +// SwiftUI NavigationBar, ensuring a consistent look across all screens. + +class MayaPortalViewController: UIViewController, NavigationBarDisplayable { + var isNavigationBarHidden: Bool { true } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.dw_secondaryBackground() - navigationItem.largeTitleDisplayMode = .never - let portalView = MayaPortalView(onConvertDash: { [weak self] in - self?.navigateToSelectCoin() - }) + let portalView = MayaPortalView( + onBack: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + onConvertDash: { [weak self] in + self?.navigateToSelectCoin() + } + ) let hostingController = UIHostingController(rootView: portalView) hostingController.view.backgroundColor = .clear @@ -59,12 +78,14 @@ class MayaPortalViewController: UIViewController { private func navigateToEnterAddress(for coin: MayaCryptoCurrency) { let enterAddressVC = EnterAddressHostingController(coin: coin) - enterAddressVC.onAddressConfirmed = { coin, address in - #if DEBUG - DSLogger.log("Maya: Address confirmed for \(coin.code): \(address)") - #endif - // TODO: Navigate to Enter Amount (Requirement 5) + enterAddressVC.onAddressConfirmed = { [weak self] coin, address in + self?.navigateToConvert(coin: coin, address: address) } navigationController?.pushViewController(enterAddressVC, animated: true) } + + private func navigateToConvert(coin: MayaCryptoCurrency, address: String) { + let convertVC = MayaConvertHostingController(coin: coin, address: address) + navigationController?.pushViewController(convertVC, animated: true) + } } diff --git a/DashWallet/Sources/UI/Maya/MayaPortalView.swift b/DashWallet/Sources/UI/Maya/MayaPortalView.swift deleted file mode 100644 index ebf57f8e0..000000000 --- a/DashWallet/Sources/UI/Maya/MayaPortalView.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// MayaPortalView.swift -// DashWallet -// -// 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 MayaPortalView: View { - var onConvertDash: (() -> Void)? - - var body: some View { - ScrollView { - VStack(spacing: 20) { - // Top intro section - VStack(spacing: 16) { - // Maya logo container - ZStack { - RoundedRectangle(cornerRadius: 20) - .fill(Color(red: 0.08, green: 0.11, blue: 0.25)) - .frame(width: 79, height: 80) - .overlay( - RoundedRectangle(cornerRadius: 20) - .strokeBorder(Color(white: 0.6, opacity: 0.5), lineWidth: 1) - ) - - Image("maya.logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 44, height: 20) - } - - VStack(spacing: 4) { - Text(NSLocalizedString("Maya", comment: "Maya Portal")) - .font(.system(size: 24, weight: .bold)) - .foregroundColor(.primaryText) - - Text(NSLocalizedString("Convert Dash from Dash Wallet to any crypto that is supported on Maya and send it to any wallet", comment: "Maya Portal")) - .font(.system(size: 14, weight: .regular)) - .foregroundColor(.secondaryText) - .multilineTextAlignment(.center) - .lineSpacing(2) - } - .padding(.horizontal, 20) - } - - // Menu card - VStack(spacing: 0) { - Button(action: { - onConvertDash?() - }) { - HStack(spacing: 16) { - Image("convert.crypto") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 26, height: 26) - - VStack(alignment: .leading, spacing: 1) { - Text(NSLocalizedString("Convert Dash", comment: "Maya Portal")) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.primaryText) - - Text(NSLocalizedString("From Dash Wallet to any crypto", comment: "Maya Portal")) - .font(.system(size: 14, weight: .regular)) - .foregroundColor(.secondaryText) - } - - Spacer() - } - .padding(.horizontal, 10) - .padding(.vertical, 12) - .frame(minHeight: 56) - } - .buttonStyle(.plain) - } - .padding(6) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) - - Spacer() - } - .padding(.horizontal, 20) - .padding(.top, 20) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.primaryBackground.ignoresSafeArea()) - } -} diff --git a/DashWallet/Sources/UI/Maya/OrderPreview/Components/MayaFeeInfoSheet.swift b/DashWallet/Sources/UI/Maya/OrderPreview/Components/MayaFeeInfoSheet.swift new file mode 100644 index 000000000..f6fae7651 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/OrderPreview/Components/MayaFeeInfoSheet.swift @@ -0,0 +1,100 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct MayaFeeInfoSheet: View { + let onDismiss: () -> Void + + @Environment(\.openURL) private var openURL + + var body: some View { + VStack(spacing: 0) { + + VStack(alignment: .leading, spacing: 6) { + Text(NSLocalizedString("Fees in crypto purchases", comment: "Maya")) + .font(Font.title1) + .foregroundColor(.primaryText) + .frame(maxWidth: .infinity, alignment: .topLeading) + + Text(NSLocalizedString( + "In addition to the displayed Maya fee, we include a spread in the price. \nCryptocurrency markets are volatile, and this allows us to temporary lock in a price for trade execution.", + comment: "Maya" + )) + .font(Font.body) + .foregroundStyle(Color.gray500) + .multilineTextAlignment(.leading) + } + .padding(.top, 20) + .padding(.bottom, 32) + .padding(.horizontal, 60) + + VStack(alignment: .leading, spacing: 16) { + // Opens the Maya fee docs in the system browser via @Environment(\.openURL). + // Using openURL (system Safari) rather than SFSafariViewController because the + // sheet has no UIViewController context for modal presentation. + DashButton(text: NSLocalizedString("Learn more", comment: "Maya")) { + openURL(MayaConstants.feesDocsURL) + } + + DashButton(text: NSLocalizedString("Close", comment: "Maya")) { + onDismiss() + } + .overrideBackgroundColor(Color.gray300Alpha10) + .overrideForegroundColor(Color.black) + } + .padding(.horizontal, 60) + .padding(.vertical, 20) + } + } +} + +#if DEBUG +#Preview("Standalone") { + MayaFeeInfoSheet(onDismiss: {}) + .background(Color.secondaryBackground) +} + +private struct MayaFeeInfoSheetPreviewHost: View { + @State private var isPresented = true + + var body: some View { + Color.primaryBackground + .ignoresSafeArea() + .sheet(isPresented: $isPresented) { +// let sheet = BottomSheet(showBackButton: .constant(false)) { +// MayaFeeInfoSheet(onDismiss: {}) +// } +// +// if #available(iOS 16.0, *) { +// sheet.presentationDetents([.height(450)]) +// } else { +// sheet +// } + + BottomSheet(showBackButton: .constant(false), fillsHeight: false) { + MayaFeeInfoSheet(onDismiss: {}) + } + .selfSizingSheet() + } + } +} + +#Preview("Bottom sheet") { + MayaFeeInfoSheetPreviewHost() +} +#endif diff --git a/DashWallet/Sources/UI/Maya/OrderPreview/Components/OrderPreviewFeeRow.swift b/DashWallet/Sources/UI/Maya/OrderPreview/Components/OrderPreviewFeeRow.swift new file mode 100644 index 000000000..864e5e846 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/OrderPreview/Components/OrderPreviewFeeRow.swift @@ -0,0 +1,89 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OrderPreviewFeeRow: View { + let feeTitle: String + let feeText: String + var feeTextSecondary: String? = nil + let rowHPadding: CGFloat + let rowVPadding: CGFloat + let labelSpacing: CGFloat + let infoSpacing: CGFloat + let rowMinHeight: CGFloat + + // Sheet state lives here: the row owns the info icon and is the natural + // owner of its tap target. OrderPreviewView is not involved. + @State private var showInfoSheet = false + + var body: some View { + HStack(alignment: .top, spacing: labelSpacing) { + HStack(spacing: infoSpacing) { + Text(feeTitle) + .font(.subheadMedium) + .foregroundColor(.tertiaryText) + + ZStack { + Circle() + .frame(width: 15, height: 15) + .foregroundStyle(Color.gray300) + + Icon(name: .custom("info-icon", maxHeight: 8)) + } + .contentShape(Circle()) + .onTapGesture { showInfoSheet = true } + } + + VStack(alignment: .trailing, spacing: 2) { + Text(feeText) + + if let secondary = feeTextSecondary { + Text(secondary) + } + } + .font(.subhead) + .foregroundColor(.primaryText) + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.horizontal, rowHPadding) + .padding(.vertical, rowVPadding) + .frame(minHeight: rowMinHeight) + .sheet(isPresented: $showInfoSheet) { + BottomSheet(showBackButton: .constant(false), fillsHeight: false) { + MayaFeeInfoSheet(onDismiss: { showInfoSheet = false }) + } + .selfSizingSheet(cornerRadius: 32) + } + } +} + +#if DEBUG +#Preview("OrderPreviewFeeRow") { + OrderPreviewFeeRow( + feeTitle: "Maya fee", + feeText: "BTC 0.00042", + rowHPadding: 10, + rowVPadding: 12, + labelSpacing: 20, + infoSpacing: 6, + rowMinHeight: 46 + ) + .padding() + .background(Color.secondaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/OrderPreview/Components/OrderPreviewTableRow.swift b/DashWallet/Sources/UI/Maya/OrderPreview/Components/OrderPreviewTableRow.swift new file mode 100644 index 000000000..41ed02fa6 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/OrderPreview/Components/OrderPreviewTableRow.swift @@ -0,0 +1,67 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OrderPreviewTableRow: View { + let leading: String + let trailing: String + var trailingSecondary: String? = nil + let rowHPadding: CGFloat + let rowVPadding: CGFloat + let labelSpacing: CGFloat + let rowMinHeight: CGFloat + + var body: some View { + HStack(alignment: .top, spacing: labelSpacing) { + Text(leading) + .font(.subheadMedium) + .foregroundColor(.tertiaryText) + .fixedSize() + + VStack(alignment: .trailing, spacing: 2) { + Text(trailing) + + if let secondary = trailingSecondary { + Text(secondary) + } + } + .font(.subhead) + .foregroundColor(.primaryText) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.horizontal, rowHPadding) + .padding(.vertical, rowVPadding) + .frame(minHeight: rowMinHeight) + } +} + +#if DEBUG +#Preview("OrderPreviewTableRow") { + OrderPreviewTableRow( + leading: "Destination address", + trailing: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + rowHPadding: 10, + rowVPadding: 12, + labelSpacing: 20, + rowMinHeight: 46 + ) + .padding() + .background(Color.secondaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewHostingController.swift b/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewHostingController.swift new file mode 100644 index 000000000..2815e65de --- /dev/null +++ b/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewHostingController.swift @@ -0,0 +1,81 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import SwiftUI +import UIKit + +final class OrderPreviewHostingController: UIViewController, NavigationBarDisplayable { + var isNavigationBarHidden: Bool { true } + private let viewModel: OrderPreviewViewModel + + // Observes swapStatus to push the full-screen status flow once a swap is submitted. + private var statusCancellable: AnyCancellable? + private var didPresentStatusScreen = false + + init(viewModel: OrderPreviewViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.dw_secondaryBackground() + + let rootView = OrderPreviewView( + viewModel: viewModel, + onCancel: { [weak self] in + // Pop one level back (convert screen). + self?.navigationController?.popViewController(animated: true) + } + ) + let hostingController = UIHostingController(rootView: rootView) + hostingController.view.backgroundColor = .clear + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + observeSwapStatus() + } + + /// Once the swap leaves `.idle` (submission started, or it failed immediately), navigate to + /// the dedicated full-screen status screen. Guarded so the push happens exactly once. + private func observeSwapStatus() { + statusCancellable = viewModel.$swapStatus + .receive(on: DispatchQueue.main) + .sink { [weak self] status in + guard let self, !self.didPresentStatusScreen, status != .idle else { return } + self.didPresentStatusScreen = true + let statusVC = MayaTransactionStatusHostingController(viewModel: self.viewModel) + self.navigationController?.pushViewController(statusVC, animated: true) + } + } +} diff --git a/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewView.swift b/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewView.swift new file mode 100644 index 000000000..d2136f839 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewView.swift @@ -0,0 +1,263 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OrderPreviewView: View { + private enum Layout { + static let cardSpacing: CGFloat = 2 + static let rowHPadding: CGFloat = 14 + static let rowVPadding: CGFloat = 12 + static let coinLogoSize: CGFloat = 20 + static let coinLogoCornerRadius: CGFloat = 7 + static let logoTextSpacing: CGFloat = 8 + static let labelSpacing: CGFloat = 20 + static let infoSpacing: CGFloat = 6 + static let rowMinHeight: CGFloat = 46 + } + + @ObservedObject var viewModel: OrderPreviewViewModel + @StateObject private var reachability = NetworkReachabilityMonitor() + let onCancel: () -> Void + + var body: some View { + // This view is only the editable order-preview form. Once a swap is submitted, the + // full-screen transaction status flow (pending → success / failure) is pushed by + // OrderPreviewHostingController, which observes `viewModel.swapStatus`. + orderPreviewContent + .alert( + NSLocalizedString("Swap in progress", comment: "Maya"), + isPresented: pendingSwapAlertIsPresented + ) { + Button(NSLocalizedString("OK", comment: "Maya")) { + viewModel.pendingSwapAlertMessage = nil + } + } message: { + Text(viewModel.pendingSwapAlertMessage ?? "") + } + } + + private var pendingSwapAlertIsPresented: Binding { + Binding( + get: { viewModel.pendingSwapAlertMessage != nil }, + set: { isPresented in + if !isPresented { + viewModel.pendingSwapAlertMessage = nil + } + } + ) + } + + private var orderPreviewContent: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(leading: { + NavigationBarElement.back.button { onCancel() } + }) + + TopIntro(title: String(format: NSLocalizedString("Order Preview", comment: "Maya"))) + .padding(.top, 10) + .padding(.horizontal, 20) + .padding(.bottom, 20) + + if reachability.isOnline { + VStack(spacing: Layout.cardSpacing) { + fromRow + toRow + addressRow + purchaseRow + feeRow + totalRow + } + .modifier(MayaMenuCardStyle(shadowRadius: 20)) + .padding(.horizontal, 20) + } else { + // Confirm and refresh-quote both need network — show the offline state + // in the content area. Cancel stays available below. + NetworkUnavailableStateView() + .frame(maxWidth: .infinity) + .padding(.horizontal, 20) + .padding(.vertical, 40) + } + + Spacer(minLength: 0) + + VStack(spacing: 20) { + DashButton(text: viewModel.confirmButtonText) { + Task { await viewModel.handlePrimaryAction() } + } + // Confirm / Refresh quote require network; Cancel below stays enabled. + .disabled(viewModel.isSubmitting || viewModel.isRefreshing || !reachability.isOnline) + + DashButton(text: NSLocalizedString("Cancel", comment: "")) { + onCancel() + } + .overrideForegroundColor(.primaryText) + .overrideBackgroundColor(.gray300Alpha10) + } + .padding(.horizontal, 60) + .padding(.vertical, 20) + } + } + + // MARK: - Rows + + private var fromRow: some View { + HStack(spacing: Layout.labelSpacing) { + Text(NSLocalizedString("From", comment: "Maya")) + .font(.subheadMedium) + .foregroundColor(.tertiaryText) + .fixedSize() + + HStack(spacing: Layout.logoTextSpacing) { + Icon(name: .custom("dashCircleFilled")) + .frame(width: Layout.coinLogoSize, height: Layout.coinLogoSize) + .clipShape(.rect(cornerRadius: Layout.coinLogoCornerRadius)) + + Text(NSLocalizedString("Dash Wallet", comment: "Maya")) + .font(.subhead) + .foregroundColor(.primaryText) + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.horizontal, Layout.rowHPadding) + .padding(.vertical, Layout.rowVPadding) + .frame(minHeight: Layout.rowMinHeight) + } + + private var toRow: some View { + HStack(spacing: Layout.labelSpacing) { + Text(NSLocalizedString("To", comment: "Maya")) + .font(.subheadMedium) + .foregroundColor(.tertiaryText) + .fixedSize() + + HStack(spacing: Layout.logoTextSpacing) { + MayaCoinIconView( + coin: viewModel.coin, + size: Layout.coinLogoSize, + cornerRadius: Layout.coinLogoCornerRadius + ) + + Text(viewModel.coin.name) + .font(.subhead) + .foregroundColor(.primaryText) + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.horizontal, Layout.rowHPadding) + .padding(.vertical, Layout.rowVPadding) + .frame(minHeight: Layout.rowMinHeight) + } + + private var addressRow: some View { + OrderPreviewTableRow( + leading: String(format: NSLocalizedString("%@ address", comment: "Maya"), viewModel.coin.name), + trailing: viewModel.address, + rowHPadding: Layout.rowHPadding, + rowVPadding: Layout.rowVPadding, + labelSpacing: Layout.labelSpacing, + rowMinHeight: Layout.rowMinHeight + ) + } + + private var purchaseRow: some View { + OrderPreviewTableRow( + leading: NSLocalizedString("Purchase", comment: "Maya"), + trailing: viewModel.purchaseAmount, + trailingSecondary: viewModel.purchaseFiatAmount, + rowHPadding: Layout.rowHPadding, + rowVPadding: Layout.rowVPadding, + labelSpacing: Layout.labelSpacing, + rowMinHeight: Layout.rowMinHeight + ) + } + + private var feeRow: some View { + OrderPreviewFeeRow( + feeTitle: NSLocalizedString("Maya fee", comment: "Maya"), + feeText: viewModel.mayaFee, + feeTextSecondary: viewModel.mayaFeeFiatAmount, + rowHPadding: Layout.rowHPadding, + rowVPadding: Layout.rowVPadding, + labelSpacing: Layout.labelSpacing, + infoSpacing: Layout.infoSpacing, + rowMinHeight: Layout.rowMinHeight + ) + } + + private var totalRow: some View { + HStack(alignment: .lastTextBaseline, spacing: Layout.labelSpacing) { + Text(NSLocalizedString("Total", comment: "Maya")) + .font(.subheadMedium) + .foregroundColor(.tertiaryText) + + Text(viewModel.totalAmount) + .font(.title3Medium) + .foregroundColor(.primaryText) + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.horizontal, Layout.rowHPadding) + .padding(.vertical, Layout.rowVPadding) + } +} + +#if DEBUG +#Preview { + let quote = MayaSwapQuote( + error: nil, + expectedAmountOut: "25700000", + dustThreshold: nil, + expiry: nil, + fees: MayaSwapFees( + affiliate: nil, + asset: nil, + liquidity: nil, + outbound: "180000", + slippageBps: nil, + total: "320000", + totalBps: nil + ), + inboundAddress: "XhzzCcWvvx3rFfbEgkf39rE5Bqt7TP66hR", + inboundConfirmationBlocks: nil, + inboundConfirmationSeconds: nil, + memo: "=:ETH.ETH:0x1234...", + notes: nil, + outboundDelayBlocks: nil, + outboundDelaySeconds: nil, + recommendedMinAmountIn: nil, + slippageBps: nil, + warning: nil, + routeId: nil, + routeProviders: nil, + executionNetwork: nil + ) + + let viewModel = OrderPreviewViewModel( + coin: MayaCryptoCurrency.supportedCoins[0], + address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + dashSatoshis: 10000000, + fromDashAmount: "0.1", + fromFiatAmount: "$2.75", + cryptoFiatRate: 4000, + fiatCurrencyCode: "USD", + initialQuote: quote + ) + + OrderPreviewView(viewModel: viewModel, onCancel: {}) + .background(Color.primaryBackground.ignoresSafeArea()) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewViewModel.swift b/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewViewModel.swift new file mode 100644 index 000000000..2faf97cee --- /dev/null +++ b/DashWallet/Sources/UI/Maya/OrderPreview/OrderPreviewViewModel.swift @@ -0,0 +1,611 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +// MARK: - MayaSuccessTrigger + +/// Controls when the success screen is shown to the user. +/// +/// **ONE-LINER Product switch** — change `OrderPreviewViewModel.successTrigger`. +enum MayaSuccessTrigger { + /// Optimistic (default). Show success as soon as the Dash tx is InstantSend-locked + /// (~5-10 s after broadcast). Mirrors Android's intent. Polling continues in background + /// to track the real backend outcome (`backendOutcome`). + case onISLock + + /// Show success once Maya has observed the inbound Dash tx on-chain (regardless of + /// whether the outbound transfer to the destination chain has completed). + case onObserved + + /// Conservative. Show success only when `observedTx.status == "done"` — i.e. funds + /// have arrived at the destination chain. Previous behaviour. + case onDone + + /// Show success once the submitted Dash transaction receives its first block + /// confirmation on the Dash network (>= 1 confirmation), i.e. the Blockchair-style + /// "In block …, Confirmations: 1" state. Detected locally via DashSync — no external + /// explorer. Maya API polling keeps running only to record `backendOutcome`; it never + /// drives the user-facing success. + case onDashConfirmation +} + +// MARK: - MayaBackendOutcome + +/// The true backend state of the swap as reported by Maya's API. +/// Never causes the user-facing `swapStatus` to regress — it is updated AFTER +/// success has already been shown, so the UI is never yanked away. +/// A post-success refund is recorded here for future surfacing in tx history. +enum MayaBackendOutcome: Equatable { + case pending // Maya has not reached a terminal state yet + case done(outHashes: [String]) // funds arrived at destination chain + case refunded // Maya returned DASH to sender ("refunded"/"aborted") +} + +// MARK: - MayaSwapStatus + +enum MayaSwapStatus: Equatable { + case idle + case pendingConfirmation // Dash tx broadcast, waiting for block + case processingSwap // Maya has observed the Dash tx, swap running + case completed(outHashes: [String]) + case failed(reason: String) +} + +@MainActor +final class OrderPreviewViewModel: ObservableObject { + private enum Constants { + static let submitCountdownSeconds = 10 + } + + // ── PRODUCT CONFIG ────────────────────────────────────────────────────── + /// Change this ONE LINE to control when the success screen appears. + static let successTrigger: MayaSuccessTrigger = .onDashConfirmation + // ──────────────────────────────────────────────────────────────────────── + + let coin: MayaCryptoCurrency + let address: String + let fromDashAmount: String + let fromFiatAmount: String + + @Published var toAmount: String = "—" + @Published var purchaseAmount: String = "—" + @Published var purchaseFiatAmount: String? + @Published var mayaFee: String = "—" + @Published var mayaFeeFiatAmount: String? + @Published var totalAmount: String = "—" + @Published var executionNetwork: String = "—" + @Published var remainingSubmitSeconds: Int = Constants.submitCountdownSeconds + @Published var isSubmitting: Bool = false + @Published var isRefreshing: Bool = false + // True while a fresh quote is being fetched for the failure-screen "Retry" action. + @Published var isRetrying: Bool = false + // Records that the Dash transaction was submitted to the blockchain network. + // This does NOT confirm Maya swap completion — that requires separate on-chain confirmation. + @Published var submittedTxId: String? + @Published var swapStatus: MayaSwapStatus = .idle + @Published var pendingSwapAlertMessage: String? + /// The true backend outcome from Maya's API, tracked independently of `swapStatus`. + /// Updated by background polling after early success is shown. + /// Never causes the success screen to be removed — only recorded for tx history. + @Published var backendOutcome: MayaBackendOutcome = .pending + + var confirmButtonText: String { + if remainingSubmitSeconds > 0 { + return String( + format: NSLocalizedString("Confirm (%lds)", comment: "Maya"), + CLong(remainingSubmitSeconds) + ) + } else { + return NSLocalizedString("Refresh quote", comment: "Maya") + } + } + + private let dashSatoshis: Int64 + // Fiat value of 1 destination coin, in `fiatCurrencyCode`. Used to derive the Purchase/fee + // fiat lines. <= 0 means the rate is unavailable → those lines are hidden. + private let cryptoFiatRate: Decimal + private let fiatCurrencyCode: String + private var quote: MayaSwapQuote + private var countdownCancellable: AnyCancellable? + private let sendCoinsService = SendCoinsService() + private var pollingTask: Task? + private var isLockCancellable: AnyCancellable? + // Strong reference to the broadcast Dash tx. DashSync mutates its `blockHeight` in place + // when the tx is included in a block, which the confirmation observer re-reads on each + // transaction-status notification. + private var submittedTransaction: DSTransaction? + private var confirmationCancellable: AnyCancellable? + + init( + coin: MayaCryptoCurrency, + address: String, + dashSatoshis: Int64, + fromDashAmount: String, + fromFiatAmount: String, + cryptoFiatRate: Decimal, + fiatCurrencyCode: String, + initialQuote: MayaSwapQuote + ) { + self.coin = coin + self.address = address + self.dashSatoshis = dashSatoshis + self.fromDashAmount = fromDashAmount + self.fromFiatAmount = fromFiatAmount + self.cryptoFiatRate = cryptoFiatRate + self.fiatCurrencyCode = fiatCurrencyCode + self.quote = initialQuote + applyQuote(initialQuote) + startCountdown() + } + + deinit { + countdownCancellable?.cancel() + pollingTask?.cancel() + isLockCancellable?.cancel() + confirmationCancellable?.cancel() + } + + func handlePrimaryAction() async { + if remainingSubmitSeconds > 0 { + await submitSwap() + } else { + await refreshQuoteForDisplay() + } + } + + func resetToIdle() { + pollingTask?.cancel() + pollingTask = nil + isLockCancellable?.cancel() + isLockCancellable = nil + confirmationCancellable?.cancel() + confirmationCancellable = nil + submittedTransaction = nil + swapStatus = .idle + submittedTxId = nil + pendingSwapAlertMessage = nil + backendOutcome = .pending + } + + /// "Retry" from the failure screen: fetches a fresh Maya quote for the SAME coin, + /// destination address, and Dash amount. + /// - Returns: a new `OrderPreviewViewModel` ready for a fresh Order Preview when the quote + /// refresh succeeds; `nil` when it fails (in which case `swapStatus` is updated with the + /// new failure reason so the caller can stay on the failed screen). + func retryQuote() async -> OrderPreviewViewModel? { + guard !isRetrying else { return nil } + isRetrying = true + defer { isRetrying = false } + + do { + let freshQuote = try await fetchFreshQuote() + if let apiError = freshQuote.error { + swapStatus = .failed(reason: apiError) + return nil + } + return OrderPreviewViewModel( + coin: coin, + address: address, + dashSatoshis: dashSatoshis, + fromDashAmount: fromDashAmount, + fromFiatAmount: fromFiatAmount, + cryptoFiatRate: cryptoFiatRate, + fiatCurrencyCode: fiatCurrencyCode, + initialQuote: freshQuote + ) + } catch { + swapStatus = .failed(reason: error.localizedDescription) + return nil + } + } + + // MARK: - Private: Countdown + + private func startCountdown() { + countdownCancellable = Timer.publish(every: 1, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + guard let self, self.remainingSubmitSeconds > 0 else { return } + self.remainingSubmitSeconds -= 1 + } + } + + private func stopCountdown() { + countdownCancellable?.cancel() + countdownCancellable = nil + } + + private func resetCountdown() { + remainingSubmitSeconds = Constants.submitCountdownSeconds + } + + // MARK: - Private: Quote Operations + + private func refreshQuoteForDisplay() async { + guard !isRefreshing else { return } + isRefreshing = true + defer { isRefreshing = false } + + do { + let newQuote = try await fetchFreshQuote() + if let apiError = newQuote.error { + setFailure(apiError) + return + } + quote = newQuote + applyQuote(newQuote) + resetCountdown() + } catch { + setFailure(error.localizedDescription) + } + } + + private func fetchFreshQuote() async throws -> MayaSwapQuote { + try await MayaAPIService.shared.fetchQuote( + dashSatoshis: dashSatoshis, + toAsset: coin.mayaAsset, + destination: address + ) + } + + private func submitSwap() async { + guard !isSubmitting else { return } + submittedTxId = nil + pendingSwapAlertMessage = nil + isSubmitting = true + defer { isSubmitting = false } + + do { + // Refresh quote immediately before commit so vault address and memo are fresh. + // Mirrors Android's getSwapInfo call in MayaBlockchainApi.commitSwapTransaction. + let freshQuote = try await fetchFreshQuote() + if let apiError = freshQuote.error { + setFailure(apiError) + return + } + quote = freshQuote + applyQuote(freshQuote) + + let execution = try resolveExecutionData(from: freshQuote) + let tx = try await submitDashTransaction(using: execution) + setSubmittedTransaction(tx) + } catch { + if case .some(.previousSwapPending) = error as? DashSpendError { + pendingSwapAlertMessage = error.localizedDescription + return + } + setFailure(error.localizedDescription) + } + } + + private func mayaFieldError(_ message: String) -> Error { + NSError(domain: "Maya", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) + } + + private func resolveExecutionData(from quote: MayaSwapQuote) throws -> SwapExecutionData { + guard let vaultAddress = quote.inboundAddress, !vaultAddress.isEmpty else { + throw mayaFieldError(NSLocalizedString("Vault address is missing. Please refresh and try again.", comment: "Maya")) + } + guard let memo = quote.memo, !memo.isEmpty else { + throw mayaFieldError(NSLocalizedString("Swap memo is missing. Please refresh and try again.", comment: "Maya")) + } + return SwapExecutionData(vaultAddress: vaultAddress, memo: memo, executionNetwork: "Maya") + } + + private func submitDashTransaction(using execution: SwapExecutionData) async throws -> DSTransaction { + try await sendCoinsService.sendMayaSwap( + vaultAddress: execution.vaultAddress, + dashAmount: UInt64(dashSatoshis), + memo: execution.memo + ) + } + + // MARK: - Private: State Mutation + + private func setFailure(_ message: String) { + // Keep Maya failures on the status sheet path so SwiftUI does not try to + // present a native alert and a bottom sheet for the same event. + swapStatus = .failed(reason: message) + } + + private func setSubmittedTransaction(_ tx: DSTransaction) { + submittedTransaction = tx + submittedTxId = tx.txHashHexString + swapStatus = .pendingConfirmation + startObservingISLock(txid: tx.txHashHexString) + if Self.successTrigger == .onDashConfirmation { + startObservingDashConfirmation(tx: tx) + } + startPolling(txid: tx.txHashHexString) + } + + // MARK: - Private: IS-Lock Observation + + private func startObservingISLock(txid: String) { + isLockCancellable = NotificationCenter.default + .publisher(for: .DSTransactionManagerTransactionStatusDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] notification in + guard let self else { return } + + guard + let userInfo = notification.userInfo, + let tx = userInfo[DSTransactionManagerNotificationTransactionKey] as? DSTransaction, + tx.txHashHexString == txid + else { return } + + guard + let changes = userInfo[DSTransactionManagerNotificationTransactionChangesKey] as? [String: Any], + changes[DSTransactionManagerNotificationInstantSendTransactionLockKey] != nil + else { return } + + DSLogger.log("sendMayaSwap IS-lock received for \(txid)") + + if case .pendingConfirmation = self.swapStatus { + switch Self.successTrigger { + case .onISLock: + // Show success immediately. Polling keeps running in the background + // to track the real backend outcome (backendOutcome). + self.swapStatus = .completed(outHashes: []) + case .onObserved, .onDone, .onDashConfirmation: + // IS-lock alone is NOT success here — it only advances the UI to + // "processing". Success requires a block confirmation (or, for the + // other triggers, their own backend condition). + self.swapStatus = .processingSwap + } + } + + self.isLockCancellable = nil // one-shot + } + } + + // MARK: - Private: Dash Confirmation Observation + + /// Observes the broadcast Dash transaction locally and shows success once it reaches + /// its first block confirmation. Uses DashSync state/notifications only — no external + /// explorer. The transaction-status notification fires on every block/mempool sync + /// update (sometimes without a transaction payload), so each firing simply re-reads the + /// tracked tx's confirmation count. + private func startObservingDashConfirmation(tx: DSTransaction) { + // Fast path: already confirmed (e.g. re-entry after a quick block). + if completeIfConfirmed(tx: tx) { return } + + confirmationCancellable = NotificationCenter.default + .publisher(for: .DSTransactionManagerTransactionStatusDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.completeIfConfirmed(tx: tx) + } + } + + /// Flips `swapStatus` to `.completed` when `tx` has >= 1 confirmation. + /// Never regresses a terminal user-facing state. Returns true once terminal so the + /// caller can stop observing. + @discardableResult + private func completeIfConfirmed(tx: DSTransaction) -> Bool { + switch swapStatus { + case .completed, .failed: + confirmationCancellable?.cancel() + confirmationCancellable = nil + return true + default: + break + } + + let confirmations = Self.confirmations(for: tx) + guard confirmations >= 1 else { return false } + + DSLogger.log("Maya: Dash tx \(tx.txHashHexString) confirmed in block " + + "(blockHeight=\(tx.blockHeight), confirmations=\(confirmations)) — showing success") + swapStatus = .completed(outHashes: []) + confirmationCancellable?.cancel() + confirmationCancellable = nil + return true + } + + /// Number of block confirmations for `tx` against the current chain tip. + /// Returns 0 while the tx is still in the mempool (`blockHeight == TX_UNCONFIRMED`). + /// Mirrors the calculation in `Transaction.swift`. + private static func confirmations(for tx: DSTransaction) -> Int { + let lastHeight = DWEnvironment.sharedInstance().currentChain.lastTerminalBlockHeight + let txHeight = tx.blockHeight + guard txHeight != UInt32(TX_UNCONFIRMED), txHeight <= lastHeight else { return 0 } + return Int(lastHeight - txHeight) + 1 + } + + // MARK: - Private: Polling + + private func startPolling(txid: String) { + pollingTask?.cancel() + // [weak self] prevents a retain cycle: the Task would otherwise keep self alive + // indefinitely even after popToRootViewController releases all external references. + // The @MainActor context is inherited, so all property accesses remain on main. + pollingTask = Task { [weak self] in + guard let self else { return } + let maxIterations = 360 // 5 s × 360 = 30 min + + for iteration in 0.. 0 { + try? await Task.sleep(nanoseconds: 5_000_000_000) + guard !Task.isCancelled else { return } + } + + do { + let info = try await MayaAPIService.shared.fetchSwapTransactionInfo(txid: txid) + + // Maya hasn't seen the Dash tx yet (block not confirmed) — keep waiting. + guard info.error == nil, let observedTx = info.observedTx else { continue } + + let outcome = Self.classifyObservedTx(observedTx) + if self.handlePollingOutcome(outcome, txid: txid) { return } + } catch { + // Transient network error — keep current status and retry next tick. + } + } + + // 30-minute timeout. + // If success was already shown, silently stop. backendOutcome stays .pending + // which signals that the terminal state was never confirmed within 30 min. + if !self.isSuccessAlreadyShown { + self.swapStatus = .failed(reason: NSLocalizedString( + "Swap timed out after 30 minutes. Contact Maya support if funds were sent.", + comment: "Maya" + )) + } + } + } + + /// Applies a single polled Maya outcome to the view-model state. + /// - Returns: `true` when a terminal state was reached and polling should stop. + private func handlePollingOutcome(_ outcome: MayaBackendOutcome, txid: String) -> Bool { + // Success already on screen — NEVER regress swapStatus; only record backendOutcome. + if isSuccessAlreadyShown { + switch outcome { + case .pending: + return false // still running, continue polling + case .done, .refunded: + backendOutcome = outcome + if case .refunded = outcome { + // Post-success refund detected. Record it; do not yank the success + // screen. Future: surface in tx history / a badge. + DSLogger.log("Maya: post-success refund detected for \(txid)") + } + return true // terminal state reached, stop polling + } + } + + // Success not yet shown — drive swapStatus. + switch outcome { + case .done(let hashes): + swapStatus = .completed(outHashes: hashes) + backendOutcome = outcome + return true + case .refunded: + swapStatus = .failed(reason: NSLocalizedString( + "Your DASH was refunded by Maya Protocol.", + comment: "Maya" + )) + backendOutcome = outcome + return true + case .pending: + advanceUIForPendingObservation() + return false + } + } + + /// Advances the UI on a non-terminal Maya observation, per the active success trigger. + private func advanceUIForPendingObservation() { + switch Self.successTrigger { + case .onObserved: + // observedTx is present and not terminal-bad → show success. + // Keep polling so backendOutcome is eventually resolved. + swapStatus = .completed(outHashes: []) + case .onISLock, .onDone, .onDashConfirmation: + // For these triggers success is driven elsewhere (IS-lock, Maya done, or the + // local block-confirmation observer). While still waiting, only advance + // pending → processing; never overwrite a success that was already shown. + if case .pendingConfirmation = swapStatus { + swapStatus = .processingSwap + } + } + } + + private var isSuccessAlreadyShown: Bool { + if case .completed = swapStatus { return true } + return false + } + + /// Maps a Maya `observed_tx.status` string to `MayaBackendOutcome`. + /// + /// Status strings verified against Maya/Thorchain API: + /// - `"done"` — funds arrived at destination chain (terminal success) + /// - `"refunded"` — Maya returned DASH to sender (terminal, treated as failure for UX) + /// - `"aborted"` — transaction aborted, may result in refund (terminal, treated as refund) + /// - anything else (nil, "unknown", in-progress states) → `.pending` + private static func classifyObservedTx(_ observedTx: MayaObservedTx) -> MayaBackendOutcome { + switch observedTx.status { + case "done": + return .done(outHashes: observedTx.outHashes ?? []) + case "refunded", "aborted": + return .refunded + default: + return .pending + } + } + + private func applyQuote(_ quote: MayaSwapQuote) { + let expectedOut = assetDecimalFromBaseUnits(quote.expectedAmountOut) + let fee = assetDecimalFromBaseUnits(quote.fees?.total ?? quote.fees?.outbound) + let total = expectedOut + fee + + toAmount = formatCryptoAmount(expectedOut) + purchaseAmount = toAmount + mayaFee = formatFeeAmount(fee) + totalAmount = formatCryptoAmount(total) + executionNetwork = quote.executionNetwork ?? "Maya" + + // Fiat lines = coin amount × cryptoFiatRate (fiat value of 1 destination coin), in the + // active fiat currency — matching how the source-side fromFiatAmount is produced. + // Rate unavailable (<= 0) → hide the lines instead of showing a wrong/zero value. + if cryptoFiatRate > 0 { + purchaseFiatAmount = formatFiat(expectedOut * cryptoFiatRate) + mayaFeeFiatAmount = fee > 0 ? formatFiat(fee * cryptoFiatRate) : nil + } else { + purchaseFiatAmount = nil + mayaFeeFiatAmount = nil + } + } + + // MARK: - Private: Formatting + + private func assetDecimalFromBaseUnits(_ raw: String?) -> Decimal { + guard let raw, let value = Decimal(string: raw) else { return 0 } + return value / Decimal(100_000_000) + } + + private func formatCryptoAmount(_ value: Decimal) -> String { + "\(coin.code) \(formatDecimal(value))" + } + + private func formatFeeAmount(_ value: Decimal) -> String { + value > 0 ? formatCryptoAmount(value) : "—" + } + + /// Formats a fiat value in `fiatCurrencyCode`. Mirrors `MayaInputFormatter.fiat(_:currencyCode:)` + /// so the Purchase/fee fiat lines match the source-side fiat format. + private func formatFiat(_ value: Decimal) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = fiatCurrencyCode + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + return formatter.string(from: value as NSDecimalNumber) ?? "\(value)" + } + + private func formatDecimal(_ value: Decimal) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 8 + formatter.decimalSeparator = "." + return formatter.string(from: value as NSDecimalNumber) ?? "\(value)" + } +} diff --git a/DashWallet/Sources/UI/Maya/GenericQRScannerController.swift b/DashWallet/Sources/UI/Maya/QRScanner/GenericQRScannerController.swift similarity index 100% rename from DashWallet/Sources/UI/Maya/GenericQRScannerController.swift rename to DashWallet/Sources/UI/Maya/QRScanner/GenericQRScannerController.swift diff --git a/DashWallet/Sources/UI/Maya/GenericQRScannerView.swift b/DashWallet/Sources/UI/Maya/QRScanner/GenericQRScannerView.swift similarity index 100% rename from DashWallet/Sources/UI/Maya/GenericQRScannerView.swift rename to DashWallet/Sources/UI/Maya/QRScanner/GenericQRScannerView.swift diff --git a/DashWallet/Sources/UI/Maya/QRCaptureView.swift b/DashWallet/Sources/UI/Maya/QRScanner/QRCaptureView.swift similarity index 100% rename from DashWallet/Sources/UI/Maya/QRCaptureView.swift rename to DashWallet/Sources/UI/Maya/QRScanner/QRCaptureView.swift diff --git a/DashWallet/Sources/UI/Maya/SelectCoin/Components/CoinRowView.swift b/DashWallet/Sources/UI/Maya/SelectCoin/Components/CoinRowView.swift new file mode 100644 index 000000000..ab32f79d1 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/SelectCoin/Components/CoinRowView.swift @@ -0,0 +1,161 @@ +// +// CoinRowView.swift +// DashWallet +// +// 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 CoinRowView: View { + + private enum Layout { + static let spacing: CGFloat = 16 + static let textSpacing: CGFloat = 1 + static let padding: CGFloat = 10 + static let iconSize: CGFloat = 30 + static let iconCornerRadius: CGFloat = 6 + static let badgeHPadding: CGFloat = 8 + static let badgeVPadding: CGFloat = 2 + static let badgeCornerRadius: CGFloat = 6 + } + + let item: CoinDisplayItem + + var body: some View { + HStack(spacing: Layout.spacing) { + coinIcon + coinInfo + trailingBadge + } + .padding(Layout.padding) + .opacity(item.isHalted ? 0.5 : 1.0) + .contentShape(.rect) + + + } + + // MARK: - Subviews + + private var coinIcon: some View { + MayaCoinIconView( + coin: item.coin, + size: Layout.iconSize, + cornerRadius: Layout.iconCornerRadius + ) + } + + private var coinInfo: some View { + VStack(alignment: .leading, spacing: Layout.textSpacing) { + Text(item.displayName) + .font(.subheadMedium) + .foregroundColor(.primaryText) + .lineLimit(1) + .truncationMode(.tail) + + Text(item.coin.code) + .font(.footnote) + .foregroundColor(.tertiaryText) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + @ViewBuilder + private var trailingBadge: some View { + if item.isHalted { + haltedLabel + } else if let price = item.fiatPrice { + Text(price) + .font(.caption1) + .foregroundColor(.tertiaryText) + } + } + + private var haltedLabel: some View { + Text(NSLocalizedString("halted", comment: "Maya")) + .font(.caption1) + .foregroundColor(.tertiaryText) + .padding(.horizontal, Layout.badgeHPadding) + .padding(.vertical, Layout.badgeVPadding) + .background(Color.black1000Alpha8) + .clipShape(.rect(cornerRadius: Layout.badgeCornerRadius)) + } +} + +#if DEBUG +private func makeItem( + id: String, coin: MayaCryptoCurrency, + displayName: String? = nil, price: String? = nil, halted: Bool = false +) -> CoinDisplayItem { + CoinDisplayItem(id: id, coin: coin, displayName: displayName ?? coin.name, fiatPrice: price, isHalted: halted) +} + +// iOS hierarchy: full name (+ network) on top, code below. +#Preview("Network-qualified rows") { + VStack(spacing: 0) { + // USDC / USD Coin (Ethereum) + CoinRowView(item: makeItem( + id: "usdc", + coin: MayaCryptoCurrency(id: "usdc", code: "USDC", name: "USD Coin", + mayaAsset: "ETH.USDC-0XA0B8...", chain: "ETH", + iconAssetName: "maya.coin.usdc"), + displayName: "USD Coin (Ethereum)", + price: "USD 1.00" + )) + // USDC / USD Coin (Arbitrum) + CoinRowView(item: makeItem( + id: "usdc_arb", + coin: MayaCryptoCurrency(id: "usdc_arb", code: "USDC", name: "USD Coin (Arbitrum)", + mayaAsset: "ARB.USDC-0XAF88...", chain: "ARB", + iconAssetName: "maya.coin.usdc"), + price: "UAH 44.54" + )) + // USDT / Tether (Ethereum) + CoinRowView(item: makeItem( + id: "usdt", + coin: MayaCryptoCurrency(id: "usdt", code: "USDT", name: "Tether", + mayaAsset: "ETH.USDT-0XDAC1...", chain: "ETH", + iconAssetName: "maya.coin.usdt"), + displayName: "Tether (Ethereum)", + price: "USD 1.00" + )) + // USDT / Tether (Arbitrum) + CoinRowView(item: makeItem( + id: "usdt_arb", + coin: MayaCryptoCurrency(id: "usdt_arb", code: "USDT", name: "Tether", + mayaAsset: "ARB.USDT-0XFD08...", chain: "ARB", + iconAssetName: "maya.coin.usdt"), + displayName: "Tether (Arbitrum)", + price: "USD 1.00" + )) + } +} + +#Preview("Halted") { + VStack { + CoinRowView(item: makeItem( + id: MayaCryptoCurrency.supportedCoins[7].id, + coin: MayaCryptoCurrency.supportedCoins[7], + price: "USD 3,200.00", halted: true + )) + CoinRowView(item: makeItem( + id: MayaCryptoCurrency.supportedCoins[1].id, + coin: MayaCryptoCurrency.supportedCoins[1], + price: "USD 3,200.00", halted: true + )) + } +} +#endif diff --git a/DashWallet/Sources/UI/Maya/SelectCoin/Components/HaltedToast.swift b/DashWallet/Sources/UI/Maya/SelectCoin/Components/HaltedToast.swift new file mode 100644 index 000000000..2762c9b39 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/SelectCoin/Components/HaltedToast.swift @@ -0,0 +1,69 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct HaltedToast: View { + + @Binding var showHaltedToast: Bool + + var body: some View { + HStack(alignment: .top, spacing: 0) { + HStack(alignment: .top, spacing: 10) { + + ZStack { + Color.black.opacity(0.001) + .frame(width: 24, height: 24) + + ExclamationIcon( + size: CGSize(width: 18, height: 17), + fillColor: .systemYellow, + strokeColor: .systemYellow, + symbolColor: .primaryText + ) + } + + + Text(NSLocalizedString("Some coins are not available because of the halted chain", comment: "Maya")) + .font(.subhead) + .foregroundColor(.white) + .padding(.vertical, 2) + + Spacer(minLength: 0) + } + + Button { + showHaltedToast = false + } label: { + XmarkIcon(size: 9, color: Color(red: 1, green: 1, blue: 1), lineWidth: 1.5) + .padding(8) + .background(Color.whiteAlpha10, in: .circle) + } + } + .padding(.leading, 12) + .padding(.trailing, 8) + .padding(.vertical, 8) + .background(Color.black1000Alpha90) + .clipShape(.rect(cornerRadius: 20)) + .contentShape(.rect) + } +} + +#Preview { + HaltedToast(showHaltedToast: .constant(false)) + .padding(20) +} diff --git a/DashWallet/Sources/UI/Maya/SelectCoin/Components/MayaCoinIconView.swift b/DashWallet/Sources/UI/Maya/SelectCoin/Components/MayaCoinIconView.swift new file mode 100644 index 000000000..ba4ba1fae --- /dev/null +++ b/DashWallet/Sources/UI/Maya/SelectCoin/Components/MayaCoinIconView.swift @@ -0,0 +1,101 @@ +// +// MayaCoinIconView.swift +// DashWallet +// +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// Displays a Maya coin icon with automatic remote fallback. +/// +/// Priority: +/// 1. Local asset (when `iconAssetName != "convert.crypto"`) +/// 2. Remote icon fetched and cached by `MayaCoinIconLoader` +/// 3. Placeholder (`convert.crypto`) if remote load fails +/// +/// Remote icons crossfade in when loaded. Fast-scrolling is safe: the +/// `task(id: coin.code)` modifier cancels in-flight requests when the view +/// disappears, and `remoteImage` resets to nil on reuse (because @State is +/// tied to view identity in LazyVStack). +struct MayaCoinIconView: View { + let coin: MayaCryptoCurrency + let size: CGFloat + let cornerRadius: CGFloat + + @State private var remoteImage: UIImage? = nil + + private var needsRemoteIcon: Bool { + coin.iconAssetName == "convert.crypto" + } + + var body: some View { + Group { + if let image = remoteImage { + Image(uiImage: image) + .resizable() + .scaledToFit() + } else { + Icon(name: .custom(coin.iconAssetName, maxHeight: size)) + } + } + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: cornerRadius)) + .task(id: coin.code) { + guard needsRemoteIcon else { return } + let loaded = await MayaCoinIconLoader.shared.loadIcon(for: coin.code) + withAnimation(.easeIn(duration: 0.15)) { + remoteImage = loaded + } + } + } +} + +#if DEBUG +#Preview { + HStack(spacing: 16) { + // Coin with local icon + MayaCoinIconView( + coin: MayaCryptoCurrency( + id: "btc", code: "BTC", name: "Bitcoin", + mayaAsset: "BTC.BTC", chain: "BTC", + iconAssetName: "maya.coin.btc" + ), + size: 26, cornerRadius: 6 + ) + + // Coin without local icon — loads remotely + MayaCoinIconView( + coin: MayaCryptoCurrency( + id: "sol", code: "SOL", name: "Solana", + mayaAsset: "SOL.SOL", chain: "SOL", + iconAssetName: "convert.crypto" + ), + size: 26, cornerRadius: 6 + ) + + // Unknown coin — graceful fallback + MayaCoinIconView( + coin: MayaCryptoCurrency( + id: "xyz", code: "XYZ", name: "Unknown", + mayaAsset: "XYZ.XYZ", chain: "XYZ", + iconAssetName: "convert.crypto" + ), + size: 26, cornerRadius: 6 + ) + } + .padding() +} +#endif diff --git a/DashWallet/Sources/UI/Maya/SelectCoinHostingController.swift b/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinHostingController.swift similarity index 79% rename from DashWallet/Sources/UI/Maya/SelectCoinHostingController.swift rename to DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinHostingController.swift index fd87fbd41..6b76ad7c3 100644 --- a/DashWallet/Sources/UI/Maya/SelectCoinHostingController.swift +++ b/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinHostingController.swift @@ -20,19 +20,23 @@ import SwiftUI import UIKit -class SelectCoinHostingController: UIViewController { +class SelectCoinHostingController: UIViewController, NavigationBarDisplayable { + var isNavigationBarHidden: Bool { true } var onCoinSelected: ((MayaCryptoCurrency) -> Void)? override func viewDidLoad() { super.viewDidLoad() - title = NSLocalizedString("Select coin", comment: "Maya") view.backgroundColor = UIColor.dw_secondaryBackground() - navigationItem.largeTitleDisplayMode = .never - let selectCoinView = SelectCoinView { [weak self] coin in - self?.onCoinSelected?(coin) - } + let selectCoinView = SelectCoinView( + onBack: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + onCoinSelected: { [weak self] coin in + self?.onCoinSelected?(coin) + } + ) let hostingController = UIHostingController(rootView: selectCoinView) hostingController.view.backgroundColor = .clear diff --git a/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinView.swift b/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinView.swift new file mode 100644 index 000000000..012c5b469 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinView.swift @@ -0,0 +1,204 @@ +// +// SelectCoinView.swift +// DashWallet +// +// 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 + +// MARK: - SelectCoinView + +struct SelectCoinView: View { + private enum Layout { + static let hPadding: CGFloat = 20 + static let topPadding: CGFloat = 10 + static let contentSpacing: CGFloat = 20 + static let listSpacing: CGFloat = 2 + static let listPadding: CGFloat = 6 + static let listCornerRadius: CGFloat = 12 + static let toastHPadding: CGFloat = 20 + static let toastBPadding: CGFloat = 16 + static let errorIconSize: CGFloat = 40 + static let errorTextHPadding: CGFloat = 40 + static let emptyStateIconSize: CGFloat = 28 + static let emptyStateSpacing: CGFloat = 8 + static let emptyStateTextHPadding: CGFloat = 24 + } + + @StateObject private var viewModel = SelectCoinViewModel() + @StateObject private var reachability = NetworkReachabilityMonitor() + var onBack: (() -> Void)? + var onCoinSelected: ((MayaCryptoCurrency) -> Void)? + + init(onBack: (() -> Void)? = nil, onCoinSelected: ((MayaCryptoCurrency) -> Void)? = nil) { + self.onBack = onBack + self.onCoinSelected = onCoinSelected + } + + var body: some View { + ZStack { + Color.primaryBackground.ignoresSafeArea() + VStack(spacing: 0) { + NavigationBar( + leading: { NavigationBarElement.back.button { onBack?() } }, + central: { Text(NSLocalizedString("Select coin", comment: "Maya")).font(.subheadMedium) } + ) + mainContent + } + } + .overlay(alignment: .bottom) { + if viewModel.showHaltedToast { + HaltedToast(showHaltedToast: $viewModel.showHaltedToast) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .padding(.horizontal, Layout.toastHPadding) + .padding(.bottom, Layout.toastBPadding) + } + } + .animation(.easeInOut(duration: 0.3), value: viewModel.showHaltedToast) + .task { + if reachability.isOnline { + await viewModel.loadCoins() + } + } + .onChange(of: reachability.isOnline) { isOnline in + // Restore the coin list when connectivity returns. loadCoins() is a no-op + // unless coins are missing or a prior fetch errored, so this never resets + // an already-loaded list or its scroll position. + guard isOnline else { return } + Task { await viewModel.loadCoins() } + } + } + + // MARK: - Main Content + + @ViewBuilder + private var mainContent: some View { + if !reachability.isOnline { + NetworkUnavailableStateView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if viewModel.isLoading { + SwiftUI.ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let error = viewModel.errorMessage { + errorView(message: error) + } else { + contentView + } + } + + private var contentView: some View { + VStack(spacing: Layout.contentSpacing) { + SearchBar(text: $viewModel.searchText) + + if viewModel.showSearchEmptyState { + emptyStateView + } else { + coinList + } + } + .padding(.horizontal, Layout.hPadding) + .padding(.top, Layout.topPadding) + } + + // MARK: - Coin List + + private var coinList: some View { + ScrollViewReader { proxy in + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(spacing: Layout.listSpacing) { + ForEach(viewModel.filteredCoins) { item in + Button { + viewModel.willSelectCoin(item) + onCoinSelected?(item.coin) + } label: { + CoinRowView(item: item) + } + .buttonStyle(.plain) + .id(item.id) + } + } + .modifier(MayaMenuCardStyle()) + } + .onAppear { + // Restore scroll position after back-navigation. + // One run-loop defer lets the LazyVStack finish initial layout. + guard let id = viewModel.scrollAnchorID else { return } + DispatchQueue.main.async { + proxy.scrollTo(id, anchor: .center) + } + } + } + } + + // MARK: - Empty State + + private var emptyStateView: some View { + VStack { + VStack(spacing: Layout.emptyStateSpacing) { + Text(NSLocalizedString("No coins found", comment: "Maya")) + .font(.footnote) + .foregroundColor(Color.gray500) + } + .padding(10) + .frame(maxWidth: .infinity, maxHeight: 100) + .modifier(MayaMenuCardStyle()) + + Spacer() + } + } + + // MARK: - Error View + + private func errorView(message: String) -> some View { + VStack(spacing: 16) { + Spacer() + + Image(systemName: "wifi.exclamationmark") + .font(.system(size: Layout.errorIconSize)) + .foregroundColor(Color.secondaryText) + + Text(message) + .font(.system(size: 14)) + .foregroundColor(Color.secondaryText) + .multilineTextAlignment(.center) + .padding(.horizontal, Layout.errorTextHPadding) + + retryButton + + Spacer() + } + } + + private var retryButton: some View { + Button(action: { + Task { await viewModel.loadCoins(force: true) } + }) { + Text(NSLocalizedString("Retry", comment: "")) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal, 24) + .padding(.vertical, 10) + .background(Color.dashBlue) + .cornerRadius(8) + } + } +} + +#if DEBUG +#Preview { + SelectCoinView(onBack: {}) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinViewModel.swift b/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinViewModel.swift new file mode 100644 index 000000000..928fe7184 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/SelectCoin/SelectCoinViewModel.swift @@ -0,0 +1,203 @@ +// +// SelectCoinViewModel.swift +// DashWallet +// +// 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 +import Foundation + +struct CoinDisplayItem: Identifiable { + let id: String + let coin: MayaCryptoCurrency + /// Effective display name for the coin row. + /// Equals `coin.name` today; structured to accept an API-provided override in future. + let displayName: String + let fiatPrice: String? + let isHalted: Bool +} + +@MainActor +class SelectCoinViewModel: ObservableObject { + // MARK: - Published State + + @Published var coins: [CoinDisplayItem] = [] + @Published var searchText: String = "" + @Published var isLoading: Bool = false + @Published var errorMessage: String? + @Published var hasHaltedCoins: Bool = false + @Published var showHaltedToast: Bool = false + + /// ID of the last coin the user tapped; used to restore scroll position on back-navigation. + private(set) var scrollAnchorID: String? + + var filteredCoins: [CoinDisplayItem] { + guard !searchText.isEmpty else { return coins } + return coins.filter { matchesSearch($0, query: searchText) } + } + + var showSearchEmptyState: Bool { + !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && + filteredCoins.isEmpty && + !isLoading && + errorMessage == nil + } + + // MARK: - Selection + + /// Records the tapped coin so `SelectCoinView` can restore scroll position on back-navigation. + func willSelectCoin(_ item: CoinDisplayItem) { + scrollAnchorID = item.id + } + + // MARK: - Loading + + /// Loads coins from the network. + /// Skips the network call if coins are already loaded and there is no pending error, + /// which prevents the `.task` re-fire on back-navigation from resetting the scroll position. + func loadCoins(force: Bool = false) async { + guard force || coins.isEmpty || errorMessage != nil else { return } + isLoading = true + errorMessage = nil + defer { isLoading = false } + + do { + async let poolsRequest = MayaAPIService.shared.fetchPools() + async let inboundRequest = MayaAPIService.shared.fetchInboundAddresses() + let (pools, inboundAddresses) = try await (poolsRequest, inboundRequest) + + let fiatCurrency = App.fiatCurrency + let formatter = makePriceFormatter(for: fiatCurrency) + let items = makeCoinItems( + pools: pools, + inboundAddresses: inboundAddresses, + fiatCurrency: fiatCurrency, + formatter: formatter + ) + + let disambiguated = disambiguateDisplayNames(items) + coins = sortCoins(disambiguated) + hasHaltedCoins = disambiguated.contains { $0.isHalted } + showHaltedToast = hasHaltedCoins + } catch { + errorMessage = error.localizedDescription + } + } + + // MARK: - Private: Item Creation + + private func makeCoinItems( + pools: [MayaPool], + inboundAddresses: [MayaInboundAddress], + fiatCurrency: String, + formatter: NumberFormatter + ) -> [CoinDisplayItem] { + let inboundChains = Set(inboundAddresses.map { $0.chain.uppercased() }) + let haltedChains = Set(inboundAddresses.filter { $0.halted }.map { $0.chain.uppercased() }) + + return pools.compactMap { pool in + guard pool.isAvailable else { return nil } + guard pool.asset.uppercased() != "DASH.DASH" else { return nil } + guard let coin = MayaCryptoCurrency.coin(for: pool.asset) else { return nil } + guard inboundChains.contains(coin.chain.uppercased()) else { return nil } + + return CoinDisplayItem( + id: coin.id, + coin: coin, + displayName: coin.name, // API-override point: swap in pool.name when available + fiatPrice: priceForCoin(pool, fiatCurrency: fiatCurrency, formatter: formatter), + isHalted: isCoinHalted(coin, haltedChains: haltedChains) + ) + } + } + + private func makePriceFormatter(for fiatCurrency: String) -> NumberFormatter { + // Decimal style so we can prepend the currency code ourselves, + // producing "USD 1.00" / "UAH 44.54" to match Android format. + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + formatter.usesGroupingSeparator = true + return formatter + } + + // MARK: - Private: Disambiguation + + /// Adds a "(ChainName)" suffix to `displayName` for every item whose coin code is shared + /// by at least one other item and whose name doesn't already carry a parenthetical qualifier. + /// This disambiguates e.g. two USDC pools: "USD Coin (Ethereum)" vs "USD Coin (Arbitrum)". + private func disambiguateDisplayNames(_ items: [CoinDisplayItem]) -> [CoinDisplayItem] { + let codeGroups = Dictionary(grouping: items, by: { $0.coin.code }) + return items.map { item in + guard (codeGroups[item.coin.code]?.count ?? 0) > 1 else { return item } + guard !item.displayName.contains("(") else { return item } + let chainLabel = MayaCryptoCurrency.chainDisplayName(item.coin.chain) + let qualifiedName = chainLabel.isEmpty + ? item.displayName + : "\(item.displayName) (\(chainLabel))" + return CoinDisplayItem( + id: item.id, coin: item.coin, + displayName: qualifiedName, + fiatPrice: item.fiatPrice, isHalted: item.isHalted + ) + } + } + + // MARK: - Private: Filtering and Sorting + + private func matchesSearch(_ item: CoinDisplayItem, query: String) -> Bool { + item.displayName.localizedCaseInsensitiveContains(query) || + item.coin.code.localizedCaseInsensitiveContains(query) || + item.coin.name.localizedCaseInsensitiveContains(query) // fallback for static name + } + + private func sortCoins(_ items: [CoinDisplayItem]) -> [CoinDisplayItem] { + items.sorted { a, b in + let codeComparison = a.coin.code.localizedCaseInsensitiveCompare(b.coin.code) + if codeComparison != .orderedSame { return codeComparison == .orderedAscending } + return a.displayName.localizedCaseInsensitiveCompare(b.displayName) == .orderedAscending + } + } + + // MARK: - Private: Helpers + + private func isCoinHalted(_ coin: MayaCryptoCurrency, haltedChains: Set) -> Bool { + haltedChains.contains(coin.chain.uppercased()) + } + + private func priceForCoin(_ pool: MayaPool, fiatCurrency: String, formatter: NumberFormatter) -> String? { + guard let priceUSD = pool.priceUSD, priceUSD > 0 else { return nil } + guard let fiatAmount = convertUSDToFiat(usdAmount: priceUSD, fiatCurrency: fiatCurrency) else { return nil } + guard let formatted = formatter.string(from: NSNumber(value: fiatAmount)) else { return nil } + // Android format: currency code followed by the numeric amount, e.g. "USD 1.00", "UAH 44.54" + return "\(fiatCurrency) \(formatted)" + } + + private func convertUSDToFiat(usdAmount: Double, fiatCurrency: String) -> Double? { + if fiatCurrency == "USD" { return usdAmount } + do { + let result = try CurrencyExchanger.shared.convert( + to: fiatCurrency, + amount: Decimal(usdAmount), + amountCurrency: "USD" + ) + return NSDecimalNumber(decimal: result).doubleValue + } catch { + return nil + } + } +} diff --git a/DashWallet/Sources/UI/Maya/SelectCoinView.swift b/DashWallet/Sources/UI/Maya/SelectCoinView.swift deleted file mode 100644 index de61f3993..000000000 --- a/DashWallet/Sources/UI/Maya/SelectCoinView.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// SelectCoinView.swift -// DashWallet -// -// 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 SelectCoinView: View { - @StateObject private var viewModel = SelectCoinViewModel() - var onCoinSelected: ((MayaCryptoCurrency) -> Void)? - - var body: some View { - ZStack { - Color.primaryBackground.ignoresSafeArea() - - if viewModel.isLoading { - SwiftUI.ProgressView() - } else if viewModel.errorMessage != nil { - errorView(message: viewModel.errorMessage ?? "") - } else { - VStack(spacing: 0) { - searchBar - .padding(.horizontal, 20) - .padding(.top, 16) - .padding(.bottom, 12) - - coinList - } - } - } - .overlay(alignment: .bottom) { - if viewModel.showHaltedToast { - haltedToast - .transition(.move(edge: .bottom).combined(with: .opacity)) - .padding(.horizontal, 20) - .padding(.bottom, 16) - } - } - .animation(.easeInOut(duration: 0.3), value: viewModel.showHaltedToast) - .task { - await viewModel.loadCoins() - } - } - - // MARK: - Search Bar - - private var searchBar: some View { - HStack(spacing: 8) { - Image(systemName: "magnifyingglass") - .foregroundColor(.gray) - .font(.system(size: 14)) - - TextField(NSLocalizedString("Search", comment: "Maya"), text: $viewModel.searchText) - .textFieldStyle(PlainTextFieldStyle()) - .font(.system(size: 14)) - } - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(Color.gray400.opacity(0.1)) - .cornerRadius(16) - } - - // MARK: - Coin List - - private var coinList: some View { - ScrollView { - LazyVStack(spacing: 0) { - ForEach(viewModel.filteredCoins) { item in - Button { - if !item.isHalted { - onCoinSelected?(item.coin) - } - } label: { - CoinRowView(item: item) - } - .buttonStyle(.plain) - .disabled(item.isHalted) - } - } - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: .shadow, radius: 10, x: 0, y: 5) - .padding(.horizontal, 20) - .padding(.bottom, viewModel.showHaltedToast ? 80 : 20) - } - } - - // MARK: - Error View - - private func errorView(message: String) -> some View { - VStack(spacing: 16) { - Spacer() - - Image(systemName: "wifi.exclamationmark") - .font(.system(size: 40)) - .foregroundColor(Color.secondaryText) - - Text(message) - .font(.system(size: 14)) - .foregroundColor(Color.secondaryText) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) - - Button(action: { - Task { - await viewModel.loadCoins() - } - }) { - Text(NSLocalizedString("Retry", comment: "")) - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(.white) - .padding(.horizontal, 24) - .padding(.vertical, 10) - .background(Color.dashBlue) - .cornerRadius(8) - } - - Spacer() - } - } - - // MARK: - Halted Toast - - private var haltedToast: some View { - HStack(spacing: 10) { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(Color.systemYellow) - .font(.system(size: 14)) - - Text(NSLocalizedString("Some coins are not available because of the halted chain", comment: "Maya")) - .font(.system(size: 12, weight: .regular)) - .foregroundColor(.white) - .lineLimit(2) - - Spacer(minLength: 4) - - Button { - viewModel.showHaltedToast = false - } label: { - Image(systemName: "xmark") - .foregroundColor(.white.opacity(0.7)) - .font(.system(size: 12, weight: .semibold)) - } - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(Color(red: 0.15, green: 0.15, blue: 0.18).opacity(0.95)) - .cornerRadius(12) - } -} diff --git a/DashWallet/Sources/UI/Maya/SelectCoinViewModel.swift b/DashWallet/Sources/UI/Maya/SelectCoinViewModel.swift deleted file mode 100644 index 5801fdc4b..000000000 --- a/DashWallet/Sources/UI/Maya/SelectCoinViewModel.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// SelectCoinViewModel.swift -// DashWallet -// -// 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 - -struct CoinDisplayItem: Identifiable { - let id: String - let coin: MayaCryptoCurrency - let fiatPrice: String? - let isHalted: Bool -} - -@MainActor -class SelectCoinViewModel: ObservableObject { - @Published var coins: [CoinDisplayItem] = [] - @Published var searchText: String = "" - @Published var isLoading: Bool = false - @Published var errorMessage: String? - @Published var hasHaltedCoins: Bool = false - @Published var showHaltedToast: Bool = false - - var filteredCoins: [CoinDisplayItem] { - if searchText.isEmpty { - return coins - } - return coins.filter { item in - item.coin.name.localizedCaseInsensitiveContains(searchText) || - item.coin.code.localizedCaseInsensitiveContains(searchText) - } - } - - func loadCoins() async { - isLoading = true - errorMessage = nil - - do { - async let poolsRequest = MayaAPIService.shared.fetchPools() - async let inboundRequest = MayaAPIService.shared.fetchInboundAddresses() - - let (pools, inboundAddresses) = try await (poolsRequest, inboundRequest) - - let poolsByAsset = Dictionary(pools.map { ($0.asset, $0) }, uniquingKeysWith: { first, _ in first }) - let haltedChains = Set(inboundAddresses.filter { $0.halted }.map { $0.chain }) - - let fiatCurrency = App.fiatCurrency - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.currencyCode = fiatCurrency - formatter.maximumFractionDigits = 2 - - var items: [CoinDisplayItem] = [] - - for coin in MayaCryptoCurrency.supportedCoins { - guard let pool = poolsByAsset[coin.mayaAsset], pool.isAvailable else { continue } - - let isHalted = haltedChains.contains(coin.chain) - var priceString: String? - - if let priceUSD = pool.priceUSD, priceUSD > 0 { - let fiatAmount = convertUSDToFiat(usdAmount: priceUSD, fiatCurrency: fiatCurrency) - if let fiatAmount { - priceString = formatter.string(from: NSNumber(value: fiatAmount)) - } - } - - items.append(CoinDisplayItem( - id: coin.id, - coin: coin, - fiatPrice: priceString, - isHalted: isHalted - )) - } - - // Sort: available coins first, then halted; alphabetically within each group - items.sort { a, b in - if a.isHalted != b.isHalted { - return !a.isHalted - } - return a.coin.name < b.coin.name - } - - coins = items - hasHaltedCoins = items.contains { $0.isHalted } - showHaltedToast = hasHaltedCoins - isLoading = false - } catch { - isLoading = false - errorMessage = error.localizedDescription - } - } - - private func convertUSDToFiat(usdAmount: Double, fiatCurrency: String) -> Double? { - if fiatCurrency == "USD" { - return usdAmount - } - - do { - let result = try CurrencyExchanger.shared.convert( - to: fiatCurrency, - amount: Decimal(usdAmount), - amountCurrency: "USD" - ) - return NSDecimalNumber(decimal: result).doubleValue - } catch { - return nil - } - } -} diff --git a/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionFailureView.swift b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionFailureView.swift new file mode 100644 index 000000000..c46ca4bc4 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionFailureView.swift @@ -0,0 +1,130 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct MayaTransactionFailureView: View { + /// Specific failure reason surfaced from the swap or submission error. + /// When nil, the generic fallback message is shown. + var reason: String? = nil + /// True while a fresh quote is being fetched after the user tapped Retry. + var isRetrying: Bool = false + /// Inbound Dash tx hash, when the swap failed AFTER broadcast. Drives the MayaScan deep link. + /// nil when the failure happened before broadcast (quote/build error, cancelled PIN). + var transactionHash: String? = nil + let onRetry: () -> Void + /// Close behavior: dismisses the failure screen (returns to the Maya Portal). + let onCancel: () -> Void + + @Environment(\.openURL) private var openURL + + var body: some View { + VStack(spacing: 0) { + + Spacer() + + ErrorIllustration() + .padding(.bottom, 30) + + VStack(spacing: 6) { + Text(NSLocalizedString("Conversion failed", comment: "Maya")) + .font(.title1) + .foregroundColor(.primaryText) + .multilineTextAlignment(.center) + + Text(reason ?? NSLocalizedString("Your DASH was not converted and no funds were deducted. Try again, or come back later.", comment: "Maya")) + .font(.subhead) + .foregroundColor(.secondaryText) + .multilineTextAlignment(.center) + .lineSpacing(3) + } + .padding(.horizontal, 60) + .padding(.bottom, 16) + + contactSupportButton + + Spacer() + + VStack(spacing: 16) { + DashButton( + text: NSLocalizedString("Retry", comment: "Maya"), + isEnabled: !isRetrying, + isLoading: isRetrying + ) { + onRetry() + } + + DashButton(text: NSLocalizedString("Close", comment: "Maya")) { + onCancel() + } + .overrideBackgroundColor(Color.gray300Alpha10) + .overrideForegroundColor(Color.black) + } + .padding(.vertical, 20) + .padding(.horizontal, 60) + } + } + + private var contactSupportButton: some View { + // With a broadcast tx, link the user to THEIR swap on MayaScan; otherwise fall back to + // the generic Maya support docs. + let (url, label): (URL, String) = { + if let transactionHash, !transactionHash.isEmpty { + return ( + MayaConstants.mayaScanTransactionURL(txHash: transactionHash), + NSLocalizedString("View transaction on MayaScan", comment: "Maya") + ) + } + return ( + MayaConstants.supportURL, + NSLocalizedString("Contact Maya Support", comment: "Maya") + ) + }() + + return Button { + openURL(url) + } label: { + Text(label) + .font(.subheadMedium) + .foregroundColor(.dashBlue) + } + .buttonStyle(.plain) + } +} + +#if DEBUG +#Preview("Before broadcast — support fallback") { + MayaTransactionFailureView( + reason: "Input 0 is already spent by a pending transaction. Wait for the previous swap to confirm before initiating a new one.", + onRetry: {}, + onCancel: {} + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.primaryBackground) +} + +#Preview("After broadcast — MayaScan link") { + MayaTransactionFailureView( + reason: "Your DASH was refunded by Maya Protocol.", + transactionHash: "d891ed43f1f3eedfb7078e02d6be0423b741533b4b0407a822307b2637703649", + onRetry: {}, + onCancel: {} + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.primaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionPendingView.swift b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionPendingView.swift new file mode 100644 index 000000000..53ee0fe6a --- /dev/null +++ b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionPendingView.swift @@ -0,0 +1,70 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct MayaTransactionPendingView: View { + let message: String + var onGoHome: () -> Void = {} + + var body: some View { + VStack(spacing: 0) { + + Spacer() + + LoadingIllustration() + .padding(.bottom, 30) + + + VStack(spacing: 6) { + Text(NSLocalizedString("Conversion in progress", comment: "Maya")) + .font(.title1) + .foregroundColor(.primaryText) + .multilineTextAlignment(.center) + + Text(message) + .font(.subhead) + .foregroundColor(.secondaryText) + .multilineTextAlignment(.center) + .lineSpacing(3) + } + .padding(.horizontal, 60) + + Spacer() + + DashButton(text: NSLocalizedString("Go to Home", comment: "Maya")) { + onGoHome() + } + .padding(.vertical, 20) + .padding(.horizontal, 60) + } + } +} + +#if DEBUG +#Preview { + MayaTransactionPendingView( + message: NSLocalizedString( + "Waiting for block confirmation. Maya swaps require one Dash block (~2–5 min) before the swap begins.", + comment: "Maya" + ), + onGoHome: {} + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.primaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionStatusHostingController.swift b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionStatusHostingController.swift new file mode 100644 index 000000000..5c49c22a6 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionStatusHostingController.swift @@ -0,0 +1,208 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import UIKit + +// MARK: - MayaTransactionStatusHostingController + +/// Full-screen owner of the Maya transaction status flow (pending → processing → +/// success / failure). Pushed onto the Maya navigation stack once a swap is submitted. +/// +/// Back navigation is intentionally blocked while this screen is on top: the UIKit nav bar is +/// hidden, `shouldPopViewController()` returns false, and the interactive swipe-back gesture is +/// disabled for as long as the screen is visible. +final class MayaTransactionStatusHostingController: UIViewController, NavigationBarDisplayable, NavigationStackControllable { + var isNavigationBarHidden: Bool { true } + + /// Defense in depth: block the UIKit back-button path even though the nav bar is hidden. + func shouldPopViewController() -> Bool { false } + + private let viewModel: OrderPreviewViewModel + + init(viewModel: OrderPreviewViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.dw_secondaryBackground() + + let statusView = MayaTransactionStatusView( + viewModel: viewModel, + onDone: { [weak self] in + guard let self else { return } + // Return to Home — handle BOTH entry paths: + // • Shortcuts: the Buy&Sell/Maya flow is a modal presented over the tab bar → dismiss. + // • More menu: the flow is PUSHED inside a tab's nav → pop it away and switch to the + // Home tab (index 0 = MainTabbarTabs.home). dismiss would be a no-op there, so branch. + if self.navigationController?.presentingViewController != nil { + self.navigationController?.dismiss(animated: true) + } else { + // `self.tabBarController` can be nil here (hidesBottomBarWhenPushed), so find the + // app's tab bar controller via the window hierarchy and select the Home tab. + let tab = self.tabBarController + ?? self.view.window?.rootViewController?.dw_firstTabBarController() + self.navigationController?.popToRootViewController(animated: false) + tab?.selectedIndex = 0 + } + }, + onClose: { [weak self] in + // Failure dismissed → return to the Maya Portal, never to Order Preview / Convert. + guard let nav = self?.navigationController else { return } + nav.popToViewController(ofType: MayaPortalViewController.self, animated: true) + }, + onRetry: { [weak self] in + self?.handleRetry() + } + ) + + let hostingController = UIHostingController(rootView: statusView) + hostingController.view.backgroundColor = .clear + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // No swipe-back while the status screen is active. + navigationController?.interactivePopGestureRecognizer?.isEnabled = false + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // Restore normal swipe-back for whatever screen comes next (Home / Portal / Order Preview). + navigationController?.interactivePopGestureRecognizer?.isEnabled = true + } + + // MARK: - Retry + + private func handleRetry() { + Task { [weak self] in + guard let self else { return } + + guard let freshViewModel = await self.viewModel.retryQuote() else { + // Quote refresh failed — the view model updated `swapStatus` with the new + // reason, so we stay on the failed screen (it re-renders automatically). + return + } + + guard let nav = self.navigationController else { return } + + // Replace BOTH the failed status screen and the stale Order Preview beneath it with + // a fresh Order Preview built from the new quote. Back from the new Order Preview + // therefore returns to Convert, not to a stale preview. + var controllers = nav.viewControllers + if controllers.last === self { + controllers.removeLast() + } + if controllers.last is OrderPreviewHostingController { + controllers.removeLast() + } + controllers.append(OrderPreviewHostingController(viewModel: freshViewModel)) + nav.setViewControllers(controllers, animated: true) + } + } +} + +// MARK: - MayaTransactionStatusView + +/// Full-screen status content. Renders one of the Maya status views based on `swapStatus`, +/// switching content in place (no sheet) so success / failure cannot flicker or be dismissed +/// by SwiftUI sheet-state changes. +private struct MayaTransactionStatusView: View { + @ObservedObject var viewModel: OrderPreviewViewModel + let onDone: () -> Void + let onClose: () -> Void + let onRetry: () -> Void + + var body: some View { + ZStack { + Color.primaryBackground.ignoresSafeArea() + content + } + } + + @ViewBuilder + private var content: some View { + switch viewModel.swapStatus { + case .idle, .pendingConfirmation: + pending(message: NSLocalizedString( + "Your Dash transaction has been sent. Waiting for block confirmation — this takes 2–5 minutes because Maya swaps don't use InstantSend.", + comment: "Maya" + )) + case .processingSwap: + pending(message: NSLocalizedString( + "Maya Protocol has received your transaction and is processing the swap.", + comment: "Maya" + )) + case .completed: + MayaTransactionSuccessView( + coinCode: viewModel.coin.code, + coinName: viewModel.coin.name, + onDone: onDone + ) + case .failed(let reason): + MayaTransactionFailureView( + reason: reason, + isRetrying: viewModel.isRetrying, + transactionHash: viewModel.submittedTxId, + onRetry: onRetry, + onCancel: onClose + ) + } + } + + private func pending(message: String) -> some View { + MayaTransactionPendingView( + message: message, + onGoHome: onDone + ) + .padding(.horizontal, 40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +private extension UIViewController { + /// Finds the first UITabBarController in the view-controller hierarchy reachable from `self` + /// (children + presented), used to switch back to the Home tab when the flow was pushed. + func dw_firstTabBarController() -> UITabBarController? { + if let tab = self as? UITabBarController { return tab } + for child in children { + if let tab = child.dw_firstTabBarController() { return tab } + } + if let presented = presentedViewController { + return presented.dw_firstTabBarController() + } + return nil + } +} diff --git a/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionSuccessView.swift b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionSuccessView.swift new file mode 100644 index 000000000..fe88c3b84 --- /dev/null +++ b/DashWallet/Sources/UI/Maya/TransactionStatus/MayaTransactionSuccessView.swift @@ -0,0 +1,67 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct MayaTransactionSuccessView: View { + let coinCode: String + let coinName: String + var onDone: () -> Void = {} + + var body: some View { + VStack(spacing: 0) { + + Spacer() + + SuccessIllustration() + .padding(.bottom, 30) + + VStack(spacing: 6) { + Text(NSLocalizedString("You successfully converted DASH to \(coinCode)", comment: "Maya")) + .font(.title1) + .foregroundColor(.primaryText) + .multilineTextAlignment(.center) + + Text(NSLocalizedString("It can take up to a few minutes for your \(coinName) to arrive at the destination address", comment: "Maya")) + .font(.subhead) + .foregroundColor(.secondaryText) + .multilineTextAlignment(.center) + .lineSpacing(3) + } + .padding(.horizontal, 60) + + Spacer() + + DashButton(text: NSLocalizedString("Done", comment: "")) { + onDone() + } + .padding(.vertical, 20) + .padding(.horizontal, 60) + } + } +} + +#if DEBUG +#Preview { + MayaTransactionSuccessView( + coinCode: "BTC", + coinName: "Bitcoin" + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.primaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift b/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift index db99b177c..ee4aa1f80 100644 --- a/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift +++ b/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift @@ -19,6 +19,7 @@ import UIKit import SwiftUI import MessageUI + class MainMenuViewController: UIViewController { // MARK: - Properties @@ -136,9 +137,6 @@ struct MainMenuScreen: View { private let onContactSupport: () -> () @ObservedObject private var viewModel: MainMenuViewModel - @State private var openSettings: Bool = false - @State private var showTools: Bool = false - @State private var showSecurity: Bool = false @State private var showMixDialog: Bool = false @State private var showDashPayInfo: Bool = false @State private var showCreditsPurchasedToast: Bool = false @@ -196,18 +194,13 @@ struct MainMenuScreen: View { var body: some View { ScrollView { - LazyVStack(alignment: .leading, spacing: 20) { - // Header - HStack { - Text(NSLocalizedString("More", comment: "")) - .font(.title) - .fontWeight(.bold) - .foregroundColor(.primaryText) - Spacer() - } - .padding(.horizontal, 20) - .padding(.top, 5) - + LazyVStack(alignment: .leading, spacing: 0) { + TopIntro(title: NSLocalizedString("More", comment: "")) + .padding(.leading, 20) + .padding(.trailing, 60) + .padding(.top, 10) + .padding(.bottom, 20) + #if DASHPAY if viewModel.userProfileModel?.showJoinDashpay == true { JoinDashPayView( @@ -227,10 +220,10 @@ struct MainMenuScreen: View { #endif // Menu items grouped in sections - VStack(spacing: 16) { - // First group - main services (first 2 items) + VStack(spacing: 20) { + // Menu list - first group (first 2 items) if viewModel.items.count >= 2 { - VStack(spacing: 0) { + VStack(spacing: 2) { ForEach(viewModel.items.prefix(2)) { item in MenuItem( title: item.title, @@ -239,18 +232,18 @@ struct MainMenuScreen: View { showChevron: false, action: item.action ) - .frame(minHeight: 60) + .frame(minHeight: 56) } } - .padding(.vertical, 5) + .padding(6) .background(Color.secondaryBackground) - .cornerRadius(12) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) } - - // Second group - settings items (remaining items) + + // Menu list - second group (remaining items) if viewModel.items.count > 2 { - VStack(spacing: 0) { + VStack(spacing: 2) { ForEach(viewModel.items.dropFirst(2)) { item in MenuItem( title: item.title, @@ -259,12 +252,12 @@ struct MainMenuScreen: View { showChevron: false, action: item.action ) - .frame(minHeight: 60) + .frame(minHeight: 56) } } - .padding(.vertical, 5) + .padding(6) .background(Color.secondaryBackground) - .cornerRadius(12) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) } } @@ -308,32 +301,6 @@ struct MainMenuScreen: View { } #endif - NavigationLink( - destination: SettingsScreen(vc: vc, onDidRescan: { - self.vc.popToRootViewController(animated: false) - self.delegateInternal.mainMenuViewControllerOpenHomeScreen() - }), - isActive: $openSettings - ) { - EmptyView() - } - - NavigationLink( - destination: ToolsMenuScreen(vc: vc, onImportPrivateKey: { - self.vc.popToRootViewController(animated: false) - self.delegateInternal.mainMenuViewControllerImportPrivateKey() - }), - isActive: $showTools - ) { - EmptyView() - } - - NavigationLink( - destination: SecurityMenuScreen(vc: vc), - isActive: $showSecurity - ) { - EmptyView() - } } .background(Color.primaryBackground) .onAppear { @@ -425,11 +392,11 @@ struct MainMenuScreen: View { case .explore: showExplore() case .security: - showSecurity = true + showSecurity() case .settings: - openSettings = true + showSettings() case .tools: - showTools = true + showTools() case .support: onContactSupport() #if DASHPAY @@ -455,14 +422,46 @@ struct MainMenuScreen: View { controller.hidesBottomBarWhenPushed = true vc.pushViewController(controller, animated: true) } - + private func showExplore() { - let controller = ExploreViewController() - controller.delegate = delegateInternal - let navigationController = BaseNavigationController(rootViewController: controller) - vc.present(navigationController, animated: true) + let screen = ExploreMenuScreen( + vc: vc, + onShowSendPayment: { delegateInternal.showPaymentsController(withActivePage: PaymentsViewControllerState.pay.rawValue) }, + onShowReceivePayment: { delegateInternal.showPaymentsController(withActivePage: PaymentsViewControllerState.receive.rawValue) }, + onShowGiftCard: { txId in delegateInternal.showGiftCard(txId) } + ) + let controller = UIHostingController(rootView: screen) + controller.hidesBottomBarWhenPushed = true + vc.pushViewController(controller, animated: true) } - + + private func showSettings() { + let screen = SettingsScreen(vc: vc, onDidRescan: { + self.vc.popToRootViewController(animated: false) + self.delegateInternal.mainMenuViewControllerOpenHomeScreen() + }) + let controller = UIHostingController(rootView: screen) + controller.hidesBottomBarWhenPushed = true + vc.pushViewController(controller, animated: true) + } + + private func showTools() { + let screen = ToolsMenuScreen(vc: vc, onImportPrivateKey: { + self.vc.popToRootViewController(animated: false) + self.delegateInternal.mainMenuViewControllerImportPrivateKey() + }) + let controller = UIHostingController(rootView: screen) + controller.hidesBottomBarWhenPushed = true + vc.pushViewController(controller, animated: true) + } + + private func showSecurity() { + let screen = SecurityMenuScreen(vc: vc) + let controller = UIHostingController(rootView: screen) + controller.hidesBottomBarWhenPushed = true + vc.pushViewController(controller, animated: true) + } + #if DASHPAY private func showInvite() { let controller = DWInvitationHistoryViewController() @@ -516,7 +515,7 @@ struct MainMenuScreen: View { extension MainMenuScreen { - class DelegateInternal: NSObject, RootEditProfileViewControllerDelegate, ExploreViewControllerDelegate { + class DelegateInternal: NSObject, RootEditProfileViewControllerDelegate { private weak var delegate: MainMenuViewControllerDelegate? private weak var wipeDelegate: DWWipeDelegate? private let viewModel: MainMenuViewModel @@ -549,18 +548,6 @@ extension MainMenuScreen { delegate?.showGiftCard(txId) } - func exploreViewControllerShowSendPayment(_ controller: ExploreViewController) { - showPaymentsController(withActivePage: PaymentsViewControllerState.pay.rawValue) - } - - func exploreViewControllerShowReceivePayment(_ controller: ExploreViewController) { - showPaymentsController(withActivePage: PaymentsViewControllerState.receive.rawValue) - } - - func exploreViewControllerShowGiftCard(_ controller: ExploreViewController, txId: Data) { - showGiftCard(txId) - } - #if DASHPAY func editProfileViewController(_ controller: RootEditProfileViewController, updateDisplayName rawDisplayName: String, diff --git a/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift index dccbe33a4..9c0472880 100644 --- a/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift @@ -78,7 +78,7 @@ class MainMenuViewModel: ObservableObject { // Buy & Sell Dash allItems.append(MenuItemModel( title: NSLocalizedString("Buy & Sell Dash", comment: ""), - icon: .custom("image.buy.and.sell", maxHeight: 22), + icon: .custom("image.buy.and.sell", maxHeight: 30), action: { [weak self] in self?.handleBuySellDash() } @@ -87,7 +87,7 @@ class MainMenuViewModel: ObservableObject { // Explore allItems.append(MenuItemModel( title: NSLocalizedString("Explore", comment: ""), - icon: .custom("image.explore", maxHeight: 22), + icon: .custom("image.explore", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .explore } @@ -96,7 +96,7 @@ class MainMenuViewModel: ObservableObject { // Security allItems.append(MenuItemModel( title: NSLocalizedString("Security", comment: ""), - icon: .custom("image.security", maxHeight: 22), + icon: .custom("image.security", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .security } @@ -105,7 +105,7 @@ class MainMenuViewModel: ObservableObject { // Settings allItems.append(MenuItemModel( title: NSLocalizedString("Settings", comment: ""), - icon: .custom("image.settings", maxHeight: 22), + icon: .custom("image.settings", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .settings } @@ -114,7 +114,7 @@ class MainMenuViewModel: ObservableObject { // Tools allItems.append(MenuItemModel( title: NSLocalizedString("Tools", comment: ""), - icon: .custom("image.tools", maxHeight: 22), + icon: .custom("image.tools", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .tools } @@ -123,7 +123,7 @@ class MainMenuViewModel: ObservableObject { // Support allItems.append(MenuItemModel( title: NSLocalizedString("Support", comment: ""), - icon: .custom("image.support", maxHeight: 22), + icon: .custom("image.support", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .support } @@ -134,7 +134,7 @@ class MainMenuViewModel: ObservableObject { if VotingPrefs.shared.votingEnabled { allItems.append(MenuItemModel( title: NSLocalizedString("Voting", comment: ""), - icon: .custom("menu_voting", maxHeight: 22), + icon: .custom("menu_voting", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .voting } diff --git a/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m b/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m index 8048e73cd..d400421ef 100644 --- a/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m +++ b/DashWallet/Sources/UI/Menu/Security/Advanced Security/Model/DWBaseAdvancedSecurityModel.m @@ -158,7 +158,9 @@ - (NSAttributedString *)currentSpendingConfirmationDescriptionWithFont:(UIFont * else if (self.hasFaceID) { string = NSLocalizedString(@"You can authenticate with Face ID for payments below", nil); } - NSParameterAssert(string); + else { + string = NSLocalizedString(@"You can authenticate with biometrics for payments below", nil); + } string = [string stringByAppendingString:@" "]; diff --git a/DashWallet/Sources/UI/Menu/Security/SecurityMenuScreen.swift b/DashWallet/Sources/UI/Menu/Security/SecurityMenuScreen.swift index baed871fe..0cc2345c5 100644 --- a/DashWallet/Sources/UI/Menu/Security/SecurityMenuScreen.swift +++ b/DashWallet/Sources/UI/Menu/Security/SecurityMenuScreen.swift @@ -34,36 +34,18 @@ struct SecurityMenuScreen: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - // Back button - HStack { - Button(action: { - vc.popViewController(animated: true) - }) { - Image(systemName: "chevron.left") - .font(.system(size: 18, weight: .medium)) - .foregroundColor(.primary) - .frame(width: 36, height: 36) - .overlay( - Circle().stroke(Color.gray300.opacity(0.3), lineWidth: 1) - ) - } - Spacer() - } - .padding(.horizontal, 5) - .padding(.top, 10) - - // Header - HStack { - Text(NSLocalizedString("Security", comment: "")) - .font(.title) - .fontWeight(.bold) - .foregroundColor(.primaryText) - Spacer() + NavBarBack { + vc.popViewController(animated: true) } - .padding(.top, 30) - .padding(.bottom, 20) - - VStack(spacing: 0) { + + TopIntro(title: NSLocalizedString("Security", comment: "")) + .padding(.leading, 20) + .padding(.trailing, 60) + .padding(.top, 10) + .padding(.bottom, 20) + + // Menu list + VStack(spacing: 2) { ForEach(viewModel.items) { item in MenuItem( title: item.title, @@ -76,17 +58,17 @@ struct SecurityMenuScreen: View { isToggled: item.isToggled, action: item.action ) - .frame(minHeight: 60) + .frame(minHeight: 56) } } - .padding(.vertical, 5) + .padding(6) .background(Color.secondaryBackground) - .cornerRadius(12) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) - + .padding(.horizontal, 20) + Spacer() } - .padding(.horizontal, 20) .background(Color.primaryBackground) .navigationBarHidden(true) .onReceive(viewModel.$navigationDestination) { destination in diff --git a/DashWallet/Sources/UI/Menu/Security/SecurityMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Security/SecurityMenuViewModel.swift index f56109c84..a15d9c667 100644 --- a/DashWallet/Sources/UI/Menu/Security/SecurityMenuViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Security/SecurityMenuViewModel.swift @@ -55,7 +55,7 @@ class SecurityMenuViewModel: ObservableObject { menuItems.append(MenuItemModel( title: NSLocalizedString("View Recovery Phrase", comment: ""), - icon: .custom("image.recovery.phrase", maxHeight: 22), + icon: .custom("image-menu-recovery_phrase", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .viewRecoveryPhrase } @@ -63,18 +63,30 @@ class SecurityMenuViewModel: ObservableObject { menuItems.append(MenuItemModel( title: NSLocalizedString("Change PIN", comment: ""), - icon: .custom("image.change.pin", maxHeight: 22), + icon: .custom("image-menu-change_pin", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .changePin } )) - if hasTouchID || hasFaceID { - let title = hasTouchID ? NSLocalizedString("Enable Touch ID", comment: "") : NSLocalizedString("Enable Face ID", comment: "") - let iconName = hasTouchID ? "image.touch.id" : "Image.face.id" + if hasTouchID { menuItems.append(MenuItemModel( - title: title, - icon: .custom(iconName, maxHeight: 22), + title: NSLocalizedString("Enable Touch ID", comment: ""), + icon: .custom("image-menu-enable_touch_id", maxHeight: 30), + showToggle: true, + isToggled: self.biometricsEnabled, + action: { [weak self] in + guard let self = self else { return } + let newValue = !self.biometricsEnabled + self.toggleBiometrics(newValue) + } + )) + } + + if hasFaceID { + menuItems.append(MenuItemModel( + title: NSLocalizedString("Enable Face ID", comment: ""), + icon: .custom("image-menu-face_id", maxHeight: 30), showToggle: true, isToggled: self.biometricsEnabled, action: { [weak self] in @@ -87,7 +99,7 @@ class SecurityMenuViewModel: ObservableObject { menuItems.append(MenuItemModel( title: NSLocalizedString("Autohide Balance", comment: ""), - icon: .custom("image.autohide.balance", maxHeight: 22), + icon: .custom("image-menu-autohide_balance", maxHeight: 30), showToggle: true, isToggled: self.balanceHidden, action: { [weak self] in @@ -100,7 +112,7 @@ class SecurityMenuViewModel: ObservableObject { menuItems.append(MenuItemModel( title: NSLocalizedString("Advanced Security", comment: ""), - icon: .custom("image.advanced.security", maxHeight: 22), + icon: .custom("image-menu-advanced_security", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .advancedSecurity } @@ -108,7 +120,7 @@ class SecurityMenuViewModel: ObservableObject { menuItems.append(MenuItemModel( title: NSLocalizedString("Reset Wallet", comment: ""), - icon: .custom("image.reset.wallet", maxHeight: 22), + icon: .custom("image-menu-reset_wallet", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .resetWallet } diff --git a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/Cells/LocalCurrencyCellView.swift b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/Cells/LocalCurrencyCellView.swift new file mode 100644 index 000000000..2aad3c678 --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/Cells/LocalCurrencyCellView.swift @@ -0,0 +1,156 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct LocalCurrencyCellView: View { + + private enum Layout { + static let spacing: CGFloat = 16 + static let horizontalPadding: CGFloat = 10 + static let flagSize: CGFloat = 30 + static let checkboxSize: CGFloat = 22 + static let checkboxSelectedBorderWidth: CGFloat = 5 + static let checkboxDefaultBorderWidth: CGFloat = 1.5 + static let trailingSpacing: CGFloat = 10 + static let infoVerticalPadding: CGFloat = 10 + static let infoSpacing: CGFloat = 1 + } + + let item: CurrencyItem + let isSelected: Bool + let searchQuery: String + + var body: some View { + HStack(spacing: Layout.spacing) { + flagSection + infoSection + trailingSection + } + .padding(.horizontal, Layout.horizontalPadding) + .clipShape(.rect) + .contentShape(.rect) + } + + private var flagSection: some View { + Group { + if let flagName = item.flagName, UIImage(named: flagName) != nil { + Image(flagName) + .resizable() + .scaledToFill() + } else { + Color.gray200 + } + } + .frame(width: Layout.flagSize, height: Layout.flagSize) + .clipShape(.circle) + } + + private var infoSection: some View { + VStack(alignment: .leading, spacing: Layout.infoSpacing) { + highlightedText(item.name, query: searchQuery) + .font(.subhead.weight(.medium)) + .foregroundColor(.primaryText) + .lineLimit(1) + + if let priceString = item.priceString { + Text(priceString) + .font(.footnote) + .foregroundColor(.tertiaryText) + .lineLimit(1) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, Layout.infoVerticalPadding) + } + + private var trailingSection: some View { + HStack(spacing: Layout.trailingSpacing) { + currencyCodeView + checkboxView + } + } + + private var currencyCodeView: some View { + highlightedText(item.code, query: searchQuery) + .font(.caption1) + .foregroundColor(.tertiaryText) + .lineLimit(1) + } + + private var checkboxView: some View { + Circle() + .strokeBorder( + isSelected ? Color.dashBlue : Color.gray300, + lineWidth: isSelected ? Layout.checkboxSelectedBorderWidth : Layout.checkboxDefaultBorderWidth + ) + .frame(width: Layout.checkboxSize, height: Layout.checkboxSize) + } + + /// Builds a SwiftUI Text with the first occurrence of `query` highlighted in dashBlue. + private func highlightedText(_ text: String, query: String) -> Text { + let trimmed = query.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty, + let range = text.range(of: trimmed, options: .caseInsensitive) else { + return Text(text) + } + let before = String(text[.. Void + var onBack: (() -> Void)? + + init( + currencyCode: String? = nil, + onSelect: @escaping (String) -> Void, + onBack: (() -> Void)? = nil + ) { + self._viewModel = StateObject(wrappedValue: LocalCurrencyViewModel(currencyCode: currencyCode)) + self.onSelect = onSelect + self.onBack = onBack + } + + var body: some View { + ZStack(alignment: .top) { + background + + LocalCurrencyScrollContentView( + headerHeight: fullHeaderSize.height, + onScrollChanged: { offset in + scrollViewOffcet = min(offset.y, 0) + }, + filteredItems: viewModel.filteredItems, + selectedCurrencyCode: viewModel.selectedCurrencyCode, + searchQuery: viewModel.searchQuery, + select: viewModel.select(currencyCode:), + onSelect: onSelect + ) + .padding(.horizontal, 20) + + LocalCurrencyTopOverlayView( + scrollOffset: scrollViewOffcet, + onBack: onBack, + searchQuery: $viewModel.searchQuery + ) + .readingFrame { frame in + if fullHeaderSize == .zero { + fullHeaderSize = frame.size + } + } + } + } + + private var background: some View { + Color.primaryBackground + } +} + +private struct LocalCurrencyScrollContentView: View { + private enum Layout { + static let containerCornerRadius: CGFloat = 20 + static let noResultsMinHeight: CGFloat = 150 + } + + let headerHeight: CGFloat + let onScrollChanged: (CGPoint) -> Void + var filteredItems: [CurrencyItem] + var selectedCurrencyCode: String + var searchQuery: String + var select: (String) -> Void + var onSelect: (String) -> Void + + private var trimmedSearchQuery: String { + searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private var shouldShowNoResults: Bool { + filteredItems.isEmpty && !trimmedSearchQuery.isEmpty + } + + private var noResultsText: String { + String.localizedStringWithFormat( + NSLocalizedString("No matches for \"%@\"", comment: "Local Currency: no search matches"), + trimmedSearchQuery + ) + } + + @ViewBuilder private var noResultsView: some View { + VStack { + Text(noResultsText) + .font(.footnote) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, minHeight: Layout.noResultsMinHeight, alignment: .center) + .background( + RoundedRectangle(cornerRadius: Layout.containerCornerRadius) + .fill(Color.secondaryBackground) + ) + } + + @ViewBuilder private var currencyContent: some View { + if shouldShowNoResults { + noResultsView + } else { + LazyVStack(spacing: 2) { + ForEach(filteredItems) { item in + LocalCurrencyCellView( + item: item, + isSelected: item.code == selectedCurrencyCode, + searchQuery: searchQuery + ) + .onTapGesture { + select(item.code) + onSelect(item.code) + } + } + } + .padding(filteredItems.isEmpty ? 0 : 6) + .background( + RoundedRectangle(cornerRadius: Layout.containerCornerRadius) + .fill(Color.secondaryBackground) + ) + } + } + + var body: some View { + ScrollViewWithOnScrollChanged(.vertical, showsIndicators: false) { + VStack { + Rectangle() + .opacity(0) + .frame(height: headerHeight) + + // Currency list + currencyContent + } + } onScrollChanged: { offset in + onScrollChanged(offset) + } + } +} + +private struct LocalCurrencyTopOverlayView: View { + let scrollOffset: CGFloat + var onBack: (() -> Void)? + @Binding var searchQuery: String + + var body: some View { + VStack { + header + + if scrollOffset > -20 { + SearchBar(text: $searchQuery) + .transition(.move(edge: .top).combined(with: .opacity)) + .padding(.horizontal, 20) + } + } + .padding(.bottom, 6) + .background(toolbarBackground) + .animation(.easeInOut(duration: 0.2), value: scrollOffset) + } + + private var header: some View { + HStack(alignment: .center, spacing: 0) { + ZStack { + if let onBack { + NavBarBack(onBack: onBack) + } + + // Title + Text(NSLocalizedString("Local Currency", comment: "Settings")) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.primaryText) + } + } + } + + @ViewBuilder private var toolbarBackground: some View { + ZStack(alignment: .bottom) { + Rectangle() + .fill(.clear) + .background(Color.primaryBackground) + .ignoresSafeArea() + + Divider() + .background(Color(red: 176 / 255, green: 182 / 255, blue: 188 / 255, opacity: 0.15)) + .opacity(scrollOffset < -20 ? 1 : 0) + } + } +} + +// MARK: - Preview + +#if DEBUG +fileprivate extension LocalCurrencyView { + init( + viewModel: LocalCurrencyViewModel, + onSelect: @escaping (String) -> Void, + onBack: (() -> Void)? = nil + ) { + self._viewModel = StateObject(wrappedValue: viewModel) + self.onSelect = onSelect + self.onBack = onBack + } +} + +fileprivate extension LocalCurrencyViewModel { + convenience init(items: [CurrencyItem], selectedCode: String) { + self.init(allItems: items, selectedCurrencyCode: selectedCode) + } +} + +#Preview { + LocalCurrencyView( + viewModel: LocalCurrencyViewModel( + items: [ + CurrencyItem( + code: "USD", + name: "US Dollar", + flagName: "united states", + priceString: "42.50" + ), + CurrencyItem( + code: "EUR", + name: "Euro", + flagName: "european union", + priceString: "39.20" + ), + CurrencyItem( + code: "GBP", + name: "British Pound", + flagName: "united kingdom", + priceString: "33.80" + ), + CurrencyItem( + code: "JPY", + name: "Japanese Yen", + flagName: "japan", + priceString: "6380.00" + ), + CurrencyItem( + code: "UAH", + name: "Ukrainian Hryvnia", + flagName: "ukraine", + priceString: "1750.00" + ), + CurrencyItem( + code: "PLN", + name: "Polish Zloty", + flagName: "poland", + priceString: "168.00" + ), + CurrencyItem( + code: "CHF", + name: "Swiss Franc", + flagName: "switzerland", + priceString: "37.10" + ) + ], + selectedCode: "USD" + ), + onSelect: { _ in }, + onBack: {} + ) +} +#endif diff --git a/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift new file mode 100644 index 000000000..f1fdae22c --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Settings/LocalCurrency/LocalCurrencyViewModel.swift @@ -0,0 +1,243 @@ +// +// Created by Roman Chornyi +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@MainActor +class LocalCurrencyViewModel: ObservableObject { + private let allItems: [CurrencyItem] + private var cancellables = Set() + + @Published var searchQuery: String = "" + @Published private(set) var filteredItems: [CurrencyItem] = [] + @Published var selectedCurrencyCode: String + + init(allItems: [CurrencyItem], selectedCurrencyCode: String) { + self.allItems = allItems + self.selectedCurrencyCode = selectedCurrencyCode + self.filteredItems = allItems + setupSearch() + } + + convenience init(currencyCode: String? = nil) { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 2 + + let items: [CurrencyItem] = CurrencyExchangerObjcWrapper.prices.map { price in + CurrencyItem( + code: price.code, + name: price.name, + flagName: LocalCurrencyViewModel.flagByCode[price.code], + priceString: formatter.string(from: price.price) + ) + } + self.init(allItems: items, selectedCurrencyCode: currencyCode ?? App.fiatCurrency) + } + + func select(currencyCode: String) { + selectedCurrencyCode = currencyCode + App.shared.fiatCurrency = currencyCode + } + + private func setupSearch() { + $searchQuery + .debounce(for: .milliseconds(150), scheduler: RunLoop.main) + .sink { [weak self] query in + self?.applyFilter(query) + } + .store(in: &cancellables) + } + + private func applyFilter(_ query: String) { + let trimmed = query.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { + filteredItems = allItems + return + } + let lowercased = trimmed.lowercased() + filteredItems = allItems.filter { + $0.code.lowercased().contains(lowercased) || $0.name.lowercased().contains(lowercased) + } + } +} + + + +// MARK: - Flag mapping + +extension LocalCurrencyViewModel { + static let flagByCode: [String: String] = [ + "AED": "united arab emirates", + "AFN": "afghanistan", + "ALL": "albania", + "AMD": "armenia", + "ANG": "sint maarten", + "AOA": "angola", + "ARS": "argentina", + "AUD": "australia", + "AWG": "aruba", + "AZN": "azerbaijan", + "BAM": "bosnia and herzegovina", + "BBD": "barbados", + "BDT": "bangladesh", + "BGN": "bulgaria", + "BHD": "bahrain", + "BIF": "burundi", + "BMD": "bermuda", + "BND": "brunei", + "BOB": "bolivia", + "BRL": "brazil", + "BSD": "bahamas", + "BTN": "bhutan", + "BWP": "botswana", + "BYN": "belarus", + "BZD": "belize", + "CAD": "canada", + "CDF": "democratic republic of congo", + "CHF": "switzerland", + "CLF": "chile", + "CLP": "chile", + "CNY": "china", + "COP": "colombia", + "CRC": "costa rica", + "CUP": "cuba", + "CVE": "cape verde", + "CZK": "czech republic", + "DJF": "djibouti", + "DKK": "denmark", + "DOP": "dominican republic", + "DZD": "Algeria", + "EGP": "egypt", + "ETB": "ethiopia", + "EUR": "european union", + "FJD": "fiji", + "FKP": "falkland islands", + "GBP": "united kingdom", + "GEL": "georgia", + "GHS": "ghana", + "GIP": "gibraltar", + "GMD": "gambia", + "GNF": "guinea", + "GTQ": "guatemala", + "GYD": "guyana", + "HKD": "hong kong", + "HNL": "honduras", + "HRK": "croatia", + "HTG": "haiti", + "HUF": "hungary", + "IDR": "indonesia", + "ILS": "israel", + "INR": "india", + "IQD": "iraq", + "IRR": "iran", + "ISK": "iceland", + "JEP": "jersey", + "JMD": "jamaica", + "JOD": "jordan", + "JPY": "japan", + "KES": "kenya", + "KGS": "kyrgyzstan", + "KHR": "cambodia", + "KMF": "comoros", + "KPW": "north korea", + "KRW": "south korea", + "KWD": "kuwait", + "KYD": "cayman islands", + "KZT": "kazakhstan", + "LAK": "laos", + "LBP": "lebanon", + "LKR": "sri lanka", + "LRD": "liberia", + "LSL": "lesotho", + "LYD": "libya", + "MAD": "morocco", + "MDL": "moldova", + "MGA": "madagascar", + "MKD": "republic of macedonia", + "MMK": "myanmar", + "MNT": "mongolia", + "MOP": "macao", + "MRU": "mauritania", + "MUR": "mauritius", + "MVR": "maldives", + "MWK": "malawi", + "MXN": "mexico", + "MYR": "malaysia", + "MZN": "mozambique", + "NAD": "namibia", + "NGN": "nigeria", + "NIO": "nicaragua", + "NOK": "norway", + "NPR": "nepal", + "NZD": "new zealand", + "OMR": "oman", + "PAB": "panama", + "PEN": "peru", + "PGK": "papua new guinea", + "PHP": "philippines", + "PKR": "pakistan", + "PLN": "poland", + "PYG": "paraguay", + "QAR": "qatar", + "RON": "romania", + "RSD": "serbia", + "RUB": "russia", + "RWF": "rwanda", + "SAR": "saudi arabia", + "SBD": "solomon islands", + "SCR": "seychelles", + "SDG": "sudan", + "SEK": "sweden", + "SGD": "singapore", + "SHP": "united kingdom", + "SLL": "sierra leone", + "SOS": "somalia", + "SRD": "suriname", + "STN": "sao tome and prince", + "SVC": "el salvador", + "SYP": "syria", + "SZL": "swaziland", + "THB": "thailand", + "TJS": "tajikistan", + "TMT": "turkmenistan", + "TND": "tunisia", + "TOP": "tonga", + "TRY": "turkey", + "TTD": "trinidad and tobago", + "TWD": "taiwan", + "TZS": "tanzania", + "UAH": "ukraine", + "UGX": "uganda", + "USD": "united states", + "UYU": "uruguay", + "UZS": "uzbekistan", + "VES": "venezuela", + "VND": "vietnam", + "VUV": "vanuatu", + "WST": "samoa", + "XAF": "central african cfa franc", + "XCD": "anguilla", + "XOF": "benin", + "XPF": "french polynesia", + "YER": "yemen", + "ZAR": "south africa", + "ZMW": "zambia", + "ZWL": "zimbabwe", + ] +} diff --git a/DashWallet/Sources/UI/Menu/Settings/SettingsMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Settings/SettingsMenuViewModel.swift index bccfe9243..44971e1e1 100644 --- a/DashWallet/Sources/UI/Menu/Settings/SettingsMenuViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Settings/SettingsMenuViewModel.swift @@ -103,14 +103,14 @@ class SettingsMenuViewModel: ObservableObject { MenuItemModel( title: NSLocalizedString("Local Currency", comment: ""), subtitle: localCurrencyCode, - icon: .custom("image.currency", maxHeight: 22), + icon: .custom("image.currency", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .currencySelector } ), MenuItemModel( - title: NSLocalizedString("Enable Receive Notifications", comment: ""), - icon: .custom("image.notifications", maxHeight: 22), + title: NSLocalizedString("Notifications", comment: ""), + icon: .custom("image.notifications", maxHeight: 30), showToggle: true, isToggled: notificationsEnabled, action: { [weak self] in @@ -123,21 +123,21 @@ class SettingsMenuViewModel: ObservableObject { MenuItemModel( title: NSLocalizedString("Network", comment: ""), subtitle: networkName, - icon: .custom("image.network.monitor", maxHeight: 22), + icon: .custom("image.network.monitor", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .network } ), MenuItemModel( title: NSLocalizedString("Rescan Blockchain", comment: ""), - icon: .custom("image.rescan", maxHeight: 22), + icon: .custom("image.rescan", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .rescan } ), MenuItemModel( title: NSLocalizedString("About", comment: ""), - icon: .custom("image.about", maxHeight: 22), + icon: .custom("image.about", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .about } diff --git a/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift b/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift index 6b206712f..7e42ac8e9 100644 --- a/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift +++ b/DashWallet/Sources/UI/Menu/Settings/SettingsScreen.swift @@ -21,53 +21,43 @@ import Combine struct SettingsScreen: View { private let vc: UINavigationController - private let delegateInternal: DelegateInternal private let onDidRescan: () -> () - + @StateObject private var viewModel = SettingsMenuViewModel() @State private var showNetworkAlert = false @State private var showRescanWarningAlert = false @State private var showRescanActionAlert = false @State private var showCSVExportActivity = false - + init(vc: UINavigationController, onDidRescan: @escaping () -> ()) { self.vc = vc self.onDidRescan = onDidRescan - self.delegateInternal = DelegateInternal(onHide: { - vc.popViewController(animated: true) - }) } var body: some View { VStack(alignment: .leading, spacing: 0) { - // Navigation bar with back button - NavigationBar { + NavBarBack { vc.popViewController(animated: true) } - // Header - HStack { - Text(NSLocalizedString("Settings", comment: "")) - .font(.title) - .fontWeight(.bold) - .foregroundColor(.primaryText) - Spacer() - } - .padding(.horizontal, 20) - .padding(.top, 30) - .padding(.bottom, 20) - - VStack(spacing: 0) { + TopIntro(title: NSLocalizedString("Settings", comment: "")) + .padding(.leading, 20) + .padding(.trailing, 60) + .padding(.top, 10) + .padding(.bottom, 20) + + // Menu list + VStack(spacing: 2) { ForEach(viewModel.items) { item in if let cjItem = item as? CoinJoinMenuItemModel { MenuItem( title: cjItem.title, subtitleView: AnyView(CoinJoinSubtitle(cjItem)), - icon: .custom("image.coinjoin.menu", maxHeight: 22), + icon: .custom("image.coinjoin.menu", maxHeight: 30), badgeText: nil, action: cjItem.action ) - .frame(minHeight: 60) + .frame(minHeight: 56) } else { MenuItem( title: item.title, @@ -80,16 +70,16 @@ struct SettingsScreen: View { isToggled: item.isToggled, action: item.action ) - .frame(minHeight: 60) + .frame(minHeight: 56) } } } - .padding(.horizontal, 20) - .padding(.vertical, 5) + .padding(6) .background(Color.secondaryBackground) - .cornerRadius(12) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) - + .padding(.horizontal, 20) + Spacer() } .background(Color.primaryBackground) @@ -191,8 +181,17 @@ struct SettingsScreen: View { } private func showCurrencySelector() { - let controller = DWLocalCurrencyViewController(navigationAppearance: .default, presentationMode: .screen, currencyCode: nil) - controller.delegate = delegateInternal + let view = LocalCurrencyView( + currencyCode: nil, + onSelect: { [weak vc] _ in + vc?.popViewController(animated: true) + }, + onBack: { [weak vc] in + vc?.popViewController(animated: true) + } + ) + let controller = LocalCurrencyHostingViewController(rootView: view) + controller.hidesBottomBarWhenPushed = true vc.pushViewController(controller, animated: true) } @@ -225,26 +224,11 @@ struct SettingsScreen: View { Text(NSLocalizedString("Turned off", comment: "CoinJoin")) .font(.caption) .foregroundColor(.tertiaryText) - .padding(.leading, 4) .padding(.top, 2) } } } -extension SettingsScreen { - class DelegateInternal: NSObject, DWLocalCurrencyViewControllerDelegate { - let onHide: () -> () - - init(onHide: @escaping () -> ()) { - self.onHide = onHide - } - - func localCurrencyViewController(_ controller: DWLocalCurrencyViewController, didSelectCurrency currencyCode: String) { - onHide() - } - func localCurrencyViewControllerDidCancel(_ controller: DWLocalCurrencyViewController) { onHide() } - } -} struct ActivityView: UIViewControllerRepresentable { let activityItems: [Any] @@ -255,3 +239,32 @@ struct ActivityView: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} } + +private final class LocalCurrencyHostingViewController: BaseViewController { + private let rootView: LocalCurrencyView + + init(rootView: LocalCurrencyView) { + self.rootView = rootView + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .dw_background() + + let hostingController = UIHostingController(rootView: rootView) + hostingController.view.backgroundColor = .clear + dw_embedChild(hostingController) + } +} + +extension LocalCurrencyHostingViewController: NavigationBarDisplayable { + var isBackButtonHidden: Bool { true } + var isNavigationBarHidden: Bool { true } +} diff --git a/DashWallet/Sources/UI/Menu/Tools/CSVExportSheet.swift b/DashWallet/Sources/UI/Menu/Tools/CSVExportSheet.swift new file mode 100644 index 000000000..802a05629 --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Tools/CSVExportSheet.swift @@ -0,0 +1,99 @@ +// +// CSVExportSheet.swift +// DashWallet +// +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct CSVExportSheet: View { + @Environment(\.presentationMode) private var presentationMode + @Environment(\.colorScheme) private var colorScheme + let onExport: () -> Void + + var body: some View { + VStack(spacing: 0) { + // Grabber + Capsule() + .fill(colorScheme == .dark ? Color.whiteAlpha20 : Color.gray300Alpha50) + .frame(width: 36, height: 5) + .padding(.top, 6) + .padding(.bottom, 6) + + // Close button + NavBarClose { + presentationMode.wrappedValue.dismiss() + } + + // Content + VStack(spacing: 0) { + // Icon + Image("csv-export-large") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 94, height: 100) + .padding(.top, 20) + .padding(.bottom, 10) + + // Text content + VStack(alignment: .leading, spacing: 6) { + Text(NSLocalizedString("The full transaction history will be exported as a CSV file", comment: "")) + .font(.system(size: 28, weight: .bold)) + .foregroundColor(Color.primaryText) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + + Text(NSLocalizedString("All payments will be considered as an expense and all incoming transactions will be income.\nThe owner of this wallet is responsible for making any cost basis adjustments in their chosen tax reporting system.", comment: "")) + .font(.system(size: 15)) + .foregroundColor(Color.secondaryText) + .multilineTextAlignment(.leading) + .lineSpacing(5) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.horizontal, 40) + .padding(.top, 20) + .padding(.bottom, 32) + } + + Spacer() + + // Button + VStack(spacing: 0) { + DashButton( + text: NSLocalizedString("Export CSV", comment: ""), + style: .filledBlue, + size: .large, + stretch: true, + isEnabled: true, + action: { + presentationMode.wrappedValue.dismiss() + onExport() + } + ) + .padding(.horizontal, 60) + } + .padding(.top, 20) + .padding(.bottom, 20) + } + .background(Color.secondaryBackground) + } +} + +struct CSVExportSheet_Previews: PreviewProvider { + static var previews: some View { + CSVExportSheet(onExport: {}) + } +} diff --git a/DashWallet/Sources/UI/Menu/Tools/ImportWallet/ImportPrivateKeySheet.swift b/DashWallet/Sources/UI/Menu/Tools/ImportWallet/ImportPrivateKeySheet.swift index abfe8ed17..d61faa920 100644 --- a/DashWallet/Sources/UI/Menu/Tools/ImportWallet/ImportPrivateKeySheet.swift +++ b/DashWallet/Sources/UI/Menu/Tools/ImportWallet/ImportPrivateKeySheet.swift @@ -32,27 +32,9 @@ struct ImportPrivateKeySheet: View { .padding(.bottom, 6) // Close button - HStack { - Spacer() - - Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Image(colorScheme == .dark ? "icon-close-sheet-white" : "icon-close-sheet") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 9, height: 9) - .foregroundColor(Color.primaryText) - .frame(width: 34, height: 34) - .overlay( - Circle() - .stroke(Color.gray300.opacity(0.3), lineWidth: 1.5) - ) - } - .padding(.horizontal, 20) + NavBarClose { + presentationMode.wrappedValue.dismiss() } - .frame(height: 64) - .background(Color.secondaryBackground) // Content VStack(spacing: 0) { diff --git a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift index 073141f54..442de9ed1 100644 --- a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift +++ b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift @@ -25,7 +25,7 @@ struct ToolsMenuScreen: View { private let onImportPrivateKey: () -> () @StateObject private var viewModel = ToolsMenuViewModel() - @State private var showCSVExportAlert = false + @State private var showCSVExportSheet = false @State private var showCSVExportActivity = false @State private var showZenLedgerSheet = false @State private var showImportPrivateKeySheet = false @@ -39,81 +39,64 @@ struct ToolsMenuScreen: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - // Back button - HStack { - Button(action: { - vc.popViewController(animated: true) - }) { - Image(systemName: "chevron.left") - .font(.system(size: 18, weight: .medium)) - .foregroundColor(.primary) - .frame(width: 36, height: 36) - .overlay( - Circle().stroke(Color.gray300.opacity(0.3), lineWidth: 1) - ) - } - Spacer() - } - .padding(.horizontal, 5) - .padding(.top, 10) - - // Header - HStack { - Text(NSLocalizedString("Tools", comment: "")) - .font(.title) - .fontWeight(.bold) - .foregroundColor(.primaryText) - Spacer() - } - .padding(.top, 30) - .padding(.bottom, 20) - - VStack(alignment: .leading, spacing: 16) { - // First group - all items except ZenLedger - VStack(spacing: 0) { - ForEach(viewModel.items.dropLast()) { item in - MenuItem( - title: item.title, - subtitle: item.subtitle, - details: item.details, - icon: item.icon, - showInfo: item.showInfo, - showChevron: false, - isToggled: item.isToggled, - action: item.action - ) - .frame(minHeight: 60) - } + NavBarBack { + vc.popViewController(animated: true) } - .padding(.vertical, 5) - .background(Color.secondaryBackground) - .cornerRadius(12) - .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) - - // Second group - ZenLedger - if let zenLedgerItem = viewModel.items.last { - VStack(spacing: 0) { - MenuItem( - title: zenLedgerItem.title, - subtitle: zenLedgerItem.subtitle, - details: zenLedgerItem.details, - icon: zenLedgerItem.icon, - showInfo: zenLedgerItem.showInfo, - showChevron: false, - isToggled: zenLedgerItem.isToggled, - action: zenLedgerItem.action - ) - .frame(minHeight: 60) + + TopIntro(title: NSLocalizedString("Tools", comment: "")) + .padding(.leading, 20) + .padding(.trailing, 60) + .padding(.top, 10) + .padding(.bottom, 20) + + VStack(alignment: .leading, spacing: 20) { + // Menu list - First group (4 buttons) + VStack(spacing: 2) { + ForEach(viewModel.items.dropLast()) { item in + MenuItem( + title: item.title, + subtitle: item.subtitle, + details: item.details, + icon: item.icon, + showInfo: item.showInfo, + showChevron: false, + isToggled: item.isToggled, + action: item.action + ) + .frame(minHeight: 56) + } } - .padding(.vertical, 5) + .padding(6) .background(Color.secondaryBackground) - .cornerRadius(12) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) - } + .padding(.horizontal, 20) + + // Menu list - ZenLedger + if let zenLedgerItem = viewModel.items.last { + VStack(spacing: 2) { + MenuItem( + title: zenLedgerItem.title, + subtitle: zenLedgerItem.subtitle, + details: zenLedgerItem.details, + icon: zenLedgerItem.icon, + showInfo: zenLedgerItem.showInfo, + showChevron: false, + isToggled: zenLedgerItem.isToggled, + action: zenLedgerItem.action + ) + .frame(minHeight: 56) + } + .padding(6) + .background(Color.secondaryBackground) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + .shadow(color: Color.shadow, radius: 20, x: 0, y: 5) + .padding(.horizontal, 20) + } + Spacer() } } - .padding(.horizontal, 20) .background(Color.primaryBackground) .navigationBarHidden(true) .onReceive(viewModel.$navigationDestination) { destination in @@ -122,13 +105,17 @@ struct ToolsMenuScreen: View { .onReceive(viewModel.$showCSVExportActivity) { show in showCSVExportActivity = show } - .alert(NSLocalizedString("CSV Export", comment: ""), isPresented: $showCSVExportAlert) { - Button(NSLocalizedString("Continue", comment: "")) { - handleCSVExport() + .sheet(isPresented: $showCSVExportSheet) { + if #available(iOS 16.4, *) { + CSVExportSheet(onExport: handleCSVExport) + .presentationDetents([.large]) + .presentationCornerRadius(32) + } else if #available(iOS 16.0, *) { + CSVExportSheet(onExport: handleCSVExport) + .presentationDetents([.large]) + } else { + CSVExportSheet(onExport: handleCSVExport) } - Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) { } - } message: { - Text(NSLocalizedString("All payments will be considered as an Expense and all incoming transactions will be Income. The owner of this wallet is responsible for making any cost basis adjustments in their chosen tax reporting system.", comment: "")) } .sheet(isPresented: $showCSVExportActivity) { if let csvData = viewModel.csvExportData { @@ -141,9 +128,13 @@ struct ToolsMenuScreen: View { viewModel.safariLink = nil } }) { - if #available(iOS 16.0, *) { + if #available(iOS 16.4, *) { + ZenLedgerInfoSheet(safariLink: $viewModel.safariLink) + .presentationDetents([.large]) + .presentationCornerRadius(32) + } else if #available(iOS 16.0, *) { ZenLedgerInfoSheet(safariLink: $viewModel.safariLink) - .presentationDetents([.height(450)]) + .presentationDetents([.large]) } else { ZenLedgerInfoSheet(safariLink: $viewModel.safariLink) } @@ -183,7 +174,7 @@ struct ToolsMenuScreen: View { case .masternodeKeys: showMasternodeKeys() case .csvExport: - showCSVExportAlert = true + showCSVExportSheet = true case .zenLedger: showZenLedgerSheet = true case .none: diff --git a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift index f16aa542c..262001ba6 100644 --- a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift @@ -42,28 +42,28 @@ class ToolsMenuViewModel: ObservableObject { items = [ MenuItemModel( title: NSLocalizedString("Import Private Key", comment: ""), - icon: .custom("image.import.private.key", maxHeight: 22), + icon: .custom("image.import.private.key", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .importPrivateKey } ), MenuItemModel( title: NSLocalizedString("Extended public key (BIP44)", comment: ""), - icon: .custom("image.extend.public.key", maxHeight: 22), + icon: .custom("image.extend.public.key", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .extendedPublicKeys } ), MenuItemModel( title: NSLocalizedString("Show Masternode Keys", comment: ""), - icon: .custom("image.masternode.keys", maxHeight: 22), + icon: .custom("image.masternode.keys", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .masternodeKeys } ), MenuItemModel( title: NSLocalizedString("CSV Export", comment: ""), - icon: .custom("image.csv.export", maxHeight: 22), + icon: .custom("image.csv.export", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .csvExport } @@ -71,7 +71,7 @@ class ToolsMenuViewModel: ObservableObject { MenuItemModel( title: NSLocalizedString("ZenLedger", comment: ""), subtitle: NSLocalizedString("Simplify your crypto taxes", comment: ""), - icon: .custom("zenledger"), + icon: .custom("zenledger", maxHeight: 30), action: { [weak self] in self?.navigationDestination = .zenLedger } diff --git a/DashWallet/Sources/UI/Menu/Tools/ZenLedger/ZenLedgerInfoSheet.swift b/DashWallet/Sources/UI/Menu/Tools/ZenLedger/ZenLedgerInfoSheet.swift index c3a48be1d..be6fc3dea 100644 --- a/DashWallet/Sources/UI/Menu/Tools/ZenLedger/ZenLedgerInfoSheet.swift +++ b/DashWallet/Sources/UI/Menu/Tools/ZenLedger/ZenLedgerInfoSheet.swift @@ -1,4 +1,4 @@ -// +// // Created by Andrei Ashikhmin // Copyright © 2024 Dash Core Group. All rights reserved. // @@ -22,36 +22,90 @@ struct ZenLedgerInfoSheet: View { @State private var showAlert: Bool = false @State private var errorAlert: Bool = false @State private var inProgress: Bool = false - @Environment(\.openURL) private var openURL @Environment(\.presentationMode) private var presentationMode - + @Environment(\.colorScheme) private var colorScheme + @Binding var safariLink: String? - + var body: some View { - BottomSheet(showBackButton: Binding.constant(false)) { - TextIntro( - icon: .custom("zenledger_large"), - buttonLabel: NSLocalizedString("Export all transactions", comment: "ZenLedger"), - action: { showAlert = true }, - inProgress: $inProgress - ) { - FeatureTopText( - title: NSLocalizedString("Simplify your crypto taxes", comment: "ZenLedger"), - text: NSLocalizedString("Connect your crypto wallets to the ZenLedger platform. Learn more and get started with your Dash Wallet transactions.", comment: "ZenLedger"), - label: "zenledger.io", - labelIcon: .custom("external.link"), - linkAction: { + VStack(spacing: 0) { + // Grabber + Capsule() + .fill(colorScheme == .dark ? Color.whiteAlpha20 : Color.gray300Alpha50) + .frame(width: 36, height: 5) + .padding(.top, 6) + .padding(.bottom, 6) + + // Close button + NavBarClose { + presentationMode.wrappedValue.dismiss() + } + + // Content + VStack(spacing: 0) { + // Icon + Image("zenledger-large") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 94, height: 100) + .padding(.top, 20) + .padding(.bottom, 10) + + // Text content + VStack(alignment: .center, spacing: 6) { + Text(NSLocalizedString("Simplify your crypto taxes", comment: "ZenLedger")) + .font(.system(size: 28, weight: .bold)) + .foregroundColor(Color.primaryText) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Text(NSLocalizedString("Connect your crypto wallets to the ZenLedger platform. Learn more and get started with your Dash Wallet transactions.", comment: "ZenLedger")) + .font(.system(size: 15)) + .foregroundColor(Color.secondaryText) + .multilineTextAlignment(.center) + .lineSpacing(5) + .fixedSize(horizontal: false, vertical: true) + + // zenledger.io link + Button(action: { safariLink = "https://app.zenledger.io/new_sign_up/" presentationMode.wrappedValue.dismiss() + }) { + Text("zenledger.io") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.blue) } + .padding(.top, 8) + } + .padding(.horizontal, 40) + .padding(.top, 20) + .padding(.bottom, 32) + } + + Spacer() + + // Button + VStack(spacing: 0) { + DashButton( + text: NSLocalizedString("Export all transactions", comment: "ZenLedger"), + style: .filledBlue, + size: .large, + stretch: true, + isEnabled: true, + isLoading: inProgress, + action: { showAlert = true } ) + .padding(.horizontal, 60) } + .padding(.top, 20) + .padding(.bottom, 20) } + .background(Color.secondaryBackground) .alert(isPresented: $showAlert) { resolveAlert() } } - + private func resolveAlert() -> Alert { if errorAlert { Alert( @@ -77,11 +131,11 @@ struct ZenLedgerInfoSheet: View { ) } } - + private func export() { Task { inProgress = true - + do { if let signupLink = try await viewModel.export() { safariLink = signupLink @@ -92,7 +146,7 @@ struct ZenLedgerInfoSheet: View { errorAlert = true showAlert = true } - + inProgress = false } } diff --git a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m index 4a455dd04..9936a6433 100644 --- a/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m +++ b/DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m @@ -18,6 +18,7 @@ #import "DWPaymentProcessor.h" #import "CurrencyExchanger_Objc.h" +#import "DSAccount+SpentInputCheck.h" #import "DWEnvironment.h" #import "DWGlobalOptions.h" #import "DWPaymentInput+Private.h" @@ -198,6 +199,20 @@ - (void)confirmPaymentOutput:(DWPaymentOutput *)paymentOutput { NSString *address = paymentOutput.address; DSPaymentProtocolRequest *protocolRequest = paymentOutput.protocolRequest; + // Blanket lock: block ANY spend while a Maya swap is still confirming, because the + // pending swap leaves a stale UTXO that coin selection can re-use (double-spend). + if ([account hasUnconfirmedSwapTransaction]) { + NSString *title = NSLocalizedString(@"Swap in progress", @"Shown when a previous Dash conversion is still confirming and blocks a new general send."); + NSString *message = NSLocalizedString(@"A previous Dash conversion is still confirming. Please wait for it to confirm (about 2–5 minutes) before sending.", @"Shown when a pending Dash conversion blocks a regular send."); + // Must pass a NON-nil error: the PaymentController delegate early-returns on a nil + // error, which would leave the Send button stuck spinning and show nothing. + NSError *swapError = [NSError errorWithDomain:@"DashWallet.MayaSwapGuard" + code:1 + userInfo:@{NSLocalizedDescriptionKey : message}]; + [self failedWithError:swapError title:title message:message]; + return; + } + self.request = protocolRequest; self.didSendRequestDelegateNotified = NO; diff --git a/DashWallet/Sources/UI/SwiftUI Components/AddressFieldView.swift b/DashWallet/Sources/UI/SwiftUI Components/AddressFieldView.swift new file mode 100644 index 000000000..8e04bdfb4 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/AddressFieldView.swift @@ -0,0 +1,237 @@ +// +// AddressFieldView.swift +// DashWallet +// +// 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 AddressFieldView: View { + + private enum Layout { + static let hSpacing: CGFloat = 20 + static let lPadding: CGFloat = 20 + static let tPadding: CGFloat = 10 + static let iconSize: CGFloat = 17 + static let cornerRadius: CGFloat = 16 + static let actionTapArea: CGFloat = 40 + } + + @Binding var text: String + let label: String + let placeholder: String + let hasError: Bool + let errorText: String? + var isDisabled: Bool = false + var onScanQR: (() -> Void)? + + @FocusState private var isTextFieldFocused: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(NSLocalizedString(label, comment: "")) + .font(.footnote) + .foregroundStyle(Color.gray500) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack(alignment: .center, spacing: Layout.hSpacing) { + textField + .padding(.vertical, 15) + + if !isDisabled && !isBlurredFilledState { + actionButton + } + } + .padding(.leading, Layout.lPadding) + .padding(.trailing, Layout.tPadding) + .background(backgroundColor) + .clipShape(.rect(cornerRadius: Layout.cornerRadius)) + .overlay( + RoundedRectangle(cornerRadius: Layout.cornerRadius) + .stroke(borderColor, lineWidth: borderWidth) + ) + + if let errorText { + Text(errorText) + .font(.footnote) + .foregroundStyle(Color(UIColor(red: 0.92, green: 0.22, blue: 0.26, alpha: 1))) + } + } + } + + // MARK: - Subviews + + @ViewBuilder private var textField: some View { + if #available(iOS 17.0, *) { + TextField( + "", + text: $text, + prompt: Text(placeholder).font(.subhead).foregroundStyle(Color.black1000Alpha30), + axis: .vertical + ) + .lineLimit(1...3) // cap growth at 3 lines; longer addresses scroll inside the field + .font(.subhead) + .autocapitalization(.none) + .disableAutocorrection(true) + .foregroundStyle(Color.primaryText) + .tint(.primaryText) + .focused($isTextFieldFocused) + .disabled(isDisabled) + } else { + TextField(NSLocalizedString(placeholder, comment: ""), text: $text) + .font(.subhead) + .autocapitalization(.none) + .disableAutocorrection(true) + .foregroundStyle(Color.primaryText) + .tint(.primaryText) + .focused($isTextFieldFocused) + .disabled(isDisabled) + } + } + + private var actionButton: some View { + Group { + if text.isEmpty { + Button(action: { onScanQR?() }) { + Icon(name: .custom("qr", maxHeight: Layout.iconSize)) + .frame(width: Layout.iconSize, height: Layout.iconSize) + } + .accessibilityLabel(NSLocalizedString("Scan QR code", comment: "Maya")) + } else { + Button(action: { text = "" }) { + Image("text-field-clear") + .renderingMode(.template) + .foregroundStyle(Color(uiColor: UIColor(red: 0.14, green: 0.12, blue: 0.13, alpha: 1))) + .frame(width: 11, height: 11) + } + .accessibilityLabel(NSLocalizedString("Clear address", comment: "Maya")) + } + } + .frame(width: Layout.actionTapArea, height: Layout.actionTapArea) + .contentShape(.rect) + } + + // MARK: - Styling + + private var backgroundColor: Color { + if isFocusedState { + return .clear + } + + if hasError { + return Color(UIColor(red: 0.92, green: 0.22, blue: 0.26, alpha: 0.05)) + } + + if isFilledState { + return Color.gray300Alpha10 + } + + return Color.gray300Alpha10 + } + + private var borderColor: Color { + isFocusedState ? .gray300Alpha40 : .clear + } + + private var borderWidth: CGFloat { + isFocusedState ? 1 : 0 + } + + private var isFocusedState: Bool { + isTextFieldFocused && !isDisabled + } + + private var isFilledState: Bool { + !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isFocusedState + } + + /// Field is unfocused ("tapped outside"), has text, and has no error — a clean read-out state + /// with no editing affordance (the trailing action button is suppressed). + /// `isFilledState` already implies `!isFocusedState` and non-empty text. + private var isBlurredFilledState: Bool { + isFilledState && !hasError && !isDisabled + } +} + +#if DEBUG +#Preview("Empty") { + AddressFieldView( + text: .constant(""), + label: "Destination address", + placeholder: "BTC address", + hasError: false, errorText: nil, + onScanQR: {} + ) + .padding() +} + +#Preview("With Text") { + AddressFieldView( + text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"), + label: "Destination address", + placeholder: "BTC address", + hasError: false, errorText: nil, + onScanQR: {} + ) + .padding() +} + +#Preview("Multiline") { + AddressFieldView( + text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh\nbc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"), + label: "Destination address", + placeholder: "BTC address", + hasError: false, errorText: nil, + onScanQR: {} + ) + .padding() +} + +#Preview("Error") { + AddressFieldView( + text: .constant("invalid-address"), + label: "Destination address", + placeholder: "BTC address", + hasError: true, errorText: "Error text", + onScanQR: {} + ) + .padding() +} + +#Preview("Filled") { + AddressFieldView( + text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"), + label: "Destination address", + placeholder: "BTC address", + hasError: false, errorText: nil, + onScanQR: {} + ) + .padding() +} + +// Blurred + filled (no error): unfocused with text → clean read-out on gray300Alpha10 in +// primaryText, with NO trailing QR/clear button. +#Preview("Blurred + filled (no action button)") { + AddressFieldView( + text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"), + label: "Destination address", + placeholder: "BTC address", + hasError: false, errorText: nil, + onScanQR: {} + ) + .padding() +} +#endif diff --git a/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift b/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift index 889c78124..beb1d53eb 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift @@ -1,4 +1,4 @@ -// +// // Created by Andrei Ashikhmin // Copyright © 2024 Dash Core Group. All rights reserved. // @@ -19,65 +19,126 @@ import SwiftUI struct BottomSheet: View { @Environment(\.presentationMode) private var presentationMode - + var title: String = "" @Binding var showBackButton: Bool var onBackButtonPressed: (() -> Void)? = nil + /// `true` (default) — greedy: content fills the sheet (use with an explicit detent or a + /// `.large`/`.medium` detent). `false` — natural height: pair with `.selfSizingSheet()` so + /// the sheet snaps to its content. + var fillsHeight: Bool = true @ViewBuilder var content: () -> Content - + var body: some View { - VStack(spacing: 0) { - HStack(alignment: .top) { - if showBackButton { - Button { - onBackButtonPressed?() - } label: { - Image("backarrow") - .offset(x: -5, y: 5) - } - .frame(maxWidth: 50, maxHeight: .infinity) - } else { - ZStack { }.frame(maxWidth: 50) - } - - Spacer() - - VStack { - Rectangle() - .fill(Color(red: 0.83, green: 0.83, blue: 0.85)) - .frame(width: 36, height: 5) - .cornerRadius(2.50) - - Text(title) - .font(.calloutMedium) - .foregroundColor(.primaryText) - .padding(.top, 10) + let sheet = VStack(spacing: 0) { + grabber + .frame(maxWidth: .infinity, minHeight: 18, maxHeight: 18, alignment: .center) + + header + + contentSection + } + .background(Color.primaryBackground) + + if fillsHeight { + sheet.edgesIgnoringSafeArea(.bottom) + } else { + // Publish the natural content height for `.selfSizingSheet()`. The bottom safe area is + // intentionally NOT ignored here, so the measured height excludes the home-indicator + // inset — `.presentationDetents([.height])` adds that inset itself. + sheet.background( + GeometryReader { proxy in + Color.clear.preference( + key: BottomSheetHeightPreferenceKey.self, + value: proxy.size.height + ) } - .padding(.top, 6) - - Spacer() + ) + } + } - Button { - presentationMode.wrappedValue.dismiss() - } label: { - Image(systemName: "xmark") - .foregroundColor(Color.primaryText) - .imageScale(.large) - .font(Font.system(size: 14).weight(.semibold)) + private var grabber: some View { + Rectangle() + .foregroundColor(.clear) + .frame(width: 36, height: 5) + .background(Color.gray300Alpha50) + .cornerRadius(5) + } + + private var header: some View { + // Reuse the shared NavigationBar (absolutely-centered title, own 64pt height + + // horizontal padding). Title styling preserved; back/close use NavigationBarElement. + NavigationBar( + leading: { + if showBackButton { + NavigationBarElement.back.button { onBackButtonPressed?() } } - .frame(maxWidth: 50, maxHeight: .infinity) + }, + central: { + Text(title) + .font(.calloutMedium) + .foregroundColor(.primaryText) + }, + trailing: { + NavigationBarElement.close.button { presentationMode.wrappedValue.dismiss() } } - .frame(height: 50) - .padding(.horizontal, 10) - .background(Color.primaryBackground) - + ) + } + + @ViewBuilder + private var contentSection: some View { + if fillsHeight { NavigationView { content() .navigationBarHidden(true) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.primaryBackground) } + } else { + // Natural height — no greedy NavigationView / maxHeight so the sheet can self-size. + content() + .frame(maxWidth: .infinity) + .background(Color.primaryBackground) + } + } +} + +// MARK: - Auto-sizing + +/// Bubbles a `BottomSheet`'s measured natural height up to `.selfSizingSheet()`. +struct BottomSheetHeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + +extension View { + /// Sizes a `BottomSheet` (built with `fillsHeight: false`) to its content's natural height — + /// no hardcoded `.height(...)` needed. On iOS < 16 it is a no-op. + @ViewBuilder + func selfSizingSheet(fallback: CGFloat = 0, maxHeightFraction: CGFloat = 0.95) -> some View { + if #available(iOS 16.0, *) { + modifier(SelfSizingSheetModifier(fallback: fallback, maxHeightFraction: maxHeightFraction)) + } else { + self } - .edgesIgnoringSafeArea(.bottom) + } +} + +@available(iOS 16.0, *) +private struct SelfSizingSheetModifier: ViewModifier { + let fallback: CGFloat + let maxHeightFraction: CGFloat + @State private var measured: CGFloat = 0 + + func body(content: Content) -> some View { + let cap = UIScreen.main.bounds.height * maxHeightFraction + let resolved = min(measured > 0 ? measured : fallback, cap) + content + .onPreferenceChange(BottomSheetHeightPreferenceKey.self) { measured = $0 } + // Before the first measurement (and when nothing is provided) fall back to .medium so + // the sheet is never given an invalid 0-height detent. + .presentationDetents(resolved > 0 ? [.height(resolved)] : [.medium]) } } diff --git a/DashWallet/Sources/UI/SwiftUI Components/Button.swift b/DashWallet/Sources/UI/SwiftUI Components/Button.swift index 085092230..3ba8a8888 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/Button.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/Button.swift @@ -45,6 +45,8 @@ struct DashButton: View { @Environment(\.overrideForegroundColor) var overridenForegroundColor @Environment(\.overrideBackgroundColor) var overridenBackgroundColor + @Environment(\.colorScheme) + private var colorScheme var body: some View { Button(action: { @@ -92,20 +94,30 @@ struct DashButton: View { RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .stroke(borderColor, lineWidth: 2) ) - .opacity(isEnabled ? 1.0 : 0.5) + .opacity(disabledOpacity) } .buttonStyle(DashButtonStyle(cornerRadius: cornerRadius)) .disabled(!isEnabled || isLoading) .background(Color.clear) } + private var disabledOpacity: Double { + guard !isEnabled else { return 1.0 } + switch style { + case .filledBlue: + return 1.0 + default: + return 0.5 + } + } + private var backgroundColor: Color { if !isEnabled { switch style { - case .filledBlue: - return Color.blackAlpha5 + case .filled, .filledBlue: + return Color.black1000Alpha5 default: - return Color.black.opacity(0.2) + return Color.black1000Alpha5 } } @@ -127,7 +139,7 @@ struct DashButton: View { case .filledBlue: return Color.blackAlpha40 default: - return Color.black.opacity(0.6) + return colorScheme == .light ? Color.black1000Alpha40 : Color.whiteAlpha20 } } @@ -289,3 +301,44 @@ struct DashButtonStyle: ButtonStyle { ) } } + +#Preview { + DashButtonPreview() +} + +private struct DashButtonPreview: View { + var body: some View { + VStack(spacing: 16) { + DashButton( + text: "Continue", + style: .filled, + isEnabled: true, + action: {} + ) + + DashButton( + text: "Disabled", + style: .filled, + isEnabled: false, + action: {} + ) + + DashButton( + text: "Loading", + style: .filled, + isEnabled: true, + isLoading: true, + action: {} + ) + + DashButton( + text: "Outlined", + style: .outlined, + stretch: false, + action: {} + ) + } + .padding(20) + .background(Color.primaryBackground) + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift b/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift index c28e98bd0..ae1cc0724 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift @@ -40,10 +40,6 @@ extension Color { // System - static var blue: Color { - Color("Blue") - } - static var dashBlue: Color { Color("DashBlueColor") } @@ -59,19 +55,37 @@ extension Color { static var systemRed: Color { Color("SystemRedColor") } - + static var systemYellow: Color { Color("SystemYellowColor") } - + +// Search + + static var searchBg: Color { + Color("SearchBackground") + } + // Blue + static var blue: Color { + Color("Blue") + } + static var blueAlpha5: Color { Color("BlueAlpha5") } + static var blueAlpha10: Color { + Color("BlueAlpha10") + } + // Black + static var black800: Color { + Color("Black800") + } + static var blackAlpha5: Color { Color("BlackAlpha5") } @@ -80,6 +94,30 @@ extension Color { Color("BlackAlpha40") } + static var black1000Alpha5: Color { + Color("Black1000Alpha5") + } + + static var black1000Alpha8: Color { + Color("Black1000Alpha8") + } + + static var black1000Alpha30: Color { + Color("Black1000Alpha30") + } + + static var black1000Alpha40: Color { + Color("Black1000Alpha40") + } + + static var black1000Alpha50: Color { + Color("Black1000Alpha50") + } + + static var black1000Alpha90: Color { + Color("Black1000Alpha90") + } + // Gray static var gray50: Color { @@ -141,7 +179,11 @@ extension Color { static var gray400: Color { Color("Gray400") } - + + static var gray400Alpha10: Color { + Color("Gray400Alpha10") + } + static var gray500: Color { Color("Gray500") } @@ -221,7 +263,7 @@ extension Color { Color(UIColor { traitCollection in switch traitCollection.userInterfaceStyle { case .dark: - return UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5) // TODO + return UIColor.clear default: return UIColor(red: 0.72, green: 0.76, blue: 0.8, alpha: 0.1) } diff --git a/DashWallet/Sources/UI/SwiftUI Components/DashStepper.swift b/DashWallet/Sources/UI/SwiftUI Components/DashStepper.swift new file mode 100644 index 000000000..35a3f2d31 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/DashStepper.swift @@ -0,0 +1,110 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DashStepper: View { + @Binding var count: Int + var maxCount: Int? + + private var plusEnabled: Bool { + guard let max = maxCount else { return true } + return count < max + } + + var body: some View { + HStack(spacing: 6) { + stepperButton( + systemImage: "minus", + enabled: count > 0, + accessibilityLabel: NSLocalizedString("Decrease quantity", comment: "DashSpend") + ) { + if count > 0 { count -= 1 } + } + + Text("\(count)") + .font(.headline.weight(.semibold)) + .foregroundColor(.primaryText) + .frame(width: 30) + .multilineTextAlignment(.center) + + stepperButton( + systemImage: "plus", + enabled: plusEnabled, + accessibilityLabel: NSLocalizedString("Increase quantity", comment: "DashSpend") + ) { + count += 1 + } + } + } + + private func stepperButton( + systemImage: String, + enabled: Bool, + accessibilityLabel: String, + action: @escaping () -> Void + ) -> some View { + Image(systemImage) + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 11, height: 11) + .foregroundColor(enabled ? .primaryText : Color.gray300) + .frame(width: 34, height: 34) + .overlay( + Circle() + .stroke( + enabled ? Color.black1000Alpha8 : Color.black1000Alpha5, + lineWidth: 1.5 + ) + ) + .contentShape(Rectangle()) + .onTapGesture { + guard enabled else { return } + action() + } + .padding(4) + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityLabel) + .accessibilityValue(Text("\(count)")) + .accessibilityHint( + Text( + enabled + ? NSLocalizedString("Double tap to change quantity", comment: "DashSpend") + : NSLocalizedString("Unavailable at this limit", comment: "DashSpend") + ) + ) + .accessibilityAddTraits(.isButton) + .accessibilityAction { + guard enabled else { return } + action() + } + } +} + +#Preview { + DashStepperPreview() + .padding(20) +} + +private struct DashStepperPreview: View { + @State private var count = 0 + + var body: some View { + DashStepper(count: $count, maxCount: 5) + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/DashSwitch.swift b/DashWallet/Sources/UI/SwiftUI Components/DashSwitch.swift new file mode 100644 index 000000000..cbf7a69b2 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/DashSwitch.swift @@ -0,0 +1,41 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DashSwitch: View { + + var body: some View { + HStack(spacing: 0) { + Rectangle() + .foregroundColor(.clear) + .frame(width: 39, height: 24) + .background(Color.white) + .cornerRadius(1000) + .shadow(color: Color(red: 0.1, green: 0.13, blue: 0.15).opacity(0.06), radius: 1, x: 0, y: 1) + .shadow(color: Color(red: 0.1, green: 0.13, blue: 0.15).opacity(0.1), radius: 1.5, x: 0, y: 1) + } + .padding(2) + .frame(width: 64, alignment: .leading) + .background(Color.gray300Alpha50) + .cornerRadius(1000) + } +} + +#Preview { + DashSwitch() +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ConfirmSpendDialog.swift b/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ConfirmSpendDialog.swift index 36b9dd47c..fd64f3c99 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ConfirmSpendDialog.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/Dialogs/ConfirmSpendDialog.swift @@ -76,7 +76,7 @@ struct ConfirmSpendDialog: View { let text = (try? CurrencyExchanger.shared.convertDash(amount: abs(dashAmount.dashAmount), to: App.fiatCurrency).formattedFiatAmount) ?? NSLocalizedString("Not available", comment: "") Text(text) .font(.subhead) - .foregroundColor(.secondaryText) + .foregroundStyle(Color.secondaryText) } } diff --git a/DashWallet/Sources/UI/SwiftUI Components/Font+DWStyle.swift b/DashWallet/Sources/UI/SwiftUI Components/Font+DWStyle.swift index 365fb8b08..fb361b595 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/Font+DWStyle.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/Font+DWStyle.swift @@ -32,6 +32,9 @@ extension Font { /// Title 3: 20pt Bold (line height: 25pt) public static let title3: Font = .system(size: 20, weight: .bold) + /// Title 3 Medium: 20pt Medium (line height: 25pt) + public static let title3Medium: Font = .system(size: 20, weight: .medium) + // MARK: - Text Styles /// Headline: 17pt Bold (line height: 22pt) diff --git a/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/FrameReader.swift b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/FrameReader.swift new file mode 100644 index 000000000..2a8265152 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/FrameReader.swift @@ -0,0 +1,96 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14, *) +/// Adds a transparent View and read it's frame. +/// +/// Adds a GeometryReader with infinity frame. +public struct FrameReader: View { + + let coordinateSpace: CoordinateSpace + let onChange: (_ frame: CGRect) -> Void + + public init(coordinateSpace: CoordinateSpace, onChange: @escaping (_ frame: CGRect) -> Void) { + self.coordinateSpace = coordinateSpace + self.onChange = onChange + } + + public var body: some View { + GeometryReader { geo in + Text("") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear(perform: { + onChange(geo.frame(in: coordinateSpace)) + }) + .onChange(of: geo.frame(in: coordinateSpace), perform: onChange) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +@available(iOS 14, *) +public extension View { + + /// Get the frame of the View + /// + /// Adds a GeometryReader to the background of a View. + func readingFrame(coordinateSpace: CoordinateSpace = .global, onChange: @escaping (_ frame: CGRect) -> ()) -> some View { + background(FrameReader(coordinateSpace: coordinateSpace, onChange: onChange)) + } +} + +@available(iOS 14, *) +struct FrameReader_Previews: PreviewProvider { + + struct PreviewView: View { + + @State private var yOffset: CGFloat = 0 + + var body: some View { + ScrollView(.vertical) { + VStack { + Text("") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + .readingFrame { frame in + yOffset = frame.minY + } + + ForEach(0..<30) { x in + Text("") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + } + } + } + .coordinateSpace(name: "test") + .overlay(Text("Offset: \(yOffset)")) + } + } + + static var previews: some View { + PreviewView() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/LocationReader.swift b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/LocationReader.swift new file mode 100644 index 000000000..d788347d5 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/LocationReader.swift @@ -0,0 +1,92 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14, *) +/// Adds a transparent View and read it's center point. +/// +/// Adds a GeometryReader with 0px by 0px frame. +public struct LocationReader: View { + + let coordinateSpace: CoordinateSpace + let onChange: (_ location: CGPoint) -> Void + + public init(coordinateSpace: CoordinateSpace, onChange: @escaping (_ location: CGPoint) -> Void) { + self.coordinateSpace = coordinateSpace + self.onChange = onChange + } + + public var body: some View { + FrameReader(coordinateSpace: coordinateSpace) { frame in + onChange(CGPoint(x: frame.midX, y: frame.midY)) + } + .frame(width: 0, height: 0, alignment: .center) + } +} + +@available(iOS 14, *) +public extension View { + + /// Get the center point of the View + /// + /// Adds a 0px GeometryReader to the background of a View. + func readingLocation(coordinateSpace: CoordinateSpace = .global, onChange: @escaping (_ location: CGPoint) -> ()) -> some View { + background(LocationReader(coordinateSpace: coordinateSpace, onChange: onChange)) + } + +} + +@available(iOS 14, *) +struct LocationReader_Previews: PreviewProvider { + + struct PreviewView: View { + + @State private var yOffset: CGFloat = 0 + + var body: some View { + ScrollView(.vertical) { + VStack { + Text("Hello, world!") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + .readingLocation { location in + yOffset = location.y + } + + ForEach(0..<30) { x in + Text("") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.green) + .padding() + } + } + } + .coordinateSpace(name: "test") + .overlay(Text("Offset: \(yOffset)")) + } + } + + static var previews: some View { + PreviewView() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/ScaleToFitWidth.swift b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/ScaleToFitWidth.swift new file mode 100644 index 000000000..0153c6c86 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Geometry Readers/ScaleToFitWidth.swift @@ -0,0 +1,77 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - ScaleToFitWidth + +private struct ScaleToFitContentSizeKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + let next = nextValue() + if next.width > 0 { value = next } + } +} + +/// Scales its content uniformly to fit the available width on a single line, shrinking the WHOLE +/// group (e.g. currency symbol + amount + logo) together — unlike `minimumScaleFactor`, which only +/// shrinks each `Text` independently. Content renders at full size when it fits and never scales +/// below `minScale`. +/// +/// The modifier reserves the content's natural single-line height (constant) so it keeps the +/// surrounding vertical layout stable while only the horizontal scale changes. +struct ScaleToFitWidth: ViewModifier { + var minScale: CGFloat = 0.35 + + @State private var naturalSize: CGSize = .zero + + func body(content: Content) -> some View { + GeometryReader { proxy in + content + .fixedSize() // natural, single-line size — the unit we scale + .scaleEffect(scale(forAvailableWidth: proxy.size.width), anchor: .center) + .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) + } + .frame(maxWidth: .infinity) + // Collapse the greedy GeometryReader to one content line so vertical layout is unaffected. + .frame(height: naturalSize.height == 0 ? nil : naturalSize.height) + .background( + // Measure the content's natural (unconstrained) size off-screen. + content + .fixedSize() + .hidden() + .background( + GeometryReader { proxy in + Color.clear.preference(key: ScaleToFitContentSizeKey.self, value: proxy.size) + } + ) + ) + .onPreferenceChange(ScaleToFitContentSizeKey.self) { naturalSize = $0 } + } + + private func scale(forAvailableWidth available: CGFloat) -> CGFloat { + guard naturalSize.width > 0, available > 0, available < naturalSize.width else { return 1 } + return max(minScale, available / naturalSize.width) + } +} + +extension View { + /// Scales the view uniformly to fit the available width on one line, down to `minScale`. + func scaleToFitWidth(minScale: CGFloat = 0.35) -> some View { + modifier(ScaleToFitWidth(minScale: minScale)) + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Icons/ArrowDownIcon.swift b/DashWallet/Sources/UI/SwiftUI Components/Icons/ArrowDownIcon.swift new file mode 100644 index 000000000..f78a43dd5 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Icons/ArrowDownIcon.swift @@ -0,0 +1,69 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - ArrowDownIcon + +/// A code-drawn downward arrow matching the source SVG (9x13 viewBox): +/// a vertical shaft and a chevron arrowhead. Scales cleanly to any `size`. +struct ArrowDownIcon: View { + var size: CGSize = CGSize(width: 9, height: 13) + var color: Color = .dashBlue + var lineWidth: CGFloat = 1.5 + + var body: some View { + ArrowDownShape() + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)) + .frame(width: size.width, height: size.height) + } +} + +// MARK: - ArrowDownShape + +/// Arrow path normalized from the source SVG 9x13 viewBox. +private struct ArrowDownShape: Shape { + func path(in rect: CGRect) -> Path { + func point(_ x: CGFloat, _ y: CGFloat) -> CGPoint { + CGPoint( + x: rect.minX + rect.width * (x / 9.0), + y: rect.minY + rect.height * (y / 13.0) + ) + } + + var path = Path() + path.move(to: point(4.0498, 0.75)) + path.addLine(to: point(4.0498, 11.75)) + + path.move(to: point(0.75, 8.44995)) + path.addLine(to: point(4.05, 11.75)) + path.addLine(to: point(7.35, 8.44995)) + + return path + } +} + +#Preview { + VStack(spacing: 24) { + ArrowDownIcon() + ArrowDownIcon(size: CGSize(width: 16, height: 24)) + ArrowDownIcon(size: CGSize(width: 24, height: 34), color: .white, lineWidth: 2) + .padding(20) + .background(Color.dashBlue, in: RoundedRectangle(cornerRadius: 12)) + } + .padding() +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Icons/ExclamationIcon.swift b/DashWallet/Sources/UI/SwiftUI Components/Icons/ExclamationIcon.swift new file mode 100644 index 000000000..9e4bd123c --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Icons/ExclamationIcon.swift @@ -0,0 +1,115 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import UIKit + +// MARK: - ExclamationIcon + +/// A code-drawn warning icon that mirrors the source SVG (18x17 viewBox): +/// a yellow rounded triangle with an exclamation mark inside. Scales cleanly +/// to any `size` without needing raster assets. +struct ExclamationIcon: View { + var size: CGSize = CGSize(width: 18, height: 17) + var fillColor: Color = Color(uiColor: UIColor(red: 1.0, green: 192.0 / 255.0, blue: 67.0 / 255.0, alpha: 1.0)) + var strokeColor: Color = Color(uiColor: UIColor(red: 1.0, green: 192.0 / 255.0, blue: 67.0 / 255.0, alpha: 1.0)) + var symbolColor: Color = Color(uiColor: UIColor(red: 10.0 / 255.0, green: 11.0 / 255.0, blue: 13.0 / 255.0, alpha: 1.0)) + var lineWidth: CGFloat = 1.5 + + var body: some View { + ZStack { + WarningTriangleShape() + .fill(fillColor) + + WarningTriangleShape() + .stroke(strokeColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)) + + ExclamationMarkShape() + .stroke(symbolColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)) + } + .frame(width: size.width, height: size.height) + } +} + +// MARK: - WarningTriangleShape + +/// Triangle path normalized from the source SVG 18x17 viewBox. +private struct WarningTriangleShape: Shape { + func path(in rect: CGRect) -> Path { + func point(_ x: CGFloat, _ y: CGFloat) -> CGPoint { + CGPoint( + x: rect.minX + rect.width * (x / 18.0), + y: rect.minY + rect.height * (y / 17.0) + ) + } + + var path = Path() + path.move(to: point(13.7031, 15.75)) + path.addLine(to: point(3.32148, 15.75)) + path.addCurve( + to: point(1.04863, 11.9904), + control1: point(1.3904, 15.75), + control2: point(0.15145, 13.6993) + ) + path.addLine(to: point(6.24373, 2.1214)) + path.addCurve( + to: point(10.7894, 2.1214), + control1: point(7.20927, 0.292865), + control2: point(9.8239, 0.292865) + ) + path.addLine(to: point(15.9845, 11.9904)) + path.addCurve( + to: point(13.7117, 15.75), + control1: point(16.8817, 13.6993), + control2: point(15.6428, 15.75) + ) + path.addLine(to: point(13.7031, 15.75)) + path.closeSubpath() + return path + } +} + +// MARK: - ExclamationMarkShape + +/// Exclamation mark path normalized from the source SVG 18x17 viewBox. +private struct ExclamationMarkShape: Shape { + func path(in rect: CGRect) -> Path { + func point(_ x: CGFloat, _ y: CGFloat) -> CGPoint { + CGPoint( + x: rect.minX + rect.width * (x / 18.0), + y: rect.minY + rect.height * (y / 17.0) + ) + } + + var path = Path() + path.move(to: point(8.50806, 9.1023)) + path.addLine(to: point(8.50806, 6.11169)) + + path.move(to: point(8.50806, 12.0923)) + path.addLine(to: point(8.50806, 12.0844)) + return path + } +} + +#Preview { + VStack(spacing: 24) { + ExclamationIcon() + ExclamationIcon(size: CGSize(width: 28, height: 26.5)) + ExclamationIcon(size: CGSize(width: 44, height: 41.5)) + } + .padding() +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Icons/XmarkIcon.swift b/DashWallet/Sources/UI/SwiftUI Components/Icons/XmarkIcon.swift new file mode 100644 index 000000000..b77d47a02 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Icons/XmarkIcon.swift @@ -0,0 +1,68 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - XmarkIcon + +/// A code-drawn "✕" (close) icon. Mirrors the source SVG (9×9 viewBox, two diagonals +/// inset from 0.75 to 7.75, round caps/joins). Scales cleanly to any `size`. +struct XmarkIcon: View { + var size: CGFloat = 9 + var color: Color = .primaryText + var lineWidth: CGFloat = 1.5 + + var body: some View { + XmarkShape() + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)) + .frame(width: size, height: size) + } +} + +// MARK: - XmarkShape + +/// Two diagonal strokes forming an "✕". Endpoints are normalized from the 9-unit +/// source viewBox (inset 0.75 → 7.75) so the cross keeps its proportions at any size. +private struct XmarkShape: Shape { + private let insetRatio: CGFloat = 0.75 / 9 + private let extentRatio: CGFloat = 7.75 / 9 + + func path(in rect: CGRect) -> Path { + let minX = rect.minX + rect.width * insetRatio + let maxX = rect.minX + rect.width * extentRatio + let minY = rect.minY + rect.height * insetRatio + let maxY = rect.minY + rect.height * extentRatio + + var path = Path() + path.move(to: CGPoint(x: minX, y: minY)) + path.addLine(to: CGPoint(x: maxX, y: maxY)) + path.move(to: CGPoint(x: minX, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: minY)) + return path + } +} + +#Preview { + VStack(spacing: 24) { + XmarkIcon() + XmarkIcon(size: 24, color: .white, lineWidth: 2) + .padding(20) + .background(Color.dashBlue, in: .circle) + XmarkIcon(size: 40, color: .red, lineWidth: 3) + } + .padding() +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Illustrations/ErrorIllustration.swift b/DashWallet/Sources/UI/SwiftUI Components/Illustrations/ErrorIllustration.swift new file mode 100644 index 000000000..0e00ca62c --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Illustrations/ErrorIllustration.swift @@ -0,0 +1,35 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct ErrorIllustration: View { + + var body: some View { + ZStack { + Color.red + + Icon(name: .custom("illustration-xmark", maxHeight: 29)) + } + .frame(width: 90, height: 90) + .clipShape(.circle) + } +} + +#Preview { + ErrorIllustration() +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Illustrations/LoadingIllustration.swift b/DashWallet/Sources/UI/SwiftUI Components/Illustrations/LoadingIllustration.swift new file mode 100644 index 000000000..7405e7c8b --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Illustrations/LoadingIllustration.swift @@ -0,0 +1,90 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// MARK: - LoadingIllustration + +/// Wrapper that centers the loading spinner inside a fixed-size frame, matching the +/// Maya design (Figma node 6:254 — a 90×90 frame containing a 61.73 spinner). +struct LoadingIllustration: View { + /// Diameter of the spinner. + var size: CGFloat = 61.73 + /// Spinner tint. + var color: Color = LoadingSpinner.defaultColor + /// Size of the surrounding square frame (the spinner is centered within it). + var containerSize: CGFloat = 90 + + var body: some View { + ZStack { + LoadingSpinner(size: size, color: color) + } + .frame(width: containerSize, height: containerSize) + } +} + +// MARK: - LoadingSpinner + +/// A spinner built from `spokeCount` capsules arranged in a ring with a graduated +/// "comet-tail" opacity. The spokes are static; the whole ring rotates continuously. +struct LoadingSpinner: View { + /// Default tint — Maya blue (#008DE4). + static let defaultColor = Color(red: 0, green: 141 / 255, blue: 228 / 255) + + /// Diameter of the spinner. + var size: CGFloat = 61.73 + /// Spoke tint. + var color: Color = defaultColor + /// Number of spokes in the ring. + var spokeCount: Int = 12 + /// Seconds for one full rotation. + var duration: Double = 1 + + @State private var isAnimating = false + + var body: some View { + ZStack { + ForEach(0.. Double { + guard spokeCount > 1 else { return 0.75 } + return 0.2 + 0.55 * Double(index) / Double(spokeCount - 1) + } +} + +#Preview { + VStack(spacing: 40) { + LoadingIllustration() + LoadingIllustration(size: 32, color: .red) + LoadingSpinner(size: 24, color: .gray, spokeCount: 8, duration: 0.8) + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/Illustrations/SuccessIllustration.swift b/DashWallet/Sources/UI/SwiftUI Components/Illustrations/SuccessIllustration.swift new file mode 100644 index 000000000..109bf5bc4 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/Illustrations/SuccessIllustration.swift @@ -0,0 +1,35 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct SuccessIllustration: View { + + var body: some View { + ZStack { + Color.green + + Icon(name: .custom("checkmark", maxHeight: 29)) + } + .frame(width: 90, height: 90) + .clipShape(.circle) + } +} + +#Preview { + SuccessIllustration() +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift b/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift index fbad1b1dd..0f77ed739 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/MenuItem.swift @@ -21,88 +21,113 @@ typealias TransactionPreview = MenuItem struct MenuItem: View { var title: String - @State var subtitleView: AnyView? = nil - var details: String? = nil - var topText: String? = nil - var icon: IconName? = nil - var secondaryIcon: IconName? = nil + var subtitleView: AnyView? + var details: String? + var topText: String? + var icon: IconName? + /// Optional custom leading view, used when the icon cannot be expressed as an `IconName` + /// (e.g. a remotely-loaded coin icon). Takes precedence over `icon` when non-nil. + var iconView: AnyView? + var secondaryIcon: IconName? + var iconAlignment: VerticalAlignment = .center var showInfo: Bool = false var showChevron: Bool = false - var badgeText: String? = nil - var dashAmount: Int64? = nil - var overrideFiatAmount: String? = nil + var badgeText: String? + var dashAmount: Int64? + var showDashAmountDirection: Bool = true + var overrideFiatAmount: String? + var trailingView: AnyView? var showToggle: Bool = false @State private var isToggled: Bool = false - var action: (() -> Void)? = nil + var action: (() -> Void)? - init(title: String, + init( + title: String, subtitle: String? = nil, + subtitleLineLimit: Int? = 1, details: String? = nil, topText: String? = nil, icon: IconName? = nil, + iconView: AnyView? = nil, secondaryIcon: IconName? = nil, + iconAlignment: VerticalAlignment = .center, showInfo: Bool = false, showChevron: Bool = false, badgeText: String? = nil, dashAmount: Int64? = nil, + showDashAmountDirection: Bool = true, overrideFiatAmount: String? = nil, + trailingView: AnyView? = nil, showToggle: Bool = false, isToggled: Bool = false, action: (() -> Void)? = nil ) { self.init( title: title, - subtitleView: subtitle.map { + subtitleView: subtitle.map { text in AnyView( - Text($0) - .font(.caption) - .lineSpacing(3) + Text(text) + .font(.footnote) + .lineSpacing(4) .foregroundColor(.tertiaryText) - .padding(.leading, 4) - .padding(.top, 2) + .lineLimit(subtitleLineLimit) + .frame(maxWidth: .infinity, alignment: .leading) ) }, details: details, topText: topText, icon: icon, + iconView: iconView, secondaryIcon: secondaryIcon, + iconAlignment: iconAlignment, showInfo: showInfo, showChevron: showChevron, badgeText: badgeText, dashAmount: dashAmount, + showDashAmountDirection: showDashAmountDirection, overrideFiatAmount: overrideFiatAmount, + trailingView: trailingView, showToggle: showToggle, isToggled: isToggled, action: action ) } - init(title: String, + init( + title: String, subtitleView: AnyView? = nil, details: String? = nil, topText: String? = nil, icon: IconName? = nil, + iconView: AnyView? = nil, secondaryIcon: IconName? = nil, + iconAlignment: VerticalAlignment = .center, showInfo: Bool = false, showChevron: Bool = false, badgeText: String? = nil, dashAmount: Int64? = nil, + showDashAmountDirection: Bool = true, overrideFiatAmount: String? = nil, + trailingView: AnyView? = nil, showToggle: Bool = false, isToggled: Bool = false, action: (() -> Void)? = nil ) { self.title = title - self._subtitleView = State(initialValue: subtitleView) + self.subtitleView = subtitleView self.details = details self.topText = topText self.icon = icon + self.iconView = iconView self.secondaryIcon = secondaryIcon + self.iconAlignment = iconAlignment self.showInfo = showInfo self.showChevron = showChevron self.dashAmount = dashAmount + self.showDashAmountDirection = showDashAmountDirection self.badgeText = badgeText self.overrideFiatAmount = overrideFiatAmount + self.trailingView = trailingView self._isToggled = State(initialValue: isToggled) self.showToggle = showToggle self.action = action @@ -116,8 +141,10 @@ struct MenuItem: View { action?() } }) { - HStack(spacing: 4) { - if let icon = icon { + HStack(alignment: iconAlignment, spacing: 10) { + if let iconView = iconView { + iconView + } else if let icon = icon { ZStack(alignment: .leading) { Icon(name: icon) .frame(width: 30, height: 30) @@ -138,17 +165,15 @@ struct MenuItem: View { } } } - .frame(width: 36, height: 36) + .frame(width: 30, height: 30) } - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 2) { if let topText = topText { Text(topText) .font(.caption) .lineSpacing(3) .foregroundColor(.tertiaryText) - .padding(.leading, 4) - .padding(.bottom, 2) } HStack(spacing: 6) { @@ -177,7 +202,6 @@ struct MenuItem: View { } } .frame(maxWidth: .infinity, alignment: .leading) - .padding(.leading, 4) if let subtitle = subtitleView { subtitle @@ -188,12 +212,11 @@ struct MenuItem: View { .font(.caption) .lineSpacing(3) .foregroundColor(.tertiaryText) - .padding(.leading, 4) - .padding(.top, 2) } } + .padding(.leading, 6) .frame(maxWidth: .infinity) - + if showToggle { Toggle(isOn: $isToggled) { } .tint(Color.dashBlue) @@ -206,12 +229,14 @@ struct MenuItem: View { .imageScale(.small) .foregroundColor(Color.gray) .padding(.trailing, 10) + } else if let trailingView = trailingView { + trailingView } else { VStack(alignment: .trailing) { if let dashAmount = dashAmount { - DashAmount(amount: dashAmount) + DashAmount(amount: dashAmount, showDirection: showDashAmountDirection) .foregroundColor(.primaryText) - + if dashAmount != 0 && dashAmount != Int64.max && dashAmount != Int64.min { if let overriden = overrideFiatAmount { Text(overriden) @@ -225,8 +250,8 @@ struct MenuItem: View { } } } + .contentShape(Rectangle()) } - .contentShape(Rectangle()) .padding(10) .frame(maxWidth: .infinity) .onChange(of: isToggled) { newValue in @@ -245,12 +270,15 @@ struct MenuItem: View { } #Preview { - MenuItem( - title: "Title", - subtitle: "Easily stake Dash and earn passive income with a few simple steps", - icon: .system("faceid"), - showInfo: true, - showToggle: true, - isToggled: true - ) + VStack { + MenuItem( + title: "Title", + subtitle: "Easily stake Dash and earn passive income with a few simple steps", + subtitleLineLimit: 2, + icon: .custom("uphold_logo"), + showInfo: true, + showToggle: true, + isToggled: true + ) + } } diff --git a/DashWallet/Sources/UI/SwiftUI Components/NavigationBar.swift b/DashWallet/Sources/UI/SwiftUI Components/NavigationBar.swift index f18b57389..6a136381e 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/NavigationBar.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/NavigationBar.swift @@ -1,4 +1,4 @@ -// + // // NavigationBar.swift // DashWallet // @@ -22,6 +22,108 @@ import SwiftUI +struct NavigationBar: View { + private let leading: Leading + private let central: Central + private let trailing: Trailing + + init( + @ViewBuilder leading: () -> Leading, + @ViewBuilder central: () -> Central, + @ViewBuilder trailing: () -> Trailing + ) { + self.leading = leading() + self.central = central() + self.trailing = trailing() + } + + var body: some View { + ZStack { + HStack { + leading + + Spacer() + + trailing + } + .padding(.horizontal, 20) + + central + } + .frame(maxWidth: .infinity, minHeight: 64) + } +} + +// MARK: - Convenience Initializers (2 elements) + +extension NavigationBar where Trailing == EmptyView { + init(@ViewBuilder leading: () -> Leading, @ViewBuilder central: () -> Central) { + self.init(leading: leading, central: central, trailing: { EmptyView() }) + } +} + +extension NavigationBar where Central == EmptyView { + init(@ViewBuilder leading: () -> Leading, @ViewBuilder trailing: () -> Trailing) { + self.init(leading: leading, central: { EmptyView() }, trailing: trailing) + } +} + +extension NavigationBar where Leading == EmptyView { + init(@ViewBuilder central: () -> Central, @ViewBuilder trailing: () -> Trailing) { + self.init(leading: { EmptyView() }, central: central, trailing: trailing) + } +} + +// MARK: - Convenience Initializers (1 element) + +extension NavigationBar where Central == EmptyView, Trailing == EmptyView { + init(@ViewBuilder leading: () -> Leading) { + self.init(leading: leading, central: { EmptyView() }, trailing: { EmptyView() }) + } +} + +extension NavigationBar where Leading == EmptyView, Trailing == EmptyView { + init(@ViewBuilder central: () -> Central) { + self.init(leading: { EmptyView() }, central: central, trailing: { EmptyView() }) + } +} + +extension NavigationBar where Leading == EmptyView, Central == EmptyView { + init(@ViewBuilder trailing: () -> Trailing) { + self.init(leading: { EmptyView() }, central: { EmptyView() }, trailing: trailing) + } +} + +enum NavigationBarElement: String { + case back = "navigationbar-back" + case close = "navigationbar-close" + case plus = "navigationbar-plus" + case info = "navigationbar-info" + + var icon: some View { + Icon(name: .custom(rawValue)) + } + + func button(action: @escaping () -> Void) -> some View { + Button(action: action) { + icon + .fixedSize() + .frame(width: 44, height: 44, alignment: .center) + .contentShape(Rectangle()) + } + .buttonStyle(NavigationBarButtonStyle()) + } +} + +private struct NavigationBarButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.88 : 1.0) + .opacity(configuration.isPressed ? 0.7 : 1.0) + .animation(.easeInOut(duration: 0.12), value: configuration.isPressed) + } +} + // MARK: - NavBarBack /// Navigation bar with only a back button @@ -203,52 +305,73 @@ struct NavBarClose: View { } } -// MARK: - Legacy Support +// MARK: - Previews -/// Legacy name for NavBarBack - use NavBarBack instead -@available(*, deprecated, renamed: "NavBarBack", message: "Use NavBarBack for clarity") -typealias NavigationBar = NavBarBack +#Preview("NavBarBack") { + NavigationBar( + leading: { NavigationBarElement.back.button { } } + ) +} -// MARK: - Previews +#Preview("NavBarBackTitle") { + NavigationBar( + leading: { NavigationBarElement.back.button { } }, + central: { Text("Title").font(.subheadMedium) } + ) +} -struct NavigationBar_Previews: PreviewProvider { - static var previews: some View { - VStack(spacing: 0) { - NavBarBack { - print("Back tapped") - } +#Preview("NavBarBackTitleInfo") { + NavigationBar( + leading: { NavigationBarElement.back.button { } }, + central: { Text("Title").font(.subheadMedium) }, + trailing: { NavigationBarElement.info.button { } } + ) +} - Spacer() - } - .previewDisplayName("NavBarBack - Light") +#Preview("NavBarBackInfo") { + NavigationBar( + leading: { NavigationBarElement.back.button { } }, + trailing: { NavigationBarElement.info.button { } } + ) +} - VStack(spacing: 0) { - NavBarBack { - print("Back tapped") - } +#Preview("NavBarTitleClose") { + NavigationBar( + central: { Text("Title").font(.subheadMedium) }, + trailing: { NavigationBarElement.close.button { } } + ) +} - Spacer() - } - .preferredColorScheme(.dark) - .previewDisplayName("NavBarBack - Dark") +#Preview("NavBarBackTitlePlus") { + NavigationBar( + leading: { NavigationBarElement.back.button { } }, + central: { Text("Title").font(.subheadMedium) }, + trailing: { NavigationBarElement.close.button { } } + ) +} - VStack(spacing: 0) { - NavBarClose { - print("Close tapped") - } +#Preview("NavBarBackPlus") { + NavigationBar( + leading: { NavigationBarElement.back.button { } }, + trailing: { NavigationBarElement.plus.button { } } + ) +} - Spacer() - } - .previewDisplayName("NavBarClose - Light") +#Preview("NavBarTitle") { + NavigationBar( + central: { Text("Title").font(.subheadMedium) } + ) +} - VStack(spacing: 0) { - NavBarClose { - print("Close tapped") - } +#Preview("NavBarClose") { + NavigationBar( + trailing: { NavigationBarElement.close.button { } } + ) +} - Spacer() - } - .preferredColorScheme(.dark) - .previewDisplayName("NavBarClose - Dark") - } +#Preview("NavBarTitleClose") { + NavigationBar( + central: { Text("Title").font(.subheadMedium) }, + trailing: { NavigationBarElement.close.button { } } + ) } diff --git a/DashWallet/Sources/UI/SwiftUI Components/NetworkUnavailableStateView.swift b/DashWallet/Sources/UI/SwiftUI Components/NetworkUnavailableStateView.swift new file mode 100644 index 000000000..f59598405 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/NetworkUnavailableStateView.swift @@ -0,0 +1,98 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +// MARK: - NetworkReachabilityMonitor + +/// SwiftUI-friendly bridge over the app's shared `DSReachabilityManager`. +/// +/// Mirrors the UIKit `NetworkReachabilityHandling` protocol used by Coinbase screens, +/// but exposes the status as an `@Published` value so SwiftUI views can react to it. +/// Reuses the single shared reachability instance and its existing change notification +/// rather than creating a separate network monitor. +final class NetworkReachabilityMonitor: ObservableObject { + @Published private(set) var networkStatus: NetworkStatus + + var isOnline: Bool { networkStatus == .online } + + private var observer: Any? + private let reachability = DSReachabilityManager.shared() + + init() { + if !reachability.isMonitoring { + reachability.startMonitoring() + } + + networkStatus = reachability.networkStatus + + observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name(rawValue: "org.dash.networking.reachability.change"), + object: nil, + queue: .main + ) { [weak self] _ in + self?.updateStatus() + } + } + + deinit { + if let observer { + NotificationCenter.default.removeObserver(observer) + } + } + + private func updateStatus() { + networkStatus = reachability.networkStatus + } +} + +// MARK: - NetworkUnavailableStateView + +/// SwiftUI equivalent of Coinbase's `NetworkUnavailableView`. +/// Centered icon + title + subtitle, shown in place of network-dependent content while offline. +struct NetworkUnavailableStateView: View { + private enum Layout { + static let mainSpacing: CGFloat = 15 + static let textSpacing: CGFloat = 7 + } + + var body: some View { + VStack(spacing: Layout.mainSpacing) { + Image("network.unavailable") + + VStack(spacing: Layout.textSpacing) { + Text(NSLocalizedString("Network Unavailable", comment: "Network Unavailable")) + .font(.system(size: 17, weight: .medium)) + .foregroundColor(.primaryText) + + Text(NSLocalizedString("Please check your network connection", comment: "Network Unavailable")) + .font(.footnote) + .foregroundColor(.secondaryText) + } + .multilineTextAlignment(.center) + } + } +} + +#if DEBUG +#Preview { + NetworkUnavailableStateView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.primaryBackground) +} +#endif diff --git a/DashWallet/Sources/UI/SwiftUI Components/NumericKeyboardView.swift b/DashWallet/Sources/UI/SwiftUI Components/NumericKeyboardView.swift index a46e3df03..33ad83777 100644 --- a/DashWallet/Sources/UI/SwiftUI Components/NumericKeyboardView.swift +++ b/DashWallet/Sources/UI/SwiftUI Components/NumericKeyboardView.swift @@ -1,19 +1,37 @@ import SwiftUI struct NumericKeyboardView: View { + private enum Layout { + static let rootSpacing: CGFloat = 20 + static let rowsSpacing: CGFloat = 10 + static let rowSpacing: CGFloat = 10 + static let horizontalPadding: CGFloat = 40 + static let keyFontSize: CGFloat = 20 + static let keyFontWeight: Font.Weight = .medium + static let disabledOpacity: CGFloat = 0.5 + static let enabledOpacity: CGFloat = 1.0 + + static let decimalKey = "." + static let zeroKey = "0" + static let emptyKey = "" + static let deleteKey = "⌫" + static let deleteSymbol = "delete.left" + } + @Binding var value: String let showDecimalSeparator: Bool let actionButtonText: String let actionEnabled: Bool let inProgress: Bool + let helperText: String? = nil let actionHandler: () -> Void private var rows: [[String]] { let lastRow: [String] if showDecimalSeparator { - lastRow = [".", "0", "⌫"] + lastRow = [Layout.decimalKey, Layout.zeroKey, Layout.deleteKey] } else { - lastRow = ["", "0", "⌫"] + lastRow = [Layout.emptyKey, Layout.zeroKey, Layout.deleteKey] } return [ @@ -25,50 +43,89 @@ struct NumericKeyboardView: View { } var body: some View { - VStack(spacing: 0) { + VStack(spacing: Layout.rootSpacing) { + keyboardRowsView + helperTextRow(helperText) + actionButtonView + } + .background(Color.secondaryBackground) + } + + private var keyboardRowsView: some View { + VStack(spacing: Layout.rowsSpacing) { ForEach(rows, id: \.self) { row in - HStack(spacing: 0) { - ForEach(row, id: \.self) { key in - Button(action: { - handleKeyPress(key) - }) { - if key == "⌫" { - Image(systemName: "delete.left") - .frame(maxWidth: .infinity, maxHeight: .infinity) - .font(.system(size: 20)) - .foregroundColor(.primaryText) - } else { - Text(key) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .font(.system(size: 20, weight: .medium)) - .foregroundColor(.primaryText) - } - } - .disabled(inProgress) - } - } - .opacity(inProgress ? 0.5 : 1.0) + keyboardRowView(row) + .opacity(inProgress ? Layout.disabledOpacity : Layout.enabledOpacity) + } - - DashButton( - text: actionButtonText, - isEnabled: !value.isEmpty && actionEnabled, - isLoading: inProgress, - action: actionHandler - ) - .padding(.top, 20) } - .background(Color.secondaryBackground) + } + + private func keyboardRowView(_ row: [String]) -> some View { + HStack(spacing: Layout.rowSpacing) { + ForEach(row, id: \.self) { key in + keyButtonView(key) + } + } + } + + private func keyButtonView(_ key: String) -> some View { + Button { + handleKeyPress(key) + } label: { + keyContentView(key) + } + .frame(height: 50) + .frame(maxWidth: .infinity) + .disabled(inProgress) + } + + @ViewBuilder + private func keyContentView(_ key: String) -> some View { + if key == Layout.deleteKey { + Image(systemName: Layout.deleteSymbol) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .font(.system(size: Layout.keyFontSize)) + .foregroundColor(.primaryText) + } else { + Text(key) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .font(.system(size: Layout.keyFontSize, weight: Layout.keyFontWeight)) + .foregroundColor(.primaryText) + } + } + + private var actionButtonView: some View { + DashButton( + text: actionButtonText, + isEnabled: !value.isEmpty && actionEnabled, + isLoading: inProgress, + action: actionHandler + ) + .padding(.horizontal, Layout.horizontalPadding) + } + + @ViewBuilder + private func helperTextRow(_ text: String?) -> some View { + if let text, !text.isEmpty { + Text(text) + .font(.footnote) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } else { + EmptyView() + } } private func handleKeyPress(_ key: String) { - if key == "⌫" { + if key == Layout.deleteKey { if !value.isEmpty { value.removeLast() } - } else if key == "." { + } else if key == Layout.decimalKey { if showDecimalSeparator && !value.contains(".") { - value += "." + value += Layout.decimalKey } } else if !key.isEmpty { value += key @@ -77,12 +134,22 @@ struct NumericKeyboardView: View { } #Preview { - NumericKeyboardView( - value: .constant(""), - showDecimalSeparator: true, - actionButtonText: NSLocalizedString("Verify", comment: "Button title for numeric keyboard action"), - actionEnabled: true, - inProgress: false, - actionHandler: { print("Action button tapped") } - ).frame(height: 400) + + VStack { + Spacer() + + NumericKeyboardView( + value: .constant(""), + showDecimalSeparator: true, + actionButtonText: NSLocalizedString("Verify", comment: "Button title for numeric keyboard action"), + actionEnabled: true, + inProgress: false, + actionHandler: { print("Action button tapped") } + ) +// .frame(height: 400) + .padding(.horizontal, 20) + .background(.red.opacity(0.3)) + } + + } diff --git a/DashWallet/Sources/UI/SwiftUI Components/ScrollViews/ScrollViewWithOnScrollChanged.swift b/DashWallet/Sources/UI/SwiftUI Components/ScrollViews/ScrollViewWithOnScrollChanged.swift new file mode 100644 index 000000000..282dc8f63 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/ScrollViews/ScrollViewWithOnScrollChanged.swift @@ -0,0 +1,79 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14, *) +public struct ScrollViewWithOnScrollChanged: View { + + let axes: Axis.Set + let showsIndicators: Bool + let content: Content + let onScrollChanged: (_ origin: CGPoint) -> () + @State private var coordinateSpaceID: String = UUID().uuidString + + public init( + _ axes: Axis.Set = .vertical, + showsIndicators: Bool = false, + @ViewBuilder content: () -> Content, + onScrollChanged: @escaping (_ origin: CGPoint) -> ()) { + self.axes = axes + self.showsIndicators = showsIndicators + self.content = content() + self.onScrollChanged = onScrollChanged + } + + public var body: some View { + ScrollView(axes, showsIndicators: showsIndicators) { + LocationReader(coordinateSpace: .named(coordinateSpaceID), onChange: onScrollChanged) + content + } + .coordinateSpace(name: coordinateSpaceID) + } +} + +@available(iOS 14, *) +struct ScrollViewWithOnScrollChanged_Previews: PreviewProvider { + + struct PreviewView: View { + + @State private var yPosition: CGFloat = 0 + + var body: some View { + ScrollViewWithOnScrollChanged { + VStack { + ForEach(0..<30) { x in + Text("x: \(x)") + .frame(maxWidth: .infinity) + .frame(height: 200) + .cornerRadius(10) + .background(Color.red) + .padding() + .id(x) + } + } + } onScrollChanged: { origin in + yPosition = origin.y + } + .overlay(Text("Offset: \(yPosition)")) + } + } + + static var previews: some View { + PreviewView() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift new file mode 100644 index 000000000..0f72449fc --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/SearchBar.swift @@ -0,0 +1,172 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct SearchBar: View { + private enum Layout { + static let fieldHeight: CGFloat = 40 + static let fieldCornerRadius: CGFloat = 14 + static let fieldHorizontalPadding: CGFloat = 14 + static let fieldSpacing: CGFloat = 10 + static let cancelHorizontalPadding: CGFloat = 12 + static let cancelVerticalPadding: CGFloat = 6 + static let animationDuration: TimeInterval = 0.25 + } + + @Binding var text: String + @FocusState private var isFocused: Bool + @State private var isEditing: Bool = false + @State private var cancelButtonWidth: CGFloat = 0 + + private let searchIcon = IconName.custom("search-icon", maxHeight: 15) + private let xmarkIcon = IconName.custom("xmark-icon", maxHeight: 15) + + var body: some View { + HStack(spacing: 0) { + HStack(spacing: Layout.fieldSpacing) { + magnifyingglass + searchField + clearButton + } + .padding(.horizontal, Layout.fieldHorizontalPadding) + .frame(height: Layout.fieldHeight) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.searchBg) + .clipShape(.rect(cornerRadius: Layout.fieldCornerRadius)) + + if isEditing { + cancelButton + .transition(.move(edge: .trailing).combined(with: .opacity)) + } + } + .animation(.easeInOut(duration: Layout.animationDuration), value: isEditing) + .onAppear { + isEditing = isFocused + } + .onChangeCompat(of: isFocused) { focused in + withAnimation(.easeInOut(duration: Layout.animationDuration)) { + isEditing = focused + } + } + } + + @ViewBuilder private var clearButton: some View { + if !text.isEmpty { + Button(action: { text = "" }, label: { Icon(name: xmarkIcon) }) + } + } + + private var cancelButton: some View { + Button(action: { + text = "" + withAnimation(.easeInOut(duration: Layout.animationDuration)) { + isEditing = false + } + isFocused = false + }, label: { + Text(NSLocalizedString("Cancel", comment: "")) + .font(.footnote.weight(.semibold)) + .padding(.horizontal, Layout.cancelHorizontalPadding) + .padding(.vertical, Layout.cancelVerticalPadding) + }) + .tint(.primaryText) + } + + private var cancelButtonMeasurement: some View { + cancelButton + .fixedSize() + .padding(.leading, Layout.fieldSpacing) + .hidden() + .captureSize { size in + if abs(cancelButtonWidth - size.width) > 0.5 { + cancelButtonWidth = size.width + } + } + } + + private var magnifyingglass: some View { + Icon(name: searchIcon) + } + + @ViewBuilder private var searchField: some View { + if #available(iOS 17.0, *) { + TextField( + text: $text, + prompt: Text(NSLocalizedString("Search", comment: "")).font(.subhead).foregroundStyle(Color.black1000Alpha30) + ) { + EmptyView() + } + .focused($isFocused) + .tint(.primaryText) + } else { + // Fallback on earlier versions + TextField(NSLocalizedString("Search", comment: ""), text: $text) + .autocapitalization(.none) + .disableAutocorrection(true) + .foregroundColor(.primaryText) + .focused($isFocused) + .tint(.primaryText) + } + } +} + +private struct SearchBarSizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } +} + +private extension View { + func onChangeCompat(of value: Value, perform action: @escaping (Value) -> Void) -> some View { + Group { + if #available(iOS 17.0, *) { + self.onChange(of: value) { _, newValue in + action(newValue) + } + } else { + self.onChange(of: value) { newValue in + action(newValue) + } + } + } + } + + func captureSize(_ onChange: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometry in + Color.clear + .preference(key: SearchBarSizePreferenceKey.self, value: geometry.size) + } + ) + .onPreferenceChange(SearchBarSizePreferenceKey.self, perform: onChange) + } +} + +#Preview { + SearchBarPreview() +} + +private struct SearchBarPreview: View { + @State private var text = "" + var body: some View { + SearchBar(text: $text) + .padding() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/SegmentedControl.swift b/DashWallet/Sources/UI/SwiftUI Components/SegmentedControl.swift new file mode 100644 index 000000000..395494da1 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/SegmentedControl.swift @@ -0,0 +1,146 @@ +// +// Created by Roman Chornyi +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +private enum SegmentedControlLayout { + static let containerPadding: CGFloat = 4 + static let containerCornerRadius: CGFloat = 20 + static let segmentCornerRadius: CGFloat = 16 + static let segmentVerticalPadding: CGFloat = 6 + static let height: CGFloat = 40 + static let shadowRadius: CGFloat = 10 + static let shadowY: CGFloat = 5 + static let springResponse: Double = 0.3 + static let springDamping: Double = 0.7 +} + +struct SegmentedControl: View { + private typealias Layout = SegmentedControlLayout + + let options: [T] + @Binding var selection: T + let label: (T) -> String + + @Environment(\.colorScheme) private var colorScheme + @State private var containerWidth: CGFloat = 0 + + private var segmentWidth: CGFloat { + guard options.count > 0 else { return 0 } + return containerWidth / CGFloat(options.count) + } + + private var indicatorOffset: CGFloat { + let index = options.firstIndex(of: selection) ?? 0 + return CGFloat(index) * segmentWidth + } + + var body: some View { + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: Layout.segmentCornerRadius) + .fill(colorScheme == .dark ? Color.whiteAlpha20 : Color.white) + .shadow(color: .shadow, radius: Layout.shadowRadius, x: 0, y: Layout.shadowY) + .frame(width: max(1, segmentWidth)) + .offset(x: indicatorOffset) + .animation(.spring(response: Layout.springResponse, dampingFraction: Layout.springDamping), value: selection) + .opacity(containerWidth > 0 ? 1 : 0) + + HStack(spacing: 0) { + ForEach(options, id: \.self) { option in + Button(action: { + select(option) + }) { + Text(label(option)) + .font(.footnoteMedium) + .foregroundColor(selection == option ? .primaryText : .primaryText.opacity(0.4)) + .lineLimit(1) + .frame(maxWidth: .infinity) + .padding(.vertical, Layout.segmentVerticalPadding) + .contentShape(Rectangle()) + .animation(.spring(response: Layout.springResponse, dampingFraction: Layout.springDamping), value: selection) + } + .buttonStyle(.plain) + .accessibilityElement(children: .ignore) + .accessibilityLabel(Text(label(option))) + .accessibilityHint(Text(NSLocalizedString("Double tap to select", comment: "SegmentedControl"))) + .accessibilityAddTraits(selection == option ? [.isButton, .isSelected] : .isButton) + } + } + } + .background( + GeometryReader { geo in + Color.clear + .onAppear { containerWidth = geo.size.width } + .onChange(of: geo.size.width) { newWidth in + containerWidth = newWidth + } + } + ) + .highPriorityGesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + guard segmentWidth > 0 else { return } + updateSelection(at: value.location.x) + } + ) + .padding(Layout.containerPadding) + .frame(height: Layout.height) + .background(Color.gray300Alpha20) + .clipShape(RoundedRectangle(cornerRadius: Layout.containerCornerRadius)) + } + + private func updateSelection(at x: CGFloat) { + let index = max(0, min(options.count - 1, Int(x / segmentWidth))) + select(options[index]) + } + + private func select(_ option: T) { + guard option != selection else { return } + withAnimation(.spring(response: Layout.springResponse, dampingFraction: Layout.springDamping)) { + selection = option + } + } +} + +extension SegmentedControl where T == String { + init(options: [T], selection: Binding) { + self.options = options + self._selection = selection + self.label = { $0 } + } +} + +#Preview { + SegmentedControlPreview() +} + +private struct SegmentedControlPreview: View { + @State private var two = "Single" + @State private var three = "Day" + @State private var four = "1D" + + var body: some View { + VStack(spacing: 24) { + SegmentedControl(options: ["Single", "Multiple"], selection: $two) + + SegmentedControl(options: ["Day", "Week", "Month"], selection: $three) + + SegmentedControl(options: ["1D", "1W", "1M", "1Y"], selection: $four) + } + .padding() + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/SelfSizingSheet.swift b/DashWallet/Sources/UI/SwiftUI Components/SelfSizingSheet.swift new file mode 100644 index 000000000..aff78ced1 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/SelfSizingSheet.swift @@ -0,0 +1,91 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// Self-sizing sheet: the sheet hugs its content height instead of using a fixed +// detent. Measures the content via GeometryReader and feeds the height into a +// single `.height` presentation detent. +// +// Notes: +// - A single detent (no .large) means the sheet cannot be dragged to full screen, +// and iOS does not show the system drag indicator. +// - Until a real measurement arrives, .medium is used as a placeholder so the +// sheet doesn't collapse (a height of 0 would make iOS fall back to full screen). +// - The measured view must have a finite intrinsic height (no greedy Spacers / +// maxHeight: .infinity), otherwise it expands to fill the offered space and the +// measurement is wrong. See BottomSheet(fillsHeight: false). + +@available(iOS 16.0, *) +private struct SelfSizingSheetModifier: ViewModifier { + @State private var height: CGFloat? + + func body(content: Content) -> some View { + content + .background( + GeometryReader { geo in + Color.clear + .onAppear { update(geo.size.height) } + .onChange(of: geo.size.height) { update($0) } + } + ) + .presentationDetents(detents) + .presentationDragIndicator(.hidden) + } + + private var detents: Set { + guard let height, height > 0 else { return [.medium] } + return [.height(height)] + } + + private func update(_ newHeight: CGFloat) { + guard newHeight > 0, newHeight != height else { return } + height = newHeight + } +} + +extension View { + /// Applies a self-sizing sheet detent so the sheet fits its content automatically. + /// + /// - Parameter cornerRadius: Optional corner radius applied via `presentationCornerRadius` + /// on iOS 16.4..<26 only. Pass `nil` (default) to skip. iOS 26+ uses the system sheet + /// corner styling, so the custom radius is intentionally not applied there. + /// + /// The iOS 16 guard is built-in; callers do NOT need their own `#available` check. + @ViewBuilder + func selfSizingSheet(cornerRadius: CGFloat? = nil) -> some View { + if #available(iOS 16.0, *) { + let modified = modifier(SelfSizingSheetModifier()) + if #available(iOS 16.4, *), let r = cornerRadius { + if #unavailable(iOS 26.0) { + // iOS 16.4..<26: apply the custom corner radius. + modified + .presentationCornerRadius(r) + .presentationBackground(Color.primaryBackground) + } else { + // iOS 26+: keep the system corner styling, skip the custom radius. + modified + .presentationBackground(Color.primaryBackground) + } + } else { + modified + } + } else { + self + } + } +} diff --git a/DashWallet/Sources/UI/SwiftUI Components/TopIntro.swift b/DashWallet/Sources/UI/SwiftUI Components/TopIntro.swift new file mode 100644 index 000000000..97b919861 --- /dev/null +++ b/DashWallet/Sources/UI/SwiftUI Components/TopIntro.swift @@ -0,0 +1,39 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2025 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct TopIntro: View { + let title: String + var subtitle: String? + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.title1) + .foregroundColor(.primaryText) + + if let subtitle = subtitle { + Text(subtitle) + .font(.subhead) + .foregroundColor(.primaryText) + } + } + + .frame(maxWidth: .infinity, alignment: .leading) + } +} diff --git a/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift b/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift index f516962e3..d17d2cb75 100644 --- a/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift +++ b/DashWallet/Sources/UI/Views/Navigation/BaseNavigationController.swift @@ -79,9 +79,10 @@ class BaseNavigationController: UINavigationController { override init(rootViewController: UIViewController) { super.init(rootViewController: rootViewController) + } - interactivePopGestureRecognizer?.delegate = self - navigationBar.tintColor = .black + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } required init?(coder aDecoder: NSCoder) { @@ -121,6 +122,8 @@ class BaseNavigationController: UINavigationController { super.viewDidLoad() super.delegate = self + interactivePopGestureRecognizer?.delegate = self + navigationBar.tintColor = .black view.backgroundColor = .dw_secondaryBackground() navigationBar.applyOpaqueAppearance(with: .dw_secondaryBackground(), shadowColor: .dw_separatorLine()) } @@ -175,7 +178,6 @@ extension BaseNavigationController: UINavigationControllerDelegate { backButton.frame = .init(x: 0, y: 0, width: 30, height: 30) backButton.setImage(UIImage(systemName: "arrow.backward"), for: .normal) backButton.tintColor = backButtonTintColor - backButton.imageEdgeInsets = .init(top: 0, left: -10, bottom: 0, right: 0) backButton.addTarget(self, action: #selector(backButtonAction), for: .touchUpInside) let item = UIBarButtonItem(customView: backButton) diff --git a/DashWallet/dashwallet-Bridging-Header.h b/DashWallet/dashwallet-Bridging-Header.h index 2c0d9209d..f71f56969 100644 --- a/DashWallet/dashwallet-Bridging-Header.h +++ b/DashWallet/dashwallet-Bridging-Header.h @@ -28,6 +28,7 @@ static const bool _SNAPSHOT = 0; #import "DSChain.h" //MARK: DashWallet +#import "DSAccount+SpentInputCheck.h" #import "DWEnvironment.h" #import "DWTitleDetailCellModel.h" #import "DWTitleDetailItem.h" diff --git a/DashWallet/de.lproj/Localizable.strings b/DashWallet/de.lproj/Localizable.strings index 8db9665ac..9c5ab7553 100644 Binary files a/DashWallet/de.lproj/Localizable.strings and b/DashWallet/de.lproj/Localizable.strings differ diff --git a/DashWallet/de.lproj/Localizable.stringsdict b/DashWallet/de.lproj/Localizable.stringsdict index fe81a5e61..3ac0a05c9 100644 --- a/DashWallet/de.lproj/Localizable.stringsdict +++ b/DashWallet/de.lproj/Localizable.stringsdict @@ -29,7 +29,7 @@ NSStringFormatValueTypeKey d one - %d Händler in %@ + %d Händler in &@ other %d Händler in %@ diff --git a/DashWallet/el.lproj/Localizable.strings b/DashWallet/el.lproj/Localizable.strings index 0c76b58a9..2d7b1bba9 100644 Binary files a/DashWallet/el.lproj/Localizable.strings and b/DashWallet/el.lproj/Localizable.strings differ diff --git a/DashWallet/en.lproj/Localizable.strings b/DashWallet/en.lproj/Localizable.strings index eebe86dd9..e2581ac48 100644 --- a/DashWallet/en.lproj/Localizable.strings +++ b/DashWallet/en.lproj/Localizable.strings @@ -1418,6 +1418,9 @@ /* Translate it as short as possible! (24 symbols max) */ "Local Currency" = "Local Currency"; +/* local currency empty state */ +"No matches for \"%@\"" = "No matches for \"%@\""; + /* No comment provided by engineer. */ "Local requested amount: %@" = "Local requested amount: %@"; @@ -2962,6 +2965,63 @@ /* DashSpend */ "You are buying a %@ gift card for %@ (%d%% discount)" = "You are buying a %1$@ gift card for %2$@ (%3$d%% discount)"; +/* DashSpend */ +"Quantity" = "Quantity"; + +/* DashSpend */ +"Merchant has only %d cards available" = "Merchant has only %d cards available"; + +/* DashSpend */ +"You can buy up to %@ in gift cards per order" = "You can buy up to %@ in gift cards per order"; + +/* DashSpend */ +"Single" = "Single"; + +/* DashSpend */ +"Multiple" = "Multiple"; + +/* DashSpend */ +"Loading gift card options..." = "Loading gift card options..."; + +/* DashSpend */ +"Unable to load merchant details. Please try again." = "Unable to load merchant details. Please try again."; + +/* DashSpend */ +"Gift card details are unavailable. Please contact support." = "Gift card details are unavailable. Please contact support."; + +/* DashSpend */ +"As soon as your code is generated, it will be displayed here" = "As soon as your code is generated, it will be displayed here"; + +/* DashSpend */ +"Original Price" = "Original Price"; + +/* DashSpend */ +"See card details" = "See card details"; + +/* DashSpend */ +"Cashier instructions" = "Cashier instructions"; + +/* DashSpend */ +"Gift card is being prepared" = "Gift card is being prepared"; + +/* DashSpend */ +"View your gift card details" = "View your gift card details"; + +/* DashSpend */ +"Decrease quantity" = "Decrease quantity"; + +/* DashSpend */ +"Increase quantity" = "Increase quantity"; + +/* DashSpend */ +"Double tap to change quantity" = "Double tap to change quantity"; + +/* DashSpend */ +"Unavailable at this limit" = "Unavailable at this limit"; + +/* SegmentedControl */ +"Double tap to select" = "Double tap to select"; + /* No comment provided by engineer. */ "You can always delete the image uploaded, as long as you have access to this wallet." = "You can always delete the image uploaded, as long as you have access to this wallet."; @@ -3187,6 +3247,9 @@ /* Savings percentage */ "~%.0f%%" = "~%.0f%%"; +/* CSV Export */ +"The full transaction history will be exported as a CSV file" = "The full transaction history will be exported as a CSV file"; + /* Import private key */ "Import private key" = "Import private key"; @@ -3204,3 +3267,63 @@ /* Share key */ "Share key" = "Share key"; + +/* Buy Sell Dash */ +"Select a service to buy, sell, convert and transfer Dash" = "Select a service to buy, sell, convert and transfer Dash"; + +/* Buy Sell Dash */ +"Powered by Uphold" = "Powered by Uphold"; + +/* Explore Dash */ +"Find merchants that accept Dash payments" = "Find merchants that accept Dash payments"; + +/* Explore Dash */ +"Find ATMs where you can buy or sell Dash" = "Find ATMs where you can buy or sell Dash"; + +/* CrowdNode */ +"Easily stake Dash and earn passive income with a few simple clicks" = "Easily stake Dash and earn passive income with a few simple clicks"; + +/* CrowdNode */ +"Wait until the chain is fully synced before using CrowdNode." = "Wait until the chain is fully synced before using CrowdNode."; + +/* Backup */ +"Back Up Your Wallet" = "Back Up Your Wallet"; + +/* Backup */ +"Back Up Now" = "Back Up Now"; + +/* Backup */ +"You haven't backed up your recovery phrase yet. Would you like to back up now before removing this shortcut?" = "You haven't backed up your recovery phrase yet. Would you like to back up now before removing this shortcut?"; + +/* Translate it as short as possible! (24 symbols max) */ +"Send to Address" = "Send to Address"; + +/* Translate it as short as possible! (24 symbols max) */ +"Send to Contact" = "Send to Contact"; + +/* Translate it as short as possible! (24 symbols max) */ +"ATM" = "ATM"; + +/* Translate it as short as possible! (24 symbols max) */ +"CrowdNode" = "CrowdNode"; + +/* Shortcut banner */ +"Customize shortcut bar" = "Customize shortcut bar"; + +/* Shortcut banner */ +"Hold any button above to replace it with the function you need" = "Hold any button above to replace it with the function you need"; + +/* No comment provided by engineer. */ +"Select option" = "Select option"; + +/* CSV Export */ +"All payments will be considered as an expense and all incoming transactions will be income.\nThe owner of this wallet is responsible for making any cost basis adjustments in their chosen tax reporting system." = "All payments will be considered as an expense and all incoming transactions will be income.\nThe owner of this wallet is responsible for making any cost basis adjustments in their chosen tax reporting system."; + +/* #bc-ignore! */ +"%ld used" = "%ld used"; + +/* Maya */ +"Convert Dash to %@" = "Convert Dash to %@"; + +/* Maya */ +"Review your Dash to %@ order" = "Review your Dash to %@ order"; diff --git a/DashWallet/es.lproj/Localizable.strings b/DashWallet/es.lproj/Localizable.strings index 5e47a1806..b5c3be9b8 100644 Binary files a/DashWallet/es.lproj/Localizable.strings and b/DashWallet/es.lproj/Localizable.strings differ diff --git a/DashWallet/fa.lproj/Localizable.strings b/DashWallet/fa.lproj/Localizable.strings index fa812eac9..78e90d3ea 100644 Binary files a/DashWallet/fa.lproj/Localizable.strings and b/DashWallet/fa.lproj/Localizable.strings differ diff --git a/DashWallet/fil.lproj/Localizable.strings b/DashWallet/fil.lproj/Localizable.strings index 62a2f5232..869692254 100644 Binary files a/DashWallet/fil.lproj/Localizable.strings and b/DashWallet/fil.lproj/Localizable.strings differ diff --git a/DashWallet/fr.lproj/Localizable.strings b/DashWallet/fr.lproj/Localizable.strings index 67e8ac97a..197179e0f 100644 Binary files a/DashWallet/fr.lproj/Localizable.strings and b/DashWallet/fr.lproj/Localizable.strings differ diff --git a/DashWallet/id.lproj/Localizable.strings b/DashWallet/id.lproj/Localizable.strings index b2f55f93c..f5a639118 100644 Binary files a/DashWallet/id.lproj/Localizable.strings and b/DashWallet/id.lproj/Localizable.strings differ diff --git a/DashWallet/it.lproj/Localizable.strings b/DashWallet/it.lproj/Localizable.strings index a73b12682..b621598e1 100644 Binary files a/DashWallet/it.lproj/Localizable.strings and b/DashWallet/it.lproj/Localizable.strings differ diff --git a/DashWallet/ja.lproj/Localizable.strings b/DashWallet/ja.lproj/Localizable.strings index 94e4e2c3f..88660b44e 100644 Binary files a/DashWallet/ja.lproj/Localizable.strings and b/DashWallet/ja.lproj/Localizable.strings differ diff --git a/DashWallet/ko.lproj/Localizable.strings b/DashWallet/ko.lproj/Localizable.strings index 533b89924..f04657930 100644 Binary files a/DashWallet/ko.lproj/Localizable.strings and b/DashWallet/ko.lproj/Localizable.strings differ diff --git a/DashWallet/nl.lproj/Localizable.strings b/DashWallet/nl.lproj/Localizable.strings index 20f556a2c..90c349504 100644 Binary files a/DashWallet/nl.lproj/Localizable.strings and b/DashWallet/nl.lproj/Localizable.strings differ diff --git a/DashWallet/pl.lproj/Localizable.strings b/DashWallet/pl.lproj/Localizable.strings index 2e567a627..8cd639326 100644 Binary files a/DashWallet/pl.lproj/Localizable.strings and b/DashWallet/pl.lproj/Localizable.strings differ diff --git a/DashWallet/pt.lproj/Localizable.strings b/DashWallet/pt.lproj/Localizable.strings index bff4574dc..a722d6f99 100644 Binary files a/DashWallet/pt.lproj/Localizable.strings and b/DashWallet/pt.lproj/Localizable.strings differ diff --git a/DashWallet/ru.lproj/Localizable.strings b/DashWallet/ru.lproj/Localizable.strings index a15897ed9..733b515ca 100644 Binary files a/DashWallet/ru.lproj/Localizable.strings and b/DashWallet/ru.lproj/Localizable.strings differ diff --git a/DashWallet/sk.lproj/Localizable.strings b/DashWallet/sk.lproj/Localizable.strings index 672e342ca..7aac17df7 100644 Binary files a/DashWallet/sk.lproj/Localizable.strings and b/DashWallet/sk.lproj/Localizable.strings differ diff --git a/DashWallet/th.lproj/Localizable.strings b/DashWallet/th.lproj/Localizable.strings index 1b9f666f7..f01134ffa 100644 Binary files a/DashWallet/th.lproj/Localizable.strings and b/DashWallet/th.lproj/Localizable.strings differ diff --git a/DashWallet/tr.lproj/Localizable.strings b/DashWallet/tr.lproj/Localizable.strings index 98531b54f..86184a8c0 100644 Binary files a/DashWallet/tr.lproj/Localizable.strings and b/DashWallet/tr.lproj/Localizable.strings differ diff --git a/DashWallet/uk.lproj/Localizable.strings b/DashWallet/uk.lproj/Localizable.strings index 72e31788d..a45c4d546 100644 Binary files a/DashWallet/uk.lproj/Localizable.strings and b/DashWallet/uk.lproj/Localizable.strings differ diff --git a/DashWallet/zh.lproj/Localizable.strings b/DashWallet/zh.lproj/Localizable.strings index 8a1922e47..026855e11 100644 Binary files a/DashWallet/zh.lproj/Localizable.strings and b/DashWallet/zh.lproj/Localizable.strings differ diff --git a/DashWallet/zh_TW.lproj/Localizable.strings b/DashWallet/zh_TW.lproj/Localizable.strings index b4419d975..f7e5bdc75 100644 Binary files a/DashWallet/zh_TW.lproj/Localizable.strings and b/DashWallet/zh_TW.lproj/Localizable.strings differ diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/BackgroundColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/BackgroundColor.colorset/Contents.json index d8d88dd78..df29b2875 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/BackgroundColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/BackgroundColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x1F", - "green" : "0x1C", - "red" : "0x19" + "blue" : "0x24", + "green" : "0x1F", + "red" : "0x1E" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha30.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha30.colorset/Contents.json new file mode 100644 index 000000000..360988d00 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha30.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha40.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha40.colorset/Contents.json new file mode 100644 index 000000000..67e1abfa6 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha40.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha5.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha5.colorset/Contents.json new file mode 100644 index 000000000..b6e267a16 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha5.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.050", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.050", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha50.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha50.colorset/Contents.json new file mode 100644 index 000000000..0d3110416 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha50.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha8.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha8.colorset/Contents.json new file mode 100644 index 000000000..fe2fe0c48 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha8.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.080", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.080", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Blue/BlueAlpha5.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Blue/BlueAlpha5.colorset/Contents.json index 7389f4f04..fd0b5f8d4 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/Blue/BlueAlpha5.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Blue/BlueAlpha5.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "0.050", - "blue" : "0xE4", - "green" : "0x8D", - "red" : "0x00" + "blue" : "0.894", + "green" : "0.553", + "red" : "0.000" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Blue/BlueAlpha90.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Blue/BlueAlpha90.colorset/Contents.json new file mode 100644 index 000000000..3cd7927ef --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Blue/BlueAlpha90.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.900", + "blue" : "0xE4", + "green" : "0x8D", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black900.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Purple.colorset/Contents.json similarity index 75% rename from Shared/Resources/SharedAssets.xcassets/Colors/Black/Black900.colorset/Contents.json rename to Shared/Resources/SharedAssets.xcassets/Colors/Custom/Purple.colorset/Contents.json index 3a5c28357..902de42d2 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/Black/Black900.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Purple.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x2E", - "green" : "0x2B", - "red" : "0x29" + "blue" : "0xD6", + "green" : "0x56", + "red" : "0x58" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Topper.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Topper.colorset/Contents.json new file mode 100644 index 000000000..56c98baa5 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Topper.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x92", + "green" : "0xF2", + "red" : "0xBC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Uphold.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Uphold.colorset/Contents.json new file mode 100644 index 000000000..977a00e47 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Custom/Uphold.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x68", + "green" : "0xCC", + "red" : "0x49" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black.colorset/Contents.json new file mode 100644 index 000000000..f910aa0af --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha10.colorset/Contents.json new file mode 100644 index 000000000..e0f3d97b9 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha10.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha15.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha15.colorset/Contents.json new file mode 100644 index 000000000..e74d8e631 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha15.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.150", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha20.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha20.colorset/Contents.json new file mode 100644 index 000000000..41ddad9bf --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha20.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.200", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha30.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha30.colorset/Contents.json new file mode 100644 index 000000000..e7760a027 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha30.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha40.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha40.colorset/Contents.json new file mode 100644 index 000000000..67e1abfa6 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha40.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha5.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha5.colorset/Contents.json new file mode 100644 index 000000000..144cd022c --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha5.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.050", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha50.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha50.colorset/Contents.json new file mode 100644 index 000000000..6ca6d9858 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha50.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha60.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha60.colorset/Contents.json new file mode 100644 index 000000000..6d82921df --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha60.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha70.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha70.colorset/Contents.json new file mode 100644 index 000000000..5b821e0ae --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha70.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.700", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha8.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha8.colorset/Contents.json new file mode 100644 index 000000000..2eec24faf --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha8.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.080", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha80.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha80.colorset/Contents.json new file mode 100644 index 000000000..699490756 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha80.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.800", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha90.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha90.colorset/Contents.json new file mode 100644 index 000000000..28075d2e0 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Black1000Alpha90.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.900", + "blue" : "0x0D", + "green" : "0x0B", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black800.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black800.colorset/Contents.json new file mode 100644 index 000000000..03c0538f6 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black800.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x24", + "green" : "0x1F", + "red" : "0x1E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black900.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black900.colorset/Contents.json new file mode 100644 index 000000000..0bac5129f --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Black900.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x19", + "green" : "0x15", + "red" : "0x14" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray100.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray100.colorset/Contents.json index afdecb1fe..e108678dd 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray100.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray100.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xF7", - "green" : "0xF6", - "red" : "0xF5" + "blue" : "0xEE", + "green" : "0xED", + "red" : "0xEB" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray200.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray200.colorset/Contents.json index afdecb1fe..0f096e69c 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray200.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray200.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xF7", - "green" : "0xF6", - "red" : "0xF5" + "blue" : "0xD5", + "green" : "0xD2", + "red" : "0xCE" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha10.colorset/Contents.json similarity index 100% rename from Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha10.colorset/Contents.json rename to Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha10.colorset/Contents.json diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha20.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha20.colorset/Contents.json new file mode 100644 index 000000000..070a90f2b --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha20.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.200", + "blue" : "0xBC", + "green" : "0xB6", + "red" : "0xB0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha30.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha30.colorset/Contents.json similarity index 100% rename from Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha30.colorset/Contents.json rename to Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha30.colorset/Contents.json diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha40.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha40.colorset/Contents.json similarity index 100% rename from Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha40.colorset/Contents.json rename to Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha40.colorset/Contents.json diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha5.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha5.colorset/Contents.json similarity index 100% rename from Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha5.colorset/Contents.json rename to Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha5.colorset/Contents.json diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha50.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha50.colorset/Contents.json similarity index 100% rename from Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha50.colorset/Contents.json rename to Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha50.colorset/Contents.json diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha20.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha60.colorset/Contents.json similarity index 91% rename from Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha20.colorset/Contents.json rename to Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha60.colorset/Contents.json index 35104f33d..90f0afc27 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha20.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha60.colorset/Contents.json @@ -4,7 +4,7 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0.200", + "alpha" : "0.600", "blue" : "0xBC", "green" : "0xB6", "red" : "0xB0" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha70.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha70.colorset/Contents.json new file mode 100644 index 000000000..88651db68 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha70.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.700", + "blue" : "0xBC", + "green" : "0xB6", + "red" : "0xB0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha80.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha80.colorset/Contents.json new file mode 100644 index 000000000..c74063053 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha80.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.800", + "blue" : "0xBC", + "green" : "0xB6", + "red" : "0xB0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha90.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha90.colorset/Contents.json new file mode 100644 index 000000000..fcf75e8b5 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300/Gray300Alpha90.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.900", + "blue" : "0xBC", + "green" : "0xB6", + "red" : "0xB0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha10.colorset/Contents.json new file mode 100644 index 000000000..284bfd853 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha10.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0x8A", + "green" : "0x80", + "red" : "0x75" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha13.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha13.colorset/Contents.json new file mode 100644 index 000000000..69d4a94c9 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha13.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.130", + "blue" : "0x8A", + "green" : "0x80", + "red" : "0x75" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha25.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha25.colorset/Contents.json new file mode 100644 index 000000000..f3ed82f77 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray400/Gray400Alpha25.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.250", + "blue" : "0x8A", + "green" : "0x80", + "red" : "0x75" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Green/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Green/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Green/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Green/Green.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Green/Green.colorset/Contents.json new file mode 100644 index 000000000..fb6c5b33d --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Green/Green.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.540", + "green" : "0.710", + "red" : "0.240" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Green/GreenAlpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Green/GreenAlpha10.colorset/Contents.json new file mode 100644 index 000000000..1c4394978 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Green/GreenAlpha10.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0.540", + "green" : "0.710", + "red" : "0.240" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/LightBlue.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/LightBlue.colorset/Contents.json new file mode 100644 index 000000000..b53905945 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/LightBlue.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.960", + "green" : "0.770", + "red" : "0.470" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/LightBlueAlpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/LightBlueAlpha10.colorset/Contents.json new file mode 100644 index 000000000..d30eb8472 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/LightBlue/LightBlueAlpha10.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0.960", + "green" : "0.770", + "red" : "0.470" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json index c6e5d3d43..a61d481a7 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/NumberKeyboardHighlightedTextColor.colorset/Contents.json @@ -1,20 +1,20 @@ { - "info" : { - "version" : 1, - "author" : "xcode" - }, "colors" : [ { - "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { - "red" : "1.000", "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000" + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" } - } + }, + "idiom" : "universal" } - ] -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Orange/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Orange/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Orange/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Orange/Orange.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Orange/Orange.colorset/Contents.json new file mode 100644 index 000000000..5c7720af3 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Orange/Orange.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.410", + "green" : "0.570", + "red" : "0.980" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Orange/OrangeAlpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Orange/OrangeAlpha10.colorset/Contents.json new file mode 100644 index 000000000..631db2672 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Orange/OrangeAlpha10.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0.410", + "green" : "0.570", + "red" : "0.980" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Red/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Red/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Red/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Red/Red.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Red/Red.colorset/Contents.json new file mode 100644 index 000000000..45a86363b --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Red/Red.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.260", + "green" : "0.220", + "red" : "0.920" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Red/RedAlpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Red/RedAlpha10.colorset/Contents.json new file mode 100644 index 000000000..9f92bcefb --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Red/RedAlpha10.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0.260", + "green" : "0.220", + "red" : "0.920" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Red/RedAlpha5.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Red/RedAlpha5.colorset/Contents.json new file mode 100644 index 000000000..b12927bcf --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Red/RedAlpha5.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.050", + "blue" : "0.260", + "green" : "0.220", + "red" : "0.920" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/SearchBackground.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/SearchBackground.colorset/Contents.json new file mode 100644 index 000000000..284bfd853 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/SearchBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0x8A", + "green" : "0x80", + "red" : "0x75" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json index c60cc7caa..7e8f38fe3 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/ShadowColor.colorset/Contents.json @@ -1,20 +1,20 @@ { - "info" : { - "version" : 1, - "author" : "xcode" - }, "colors" : [ { - "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { - "red" : "0.000", "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.000" + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" } - } + }, + "idiom" : "universal" } - ] -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/System/GreenColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/System/GreenColor.colorset/Contents.json index 531daaad0..8cd05445e 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/System/GreenColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/System/GreenColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x89", - "green" : "0xB4", - "red" : "0x3E" + "blue" : "0.537", + "green" : "0.706", + "red" : "0.243" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json index eb69d3844..f17e17924 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/TertiaryBackgroundColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "250", - "green" : "250", - "red" : "250" + "blue" : "0xFA", + "green" : "0xFA", + "red" : "0xFA" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "35", - "green" : "32", - "red" : "29" + "blue" : "0x23", + "green" : "0x20", + "red" : "0x1D" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/TextColors/TertiaryTextColor.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/TextColors/TertiaryTextColor.colorset/Contents.json index a9ef40fea..6f0668788 100644 --- a/Shared/Resources/SharedAssets.xcassets/Colors/TextColors/TertiaryTextColor.colorset/Contents.json +++ b/Shared/Resources/SharedAssets.xcassets/Colors/TextColors/TertiaryTextColor.colorset/Contents.json @@ -22,10 +22,10 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "1.000", - "blue" : "0xDB", - "green" : "0xDB", - "red" : "0xDB" + "alpha" : "0.600", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/Yellow.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/Yellow.colorset/Contents.json new file mode 100644 index 000000000..387c8b2a1 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/Yellow.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x43", + "green" : "0xC0", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/YellowAlpha10.colorset/Contents.json b/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/YellowAlpha10.colorset/Contents.json new file mode 100644 index 000000000..bd7ff6630 --- /dev/null +++ b/Shared/Resources/SharedAssets.xcassets/Colors/Yellow/YellowAlpha10.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0x43", + "green" : "0xC0", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fastlane/metadata/ja/description.txt b/fastlane/metadata/ja/description.txt index 64fc5461c..13f97bdb9 100644 --- a/fastlane/metadata/ja/description.txt +++ b/fastlane/metadata/ja/description.txt @@ -10,11 +10,11 @@ DashウォレットはDashネットワークに直接接続する最初のiOSウ * オンラインでも対面でも、Dashでの支払いを瞬時に送受信 * Dashに対応する事業者やウェブサイトへのワンタッチ決済 -* 顔認証で速やかな支払い +*顔認証で速やかな支払い * ペーパーウォレット(コールドストレージとして使われるもの)からの取り出しができます。 -* ユーザーのDashの売買を可能にするサードパーティの統合 +*ユーザーのDashの売買を可能にするサードパーティの統合 * デバイスの紛失・破損時でも簡単な復元フレーズによって資金を回復することが可能 -* 様々なカスタムセキュリティに対する志向に対応する柔軟な設定 +*様々なカスタムセキュリティに対する志向に対応する柔軟な設定 * ダイレクトDashネットワーククライアント - ハッキングされたりダウンする中央サーバーがありません Dashウォレットはオープンソースで無料です。 追加のトランザクション手数料やその他のコストはかかりません。 瞬時に、世界中の誰にでも、金額を問わず送金ができます。 これがお金のあるべき姿です。 \ No newline at end of file