diff --git a/BeamWallet.xcodeproj/project.pbxproj b/BeamWallet.xcodeproj/project.pbxproj index 7a6b3ad0..3e608904 100644 --- a/BeamWallet.xcodeproj/project.pbxproj +++ b/BeamWallet.xcodeproj/project.pbxproj @@ -3,13 +3,85 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 000A92658A9461591881CE8C /* SplitCoinsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23EA93CE3207A2EBB4D17FD /* SplitCoinsViewModel.swift */; }; + 00947887B27F2F825E1E67CC /* MessengerNewChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CF5C205DC060CADA96F025 /* MessengerNewChatViewController.swift */; }; + 010A73D4AEEB9251461112AB /* dapps_store_app.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 5FEED174B3A5FDB8D14C9963 /* dapps_store_app.wasm */; }; + 042BE224A8CF0171B513BED0 /* OraclePriceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE8E2F17C9FA369F86F60DB /* OraclePriceManager.swift */; }; + 05548629568C6211F87DA630 /* MessengerChatListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DFF1052A687C5F2265E618F /* MessengerChatListViewController.swift */; }; + 066620C96ABA1816759B7AE3 /* MessengerConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A439F6A64EB61AD3DF6945B /* MessengerConversationViewModel.swift */; }; 09BB8194C318850B9C687050 /* Pods_BeamWallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B5894AC3136298FE0B7E8E3 /* Pods_BeamWallet.framework */; }; + 0A44DCF3575DA823BA60DEC5 /* OraclePriceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE8E2F17C9FA369F86F60DB /* OraclePriceManager.swift */; }; + 0DB9711D993CFA1D225B53BA /* BMInstalledDApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 788AE10382B92E35A434F2E3 /* BMInstalledDApp.m */; }; 0E952497DFB46C7621546999 /* Pods_BeamWalletTestNet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36A85446EDA45DA6335930C5 /* Pods_BeamWalletTestNet.framework */; }; + 0EC9234AAD2DC0A1BF7600B4 /* AssetUTXOSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8865BEE3DDBE9806B38DDE04 /* AssetUTXOSectionHeaderView.swift */; }; + 11476E71A287700B5F7CBB8F /* libasio-ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C08DFF83966F34F6D7F6AFC /* libasio-ipfs.a */; }; + 138076EB2F04AEFB33F90387 /* libasio-ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C08DFF83966F34F6D7F6AFC /* libasio-ipfs.a */; }; + 14458CA45E1281FAF80D496D /* MessengerNewChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CF5C205DC060CADA96F025 /* MessengerNewChatViewController.swift */; }; + 15A094B1963EAE81E9B86B33 /* BMDexOrder.mm in Sources */ = {isa = PBXBuildFile; fileRef = 46577647151F6240CBA9CE2D /* BMDexOrder.mm */; }; + 187B4E604FD2814B8018C796 /* AssetUTXOListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AA87DBB7C958778D72096C /* AssetUTXOListViewController.swift */; }; + 1A6B80C9C9EDCE967358F721 /* BMPublisher.m in Sources */ = {isa = PBXBuildFile; fileRef = 5947BB8B40C5D62DAA9D01BF /* BMPublisher.m */; }; + 1C28371060472F1EFAF00E7B /* DAppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B64630C2A5874889F91FC /* DAppStore.swift */; }; + 215454B3AA22BD789F53A90C /* BMChat.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8C5EB5519EB54866BA8DB9A4 /* BMChat.mm */; }; + 21C1E1B8354DCDBDB0A9ABB8 /* AssetUTXOListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AA87DBB7C958778D72096C /* AssetUTXOListViewController.swift */; }; + 2361D1F909D78EC50B8C5D5C /* MessengerConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A439F6A64EB61AD3DF6945B /* MessengerConversationViewModel.swift */; }; + 26D73AAEC26B985DD2704C90 /* BMAvailableDApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 17A4C97F6AE81EDDEC7DB4B9 /* BMAvailableDApp.m */; }; + 275E60FB3A8D4704581DF986 /* SplitCoinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5FD8229312777D1D2BB813 /* SplitCoinsViewController.swift */; }; + 28B1AF7C15F50CEC869952BD /* DAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B86688612ED1A5FF45D1BAD /* DAppManager.swift */; }; + 2964ED046A04435DB6FD977D /* BMAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C2A22B96525EB07D51F607 /* BMAlertViewController.swift */; }; + 2B7C41C938A0B67A9D143E3D /* MyDAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008792DADDD5C9FF28341A31 /* MyDAppsViewController.swift */; }; + 2C2D70373756666705D14FDE /* AssetSwapCreateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B1E2B05648164FC49C46F7 /* AssetSwapCreateViewModel.swift */; }; + 2F81D3CC162C2FD8201B35C0 /* libipfs-bindings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 344F317D8FBFA32FFDA811F2 /* libipfs-bindings.a */; }; + 30BE993E2DFFA6452A536125 /* BMInstantMessage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 20B7F5341703980CD2D3941F /* BMInstantMessage.mm */; }; + 31509E4F991BC56BF7F10098 /* AssetSwapDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C9307A797CCA7FEAABBFD8 /* AssetSwapDetailsViewModel.swift */; }; + 35726BA6BFE79F50A19E7054 /* MessengerChatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390F813D5F08AC3C0F47183F /* MessengerChatCell.swift */; }; + 3C4B4023BBF6C6624DDC279E /* BMOptionPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ECF740A61C5F0D941B8B36 /* BMOptionPickerViewController.swift */; }; + 434CC206E4AD461CECAD5DFB /* BMInstantMessage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 20B7F5341703980CD2D3941F /* BMInstantMessage.mm */; }; + 46E422749DE0BAE1BCF8B4EB /* BMPublisher.m in Sources */ = {isa = PBXBuildFile; fileRef = 5947BB8B40C5D62DAA9D01BF /* BMPublisher.m */; }; + 492305E92FA7A41129BF7346 /* MessengerBubbleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E08B41CEDA5A06EBFD775E76 /* MessengerBubbleCell.swift */; }; + 4B2FC15209C14B359E3651BD /* dapps_store_app.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 5FEED174B3A5FDB8D14C9963 /* dapps_store_app.wasm */; }; + 4B72CD41AF6A8F47020BC536 /* AssetUTXOListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AA87DBB7C958778D72096C /* AssetUTXOListViewController.swift */; }; + 4D7C86E3D4DAAAD7DDFE22E7 /* MessengerChatListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DFF1052A687C5F2265E618F /* MessengerChatListViewController.swift */; }; + 50B804DB7153D959B75A9D13 /* BMAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C2A22B96525EB07D51F607 /* BMAlertViewController.swift */; }; + 5101D1DB95B6BECFE333B53C /* AssetUTXOSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8865BEE3DDBE9806B38DDE04 /* AssetUTXOSectionHeaderView.swift */; }; + 532955652A405010EC21607C /* AssetUTXOSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8865BEE3DDBE9806B38DDE04 /* AssetUTXOSectionHeaderView.swift */; }; 53E3D6F8F19BD21A69380CBA /* Pods_BeamWalletMasterNet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18CF009621EA8E5970DA1D87 /* Pods_BeamWalletMasterNet.framework */; }; + 56DCA641FEC90020A7C2686E /* PublishersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6831B2B28CCBD5D776DCF34B /* PublishersViewController.swift */; }; + 59D5C848EC6C80442BC6A8A3 /* BMOptionPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ECF740A61C5F0D941B8B36 /* BMOptionPickerViewController.swift */; }; + 5A1A647678A2061222332D16 /* MessengerChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E4020D11686B93F29A46DA /* MessengerChatViewController.swift */; }; + 5A4A0ACB5C6CA50F1E5FCBE8 /* MessengerChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFF464435B3D65DCFC432FA3 /* MessengerChatListViewModel.swift */; }; + 5DAA3CCCB00AECEACD182FBF /* AssetSwapOrderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDD341D6CFC3AD26C3F4374 /* AssetSwapOrderCell.swift */; }; + 6029A6A13D53093CF6EC2FB6 /* dapps_store_app.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 5FEED174B3A5FDB8D14C9963 /* dapps_store_app.wasm */; }; + 610B9971C155D841609ACE0F /* libdex.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 57D7E7F685327E2CAB9E7C69 /* libdex.a */; }; + 62D6B4FFAD6FCD00C5206630 /* AssetSwapsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21968CB26C3B977852247761 /* AssetSwapsViewController.swift */; }; + 6315A76D06343C9CEDE2EF77 /* AssetSwapsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21968CB26C3B977852247761 /* AssetSwapsViewController.swift */; }; + 65A9895370E84E09300D7548 /* PublishersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6831B2B28CCBD5D776DCF34B /* PublishersViewController.swift */; }; + 6854C506C6836F88B8F8484E /* SplitCoinsStripeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 603CF8C20147C7F9E087C33D /* SplitCoinsStripeView.swift */; }; + 69B13BFFB153BF4611F30888 /* MessengerNewChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CF5C205DC060CADA96F025 /* MessengerNewChatViewController.swift */; }; + 6EA6E49364A07F3740D84921 /* SplitCoinsStripeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 603CF8C20147C7F9E087C33D /* SplitCoinsStripeView.swift */; }; + 6F6101D0D3E633F1D68554F9 /* DAppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B64630C2A5874889F91FC /* DAppStore.swift */; }; + 705F9944DC83B6CC6816678F /* BMOptionPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ECF740A61C5F0D941B8B36 /* BMOptionPickerViewController.swift */; }; + 7076239CA6CBD5D923ED8EE8 /* libipfs-bindings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 344F317D8FBFA32FFDA811F2 /* libipfs-bindings.a */; }; + 7209CB87405DF7AC0B7CBE89 /* dao-core-app.dapp in Resources */ = {isa = PBXBuildFile; fileRef = 9CC651F674E6883AFB9709D5 /* dao-core-app.dapp */; }; + 75516585C510BB694A1B9AED /* MessengerBubbleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E08B41CEDA5A06EBFD775E76 /* MessengerBubbleCell.swift */; }; + 75B069A9EC9A517ABCACFC1E /* MessengerChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E4020D11686B93F29A46DA /* MessengerChatViewController.swift */; }; + 77CF51BBE36DCCD780857772 /* AssetSwapCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A43055CFB5C5820B099A1 /* AssetSwapCreateViewController.swift */; }; + 7A9AD3ED311827FB6EF39D54 /* BMAvailableDApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 17A4C97F6AE81EDDEC7DB4B9 /* BMAvailableDApp.m */; }; + 7AB2A2A48540A9D102F56D7C /* DAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B86688612ED1A5FF45D1BAD /* DAppManager.swift */; }; + 7ED2D2980F560C573C46D8CC /* AssetSwapsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDAF01730E8C754AA8C5175 /* AssetSwapsViewModel.swift */; }; + 82EC6CA54D5C74FD16085DE0 /* DAppStoreCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E72505C417B965A57BB066 /* DAppStoreCells.swift */; }; + 8313E7C13424087CE8545F59 /* PublishersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6831B2B28CCBD5D776DCF34B /* PublishersViewController.swift */; }; + 83EC213430E0CFD381D45A13 /* AssetSwapDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C9307A797CCA7FEAABBFD8 /* AssetSwapDetailsViewModel.swift */; }; + 84D2719B950F43294039957A /* AssetSwapOrderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDD341D6CFC3AD26C3F4374 /* AssetSwapOrderCell.swift */; }; + 87B508D6F91B6BCDE6EC712F /* BMInstantMessage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 20B7F5341703980CD2D3941F /* BMInstantMessage.mm */; }; + 88D6972926A283D03D289A48 /* AssetSwapsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDAF01730E8C754AA8C5175 /* AssetSwapsViewModel.swift */; }; + 8B70C9ED979CE8D4574FC1EA /* libwallet_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AC8EF915253F6CCEA3788BE /* libwallet_ipfs.a */; }; + 8E6EABD786898410576636A4 /* BMChat.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8C5EB5519EB54866BA8DB9A4 /* BMChat.mm */; }; + 8F9D17159CF9400338FE47EB /* oracle2-app.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 3149E0903940C330CAE827F5 /* oracle2-app.wasm */; }; + 911AF3DF99FDD7FFD07F4830 /* AssetUTXOGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9B63F6E98BA8303EE2FEAD /* AssetUTXOGroup.swift */; }; 9207DD8726778DF900250E80 /* AssetsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9207DD8626778DF900250E80 /* AssetsViewController.swift */; }; 9207DD8826778DF900250E80 /* AssetsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9207DD8626778DF900250E80 /* AssetsViewController.swift */; }; 9207DD8926778DF900250E80 /* AssetsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9207DD8626778DF900250E80 /* AssetsViewController.swift */; }; @@ -282,9 +354,6 @@ 922B637C2552F1CA00A2AED2 /* ReceiveTokenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922B637A2552F1CA00A2AED2 /* ReceiveTokenCell.swift */; }; 922B637D2552F1CA00A2AED2 /* ReceiveTokenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922B637A2552F1CA00A2AED2 /* ReceiveTokenCell.swift */; }; 922B637E2552F1CA00A2AED2 /* ReceiveTokenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922B637A2552F1CA00A2AED2 /* ReceiveTokenCell.swift */; }; - 922B637F2552F1CA00A2AED2 /* ReceiveTokenCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 922B637B2552F1CA00A2AED2 /* ReceiveTokenCell.xib */; }; - 922B63802552F1CA00A2AED2 /* ReceiveTokenCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 922B637B2552F1CA00A2AED2 /* ReceiveTokenCell.xib */; }; - 922B63812552F1CA00A2AED2 /* ReceiveTokenCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 922B637B2552F1CA00A2AED2 /* ReceiveTokenCell.xib */; }; 922F2E7122E1F7F6005D3F33 /* BMMultiLineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922F2E7022E1F7F6005D3F33 /* BMMultiLineItem.swift */; }; 922F2E7222E1F7F6005D3F33 /* BMMultiLineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922F2E7022E1F7F6005D3F33 /* BMMultiLineItem.swift */; }; 922F2E7322E1F7F6005D3F33 /* BMMultiLineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922F2E7022E1F7F6005D3F33 /* BMMultiLineItem.swift */; }; @@ -373,9 +442,6 @@ 9244A5102A01465E007B03D0 /* libkeykeeper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4E82A014656007B03D0 /* libkeykeeper.a */; }; 9244A5112A01465E007B03D0 /* libkeykeeper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4E82A014656007B03D0 /* libkeykeeper.a */; }; 9244A5122A01465E007B03D0 /* libkeykeeper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4E82A014656007B03D0 /* libkeykeeper.a */; }; - 9244A5132A01465E007B03D0 /* libwallet_api_no_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4E92A014656007B03D0 /* libwallet_api_no_ipfs.a */; }; - 9244A5142A01465E007B03D0 /* libwallet_api_no_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4E92A014656007B03D0 /* libwallet_api_no_ipfs.a */; }; - 9244A5152A01465E007B03D0 /* libwallet_api_no_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4E92A014656007B03D0 /* libwallet_api_no_ipfs.a */; }; 9244A5162A01465E007B03D0 /* libre2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4EA2A014656007B03D0 /* libre2.a */; }; 9244A5172A01465E007B03D0 /* libre2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4EA2A014656007B03D0 /* libre2.a */; }; 9244A5182A01465E007B03D0 /* libre2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A4EA2A014656007B03D0 /* libre2.a */; }; @@ -451,9 +517,6 @@ 9244A55E2A01465E007B03D0 /* libkeccak.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A5022A01465D007B03D0 /* libkeccak.a */; }; 9244A55F2A01465E007B03D0 /* libkeccak.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A5022A01465D007B03D0 /* libkeccak.a */; }; 9244A5602A01465E007B03D0 /* libkeccak.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A5022A01465D007B03D0 /* libkeccak.a */; }; - 9244A5612A01465E007B03D0 /* libwallet_client_no_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A5032A01465E007B03D0 /* libwallet_client_no_ipfs.a */; }; - 9244A5622A01465E007B03D0 /* libwallet_client_no_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A5032A01465E007B03D0 /* libwallet_client_no_ipfs.a */; }; - 9244A5632A01465E007B03D0 /* libwallet_client_no_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9244A5032A01465E007B03D0 /* libwallet_client_no_ipfs.a */; }; 9246F75A227C48830091FB0B /* BMEmptyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9246F758227C48830091FB0B /* BMEmptyCell.swift */; }; 9246F75B227C48830091FB0B /* BMEmptyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9246F758227C48830091FB0B /* BMEmptyCell.swift */; }; 9246F75C227C48830091FB0B /* BMEmptyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9246F758227C48830091FB0B /* BMEmptyCell.swift */; }; @@ -1354,17 +1417,89 @@ 92FE264122A11E4E00102DB4 /* BMExpandCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 92FE263A22A11E4D00102DB4 /* BMExpandCell.xib */; }; 92FE264222A11E4E00102DB4 /* BMExpandCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 92FE263A22A11E4D00102DB4 /* BMExpandCell.xib */; }; 92FE264322A11E4E00102DB4 /* BMExpandCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 92FE263A22A11E4D00102DB4 /* BMExpandCell.xib */; }; + 94E405DB59B47772F0662861 /* AssetSwapCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A43055CFB5C5820B099A1 /* AssetSwapCreateViewController.swift */; }; + 966EF2ACFF217F06AE183E6E /* SplitCoinsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23EA93CE3207A2EBB4D17FD /* SplitCoinsViewModel.swift */; }; + 984E5950BBB86D093251AB57 /* MessengerChatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390F813D5F08AC3C0F47183F /* MessengerChatCell.swift */; }; + 9A1790F44DE275FD9644A9F7 /* AssetSwapCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A43055CFB5C5820B099A1 /* AssetSwapCreateViewController.swift */; }; + 9C3478CC0AB10D577E97E53E /* MessengerChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFF464435B3D65DCFC432FA3 /* MessengerChatListViewModel.swift */; }; + 9C5E69AB2AC0AC5E4D122185 /* BMInstalledDApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 788AE10382B92E35A434F2E3 /* BMInstalledDApp.m */; }; + 9D5813AA76650CB6EBA22D34 /* dao-voting-app.dapp in Resources */ = {isa = PBXBuildFile; fileRef = 8923664227A70DFEBE27B5ED /* dao-voting-app.dapp */; }; + 9D8BAACCF64EA977E69273F0 /* libdex.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 57D7E7F685327E2CAB9E7C69 /* libdex.a */; }; + 9E75E31529DE385C7D153631 /* MessengerChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E4020D11686B93F29A46DA /* MessengerChatViewController.swift */; }; + A04389E8E8EE7AB9E4D87007 /* BMChat.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8C5EB5519EB54866BA8DB9A4 /* BMChat.mm */; }; + A4044FC0C7CFF97F7D02D3F5 /* DAppURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCB8FF9B7300E47ABEF4D9A /* DAppURLSchemeHandler.swift */; }; + A4D6D5A9EC5F4DC3DD3E4682 /* oracle2-app.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 3149E0903940C330CAE827F5 /* oracle2-app.wasm */; }; + A522EB1A22FF64A990518431 /* AssetSwapDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A467070BE6633F1B715DAD /* AssetSwapDetailsViewController.swift */; }; + A7290B4EEDC78DFD74B80F6F /* DAppURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCB8FF9B7300E47ABEF4D9A /* DAppURLSchemeHandler.swift */; }; + A8131AC203B7FC699E7DCA45 /* BMAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C2A22B96525EB07D51F607 /* BMAlertViewController.swift */; }; + AAE84220C256A1EFAB30D9B3 /* DAppURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCB8FF9B7300E47ABEF4D9A /* DAppURLSchemeHandler.swift */; }; + AAFE8C80296CA6DAA3BC3B1F /* AssetUTXOGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9B63F6E98BA8303EE2FEAD /* AssetUTXOGroup.swift */; }; + AB6BA79FF389E013065D35C9 /* libdex_board.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F426BB6F15C2E08A3E81B87D /* libdex_board.a */; }; + ABA34015221EA45051CC20EB /* SplitCoinsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23EA93CE3207A2EBB4D17FD /* SplitCoinsViewModel.swift */; }; + AC3CF90BF3C1ED7E2D19D65A /* SplitCoinPreviewCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B33EC6A5FD1571ED9D1EFFB /* SplitCoinPreviewCard.swift */; }; + ACD5F2A82D411FA025F5709A /* DAppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B64630C2A5874889F91FC /* DAppStore.swift */; }; + AD534670E90AA9554A8C5F74 /* DAppStoreCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E72505C417B965A57BB066 /* DAppStoreCells.swift */; }; + B3476711F58F83C2F1CB8F2C /* AssetUTXOGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9B63F6E98BA8303EE2FEAD /* AssetUTXOGroup.swift */; }; + B4FE33729385D12603589437 /* AssetSwapDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A467070BE6633F1B715DAD /* AssetSwapDetailsViewController.swift */; }; + B5918DE6DE94C43C0CBB411F /* libwallet_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AC8EF915253F6CCEA3788BE /* libwallet_ipfs.a */; }; + B5E7CC16D70E7D12DD9315FC /* MessengerConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A439F6A64EB61AD3DF6945B /* MessengerConversationViewModel.swift */; }; + B6037162941C5AB71A356FF3 /* AssetSwapDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A467070BE6633F1B715DAD /* AssetSwapDetailsViewController.swift */; }; + B93707F60996A4898F0B88E2 /* NodePeersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13188DF7FB2332EDBB6929F5 /* NodePeersViewController.swift */; }; + BCB254DF739D2FA8FF92C672 /* AssetSwapDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C9307A797CCA7FEAABBFD8 /* AssetSwapDetailsViewModel.swift */; }; + BDDA78B4E7DDB519C8591B40 /* BMAvailableDApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 17A4C97F6AE81EDDEC7DB4B9 /* BMAvailableDApp.m */; }; + BE96C4A71A3D47721D003ACF /* AssetSwapOrderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDD341D6CFC3AD26C3F4374 /* AssetSwapOrderCell.swift */; }; + BFF9839E0FE16D5A8645D394 /* SplitCoinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5FD8229312777D1D2BB813 /* SplitCoinsViewController.swift */; }; + C07BD2850E0E4E55AC706985 /* AssetSwapsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21968CB26C3B977852247761 /* AssetSwapsViewController.swift */; }; + C51DBD848CA77EC12F48698C /* BMPublisher.m in Sources */ = {isa = PBXBuildFile; fileRef = 5947BB8B40C5D62DAA9D01BF /* BMPublisher.m */; }; + C6FB138B2A261CBC43626204 /* SplitCoinsStripeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 603CF8C20147C7F9E087C33D /* SplitCoinsStripeView.swift */; }; + CA7E68B52D3092D76CBACF7C /* SplitCoinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5FD8229312777D1D2BB813 /* SplitCoinsViewController.swift */; }; + CCB34673E007AADB0D649F2C /* libdex_board.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F426BB6F15C2E08A3E81B87D /* libdex_board.a */; }; + CDEA498CC695D89B35B0678E /* NodePeersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13188DF7FB2332EDBB6929F5 /* NodePeersViewController.swift */; }; + D0154E933CB0A42495F493B4 /* BMInstalledDApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 788AE10382B92E35A434F2E3 /* BMInstalledDApp.m */; }; + D0CC2976D1595860BB67C58C /* dao-core-app.dapp in Resources */ = {isa = PBXBuildFile; fileRef = 9CC651F674E6883AFB9709D5 /* dao-core-app.dapp */; }; + D134B7B4257517D9037B6301 /* dao-voting-app.dapp in Resources */ = {isa = PBXBuildFile; fileRef = 8923664227A70DFEBE27B5ED /* dao-voting-app.dapp */; }; + D28537CD882B83167B426EA0 /* DAppStoreCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E72505C417B965A57BB066 /* DAppStoreCells.swift */; }; + D6F2A69901F8BE2CAE4846B0 /* OraclePriceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE8E2F17C9FA369F86F60DB /* OraclePriceManager.swift */; }; + D91F481FE2DA5BE3465EFEFD /* oracle2-app.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 3149E0903940C330CAE827F5 /* oracle2-app.wasm */; }; + DA682CA91CC907ACC61E9C6B /* libipfs-bindings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 344F317D8FBFA32FFDA811F2 /* libipfs-bindings.a */; }; + DAFECCA6179EA6FE92C0ABC7 /* SplitCoinPreviewCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B33EC6A5FD1571ED9D1EFFB /* SplitCoinPreviewCard.swift */; }; + DEF279257A2A5B949FBA8B01 /* libdex.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 57D7E7F685327E2CAB9E7C69 /* libdex.a */; }; + DFC07B98C98F293C31F86A8C /* MessengerChatListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DFF1052A687C5F2265E618F /* MessengerChatListViewController.swift */; }; + E153DCA098F777EAFAA1D91A /* AssetSwapCreateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B1E2B05648164FC49C46F7 /* AssetSwapCreateViewModel.swift */; }; + E167B53AA2D8218D0710AA3E /* BMDexOrder.mm in Sources */ = {isa = PBXBuildFile; fileRef = 46577647151F6240CBA9CE2D /* BMDexOrder.mm */; }; + E28445CBD68FD65137EB5624 /* WalletAPIClient.mm in Sources */ = {isa = PBXBuildFile; fileRef = B48933E92F4932F3A119DBB0 /* WalletAPIClient.mm */; }; + E4FDD3C6A0F5C3758751901D /* dao-core-app.dapp in Resources */ = {isa = PBXBuildFile; fileRef = 9CC651F674E6883AFB9709D5 /* dao-core-app.dapp */; }; + E545F78761926F0D8A51E9B2 /* BMDexOrder.mm in Sources */ = {isa = PBXBuildFile; fileRef = 46577647151F6240CBA9CE2D /* BMDexOrder.mm */; }; + E6218D6C7C5BE83B9707F158 /* MessengerChatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390F813D5F08AC3C0F47183F /* MessengerChatCell.swift */; }; + E7EA1E52231681BDE3D26C73 /* AssetSwapsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDAF01730E8C754AA8C5175 /* AssetSwapsViewModel.swift */; }; + EA4468F36E3D2550A6D3E783 /* libasio-ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C08DFF83966F34F6D7F6AFC /* libasio-ipfs.a */; }; + ECF8F9E33148D1833480ADFA /* SplitCoinPreviewCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B33EC6A5FD1571ED9D1EFFB /* SplitCoinPreviewCard.swift */; }; + ED06A371AF87628171B6FB9B /* MyDAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008792DADDD5C9FF28341A31 /* MyDAppsViewController.swift */; }; + EE21FF606586049461645B3E /* WalletAPIClient.mm in Sources */ = {isa = PBXBuildFile; fileRef = B48933E92F4932F3A119DBB0 /* WalletAPIClient.mm */; }; + EEE47E74C114A85B33A3A618 /* MessengerChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFF464435B3D65DCFC432FA3 /* MessengerChatListViewModel.swift */; }; + EF72B4145F6E6C9870670B96 /* libwallet_ipfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AC8EF915253F6CCEA3788BE /* libwallet_ipfs.a */; }; + F00F7FD991CEA0402E131CFF /* AssetSwapCreateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B1E2B05648164FC49C46F7 /* AssetSwapCreateViewModel.swift */; }; + F0F0F0F0AAAA0002ABCD0001 /* AssetSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F0F0F0AAAA0001ABCD0001 /* AssetSearchViewController.swift */; }; + F0F0F0F0AAAA0003ABCD0001 /* AssetSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F0F0F0AAAA0001ABCD0001 /* AssetSearchViewController.swift */; }; + F0F0F0F0AAAA0004ABCD0001 /* AssetSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F0F0F0AAAA0001ABCD0001 /* AssetSearchViewController.swift */; }; + F43301CC1012AD1BCD74EFFE /* MyDAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008792DADDD5C9FF28341A31 /* MyDAppsViewController.swift */; }; + F51D4BA4D0687745D2E60254 /* dao-voting-app.dapp in Resources */ = {isa = PBXBuildFile; fileRef = 8923664227A70DFEBE27B5ED /* dao-voting-app.dapp */; }; + F5BD7F278CC7CCFADF63E7AF /* NodePeersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13188DF7FB2332EDBB6929F5 /* NodePeersViewController.swift */; }; + F7E52918A59C06FDF87D69C9 /* DAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B86688612ED1A5FF45D1BAD /* DAppManager.swift */; }; + F8B20149A3E6BEDAE7F20E57 /* WalletAPIClient.mm in Sources */ = {isa = PBXBuildFile; fileRef = B48933E92F4932F3A119DBB0 /* WalletAPIClient.mm */; }; + F9C79FBC040297C8E235C660 /* libdex_board.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F426BB6F15C2E08A3E81B87D /* libdex_board.a */; }; + FD87FA1DDA9C27F19451AB4B /* MessengerBubbleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E08B41CEDA5A06EBFD775E76 /* MessengerBubbleCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ - 9217D31E2652AB67006DD6F1 /* Embed App Extensions */ = { + 9217D31E2652AB67006DD6F1 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( ); - name = "Embed App Extensions"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; 92365AE2222D4039000D7064 /* Embed Frameworks */ = { @@ -1389,14 +1524,14 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 92C4DBDF227906800073AEED /* Embed App Extensions */ = { + 92C4DBDF227906800073AEED /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( ); - name = "Embed App Extensions"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; 92E2EACC226F102100367928 /* Embed Watch Content */ = { @@ -1433,11 +1568,48 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 008792DADDD5C9FF28341A31 /* MyDAppsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MyDAppsViewController.swift; sourceTree = ""; }; 024109A68A03B9C8D11B9895 /* Pods-BeamWallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeamWallet.release.xcconfig"; path = "Target Support Files/Pods-BeamWallet/Pods-BeamWallet.release.xcconfig"; sourceTree = ""; }; + 13188DF7FB2332EDBB6929F5 /* NodePeersViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodePeersViewController.swift; sourceTree = ""; }; + 13CF5C205DC060CADA96F025 /* MessengerNewChatViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessengerNewChatViewController.swift; sourceTree = ""; }; + 17A4C97F6AE81EDDEC7DB4B9 /* BMAvailableDApp.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = BMAvailableDApp.m; sourceTree = ""; }; 18CF009621EA8E5970DA1D87 /* Pods_BeamWalletMasterNet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BeamWalletMasterNet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1A2828F3DFD5228179C17B8D /* BMInstalledDApp.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BMInstalledDApp.h; sourceTree = ""; }; + 1A5FD8229312777D1D2BB813 /* SplitCoinsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SplitCoinsViewController.swift; sourceTree = ""; }; + 1AD88774871E2784A73C35A8 /* BMAvailableDApp.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BMAvailableDApp.h; sourceTree = ""; }; + 20B7F5341703980CD2D3941F /* BMInstantMessage.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = BMInstantMessage.mm; sourceTree = ""; }; + 21968CB26C3B977852247761 /* AssetSwapsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSwapsViewController.swift; sourceTree = ""; }; + 2BE8E2F17C9FA369F86F60DB /* OraclePriceManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OraclePriceManager.swift; sourceTree = ""; }; + 3149E0903940C330CAE827F5 /* oracle2-app.wasm */ = {isa = PBXFileReference; includeInIndex = 1; path = "oracle2-app.wasm"; sourceTree = ""; }; + 344F317D8FBFA32FFDA811F2 /* libipfs-bindings.a */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = archive.ar; path = "libipfs-bindings.a"; sourceTree = ""; }; + 35296D58A729F092AFF0C821 /* BMChat.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BMChat.h; sourceTree = ""; }; 36A85446EDA45DA6335930C5 /* Pods_BeamWalletTestNet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BeamWalletTestNet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 390F813D5F08AC3C0F47183F /* MessengerChatCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessengerChatCell.swift; sourceTree = ""; }; 3B5894AC3136298FE0B7E8E3 /* Pods_BeamWallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BeamWallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3C08DFF83966F34F6D7F6AFC /* libasio-ipfs.a */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = archive.ar; path = "libasio-ipfs.a"; sourceTree = ""; }; 456B838BA83C92E05D36B091 /* Pods-BeamWalletTestNet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeamWalletTestNet.release.xcconfig"; path = "Target Support Files/Pods-BeamWalletTestNet/Pods-BeamWalletTestNet.release.xcconfig"; sourceTree = ""; }; + 46577647151F6240CBA9CE2D /* BMDexOrder.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = BMDexOrder.mm; sourceTree = ""; }; + 46CAE6BC4C48E30E26AE304F /* BMInstantMessage.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BMInstantMessage.h; sourceTree = ""; }; + 4A6A43055CFB5C5820B099A1 /* AssetSwapCreateViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSwapCreateViewController.swift; sourceTree = ""; }; + 4B33EC6A5FD1571ED9D1EFFB /* SplitCoinPreviewCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SplitCoinPreviewCard.swift; sourceTree = ""; }; + 4C1212B607A6369157662664 /* WalletAPIClient+Internal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "WalletAPIClient+Internal.h"; sourceTree = ""; }; + 4F9B63F6E98BA8303EE2FEAD /* AssetUTXOGroup.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetUTXOGroup.swift; sourceTree = ""; }; + 53ECF740A61C5F0D941B8B36 /* BMOptionPickerViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BMOptionPickerViewController.swift; sourceTree = ""; }; + 57D7E7F685327E2CAB9E7C69 /* libdex.a */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = archive.ar; path = libdex.a; sourceTree = ""; }; + 5947BB8B40C5D62DAA9D01BF /* BMPublisher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = BMPublisher.m; sourceTree = ""; }; + 5FEED174B3A5FDB8D14C9963 /* dapps_store_app.wasm */ = {isa = PBXFileReference; includeInIndex = 1; path = dapps_store_app.wasm; sourceTree = ""; }; + 603CF8C20147C7F9E087C33D /* SplitCoinsStripeView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SplitCoinsStripeView.swift; sourceTree = ""; }; + 6831B2B28CCBD5D776DCF34B /* PublishersViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PublishersViewController.swift; sourceTree = ""; }; + 75A467070BE6633F1B715DAD /* AssetSwapDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSwapDetailsViewController.swift; sourceTree = ""; }; + 788AE10382B92E35A434F2E3 /* BMInstalledDApp.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = BMInstalledDApp.m; sourceTree = ""; }; + 7A439F6A64EB61AD3DF6945B /* MessengerConversationViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessengerConversationViewModel.swift; sourceTree = ""; }; + 7AC8EF915253F6CCEA3788BE /* libwallet_ipfs.a */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = archive.ar; path = libwallet_ipfs.a; sourceTree = ""; }; + 7CDD341D6CFC3AD26C3F4374 /* AssetSwapOrderCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSwapOrderCell.swift; sourceTree = ""; }; + 8865BEE3DDBE9806B38DDE04 /* AssetUTXOSectionHeaderView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetUTXOSectionHeaderView.swift; sourceTree = ""; }; + 8923664227A70DFEBE27B5ED /* dao-voting-app.dapp */ = {isa = PBXFileReference; includeInIndex = 1; path = "dao-voting-app.dapp"; sourceTree = ""; }; + 8B86688612ED1A5FF45D1BAD /* DAppManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppManager.swift; sourceTree = ""; }; + 8C5EB5519EB54866BA8DB9A4 /* BMChat.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = BMChat.mm; sourceTree = ""; }; + 8DFF1052A687C5F2265E618F /* MessengerChatListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessengerChatListViewController.swift; sourceTree = ""; }; 9205284E237B11F1001F8E31 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 9205284F237B11F1001F8E31 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; 9207DD8626778DF900250E80 /* AssetsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsViewController.swift; sourceTree = ""; }; @@ -1550,7 +1722,6 @@ 9227AEEA2282F6F800FB221F /* BMDuration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BMDuration.h; sourceTree = ""; }; 9227AEEB2282F6F800FB221F /* BMDuration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BMDuration.m; sourceTree = ""; }; 922B637A2552F1CA00A2AED2 /* ReceiveTokenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveTokenCell.swift; sourceTree = ""; }; - 922B637B2552F1CA00A2AED2 /* ReceiveTokenCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReceiveTokenCell.xib; sourceTree = ""; }; 922EE89222F18C5800BBFDEF /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 922F2E7022E1F7F6005D3F33 /* BMMultiLineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BMMultiLineItem.swift; sourceTree = ""; }; 922FB15D22B3E134007AD689 /* BMSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BMSearchView.swift; sourceTree = ""; }; @@ -1858,6 +2029,7 @@ 92C5EBA1229BE48E00ABFFF7 /* PreviewQRViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewQRViewController.swift; sourceTree = ""; }; 92C5EBB1229BFD1100ABFFF7 /* BMDetailCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BMDetailCell.swift; sourceTree = ""; }; 92C5EBB2229BFD1100ABFFF7 /* BMDetailCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BMDetailCell.xib; sourceTree = ""; }; + 92C9307A797CCA7FEAABBFD8 /* AssetSwapDetailsViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSwapDetailsViewModel.swift; sourceTree = ""; }; 92CC40E622E1DC7300006E5C /* BMGroupedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BMGroupedCell.swift; sourceTree = ""; }; 92CC40E722E1DC7300006E5C /* BMGroupedCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BMGroupedCell.xib; sourceTree = ""; }; 92CF065822E112570021B9CF /* SearchTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTableView.swift; sourceTree = ""; }; @@ -1975,10 +2147,28 @@ 92FE263822A11E4900102DB4 /* BMPickedAddressCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BMPickedAddressCell.xib; sourceTree = ""; }; 92FE263922A11E4B00102DB4 /* BMExpandCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMExpandCell.swift; sourceTree = ""; }; 92FE263A22A11E4D00102DB4 /* BMExpandCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BMExpandCell.xib; sourceTree = ""; }; + 969B64630C2A5874889F91FC /* DAppStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppStore.swift; sourceTree = ""; }; + 9CC651F674E6883AFB9709D5 /* dao-core-app.dapp */ = {isa = PBXFileReference; includeInIndex = 1; path = "dao-core-app.dapp"; sourceTree = ""; }; + AFF464435B3D65DCFC432FA3 /* MessengerChatListViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessengerChatListViewModel.swift; sourceTree = ""; }; + B48933E92F4932F3A119DBB0 /* WalletAPIClient.mm */ = {isa = PBXFileReference; includeInIndex = 1; path = WalletAPIClient.mm; sourceTree = ""; }; + B5B1E2B05648164FC49C46F7 /* AssetSwapCreateViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSwapCreateViewModel.swift; sourceTree = ""; }; BA659A4953BE271B118EB5D6 /* Pods-BeamWalletMasterNet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeamWalletMasterNet.debug.xcconfig"; path = "Target Support Files/Pods-BeamWalletMasterNet/Pods-BeamWalletMasterNet.debug.xcconfig"; sourceTree = ""; }; + C16867B8CD04CD914FE6A1CA /* WalletAPIClient.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = WalletAPIClient.h; sourceTree = ""; }; + C8C2A22B96525EB07D51F607 /* BMAlertViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BMAlertViewController.swift; sourceTree = ""; }; D303009FB5F0DA4093A89DFB /* Pods-BeamWalletTestNet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeamWalletTestNet.debug.xcconfig"; path = "Target Support Files/Pods-BeamWalletTestNet/Pods-BeamWalletTestNet.debug.xcconfig"; sourceTree = ""; }; D601ED375D9C9B9FAB70DE0B /* Pods-BeamWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeamWallet.debug.xcconfig"; path = "Target Support Files/Pods-BeamWallet/Pods-BeamWallet.debug.xcconfig"; sourceTree = ""; }; + D8EE91DF3849A709C3F01686 /* BMPublisher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BMPublisher.h; sourceTree = ""; }; + E08B41CEDA5A06EBFD775E76 /* MessengerBubbleCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessengerBubbleCell.swift; sourceTree = ""; }; + E23EA93CE3207A2EBB4D17FD /* SplitCoinsViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SplitCoinsViewModel.swift; sourceTree = ""; }; + E7E72505C417B965A57BB066 /* DAppStoreCells.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppStoreCells.swift; sourceTree = ""; }; EAEED72AAED404575AB9A6E5 /* Pods-BeamWalletMasterNet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeamWalletMasterNet.release.xcconfig"; path = "Target Support Files/Pods-BeamWalletMasterNet/Pods-BeamWalletMasterNet.release.xcconfig"; sourceTree = ""; }; + F0AA87DBB7C958778D72096C /* AssetUTXOListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetUTXOListViewController.swift; sourceTree = ""; }; + F0F0F0F0AAAA0001ABCD0001 /* AssetSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetSearchViewController.swift; sourceTree = ""; }; + F426BB6F15C2E08A3E81B87D /* libdex_board.a */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = archive.ar; path = libdex_board.a; sourceTree = ""; }; + F9E4020D11686B93F29A46DA /* MessengerChatViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessengerChatViewController.swift; sourceTree = ""; }; + FDCB8FF9B7300E47ABEF4D9A /* DAppURLSchemeHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppURLSchemeHandler.swift; sourceTree = ""; }; + FECECF4C7F96F708C05AAF05 /* BMDexOrder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BMDexOrder.h; sourceTree = ""; }; + FFDAF01730E8C754AA8C5175 /* AssetSwapsViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSwapsViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2004,7 +2194,6 @@ 9244A5282A01465E007B03D0 /* libutility.a in Frameworks */, 9244A5342A01465E007B03D0 /* libblake2b.a in Frameworks */, 9244A5252A01465E007B03D0 /* libsqlite.a in Frameworks */, - 9244A5132A01465E007B03D0 /* libwallet_api_no_ipfs.a in Frameworks */, 9244A5582A01465E007B03D0 /* libexport.a in Frameworks */, 9244A55B2A01465E007B03D0 /* libp2p.a in Frameworks */, 9244A55E2A01465E007B03D0 /* libkeccak.a in Frameworks */, @@ -2017,10 +2206,14 @@ 9244A5552A01465E007B03D0 /* libmnemonic.a in Frameworks */, 9244A5162A01465E007B03D0 /* libre2.a in Frameworks */, 9244A52B2A01465E007B03D0 /* libassets.a in Frameworks */, - 9244A5612A01465E007B03D0 /* libwallet_client_no_ipfs.a in Frameworks */, 9244A5042A01465E007B03D0 /* libwallet_api.a in Frameworks */, 9244A50D2A01465E007B03D0 /* libwallet_client.a in Frameworks */, 9244A51C2A01465E007B03D0 /* libhw_crypto.a in Frameworks */, + 610B9971C155D841609ACE0F /* libdex.a in Frameworks */, + CCB34673E007AADB0D649F2C /* libdex_board.a in Frameworks */, + 11476E71A287700B5F7CBB8F /* libasio-ipfs.a in Frameworks */, + DA682CA91CC907ACC61E9C6B /* libipfs-bindings.a in Frameworks */, + 8B70C9ED979CE8D4574FC1EA /* libwallet_ipfs.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2046,7 +2239,6 @@ 9244A5292A01465E007B03D0 /* libutility.a in Frameworks */, 9244A5352A01465E007B03D0 /* libblake2b.a in Frameworks */, 9244A5262A01465E007B03D0 /* libsqlite.a in Frameworks */, - 9244A5142A01465E007B03D0 /* libwallet_api_no_ipfs.a in Frameworks */, 9244A5592A01465E007B03D0 /* libexport.a in Frameworks */, 9244A55C2A01465E007B03D0 /* libp2p.a in Frameworks */, 9244A55F2A01465E007B03D0 /* libkeccak.a in Frameworks */, @@ -2059,10 +2251,14 @@ 9244A5562A01465E007B03D0 /* libmnemonic.a in Frameworks */, 9244A5172A01465E007B03D0 /* libre2.a in Frameworks */, 9244A52C2A01465E007B03D0 /* libassets.a in Frameworks */, - 9244A5622A01465E007B03D0 /* libwallet_client_no_ipfs.a in Frameworks */, 9244A5052A01465E007B03D0 /* libwallet_api.a in Frameworks */, 9244A50E2A01465E007B03D0 /* libwallet_client.a in Frameworks */, 9244A51D2A01465E007B03D0 /* libhw_crypto.a in Frameworks */, + DEF279257A2A5B949FBA8B01 /* libdex.a in Frameworks */, + F9C79FBC040297C8E235C660 /* libdex_board.a in Frameworks */, + EA4468F36E3D2550A6D3E783 /* libasio-ipfs.a in Frameworks */, + 7076239CA6CBD5D923ED8EE8 /* libipfs-bindings.a in Frameworks */, + EF72B4145F6E6C9870670B96 /* libwallet_ipfs.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2088,7 +2284,6 @@ 9244A52A2A01465E007B03D0 /* libutility.a in Frameworks */, 9244A5362A01465E007B03D0 /* libblake2b.a in Frameworks */, 9244A5272A01465E007B03D0 /* libsqlite.a in Frameworks */, - 9244A5152A01465E007B03D0 /* libwallet_api_no_ipfs.a in Frameworks */, 9244A55A2A01465E007B03D0 /* libexport.a in Frameworks */, 9244A55D2A01465E007B03D0 /* libp2p.a in Frameworks */, 9244A5602A01465E007B03D0 /* libkeccak.a in Frameworks */, @@ -2101,16 +2296,53 @@ 9244A5572A01465E007B03D0 /* libmnemonic.a in Frameworks */, 9244A5182A01465E007B03D0 /* libre2.a in Frameworks */, 9244A52D2A01465E007B03D0 /* libassets.a in Frameworks */, - 9244A5632A01465E007B03D0 /* libwallet_client_no_ipfs.a in Frameworks */, 9244A5062A01465E007B03D0 /* libwallet_api.a in Frameworks */, 9244A50F2A01465E007B03D0 /* libwallet_client.a in Frameworks */, 9244A51E2A01465E007B03D0 /* libhw_crypto.a in Frameworks */, + 9D8BAACCF64EA977E69273F0 /* libdex.a in Frameworks */, + AB6BA79FF389E013065D35C9 /* libdex_board.a in Frameworks */, + 138076EB2F04AEFB33F90387 /* libasio-ipfs.a in Frameworks */, + 2F81D3CC162C2FD8201B35C0 /* libipfs-bindings.a in Frameworks */, + B5918DE6DE94C43C0CBB411F /* libwallet_ipfs.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 78028519D7C340186F689742 /* AssetSwaps */ = { + isa = PBXGroup; + children = ( + ECB36A016D50624397AFFA33 /* Cells */, + 21968CB26C3B977852247761 /* AssetSwapsViewController.swift */, + 4A6A43055CFB5C5820B099A1 /* AssetSwapCreateViewController.swift */, + 75A467070BE6633F1B715DAD /* AssetSwapDetailsViewController.swift */, + ); + path = AssetSwaps; + sourceTree = ""; + }; + 8110C1340F23638CECA5A0CA /* Messenger */ = { + isa = PBXGroup; + children = ( + 8DFF1052A687C5F2265E618F /* MessengerChatListViewController.swift */, + F9E4020D11686B93F29A46DA /* MessengerChatViewController.swift */, + 13CF5C205DC060CADA96F025 /* MessengerNewChatViewController.swift */, + E08B41CEDA5A06EBFD775E76 /* MessengerBubbleCell.swift */, + 390F813D5F08AC3C0F47183F /* MessengerChatCell.swift */, + ); + path = Messenger; + sourceTree = ""; + }; + 838B90CD5474E2AC0F70CB0D /* AssetSwaps */ = { + isa = PBXGroup; + children = ( + FFDAF01730E8C754AA8C5175 /* AssetSwapsViewModel.swift */, + B5B1E2B05648164FC49C46F7 /* AssetSwapCreateViewModel.swift */, + 92C9307A797CCA7FEAABBFD8 /* AssetSwapDetailsViewModel.swift */, + ); + path = AssetSwaps; + sourceTree = ""; + }; 9207DD8526778DD500250E80 /* Assets */ = { isa = PBXGroup; children = ( @@ -2302,6 +2534,7 @@ children = ( 920EBE1526E8D48A003FD0A8 /* DAOAppCell.swift */, 920EBE1626E8D48A003FD0A8 /* DAOAppCell.xib */, + E7E72505C417B965A57BB066 /* DAppStoreCells.swift */, ); path = Cell; sourceTree = ""; @@ -2361,7 +2594,7 @@ 9253A76F25A5C6A500E456A2 /* openssl.framework */, 92E2EB8D226F24C200367928 /* masternet */, 92D3CBF02264B56D007E3C55 /* testnet */, - 92D3CBD32264B562007E3C55 /* mainnet */, + 92D3CBD32264B562007E3C55 /* lib */, 927367212277AB330060CB67 /* UserNotifications.framework */, 927367232277AB340060CB67 /* UserNotificationsUI.framework */, 3B5894AC3136298FE0B7E8E3 /* Pods_BeamWallet.framework */, @@ -2452,6 +2685,18 @@ 92FA196C266F893D002780DC /* BMAsset.m */, 926DB36F26DFC91A00E85F0B /* BMApp.h */, 926DB37026DFC91A00E85F0B /* BMApp.m */, + 46CAE6BC4C48E30E26AE304F /* BMInstantMessage.h */, + 20B7F5341703980CD2D3941F /* BMInstantMessage.mm */, + 35296D58A729F092AFF0C821 /* BMChat.h */, + 8C5EB5519EB54866BA8DB9A4 /* BMChat.mm */, + FECECF4C7F96F708C05AAF05 /* BMDexOrder.h */, + 46577647151F6240CBA9CE2D /* BMDexOrder.mm */, + 1A2828F3DFD5228179C17B8D /* BMInstalledDApp.h */, + 788AE10382B92E35A434F2E3 /* BMInstalledDApp.m */, + 1AD88774871E2784A73C35A8 /* BMAvailableDApp.h */, + 17A4C97F6AE81EDDEC7DB4B9 /* BMAvailableDApp.m */, + D8EE91DF3849A709C3F01686 /* BMPublisher.h */, + 5947BB8B40C5D62DAA9D01BF /* BMPublisher.m */, ); path = Objects; sourceTree = ""; @@ -2469,6 +2714,8 @@ 92554D1622896BE600FE6041 /* Addresses */, 92F6B79C22A5B6C60055C3C3 /* QRCode */, 923A45D5235DE4000084C368 /* Passwords */, + 8110C1340F23638CECA5A0CA /* Messenger */, + 78028519D7C340186F689742 /* AssetSwaps */, ); path = Main; sourceTree = ""; @@ -2496,6 +2743,7 @@ 92C00D2222A00E2A00DDB579 /* OwnerKeyViewController.xib */, 92B7DA2E22D63EFB00D4F88B /* TrustedNodeViewController.swift */, 92302046262F22C5001A1883 /* SelectNodeViewController.swift */, + 13188DF7FB2332EDBB6929F5 /* NodePeersViewController.swift */, ); path = Settings; sourceTree = ""; @@ -2507,6 +2755,8 @@ 92435258222E73FC00A0EF37 /* UTXOViewController.swift */, 92F68C762244E37200197D4D /* UTXODetailViewController.swift */, 9212DBAC22E0A1730053FC99 /* UTXOTableView.swift */, + 1A5FD8229312777D1D2BB813 /* SplitCoinsViewController.swift */, + F0AA87DBB7C958778D72096C /* AssetUTXOListViewController.swift */, ); path = UTXO; sourceTree = ""; @@ -2530,7 +2780,6 @@ 9225D556229455CA0067B4EA /* ReceiveAddressButtonsCell.swift */, 9225D557229455CA0067B4EA /* ReceiveAddressButtonsCell.xib */, 922B637A2552F1CA00A2AED2 /* ReceiveTokenCell.swift */, - 922B637B2552F1CA00A2AED2 /* ReceiveTokenCell.xib */, 9223D538261F480E0086D95C /* ReceiveTransactionTypeCell.swift */, 9223D539261F480E0086D95C /* ReceiveTransactionTypeCell.xib */, ); @@ -2548,6 +2797,9 @@ 92506BE926DE5C17001804BA /* Public.cpp */, 926DB36A26DFC75200E85F0B /* DAOManager.h */, 926DB36B26DFC75200E85F0B /* DAOManager.mm */, + C16867B8CD04CD914FE6A1CA /* WalletAPIClient.h */, + B48933E92F4932F3A119DBB0 /* WalletAPIClient.mm */, + 4C1212B607A6369157662664 /* WalletAPIClient+Internal.h */, ); path = DAO; sourceTree = ""; @@ -2626,6 +2878,10 @@ 92B13274227B2CE30099B86C /* BeamWalletMasterNet.entitlements */, 92B13275227B2CEA0099B86C /* BeamWalletTestNet.entitlements */, 927993C822F424D300FF8ECC /* InfoPlist.strings */, + 5FEED174B3A5FDB8D14C9963 /* dapps_store_app.wasm */, + 9CC651F674E6883AFB9709D5 /* dao-core-app.dapp */, + 8923664227A70DFEBE27B5ED /* dao-voting-app.dapp */, + 3149E0903940C330CAE827F5 /* oracle2-app.wasm */, ); path = Resources; sourceTree = ""; @@ -2696,6 +2952,8 @@ 92D9F7A5266FAB55006FB555 /* BMGradientView.swift */, 92B340AA2670E68C00936168 /* AssetIconView.swift */, 9249A5112694584900FF6C00 /* BMRippleCell.swift */, + 53ECF740A61C5F0D941B8B36 /* BMOptionPickerViewController.swift */, + C8C2A22B96525EB07D51F607 /* BMAlertViewController.swift */, ); path = Controls; sourceTree = ""; @@ -2855,6 +3113,7 @@ 92480DAC229547180084512A /* Cells */, 9235EB0D2296F6DF00BDC232 /* ReceiveListViewController.swift */, 9265CDCE2292F233003C98B2 /* ReceiveViewController.swift */, + F0F0F0F0AAAA0001ABCD0001 /* AssetSearchViewController.swift */, 92DFD5CA255031FA00B9751C /* OfflineAddressViewController.swift */, 92DFD5CB255031FA00B9751C /* OfflineAddressViewController.xib */, ); @@ -2898,6 +3157,8 @@ children = ( 92B7E4D422AF0067001708E4 /* UTXOViewModel.swift */, 92B7E4D822AF064A001708E4 /* DetailUTXOViewModel.swift */, + 4F9B63F6E98BA8303EE2FEAD /* AssetUTXOGroup.swift */, + E23EA93CE3207A2EBB4D17FD /* SplitCoinsViewModel.swift */, ); path = UTXO; sourceTree = ""; @@ -2952,7 +3213,7 @@ path = Images; sourceTree = ""; }; - 92D3CBD32264B562007E3C55 /* mainnet */ = { + 92D3CBD32264B562007E3C55 /* lib */ = { isa = PBXGroup; children = ( 9244A4F12A014658007B03D0 /* libassets.a */, @@ -2987,8 +3248,13 @@ 9244A5032A01465E007B03D0 /* libwallet_client_no_ipfs.a */, 9244A4E72A014656007B03D0 /* libwallet_client.a */, 9244A4E62A014655007B03D0 /* libwallet_core.a */, + 57D7E7F685327E2CAB9E7C69 /* libdex.a */, + F426BB6F15C2E08A3E81B87D /* libdex_board.a */, + 3C08DFF83966F34F6D7F6AFC /* libasio-ipfs.a */, + 344F317D8FBFA32FFDA811F2 /* libipfs-bindings.a */, + 7AC8EF915253F6CCEA3788BE /* libwallet_ipfs.a */, ); - path = mainnet; + path = "beam-ios-7.5.14510/lib"; sourceTree = ""; }; 92D3CBF02264B56D007E3C55 /* testnet */ = { @@ -3030,6 +3296,8 @@ 920EBE0D26E8D06D003FD0A8 /* DAOAppsViewController.swift */, 92ED772526E63EB000B41B3C /* DAOConfirmViewController.swift */, 9293114A2718EA1B005E3023 /* DAOConfirmViewController.xib */, + 008792DADDD5C9FF28341A31 /* MyDAppsViewController.swift */, + 6831B2B28CCBD5D776DCF34B /* PublishersViewController.swift */, ); path = DAO; sourceTree = ""; @@ -3051,6 +3319,7 @@ 92D729D726E2369000A92456 /* XWVScripting.swift */, 92D729D626E2369000A92456 /* XWVScriptObject.swift */, 92D729D426E2368F00A92456 /* XWVUserScript.swift */, + FDCB8FF9B7300E47ABEF4D9A /* DAppURLSchemeHandler.swift */, ); path = JS; sourceTree = ""; @@ -3093,6 +3362,9 @@ 92F68C7D2244E77500197D4D /* UTXODetailCell.xib */, 92F68C8B2245070200197D4D /* UTXOTransactionCell.swift */, 92F68C8C2245070200197D4D /* UTXOTransactionCell.xib */, + 8865BEE3DDBE9806B38DDE04 /* AssetUTXOSectionHeaderView.swift */, + 603CF8C20147C7F9E087C33D /* SplitCoinsStripeView.swift */, + 4B33EC6A5FD1571ED9D1EFFB /* SplitCoinPreviewCard.swift */, ); path = Cells; sourceTree = ""; @@ -3192,6 +3464,11 @@ 92B7E4E822AFD71B001708E4 /* Transaction */, 92B7E4E622AFD70D001708E4 /* UTXO */, 92B7E4E922AFD748001708E4 /* Settings */, + CD37C67AA40FF44BBFFD57B4 /* Messenger */, + 838B90CD5474E2AC0F70CB0D /* AssetSwaps */, + 8B86688612ED1A5FF45D1BAD /* DAppManager.swift */, + 969B64630C2A5874889F91FC /* DAppStore.swift */, + 2BE8E2F17C9FA369F86F60DB /* OraclePriceManager.swift */, ); path = Model; sourceTree = ""; @@ -3239,6 +3516,23 @@ path = Pods; sourceTree = ""; }; + CD37C67AA40FF44BBFFD57B4 /* Messenger */ = { + isa = PBXGroup; + children = ( + AFF464435B3D65DCFC432FA3 /* MessengerChatListViewModel.swift */, + 7A439F6A64EB61AD3DF6945B /* MessengerConversationViewModel.swift */, + ); + path = Messenger; + sourceTree = ""; + }; + ECB36A016D50624397AFFA33 /* Cells */ = { + isa = PBXGroup; + children = ( + 7CDD341D6CFC3AD26C3F4374 /* AssetSwapOrderCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -3274,7 +3568,7 @@ 9288DD93223A55410020C501 /* Embed Frameworks */, 9288DD97223A55410020C501 /* ShellScript */, 92E2EACC226F102100367928 /* Embed Watch Content */, - 92C4DBDF227906800073AEED /* Embed App Extensions */, + 92C4DBDF227906800073AEED /* Embed Foundation Extensions */, 2938815B995B37F4BB538A0A /* [CP] Embed Pods Frameworks */, ); buildRules = ( @@ -3298,7 +3592,7 @@ 92E2EB81226F21C700367928 /* Embed Watch Content */, 922474FD24E3E694006FE2B2 /* ShellScript */, 03901693440105723276594D /* [CP] Embed Pods Frameworks */, - 9217D31E2652AB67006DD6F1 /* Embed App Extensions */, + 9217D31E2652AB67006DD6F1 /* Embed Foundation Extensions */, ); buildRules = ( ); @@ -3315,9 +3609,10 @@ 925AD5D02228254E00DDE66B /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; DefaultBuildSystemTypeForWorkspace = Latest; LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1240; + LastUpgradeCheck = 1600; ORGANIZATIONNAME = Denis; TargetAttributes = { 925AD5D72228254E00DDE66B = { @@ -3441,7 +3736,6 @@ 92F68C8F2245070200197D4D /* UTXOTransactionCell.xib in Resources */, 9225D54B229449010067B4EA /* BMFieldCell.xib in Resources */, 927EDE3F22A6B47C005D5CEA /* BMMultiLinesCell.xib in Resources */, - 922B637F2552F1CA00A2AED2 /* ReceiveTokenCell.xib in Resources */, 92AE366D22383D2300E0810B /* QRScannerViewController.xib in Resources */, 925AD60122282C8400DDE66B /* SF-Pro-Display-Bold.otf in Resources */, 92F6B78C22A541040055C3C3 /* SendAllCell.xib in Resources */, @@ -3501,6 +3795,10 @@ 92DF0D0022CAB69D0092CD4D /* Localizable.strings in Resources */, 92C00D2622A00E2A00DDB579 /* OwnerKeyViewController.xib in Resources */, 926F4B2C22E0CDB7007755D9 /* README.md in Resources */, + 010A73D4AEEB9251461112AB /* dapps_store_app.wasm in Resources */, + D0CC2976D1595860BB67C58C /* dao-core-app.dapp in Resources */, + 9D5813AA76650CB6EBA22D34 /* dao-voting-app.dapp in Resources */, + A4D6D5A9EC5F4DC3DD3E4682 /* oracle2-app.wasm in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3570,7 +3868,6 @@ 92CC40EC22E1DC7300006E5C /* BMGroupedCell.xib in Resources */, 922393A3235D1579002ADA27 /* RestoreOptionsViewController.xib in Resources */, 92C5EBB7229BFD1100ABFFF7 /* BMDetailCell.xib in Resources */, - 922B63802552F1CA00A2AED2 /* ReceiveTokenCell.xib in Resources */, 9261C269228BF55800D666E0 /* BMPickerCell.xib in Resources */, 9288DD8A223A55410020C501 /* QRViewController.xib in Resources */, 92F78BC024D06D2200489229 /* QRCodeSmallViewController.xib in Resources */, @@ -3604,6 +3901,10 @@ 92090D0D223A684000A1CFBB /* bgTestnet.jpg in Resources */, 92E4AECB23599D46004062A1 /* CollectionTextHeader.xib in Resources */, 92C00D2722A00E2A00DDB579 /* OwnerKeyViewController.xib in Resources */, + 4B2FC15209C14B359E3651BD /* dapps_store_app.wasm in Resources */, + E4FDD3C6A0F5C3758751901D /* dao-core-app.dapp in Resources */, + D134B7B4257517D9037B6301 /* dao-voting-app.dapp in Resources */, + D91F481FE2DA5BE3465EFEFD /* oracle2-app.wasm in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3629,7 +3930,6 @@ 92E2EB4B226F21C700367928 /* SF-Pro-Display-RegularItalic.otf in Resources */, 926EB47D231E843800D059BC /* WalletTransactionCell.xib in Resources */, 92E2EB4C226F21C700367928 /* InputWordCell.xib in Resources */, - 922B63812552F1CA00A2AED2 /* ReceiveTokenCell.xib in Resources */, 92E4AECC23599D46004062A1 /* CollectionTextHeader.xib in Resources */, 92302055262F23AE001A1883 /* NodeCell.xib in Resources */, 92FE264322A11E4E00102DB4 /* BMExpandCell.xib in Resources */, @@ -3709,6 +4009,10 @@ 920D6C41270AEE3D00D953B8 /* BMDetailAmountCell.xib in Resources */, 92E2EB7A226F21C700367928 /* UTXODetailCell.xib in Resources */, 92E2EB7B226F21C700367928 /* bgTestnet.jpg in Resources */, + 6029A6A13D53093CF6EC2FB6 /* dapps_store_app.wasm in Resources */, + 7209CB87405DF7AC0B7CBE89 /* dao-core-app.dapp in Resources */, + F51D4BA4D0687745D2E60254 /* dao-voting-app.dapp in Resources */, + 8F9D17159CF9400338FE47EB /* oracle2-app.wasm in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4209,6 +4513,45 @@ 928B0AB222425329003CE9E7 /* BMPopoverMenu.swift in Sources */, 92B837492285816D002B385B /* Progress.swift in Sources */, 9265CDCF2292F233003C98B2 /* ReceiveViewController.swift in Sources */, + F0F0F0F0AAAA0002ABCD0001 /* AssetSearchViewController.swift in Sources */, + 30BE993E2DFFA6452A536125 /* BMInstantMessage.mm in Sources */, + 8E6EABD786898410576636A4 /* BMChat.mm in Sources */, + 5A4A0ACB5C6CA50F1E5FCBE8 /* MessengerChatListViewModel.swift in Sources */, + 066620C96ABA1816759B7AE3 /* MessengerConversationViewModel.swift in Sources */, + 4D7C86E3D4DAAAD7DDFE22E7 /* MessengerChatListViewController.swift in Sources */, + 75B069A9EC9A517ABCACFC1E /* MessengerChatViewController.swift in Sources */, + 14458CA45E1281FAF80D496D /* MessengerNewChatViewController.swift in Sources */, + 75516585C510BB694A1B9AED /* MessengerBubbleCell.swift in Sources */, + 35726BA6BFE79F50A19E7054 /* MessengerChatCell.swift in Sources */, + 15A094B1963EAE81E9B86B33 /* BMDexOrder.mm in Sources */, + 7ED2D2980F560C573C46D8CC /* AssetSwapsViewModel.swift in Sources */, + 2C2D70373756666705D14FDE /* AssetSwapCreateViewModel.swift in Sources */, + 31509E4F991BC56BF7F10098 /* AssetSwapDetailsViewModel.swift in Sources */, + 62D6B4FFAD6FCD00C5206630 /* AssetSwapsViewController.swift in Sources */, + 94E405DB59B47772F0662861 /* AssetSwapCreateViewController.swift in Sources */, + B4FE33729385D12603589437 /* AssetSwapDetailsViewController.swift in Sources */, + 5DAA3CCCB00AECEACD182FBF /* AssetSwapOrderCell.swift in Sources */, + 59D5C848EC6C80442BC6A8A3 /* BMOptionPickerViewController.swift in Sources */, + 911AF3DF99FDD7FFD07F4830 /* AssetUTXOGroup.swift in Sources */, + 966EF2ACFF217F06AE183E6E /* SplitCoinsViewModel.swift in Sources */, + CA7E68B52D3092D76CBACF7C /* SplitCoinsViewController.swift in Sources */, + 0EC9234AAD2DC0A1BF7600B4 /* AssetUTXOSectionHeaderView.swift in Sources */, + C6FB138B2A261CBC43626204 /* SplitCoinsStripeView.swift in Sources */, + AC3CF90BF3C1ED7E2D19D65A /* SplitCoinPreviewCard.swift in Sources */, + CDEA498CC695D89B35B0678E /* NodePeersViewController.swift in Sources */, + 0DB9711D993CFA1D225B53BA /* BMInstalledDApp.m in Sources */, + BDDA78B4E7DDB519C8591B40 /* BMAvailableDApp.m in Sources */, + 46E422749DE0BAE1BCF8B4EB /* BMPublisher.m in Sources */, + F8B20149A3E6BEDAE7F20E57 /* WalletAPIClient.mm in Sources */, + 28B1AF7C15F50CEC869952BD /* DAppManager.swift in Sources */, + 6F6101D0D3E633F1D68554F9 /* DAppStore.swift in Sources */, + F43301CC1012AD1BCD74EFFE /* MyDAppsViewController.swift in Sources */, + 8313E7C13424087CE8545F59 /* PublishersViewController.swift in Sources */, + D28537CD882B83167B426EA0 /* DAppStoreCells.swift in Sources */, + 187B4E604FD2814B8018C796 /* AssetUTXOListViewController.swift in Sources */, + AAE84220C256A1EFAB30D9B3 /* DAppURLSchemeHandler.swift in Sources */, + 2964ED046A04435DB6FD977D /* BMAlertViewController.swift in Sources */, + 042BE224A8CF0171B513BED0 /* OraclePriceManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4532,6 +4875,45 @@ 928B0AB322425329003CE9E7 /* BMPopoverMenu.swift in Sources */, 92B8374A2285816D002B385B /* Progress.swift in Sources */, 9265CDD02292F233003C98B2 /* ReceiveViewController.swift in Sources */, + F0F0F0F0AAAA0003ABCD0001 /* AssetSearchViewController.swift in Sources */, + 87B508D6F91B6BCDE6EC712F /* BMInstantMessage.mm in Sources */, + A04389E8E8EE7AB9E4D87007 /* BMChat.mm in Sources */, + 9C3478CC0AB10D577E97E53E /* MessengerChatListViewModel.swift in Sources */, + 2361D1F909D78EC50B8C5D5C /* MessengerConversationViewModel.swift in Sources */, + DFC07B98C98F293C31F86A8C /* MessengerChatListViewController.swift in Sources */, + 9E75E31529DE385C7D153631 /* MessengerChatViewController.swift in Sources */, + 00947887B27F2F825E1E67CC /* MessengerNewChatViewController.swift in Sources */, + 492305E92FA7A41129BF7346 /* MessengerBubbleCell.swift in Sources */, + 984E5950BBB86D093251AB57 /* MessengerChatCell.swift in Sources */, + E545F78761926F0D8A51E9B2 /* BMDexOrder.mm in Sources */, + E7EA1E52231681BDE3D26C73 /* AssetSwapsViewModel.swift in Sources */, + E153DCA098F777EAFAA1D91A /* AssetSwapCreateViewModel.swift in Sources */, + BCB254DF739D2FA8FF92C672 /* AssetSwapDetailsViewModel.swift in Sources */, + C07BD2850E0E4E55AC706985 /* AssetSwapsViewController.swift in Sources */, + 9A1790F44DE275FD9644A9F7 /* AssetSwapCreateViewController.swift in Sources */, + A522EB1A22FF64A990518431 /* AssetSwapDetailsViewController.swift in Sources */, + BE96C4A71A3D47721D003ACF /* AssetSwapOrderCell.swift in Sources */, + 705F9944DC83B6CC6816678F /* BMOptionPickerViewController.swift in Sources */, + AAFE8C80296CA6DAA3BC3B1F /* AssetUTXOGroup.swift in Sources */, + 000A92658A9461591881CE8C /* SplitCoinsViewModel.swift in Sources */, + 275E60FB3A8D4704581DF986 /* SplitCoinsViewController.swift in Sources */, + 5101D1DB95B6BECFE333B53C /* AssetUTXOSectionHeaderView.swift in Sources */, + 6854C506C6836F88B8F8484E /* SplitCoinsStripeView.swift in Sources */, + ECF8F9E33148D1833480ADFA /* SplitCoinPreviewCard.swift in Sources */, + F5BD7F278CC7CCFADF63E7AF /* NodePeersViewController.swift in Sources */, + D0154E933CB0A42495F493B4 /* BMInstalledDApp.m in Sources */, + 26D73AAEC26B985DD2704C90 /* BMAvailableDApp.m in Sources */, + C51DBD848CA77EC12F48698C /* BMPublisher.m in Sources */, + EE21FF606586049461645B3E /* WalletAPIClient.mm in Sources */, + 7AB2A2A48540A9D102F56D7C /* DAppManager.swift in Sources */, + ACD5F2A82D411FA025F5709A /* DAppStore.swift in Sources */, + 2B7C41C938A0B67A9D143E3D /* MyDAppsViewController.swift in Sources */, + 56DCA641FEC90020A7C2686E /* PublishersViewController.swift in Sources */, + 82EC6CA54D5C74FD16085DE0 /* DAppStoreCells.swift in Sources */, + 21C1E1B8354DCDBDB0A9ABB8 /* AssetUTXOListViewController.swift in Sources */, + A4044FC0C7CFF97F7D02D3F5 /* DAppURLSchemeHandler.swift in Sources */, + 50B804DB7153D959B75A9D13 /* BMAlertViewController.swift in Sources */, + D6F2A69901F8BE2CAE4846B0 /* OraclePriceManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4855,6 +5237,45 @@ 92E2EB2E226F21C700367928 /* BMPopoverMenu.swift in Sources */, 92B8374B2285816D002B385B /* Progress.swift in Sources */, 9265CDD12292F233003C98B2 /* ReceiveViewController.swift in Sources */, + F0F0F0F0AAAA0004ABCD0001 /* AssetSearchViewController.swift in Sources */, + 434CC206E4AD461CECAD5DFB /* BMInstantMessage.mm in Sources */, + 215454B3AA22BD789F53A90C /* BMChat.mm in Sources */, + EEE47E74C114A85B33A3A618 /* MessengerChatListViewModel.swift in Sources */, + B5E7CC16D70E7D12DD9315FC /* MessengerConversationViewModel.swift in Sources */, + 05548629568C6211F87DA630 /* MessengerChatListViewController.swift in Sources */, + 5A1A647678A2061222332D16 /* MessengerChatViewController.swift in Sources */, + 69B13BFFB153BF4611F30888 /* MessengerNewChatViewController.swift in Sources */, + FD87FA1DDA9C27F19451AB4B /* MessengerBubbleCell.swift in Sources */, + E6218D6C7C5BE83B9707F158 /* MessengerChatCell.swift in Sources */, + E167B53AA2D8218D0710AA3E /* BMDexOrder.mm in Sources */, + 88D6972926A283D03D289A48 /* AssetSwapsViewModel.swift in Sources */, + F00F7FD991CEA0402E131CFF /* AssetSwapCreateViewModel.swift in Sources */, + 83EC213430E0CFD381D45A13 /* AssetSwapDetailsViewModel.swift in Sources */, + 6315A76D06343C9CEDE2EF77 /* AssetSwapsViewController.swift in Sources */, + 77CF51BBE36DCCD780857772 /* AssetSwapCreateViewController.swift in Sources */, + B6037162941C5AB71A356FF3 /* AssetSwapDetailsViewController.swift in Sources */, + 84D2719B950F43294039957A /* AssetSwapOrderCell.swift in Sources */, + 3C4B4023BBF6C6624DDC279E /* BMOptionPickerViewController.swift in Sources */, + B3476711F58F83C2F1CB8F2C /* AssetUTXOGroup.swift in Sources */, + ABA34015221EA45051CC20EB /* SplitCoinsViewModel.swift in Sources */, + BFF9839E0FE16D5A8645D394 /* SplitCoinsViewController.swift in Sources */, + 532955652A405010EC21607C /* AssetUTXOSectionHeaderView.swift in Sources */, + 6EA6E49364A07F3740D84921 /* SplitCoinsStripeView.swift in Sources */, + DAFECCA6179EA6FE92C0ABC7 /* SplitCoinPreviewCard.swift in Sources */, + B93707F60996A4898F0B88E2 /* NodePeersViewController.swift in Sources */, + 9C5E69AB2AC0AC5E4D122185 /* BMInstalledDApp.m in Sources */, + 7A9AD3ED311827FB6EF39D54 /* BMAvailableDApp.m in Sources */, + 1A6B80C9C9EDCE967358F721 /* BMPublisher.m in Sources */, + E28445CBD68FD65137EB5624 /* WalletAPIClient.mm in Sources */, + F7E52918A59C06FDF87D69C9 /* DAppManager.swift in Sources */, + 1C28371060472F1EFAF00E7B /* DAppStore.swift in Sources */, + ED06A371AF87628171B6FB9B /* MyDAppsViewController.swift in Sources */, + 65A9895370E84E09300D7548 /* PublishersViewController.swift in Sources */, + AD534670E90AA9554A8C5F74 /* DAppStoreCells.swift in Sources */, + 4B72CD41AF6A8F47020BC536 /* AssetUTXOListViewController.swift in Sources */, + A7290B4EEDC78DFD74B80F6F /* DAppURLSchemeHandler.swift in Sources */, + A8131AC203B7FC699E7DCA45 /* BMAlertViewController.swift in Sources */, + 0A44DCF3575DA823BA60DEC5 /* OraclePriceManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4967,8 +5388,11 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_BITCODE = NO; + ENABLE_DEBUG_DYLIB_OVERRIDE = NO; + ENABLE_DEBUG_DYLIB_SUPPORT = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -4976,6 +5400,8 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -4984,7 +5410,7 @@ GCC_WARN_UNUSED_FUNCTION = NO; GCC_WARN_UNUSED_VALUE = NO; GCC_WARN_UNUSED_VARIABLE = NO; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -5036,8 +5462,14 @@ ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -5045,7 +5477,7 @@ GCC_WARN_UNUSED_FUNCTION = NO; GCC_WARN_UNUSED_VALUE = NO; GCC_WARN_UNUSED_VARIABLE = NO; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_LDFLAGS = "-w"; @@ -5065,29 +5497,37 @@ CLANG_ENABLE_MODULES = YES; CLANG_WARN_ENUM_CONVERSION = NO; CODE_SIGN_ENTITLEMENTS = Resources/BeamWallet.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 8; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = KNU2R94BJK; + DEVELOPMENT_TEAM = AB54LLK6SM; ENABLE_BITCODE = NO; + ENABLE_DEBUG_DYLIB_OVERRIDE = NO; + ENABLE_DEBUG_DYLIB_SUPPORT = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/BeamWallet", "$(PROJECT_DIR)/Frameworks", "$(PROJECT_DIR)/Frameworks/boost", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", + ); HEADER_SEARCH_PATHS = ( - "\"$(SRCROOT)/../include\"", - "\"$(SRCROOT)/../include/3rdparty\"", - "\"$(SRCROOT)/../include/3rdparty/libuv/include\"", - "\"$(SRCROOT)/../include/core\"", - "\"$(SRCROOT)/../include/utility\"", - "\"$(SRCROOT)/../include/wallet\"", - "\"$(SRCROOT)/../include/3rdparty/sqlite\"", - "\"$(SRCROOT)/../include/keykeeper\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/libuv/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/core\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/utility\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/wallet\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/sqlite\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/keykeeper\"", ); INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5096,13 +5536,14 @@ "$(inherited)", "$(PROJECT_DIR)/BeamWallet/BeamSDK", "$(PROJECT_DIR)/Frameworks", - "$(PROJECT_DIR)/Frameworks/mainnet", + "$(PROJECT_DIR)/Frameworks/beam-ios-7.5.14510/lib", "$(PROJECT_DIR)", "$(PROJECT_DIR)/Frameworks/boost", ); - MARKETING_VERSION = 7.3; + MARKETING_VERSION = 7.4; OTHER_LDFLAGS = ( "$(inherited)", + "-Wl,-force_load,$(SRCROOT)/Frameworks/beam-ios-7.5.14510/lib/libsqlite.a", "-ObjC", "-l\"c++\"", "-l\"z\"", @@ -5122,8 +5563,9 @@ "\"UIKit\"", "-w", ); - PRODUCT_BUNDLE_IDENTIFIER = com.mw.beam.beamwalletiOSMainnet; + PRODUCT_BUNDLE_IDENTIFIER = com.mw.beam.beamwalletiOSMainnet2; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "BeamWallet/BeamWallet-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -5140,9 +5582,10 @@ CLANG_ENABLE_MODULES = YES; CLANG_WARN_ENUM_CONVERSION = NO; CODE_SIGN_ENTITLEMENTS = Resources/BeamWallet.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 8; - DEVELOPMENT_TEAM = KNU2R94BJK; + DEVELOPMENT_TEAM = AB54LLK6SM; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -5150,18 +5593,23 @@ "$(PROJECT_DIR)/Frameworks", "$(PROJECT_DIR)/Frameworks/boost", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", + ); HEADER_SEARCH_PATHS = ( - "\"$(SRCROOT)/../include\"", - "\"$(SRCROOT)/../include/3rdparty\"", - "\"$(SRCROOT)/../include/3rdparty/libuv/include\"", - "\"$(SRCROOT)/../include/core\"", - "\"$(SRCROOT)/../include/utility\"", - "\"$(SRCROOT)/../include/wallet\"", - "\"$(SRCROOT)/../include/3rdparty/sqlite\"", - "\"$(SRCROOT)/../include/keykeeper\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/libuv/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/core\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/utility\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/wallet\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/sqlite\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/keykeeper\"", ); INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5170,13 +5618,14 @@ "$(inherited)", "$(PROJECT_DIR)/BeamWallet/BeamSDK", "$(PROJECT_DIR)/Frameworks", - "$(PROJECT_DIR)/Frameworks/mainnet", + "$(PROJECT_DIR)/Frameworks/beam-ios-7.5.14510/lib", "$(PROJECT_DIR)", "$(PROJECT_DIR)/Frameworks/boost", ); - MARKETING_VERSION = 7.3; + MARKETING_VERSION = 7.4; OTHER_LDFLAGS = ( "$(inherited)", + "-Wl,-force_load,$(SRCROOT)/Frameworks/beam-ios-7.5.14510/lib/libsqlite.a", "-ObjC", "-l\"c++\"", "-l\"z\"", @@ -5196,8 +5645,9 @@ "\"UIKit\"", "-w", ); - PRODUCT_BUNDLE_IDENTIFIER = com.mw.beam.beamwalletiOSMainnet; + PRODUCT_BUNDLE_IDENTIFIER = com.mw.beam.beamwalletiOSMainnet2; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "BeamWallet/BeamWallet-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -5208,7 +5658,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = D303009FB5F0DA4093A89DFB /* Pods-BeamWalletTestNet.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; @@ -5220,21 +5669,28 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = KNU2R94BJK; ENABLE_BITCODE = NO; + ENABLE_DEBUG_DYLIB_OVERRIDE = NO; + ENABLE_DEBUG_DYLIB_SUPPORT = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/BeamWallet", "$(PROJECT_DIR)/Frameworks", "$(PROJECT_DIR)/Frameworks/boost", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", + ); HEADER_SEARCH_PATHS = ( - "\"$(SRCROOT)/../include\"", - "\"$(SRCROOT)/../include/3rdparty\"", - "\"$(SRCROOT)/../include/3rdparty/libuv/include\"", - "\"$(SRCROOT)/../include/core\"", - "\"$(SRCROOT)/../include/utility\"", - "\"$(SRCROOT)/../include/wallet\"", - "\"$(SRCROOT)/../include/3rdparty/sqlite\"", - "\"$(SRCROOT)/../include/keykeeper\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/libuv/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/core\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/utility\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/wallet\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/sqlite\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/keykeeper\"", ); INFOPLIST_FILE = "$(SRCROOT)/Resources/InfoTestnet.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -5246,13 +5702,14 @@ "$(inherited)", "$(PROJECT_DIR)/BeamWallet/BeamSDK", "$(PROJECT_DIR)/Frameworks", - "$(PROJECT_DIR)/Frameworks/mainnet", + "$(PROJECT_DIR)/Frameworks/beam-ios-7.5.14510/lib", "$(PROJECT_DIR)", "$(PROJECT_DIR)/Frameworks/boost", ); MARKETING_VERSION = 7.1; OTHER_LDFLAGS = ( "$(inherited)", + "-Wl,-force_load,$(SRCROOT)/Frameworks/beam-ios-7.5.14510/lib/libsqlite.a", "-ObjC", "-l\"c++\"", "-l\"z\"", @@ -5286,7 +5743,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = 456B838BA83C92E05D36B091 /* Pods-BeamWalletTestNet.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; @@ -5312,16 +5768,18 @@ "PB_FIELD_32BIT=1", "PB_NO_PACKED_STRUCTS=1", "PB_ENABLE_MALLOC=1", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", ); HEADER_SEARCH_PATHS = ( - "\"$(SRCROOT)/../include\"", - "\"$(SRCROOT)/../include/3rdparty\"", - "\"$(SRCROOT)/../include/3rdparty/libuv/include\"", - "\"$(SRCROOT)/../include/core\"", - "\"$(SRCROOT)/../include/utility\"", - "\"$(SRCROOT)/../include/wallet\"", - "\"$(SRCROOT)/../include/3rdparty/sqlite\"", - "\"$(SRCROOT)/../include/keykeeper\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/libuv/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/core\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/utility\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/wallet\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/sqlite\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/keykeeper\"", ); INFOPLIST_FILE = "$(SRCROOT)/Resources/InfoTestnet.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -5333,13 +5791,14 @@ "$(inherited)", "$(PROJECT_DIR)/BeamWallet/BeamSDK", "$(PROJECT_DIR)/Frameworks", - "$(PROJECT_DIR)/Frameworks/mainnet", + "$(PROJECT_DIR)/Frameworks/beam-ios-7.5.14510/lib", "$(PROJECT_DIR)", "$(PROJECT_DIR)/Frameworks/boost", ); MARKETING_VERSION = 7.1; OTHER_LDFLAGS = ( "$(inherited)", + "-Wl,-force_load,$(SRCROOT)/Frameworks/beam-ios-7.5.14510/lib/libsqlite.a", "-ObjC", "-l\"c++\"", "-l\"z\"", @@ -5372,7 +5831,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = BA659A4953BE271B118EB5D6 /* Pods-BeamWalletMasterNet.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIconMaster; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; @@ -5384,21 +5842,28 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = KNU2R94BJK; ENABLE_BITCODE = NO; + ENABLE_DEBUG_DYLIB_OVERRIDE = NO; + ENABLE_DEBUG_DYLIB_SUPPORT = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/BeamWallet", "$(PROJECT_DIR)/Frameworks", "$(PROJECT_DIR)/Frameworks/boost", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", + ); HEADER_SEARCH_PATHS = ( - "\"$(SRCROOT)/../include\"", - "\"$(SRCROOT)/../include/3rdparty\"", - "\"$(SRCROOT)/../include/3rdparty/libuv/include\"", - "\"$(SRCROOT)/../include/core\"", - "\"$(SRCROOT)/../include/utility\"", - "\"$(SRCROOT)/../include/wallet\"", - "\"$(SRCROOT)/../include/3rdparty/sqlite\"", - "\"$(SRCROOT)/../include/keykeeper\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/libuv/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/core\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/utility\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/wallet\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/sqlite\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/keykeeper\"", ); INFOPLIST_FILE = "$(SRCROOT)/Resources/InfoMasternet.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -5414,11 +5879,12 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/Frameworks/boost", "$(PROJECT_DIR)/Frameworks/testnet", - "$(PROJECT_DIR)/Frameworks/mainnet", + "$(PROJECT_DIR)/Frameworks/beam-ios-7.5.14510/lib", ); - MARKETING_VERSION = 7.3; + MARKETING_VERSION = 7.4; OTHER_LDFLAGS = ( "$(inherited)", + "-Wl,-force_load,$(SRCROOT)/Frameworks/beam-ios-7.5.14510/lib/libsqlite.a", "-ObjC", "-l\"c++\"", "-l\"z\"", @@ -5452,7 +5918,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = EAEED72AAED404575AB9A6E5 /* Pods-BeamWalletMasterNet.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIconMaster; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; @@ -5469,15 +5934,20 @@ "$(PROJECT_DIR)/Frameworks", "$(PROJECT_DIR)/Frameworks/boost", ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "BEAM_ASSET_SWAP_SUPPORT=1", + "BEAM_IPFS_SUPPORT=1", + ); HEADER_SEARCH_PATHS = ( - "\"$(SRCROOT)/../include\"", - "\"$(SRCROOT)/../include/3rdparty\"", - "\"$(SRCROOT)/../include/3rdparty/libuv/include\"", - "\"$(SRCROOT)/../include/core\"", - "\"$(SRCROOT)/../include/utility\"", - "\"$(SRCROOT)/../include/wallet\"", - "\"$(SRCROOT)/../include/3rdparty/sqlite\"", - "\"$(SRCROOT)/../include/keykeeper\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/libuv/include\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/core\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/utility\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/wallet\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/3rdparty/sqlite\"", + "\"$(SRCROOT)/Frameworks/beam-ios-7.5.14510/include/keykeeper\"", ); INFOPLIST_FILE = "$(SRCROOT)/Resources/InfoMasternet.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -5493,11 +5963,12 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/Frameworks/boost", "$(PROJECT_DIR)/Frameworks/testnet", - "$(PROJECT_DIR)/Frameworks/mainnet", + "$(PROJECT_DIR)/Frameworks/beam-ios-7.5.14510/lib", ); - MARKETING_VERSION = 7.3; + MARKETING_VERSION = 7.4; OTHER_LDFLAGS = ( "$(inherited)", + "-Wl,-force_load,$(SRCROOT)/Frameworks/beam-ios-7.5.14510/lib/libsqlite.a", "-ObjC", "-l\"c++\"", "-l\"z\"", diff --git a/BeamWallet.xcodeproj/xcshareddata/xcschemes/BeamWallet.xcscheme b/BeamWallet.xcodeproj/xcshareddata/xcschemes/BeamWallet.xcscheme index 0f1e58c2..56f40e83 100644 --- a/BeamWallet.xcodeproj/xcshareddata/xcschemes/BeamWallet.xcscheme +++ b/BeamWallet.xcodeproj/xcshareddata/xcschemes/BeamWallet.xcscheme @@ -1,6 +1,6 @@ Bool { CrashEye.add(delegate: self) - + if #available(iOS 15.0, *) { UITableView.appearance().sectionHeaderTopPadding = CGFloat(0) } - + if #available(iOS 13.0, *) { let SVGCoder = SDImageSVGCoder.shared SDImageCodersManager.shared.addCoder(SVGCoder) } - + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) - + UIApplication.shared.isIdleTimerDisabled = true - + Localizable.shared.reset() Settings.sharedManager() - + KeyboardListener.shared.start() - + NotificationManager.sharedManager.requestPermissions() - + // if Settings.sharedManager().target != Mainnet { // CrowdinManager.updateLocalizations() // } - + AppModel.sharedManager().checkRecoveryWallet() AppModel.sharedManager().addDelegate(self) Settings.sharedManager().addDelegate(self) let added = AppModel.sharedManager().isWalletAlreadyAdded() - - let rootController = BaseNavigationController.navigationController(rootViewController: added ? EnterWalletPasswordViewController() : WellcomeViewController()) - + + // Backfill the integrity marker for installs that predate the flag + // (v7.3 → v7.4 upgrade): if the DB file is on disk, the wallet was + // healthy before the upgrade — set the flag so the recovery prompt + // below doesn't wipe their data on first launch. + if added && !OnboardManager.shared.isWalletInitializedFlag() { + OnboardManager.shared.markWalletInitialized() + } + + // DB file present but integrity marker missing implies a crash between + // DB init and onWalledOpened — fall back to onboarding with a recovery prompt. + let needsRecovery = added && !OnboardManager.shared.isWalletInitializedFlag() + + let rootViewController: UIViewController = (added && !needsRecovery) + ? EnterWalletPasswordViewController() + : WellcomeViewController() + let rootController = BaseNavigationController.navigationController(rootViewController: rootViewController) + self.window = UIWindow(frame: UIScreen.main.bounds) self.window?.backgroundColor = UIColor.main.navy self.window?.rootViewController = rootController self.window?.makeKeyAndVisible() - - if #available(iOS 12.0, *) { - let isDark = self.window?.rootViewController?.traitCollection.userInterfaceStyle == .dark - Settings.sharedManager().setDefaultDarkMode(isDark) - } - else { - Settings.sharedManager().setDefaultDarkMode(false) + + if needsRecovery { + DispatchQueue.main.async { + rootController.confirmAlert( + title: Localizable.shared.strings.wallet_recovery_title, + message: Localizable.shared.strings.wallet_recovery_message, + cancelTitle: Localizable.shared.strings.cancel, + confirmTitle: Localizable.shared.strings.reset_and_retry, + cancelHandler: { _ in }, + confirmHandler: { _ in + AppModel.sharedManager().resetWallet(true) + } + ) + } } + Settings.sharedManager().setDefaultDarkMode(false) + ShortcutManager.launchWithOptions(launchOptions: launchOptions) if let crash = UserDefaults.standard.string(forKey: "crash"), let crash_name = UserDefaults.standard.string(forKey: "crash_name") { - var name = ""; + var name = "" if Settings.sharedManager().target == Mainnet { name = "GoogleServiceMain" @@ -240,9 +264,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - - //options[.sourceApplication] as? String == "com.beam.runner" - + + // options[.sourceApplication] as? String == "com.beam.runner" + + if url.isFileURL && url.pathExtension.lowercased() == "dapp" { + return handleSideloadedDApp(url: url) + } + if let params = url.queryParameters, params.count == 2, let amount = params["amount"], let userId = params["user_id"] { @@ -263,6 +291,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + private func handleSideloadedDApp(url: URL) -> Bool { + guard AppModel.sharedManager().isLoggedin else { return false } + let secured = url.startAccessingSecurityScopedResource() + defer { if secured { url.stopAccessingSecurityScopedResource() } } + guard let data = try? Data(contentsOf: url) else { return false } + + do { + _ = try DAppManager.shared.installFromZip(data: data, + fallbackName: Localizable.shared.strings.dapps_sideloaded_name, + fallbackIcon: "") + } catch { + return false + } + + guard let top = UIApplication.getTopMostViewController(), + let nav = top.navigationController else { return true } + nav.pushViewController(MyDAppsViewController(), animated: true) + return true + } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { } @@ -325,11 +373,11 @@ extension AppDelegate: WalletModelDelegate { if let result = data, result.type == .address { AppModel.sharedManager().cancelDeleteAddress(result.id) } - }) { data in + }, ended: { data in if let result = data, result.type == .address { AppModel.sharedManager().deletePreparedAddresses(result.id) } - } + }) } } @@ -341,11 +389,11 @@ extension AppDelegate: WalletModelDelegate { if let result = data, result.type == .transaction { AppModel.sharedManager().cancelPreparedTransaction(result.id) } - }) { data in + }, ended: { data in if let result = data, result.type == .transaction { AppModel.sharedManager().sendPreparedTransaction(result.id) } - } + }) } } @@ -363,11 +411,9 @@ extension AppDelegate: WalletModelDelegate { t.isIncome && !t.isSelf } - for transaction in filtered { - if oldTransactions.first(where: { $0 == transaction.id }) == nil { - NotificationManager.sharedManager.scheduleNotification(transaction: transaction) - oldTransactions.append(transaction.id) - } + for transaction in filtered where oldTransactions.first(where: { $0 == transaction.id }) == nil { + NotificationManager.sharedManager.scheduleNotification(transaction: transaction) + oldTransactions.append(transaction.id) } userDefaults.set(oldTransactions, forKey: "transactions_ids") diff --git a/BeamWallet/BeamSDK/AppModel.h b/BeamWallet/BeamSDK/AppModel.h index cac5c73f..ddaec49a 100644 --- a/BeamWallet/BeamSDK/AppModel.h +++ b/BeamWallet/BeamSDK/AppModel.h @@ -2,7 +2,7 @@ // AppModel.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,6 +44,14 @@ #import "AssetsManager.h" #import "StringManager.h" #import "BMApp.h" +#import "BMInstantMessage.h" +#import "BMChat.h" +#import "BMDexOrder.h" +#import "BMInstalledDApp.h" +#import "BMAvailableDApp.h" +#import "BMPublisher.h" + +@class WalletAPIClient; enum { BMRestoreManual = 0, @@ -79,8 +87,14 @@ typedef int BMRestoreType; -(void)onNotificationsChanged; -(void)onChangeCalculated:(double)amount; -(void)onMaxPrivacyTokensLeft:(int)tokens; +-(void)onOfflinePaymentsCountForWalletId:(NSString*_Nonnull)walletId count:(int)count; -(void)onAssetInfoChange; -(void)onDAPPsLoaded; +-(void)onChatListChanged; +-(void)onChatMessagesLoaded:(NSString*_Nonnull)peerWalletId messages:(NSArray*_Nonnull)messages; +-(void)onInstantMessageReceived:(BMInstantMessage*_Nonnull)message; +-(void)onChatRemoved:(NSString*_Nonnull)peerWalletId; +-(void)onDexOrdersChanged:(NSArray*_Nonnull)orders; @end typedef void(^NewAddressGeneratedBlock)(BMAddress* _Nullable address, NSError* _Nullable error); @@ -104,12 +118,14 @@ typedef void(^ExportCSVBlock)(NSString * _Nonnull data, NSURL * _Nonnull url); @property (nonatomic,assign) BOOL isUpdating; @property (nonatomic,assign) BOOL isConnecting; @property (nonatomic,assign) BOOL isLoggedin; +// Read from background queues during restore; BOOL writes are word-atomic. @property (nonatomic,assign) BOOL isRestoreFlow; @property (nonatomic,assign) BOOL isNodeChanging; @property (nonatomic,assign) BOOL isOwnNode; @property (nonatomic,assign) BMRestoreType restoreType; @property (nonatomic,assign) BOOL isMaxPrivacyRequest; @property (nonatomic,assign) BOOL isConfigured; +@property (nonatomic,assign) BOOL didLoadFullAssetsList; @property (nonatomic,strong) BMWalletStatus* _Nullable walletStatus; @@ -125,10 +141,17 @@ typedef void(^ExportCSVBlock)(NSString * _Nonnull data, NSURL * _Nonnull url); @property (nonatomic,strong) NSMutableDictionary*_Nonnull deletedNotifications; @property (nonatomic,strong) NSMutableArray*_Nonnull apps; @property (nonatomic,strong) NSMutableDictionary*_Nonnull needSaveContacts; +@property (nonatomic,strong) NSMutableArray*_Nonnull chats; +@property (nonatomic,strong) NSMutableDictionary*>*_Nonnull messagesByPeer; +@property (nonatomic,strong) NSMutableArray*_Nonnull dexOrders; + +@property (nonatomic,strong) NSMutableDictionary*_Nonnull offlinePaymentsByWalletId; @property (nonatomic, strong) NSTimer * _Nullable connectionTimer; @property (nonatomic, strong) NSTimer * _Nullable connectionAfterOnlineTimer; +@property (nonatomic, strong, readonly) NSDate * _Nullable lastConnectionChangedAt; + @property (nonatomic, strong) NSString * _Nullable addressGeneratedID; -(void)handleTimer; @@ -137,6 +160,7 @@ typedef void(^ExportCSVBlock)(NSString * _Nonnull data, NSURL * _Nonnull url); +(AppModel*_Nonnull)sharedManager; +(NSString*_Nonnull)chooseRandomNode; ++(NSArray*_Nonnull)defaultPeerAddresses; // delegates -(void)addDelegate:(id_Nullable) delegate; @@ -153,6 +177,7 @@ typedef void(^ExportCSVBlock)(NSString * _Nonnull data, NSURL * _Nonnull url); -(BOOL)canOpenWallet:(NSString*_Nonnull)pass; -(void)restore:(NSString*_Nonnull)path; -(void)resetWallet:(BOOL)removeDatabase; +-(void)abortCreateAndReset; -(void)resetOnlyWallet; -(void)restartWallet; -(void)start; @@ -183,9 +208,14 @@ typedef void(^ExportCSVBlock)(NSString * _Nonnull data, NSURL * _Nonnull url); -(void)generateWithdrawAddress:(NewAddressGeneratedBlock _Nonnull )block; -(void)generateNewWalletAddressWithBlockAndAmount:(int)assetId amount:(double)amount result:(NewAddressGeneratedBlock _Nonnull)block; --(void)generateOfflineAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount result:(PublicAddressBlock _Nonnull)block; +-(void)generateOfflineAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount offlineCount:(uint32_t)offlineCount result:(PublicAddressBlock _Nonnull)block; -(NSString*_Nonnull)generateRegularAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount isPermanentAddress:(BOOL)isPermanentAddress; -(void)generateMaxPrivacyAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount result:(PublicAddressBlock _Nonnull)block; +-(void)generateSBBSAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount result:(PublicAddressBlock _Nonnull)block; +-(void)generatePublicOfflineAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount result:(PublicAddressBlock _Nonnull)block; + +-(int)offlinePaymentsCountForWalletId:(NSString*_Nonnull)walletId; +-(void)requestOfflinePaymentsCountForWalletId:(NSString*_Nonnull)walletId; // addresses @@ -236,6 +266,9 @@ typedef void(^ExportCSVBlock)(NSString * _Nonnull data, NSURL * _Nonnull url); -(BMTransactionParameters*_Nonnull)getTransactionParameters:(NSString*_Nonnull)token; -(void)calculateFee:(double)amount assetId:(int)assetId fee:(double)fee isShielded:(BOOL) isShielded result:(FeecalculatedBlock _Nonnull )block; +// split +-(void)splitCoins:(int)assetId outputGroths:(NSArray*_Nonnull)groths fee:(double)fee; + // logs -(NSString*_Nonnull)getZipLogs ; -(void)clearLogs; @@ -318,15 +351,41 @@ typedef void(^ExportCSVBlock)(NSString * _Nonnull data, NSURL * _Nonnull url); -(void)loadApps; -(void)stopDAO; -(void)startApp:(UIViewController*_Nonnull)controller app:(BMApp*_Nonnull)app; +-(void)startApp:(UIViewController*_Nonnull)controller app:(BMApp*_Nonnull)app installedRoot:(NSURL* _Nullable)installedRoot; -(void)startBeamXDaoApp:(UINavigationController*_Nonnull)controller app:(BMApp*_Nonnull)app; -(void)sendDAOApiResult:(NSString*_Nonnull)json; +-(WalletAPIClient*_Nullable)walletAPIClient; -(void)approveContractInfo:(NSString*_Nonnull)json info:(NSString*_Nonnull)info amounts:(NSString*_Nonnull)amounts; -(void)getAssetInfoAsync:(int)assetId; +-(void)loadFullAssetsList; -(BMApp*_Nonnull)DAOBeamXApp; -(BMApp*_Nonnull)daoGalleryApp; -(BMApp*_Nonnull)daoFaucetApp; -(BMApp*_Nonnull)votingApp; +// Messenger +-(void)requestChats; +-(void)requestMessagesForPeer:(NSString*_Nonnull)peerWalletId; +-(void)sendInstantMessage:(NSString*_Nonnull)peerWalletId + fromAddress:(NSString*_Nonnull)myWalletId + message:(NSString*_Nonnull)message; +-(void)markChatAsRead:(NSString*_Nonnull)peerWalletId; +-(void)removeChat:(NSString*_Nonnull)peerWalletId; +-(void)addChatStub:(NSString*_Nonnull)peerWalletId contactName:(NSString*_Nullable)contactName myWalletId:(NSString*_Nullable)myWalletId; +-(NSArray*_Nonnull)cachedMessagesForPeer:(NSString*_Nonnull)peerWalletId; +-(NSString*_Nullable)lastMyAddressForPeer:(NSString*_Nonnull)peerWalletId; +-(NSString*_Nonnull)resolvedPeerWalletId:(NSString*_Nonnull)peerWalletId; + +// Asset Swaps (DEX) +-(void)requestDexOrders; +-(BOOL)publishDexOrderWithSendAsset:(UInt32)sendAssetId + sendAmount:(UInt64)sendAmount + receiveAsset:(UInt32)receiveAssetId + receiveAmount:(UInt64)receiveAmount + expirationMinutes:(UInt32)expirationMinutes; +-(void)cancelDexOrderWithID:(NSString*_Nonnull)hexOrderID; +-(BOOL)acceptDexOrder:(BMDexOrder*_Nonnull)order; + @end diff --git a/BeamWallet/BeamSDK/AppModel.mm b/BeamWallet/BeamSDK/AppModel.mm index 1ee7cd67..3f7a3693 100644 --- a/BeamWallet/BeamSDK/AppModel.mm +++ b/BeamWallet/BeamSDK/AppModel.mm @@ -2,7 +2,7 @@ // AppModel.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ #import "WalletModel.h" #import "StringStd.h" #import "DAOManager.h" +#import "WalletAPIClient+Internal.h" #import "RecoveryProgress.h" #import "DiskStatusManager.h" @@ -51,10 +52,15 @@ #include "wallet/client/wallet_client.h" #include "wallet/core/default_peers.h" +#ifdef BEAM_IPFS_SUPPORT +#include "wallet/ipfs/ipfs_config.h" +#endif + #include "core/block_rw.h" #include "build/core/version.h" #include "utility/bridge.h" +#include "utility/logger.h" #include "utility/string_helpers.h" #include "utility/fsutils.h" @@ -69,7 +75,13 @@ #include "wallet/core/common_utils.h" #include "wallet/core/common.h" #include "common.h" +#include "sqlite3.h" #include +#include + +// beam::Rules::s_pInstance is declared thread_local in the header but its +// out-of-class definition is missing from the provided static libraries. +thread_local const beam::Rules* beam::Rules::s_pInstance = nullptr; #import #import "BeamWallet-Swift.h" @@ -111,6 +123,24 @@ const bool isSecondCurrencyEnabled = true; typedef void(^NewGenerateVaucherBlock)(ShieldedVoucherList v); +// BEAM's bundled sqlite needs a writable temp dir; iOS sandbox blocks /tmp. +// Point sqlite3_temp_directory at the app's NSTemporaryDirectory once per +// process, before any DB open, so CREATE TABLE / journaling has a place to +// spill — otherwise WalletDB::open / WalletDB::init can fail with +// SQLITE_MISUSE on existing installs as well as fresh creates. +// dispatch_once guarantees a single sqlite3_mprintf allocation; no +// sqlite3_free needed because the value is owned for the lifetime of the +// process. +static void ensureSqliteTempDir(void) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *tmpDir = NSTemporaryDirectory(); + if (tmpDir.length > 0) { + sqlite3_temp_directory = sqlite3_mprintf("%s", [tmpDir UTF8String]); + } + }); +} + struct GenerateVaucherFunc { NewGenerateVaucherBlock newGenerateVaucherBlock; @@ -156,6 +186,10 @@ void operator() (std::string token){ static dispatch_once_t * once_token_model; +@interface AppModel () +- (void)configureEmbeddedIPFS; +@end + @implementation AppModel { BOOL isStarted; BOOL isRunning; @@ -182,12 +216,19 @@ @implementation AppModel { DAOManager *daoManager; DAOViewController *daoViewController; + WalletAPIClient *walletAPIClient; boost::optional _receiverAddress; RecoveryProgress recoveryProgress; } ++ (void)load { + // Set sqlite3_temp_directory before any code (incl. AppDelegate's + // canOpenWallet probe on warm-launch) touches the BEAM SQLite handle. + ensureSqliteTempDir(); +} + + (AppModel*_Nonnull)sharedManager { static AppModel *sharedMyManager = nil; @@ -206,7 +247,9 @@ -(void)didGenerateVauchers:(ShieldedVoucherList) v { -(id)init{ self = [super init]; - + + [self loadRules]; + [self createLogger]; wallet::g_AssetsEnabled = true; @@ -234,6 +277,10 @@ -(id)init{ _isRestoreFlow = [[NSUserDefaults standardUserDefaults] boolForKey:restoreFlowKey]; _apps = [[NSMutableArray alloc] init]; + _chats = [[NSMutableArray alloc] init]; + _messagesByPeer = [[NSMutableDictionary alloc] init]; + _dexOrders = [[NSMutableArray alloc] init]; + _offlinePaymentsByWalletId = [[NSMutableDictionary alloc] init]; NSData *dataStatus = [[NSUserDefaults standardUserDefaults] objectForKey:walletStatusKey]; if(dataStatus != nil) { @@ -272,16 +319,44 @@ -(id)init{ [self checkInternetConnection]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActiveNotification) name:UIApplicationDidBecomeActiveNotification object:nil]; - - [self loadRules]; - + return self; } +// Raw storage so we can publish s_pInstance before Rules::Rules() runs — +// the constructor calls Rules::get() internally and would throw otherwise. +static unsigned char g_rulesStorage[sizeof(beam::Rules)] __attribute__((aligned(__alignof__(beam::Rules)))); + +// Non-thread-local handle WalletModel.mm uses to install s_pInstance on worker threads. +beam::Rules* g_pConfiguredRules = nullptr; + +static beam::Rules& getConfiguredRules() { + static std::once_flag s_flag; + std::call_once(s_flag, []() { + beam::Rules* pRules = reinterpret_cast(g_rulesStorage); + beam::Rules::s_pInstance = pRules; + new(pRules) beam::Rules(); + + NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; + if ([bundleID containsString:@"Masternet"] || [bundleID containsString:@"masternet"]) { + pRules->m_Network = beam::Rules::Network::masternet; + } else if ([bundleID containsString:@"Mainnet"] || [bundleID containsString:@"mainnet"]) { + pRules->m_Network = beam::Rules::Network::mainnet; + } else { + pRules->m_Network = beam::Rules::Network::testnet; + } + pRules->SetNetworkParams(); + pRules->UpdateChecksum(); + g_pConfiguredRules = pRules; + }); + return *reinterpret_cast(g_rulesStorage); +} + -(void)loadRules{ - Rules::get().UpdateChecksum(); - LOG_INFO() << "Rules signature: " << Rules::get().get_SignatureStr(); + beam::Rules& rules = getConfiguredRules(); + beam::Rules::s_pInstance = &rules; // Install on this thread (s_pInstance is thread_local) + BEAM_LOG_INFO() << "Rules signature: " << rules.get_SignatureStr(); } +(NSString*_Nonnull)chooseRandomNodeWithoutNodes:(NSArray*)nodes { @@ -317,25 +392,33 @@ +(NSString*_Nonnull)chooseRandomNodeWithoutNodes:(NSArray*)nodes { return @""; } -+(NSString*_Nonnull)chooseRandomNode { ++(NSArray*_Nonnull)defaultPeerAddresses { + // Settings.init calls this class method before AppModel.init runs, so the + // BEAM library hasn't been initialized yet — install Rules first. + beam::Rules::s_pInstance = &getConfiguredRules(); auto peers = getDefaultPeers(); - - NSMutableArray *array = [NSMutableArray array]; - + + NSMutableArray *array = [NSMutableArray array]; + for (const auto& item : peers) { NSString *address = [NSString stringWithUTF8String:item.c_str()]; - if([address rangeOfString:@"shanghai"].location == NSNotFound - && [address rangeOfString:@"raskul"].location == NSNotFound - && [address rangeOfString:@"45."].location == NSNotFound) { + if ([address rangeOfString:@"shanghai"].location == NSNotFound + && [address rangeOfString:@"raskul"].location == NSNotFound + && [address rangeOfString:@"45."].location == NSNotFound) { [array addObject:address]; } } - - srand([[NSDate date] timeIntervalSince1970]); - - int inx = rand()%[array count]; - - return [array objectAtIndex:inx]; + + return array; +} + ++(NSString*_Nonnull)chooseRandomNode { + NSArray *array = [AppModel defaultPeerAddresses]; + if (array.count == 0) { + return @""; + } + srand([[NSDate date] timeIntervalSince1970]); + return array[rand() % array.count]; } -(void)setWalletAddresses:(NSMutableArray *)walletAddresses { @@ -455,8 +538,11 @@ -(void)setIsConnected:(BOOL)isConnected { [self getWalletStatus]; [self getUTXO]; } + if (_isConnected != isConnected) { + _lastConnectionChangedAt = [NSDate date]; + } _isConnected = isConnected; - + if (wallet != nil) { _isConfigured = wallet->isConnectionTrusted(); } @@ -524,13 +610,22 @@ -(BOOL)isWalletAlreadyAdded { } -(BOOL)openWallet:(NSString*)pass { - if (walletReactor == nil) { - walletReactor = Reactor::create(); - io::Reactor::Scope s(*walletReactor); // do it in main thread - } - + // Belt-and-braces: +load already set this, but cover the case where a + // future caller skips +load (e.g. unit harness) so the DB always sees a + // writable temp dir. + ensureSqliteTempDir(); + + // BEAM 7.5.14432+ rejects WalletDB::open with SQLITE_MISUSE when the + // current reactor has been scoped in/out previously (e.g. the one created + // in AppModel.init). Drop and recreate so open runs against a fresh one. + walletReactor.reset(); + walletReactor = Reactor::create(); + // Scope must outlive WalletDB::open so the reactor is registered as current + // when the DB sets up its internal I/O callbacks (required in 7.5.14432+). + io::Reactor::Scope s(*walletReactor); + string dbFilePath = Settings.sharedManager.walletStoragePath.string; - + if (!walletDb) { try{ walletDb = WalletDB::open(dbFilePath, pass.string); @@ -552,17 +647,22 @@ -(BOOL)openWallet:(NSString*)pass { } -(BOOL)canOpenWallet:(NSString*)pass { - if (walletReactor == nil) { - walletReactor = Reactor::create(); - io::Reactor::Scope s(*walletReactor); // do it in main thread - } - + // Same temp-dir guarantee as openWallet:; idempotent via dispatch_once. + ensureSqliteTempDir(); + + // See openWallet: BEAM 7.5.14432+ requires a fresh reactor here. + walletReactor.reset(); + walletReactor = Reactor::create(); + // Scope must outlive WalletDB::open so the reactor is registered as current + // when the DB sets up its internal I/O callbacks (required in 7.5.14432+). + io::Reactor::Scope s(*walletReactor); + string dbFilePath = [Settings sharedManager].walletStoragePath.string; - + if (walletDb != nil) { return YES; } - + try{ walletDb = WalletDB::open(dbFilePath, pass.string); } @@ -584,69 +684,102 @@ -(BOOL)isValidPassword:(NSString*_Nonnull)pass { } -(BOOL)createWallet:(NSString*)phrase pass:(NSString*)pass { - if (self.isInternetAvailable == NO) { - return NO; - } - else if (walletDb != nil) + // DB init is purely local — do not gate on reachability. If the device is + // offline the wallet is created locally and sync resumes when reachability + // returns (Reachability already broadcasts onNoInternetConnection). + if (walletDb != nil) { [self onWalledOpened:SecString(pass.string)]; - + return YES; } string dbFilePath = [Settings sharedManager].walletStoragePath.string; - - //already created. restore wallet? - if (WalletDB::isInitialized(dbFilePath)) { - return NO; - } - + //invalid parameters if ((phrase==nil || phrase.length==0) || (pass==nil || pass.length==0)) { return NO; } - + //convert string phrase to mnemonic WordList NSArray *wordsArray = [phrase componentsSeparatedByString:@";"]; - + vector wordList(wordsArray.count); - + for(int i=0; icreateAddress(address); -// address.m_label = "Default"; -// address.setExpirationStatus(beam::wallet::WalletAddress::ExpirationStatus::Never); -// walletDb->saveAddress(address); - + [self onWalledOpened:SecString(pass.string)]; - + return YES; } +-(void)abortCreateAndReset { + self.isRestoreFlow = NO; + [self resetWallet:YES]; +} + -(void)resetOnlyWallet { isStarted = NO; isRunning = NO; - + self.didLoadFullAssetsList = NO; + if (wallet!=nil){ wallet.reset(); } @@ -654,11 +787,12 @@ -(void)resetOnlyWallet { } -(void)restartWallet { - LOG_INFO() << "restart wallet"; - + BEAM_LOG_INFO() << "restart wallet"; + isStarted = NO; isRunning = NO; - + self.didLoadFullAssetsList = NO; + if (wallet!=nil){ wallet.reset(); } @@ -681,45 +815,46 @@ -(void)resetWallet:(BOOL)removeDatabase { if (self.isRestoreFlow) { self.isRestoreFlow = NO; } - + [daoManager destroy]; daoManager = nil; - + + [walletAPIClient destroy]; + walletAPIClient = nil; + + dispatch_async(dispatch_get_main_queue(), ^{ + [[OraclePriceManager shared] stop]; + }); + isStarted = NO; isRunning = NO; - + self.didLoadFullAssetsList = NO; + wallet.reset(); - + walletDb.reset(); - -// if (wallet!=nil){ -// wallet.reset(); -// } -// -// if (walletReactor!=nil){ -// walletReactor.reset(); -// } -// if (walletDb!=nil){ -// walletDb.reset(); -// } -// -// walletReactor = nil; -// wallet = nil; -// walletDb = nil; - + if(removeDatabase) { - NSString *recoverPath = [[Settings sharedManager].walletStoragePath stringByAppendingString:@"_recover"]; - - fsutils::remove([Settings sharedManager].walletStoragePath.string); + NSString *dbPath = [Settings sharedManager].walletStoragePath; + + NSFileManager *fm = [NSFileManager defaultManager]; + [fm removeItemAtPath:[dbPath stringByAppendingString:@"-wal"] error:nil]; + [fm removeItemAtPath:[dbPath stringByAppendingString:@"-shm"] error:nil]; + [fm removeItemAtPath:[dbPath stringByAppendingString:@"-journal"] error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:[Settings sharedManager].walletStoragePath error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:[Settings sharedManager].localNodeStorage error:nil]; + fsutils::remove(dbPath.string); + [fm removeItemAtPath:dbPath error:nil]; + + [fm removeItemAtPath:[Settings sharedManager].localNodeStorage error:nil]; [[Settings sharedManager] resetDataBase]; + + [[OnboardManager shared] clearWalletInitialized]; } _walletStatus = [BMWalletStatus new]; [_transactions removeAllObjects]; [_notifications removeAllObjects]; + [_offlinePaymentsByWalletId removeAllObjects]; [[AssetsManager.sharedManager assets] removeAllObjects]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:notificationsKey]; @@ -825,7 +960,11 @@ -(BOOL)isWalletRunning { -(void)onWalledOpened:(const SecString&) pass { passwordHash = pass.hash(); - + + // Marker that distinguishes a healthy wallet from a DB file orphaned by + // a crash mid-create. AppDelegate checks this on launch. + [[OnboardManager shared] markWalletInitialized]; + if(!self.isRestoreFlow) { [self start]; @@ -838,37 +977,72 @@ -(void)onWalledOpened:(const SecString&) pass { -(void)restore:(NSString*_Nonnull)path{ if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { string recoveryPath = path.string; - + + // Caller dispatches us onto a background queue, so Rules::s_pInstance + // (thread_local) isn't set on this thread yet — install it before any + // BEAM API runs, otherwise WalletModel ctor / wallet->start() throw. + [self loadRules]; + try{ + // Reactor must be current while WalletModel binds to walletDb, + // otherwise the worker thread inherits no current reactor and + // sqlite calls fail with SQLITE_MISUSE. + if (walletReactor == nil) { + walletReactor = Reactor::create(); + } + io::Reactor::Scope reactorScope(*walletReactor); + string nodeAddrStr = [Settings sharedManager].nodeAddress.string; - + auto pushTxCreator = std::make_shared([=]() { return walletDb; }); auto additionalTxCreators = std::make_shared>(); additionalTxCreators->emplace(TxType::PushTransaction, pushTxCreator); wallet = make_shared(walletDb, nodeAddrStr, walletReactor); - + NSLog(@"NODE ADDRESS: %@", [Settings sharedManager].nodeAddress); wallet->getAsync()->setNodeAddress(nodeAddrStr); - + if ([Settings sharedManager].isNodeProtocolEnabled) { [Settings sharedManager].isNodeProtocolEnabled = NO; wallet->getAsync()->enableBodyRequests(false); } - + wallet->start(activeNotifications, isSecondCurrencyEnabled, additionalTxCreators); - - daoManager = [[DAOManager alloc] initWithWallet:wallet]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - self->wallet->getAsync()->importRecovery(recoveryPath); + __weak typeof(self) weakSelf = self; + wallet->getAsync()->makeIWTCall([]() -> boost::any { + if (!beam::Rules::s_pInstance && g_pConfiguredRules) { + beam::Rules::s_pInstance = g_pConfiguredRules; + } + return boost::any{}; + }, [weakSelf, recoveryPath](const boost::any&) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf && strongSelf->wallet) { + strongSelf->wallet->getAsync()->importRecovery(recoveryPath); + } }); + + daoManager = [[DAOManager alloc] initWithWallet:wallet]; + // Configure the embedded IPFS daemon *before* the wallet-owned + // AppsApi is created. WebAPICreator::createApi requests an IPFS + // service handle (ipfsnode=true), which makes the wallet thread + // call IWThread_startIPFSNode — and that needs a valid repo path + // queued via setIPFSConfig first. + [self configureEmbeddedIPFS]; + walletAPIClient = [[WalletAPIClient alloc] initWithWallet:wallet]; + [walletAPIClient ensureApi]; + if ([Settings sharedManager].isOracleEnabled) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[OraclePriceManager shared] start]; + }); + } } catch (const std::exception& e) { NSLog(@"ImportRecovery failed %s",e.what()); - + NSString *erorString = [NSString stringWithUTF8String:e.what()]; NSArray *delegates = [AppModel sharedManager].delegates.allObjects; @@ -884,7 +1058,7 @@ -(void)restore:(NSString*_Nonnull)path{ } catch (...) { NSLog(@"ImportRecovery failed"); - + NSString *erorString = @"Recovery failed"; NSArray *delegates = [AppModel sharedManager].delegates.allObjects; @@ -907,35 +1081,73 @@ bool OnProgress(uint64_t done, uint64_t total) { -(void)start { if (isStarted == NO && walletDb != nil) { - string nodeAddrStr = [Settings sharedManager].nodeAddress.string; - - auto pushTxCreator = std::make_shared([=]() { return walletDb; }); - auto additionalTxCreators = std::make_shared>(); - additionalTxCreators->emplace(TxType::PushTransaction, pushTxCreator); - - wallet = make_shared(walletDb, nodeAddrStr, walletReactor); - - NSLog(@"NODE ADDRESS: %@", [Settings sharedManager].nodeAddress); + // Reactor must be current while WalletModel constructs and wallet->start() + // runs, so DB callbacks bind to the same reactor walletDb was opened under. + if (walletReactor == nil) { + walletReactor = Reactor::create(); + } + io::Reactor::Scope reactorScope(*walletReactor); - wallet->getAsync()->setNodeAddress(nodeAddrStr); - - wallet->getAsync()->enableBodyRequests([Settings sharedManager].isNodeProtocolEnabled); - - wallet->start(activeNotifications, isSecondCurrencyEnabled, additionalTxCreators); - - isRunning = YES; - isStarted = YES; - - daoManager = [[DAOManager alloc] initWithWallet:wallet]; - } - else if(self.isConnected && isStarted && walletDb != nil && self.isInternetAvailable) { - NSArray *delegates = [AppModel sharedManager].delegates.allObjects; - for(id delegate in delegates) - { - if ([delegate respondsToSelector:@selector(onSyncProgressUpdated: total:)]) { - [delegate onSyncProgressUpdated:0 total:0]; + try { + string nodeAddrStr = [Settings sharedManager].nodeAddress.string; + + auto pushTxCreator = std::make_shared([=]() { return walletDb; }); + auto additionalTxCreators = std::make_shared>(); + additionalTxCreators->emplace(TxType::PushTransaction, pushTxCreator); + + wallet = make_shared(walletDb, nodeAddrStr, walletReactor); + + NSLog(@"NODE ADDRESS: %@", [Settings sharedManager].nodeAddress); + + wallet->getAsync()->setNodeAddress(nodeAddrStr); + wallet->getAsync()->enableBodyRequests([Settings sharedManager].isNodeProtocolEnabled); + wallet->start(activeNotifications, isSecondCurrencyEnabled, additionalTxCreators); + + // Install Rules::s_pInstance on the reactor's worker thread as the + // first queued op so DB callbacks find a valid Rules pointer. + wallet->getAsync()->makeIWTCall([]() -> boost::any { + if (!beam::Rules::s_pInstance && g_pConfiguredRules) { + beam::Rules::s_pInstance = g_pConfiguredRules; + } + return boost::any{}; + }, [](const boost::any&){}); + + isRunning = YES; + isStarted = YES; + + // Prefetch the full asset registry once per session so Receive / + // Asset Search / Asset Swap don't pay a network round-trip on + // first open. Must run after wallet->start() so `wallet` is + // non-null — otherwise loadFullAssetsList short-circuits and the + // didLoadFullAssetsList flag is never set on the restore path. + [self loadFullAssetsList]; + + daoManager = [[DAOManager alloc] initWithWallet:wallet]; + // Configure the embedded IPFS daemon *before* the wallet-owned + // AppsApi is created. WebAPICreator::createApi requests an IPFS + // service handle (ipfsnode=true), which makes the wallet thread + // call IWThread_startIPFSNode — and that needs a valid repo path + // queued via setIPFSConfig first. + [self configureEmbeddedIPFS]; + walletAPIClient = [[WalletAPIClient alloc] initWithWallet:wallet]; + [walletAPIClient ensureApi]; + if ([Settings sharedManager].isOracleEnabled) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[OraclePriceManager shared] start]; + }); } } + catch (const std::exception& e) { + BEAM_LOG_ERROR() << "AppModel.start failed: " << e.what(); + wallet.reset(); + isRunning = NO; + isStarted = NO; + } + } + else if(self.isConnected && isStarted && walletDb != nil && self.isInternetAvailable) { + // No-op: previously broadcast onSyncProgressUpdated:0 total:0, which + // progress UIs interpret as "starting over" and reset their bars. + // Callers that need a fresh status should use getWalletStatus. } else if(wallet != nil) { @@ -1044,6 +1256,9 @@ -(void)getWalletStatus { wallet->getAsync()->getNotifications(); [self getMinConfirmations]; [self getMaxPrivacyLock]; + if ([Settings sharedManager].isOracleEnabled) { + [[OraclePriceManager shared] fetchPriceNow]; + } } } @@ -1149,43 +1364,19 @@ -(void)generateWithdrawAddress:(NewAddressGeneratedBlock _Nonnull )block { } --(void)generateOfflineAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount result:(PublicAddressBlock _Nonnull)block { - - if (wallet!=nil) { - uint32_t bAsset = (uint32_t)assetId; - uint64_t bAmount = round(amount * Rules::Coin); - - auto func = NewTokenGeneratedFunc(); - func.block = ^(std::string token) { - NSString *sToken = [NSString stringWithUTF8String:token.c_str()]; - block(sToken); - }; - wallet->getAsync()->generateToken(TokenType::Offline, bAmount, bAsset, std::string(BEAM_LIB_VERSION), func); - } - -// uint32_t bAsset = (uint32_t)assetId; -// uint64_t bAmount = round(amount * Rules::Coin); -// -// WalletID m_walletID(Zero); -// m_walletID.FromHex(walletId.string); -// -// auto address = walletDb->getAddress(m_walletID); -// auto lastVouchers = GenerateVoucherList(walletDb->get_KeyKeeper(), address->m_OwnID, 1); -// -// TxParameters offlineParameters; -// offlineParameters.SetParameter(TxParameterID::TransactionType, beam::wallet::TxType::PushTransaction); -// offlineParameters.SetParameter(TxParameterID::ShieldedVoucherList, lastVouchers); -// offlineParameters.SetParameter(TxParameterID::PeerAddr, address->m_BbsAddr); -// offlineParameters.SetParameter(TxParameterID::PeerEndpoint, address->m_Endpoint); -// offlineParameters.SetParameter(TxParameterID::IsPermanentPeerID, true); -// offlineParameters.SetParameter(TxParameterID::AssetID, beam::Asset::ID(bAsset)); -// if (bAmount > 0) { -// offlineParameters.SetParameter(TxParameterID::Amount, bAmount); -// } -// auto token = to_string(offlineParameters); -// block([NSString stringWithUTF8String:token.c_str()]); - - +-(void)generateOfflineAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount offlineCount:(uint32_t)offlineCount result:(PublicAddressBlock _Nonnull)block { + if (wallet == nil || walletDb == nullptr) return; + + WalletID wid(Zero); + if (!wid.FromHex(walletId.string)) return; + + auto address = walletDb->getAddress(wid); + if (!address) return; + + uint32_t bAsset = (uint32_t)assetId; + uint64_t bAmount = round(amount * Rules::Coin); + auto token = GenerateOfflineToken(*address, *walletDb, bAmount, bAsset, std::string(BEAM_LIB_VERSION), offlineCount); + block([NSString stringWithUTF8String:token.c_str()]); } -(NSString*_Nonnull)generateRegularAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount isPermanentAddress:(BOOL)isPermanentAddress { @@ -1211,7 +1402,7 @@ -(void)generateMaxPrivacyAddress:(NSString*_Nonnull)walletId assetId:(int)assetI NSString *sToken = [NSString stringWithUTF8String:token.c_str()]; block(sToken); }; - wallet->getAsync()->generateToken(TokenType::MaxPrivacy, bAmount, bAsset, std::string(BEAM_LIB_VERSION), func); + wallet->getAsync()->generateToken(TokenType::MaxPrivacy, bAmount, bAsset, std::string(BEAM_LIB_VERSION), false, func); } // uint64_t bAmount = round(amount * Rules::Coin); // uint32_t bAsset = (uint32_t)assetId; @@ -1224,10 +1415,58 @@ -(void)generateMaxPrivacyAddress:(NSString*_Nonnull)walletId assetId:(int)assetI // block([NSString stringWithUTF8String:maxPrivacyAddress.c_str()]); } +-(void)generateSBBSAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount result:(PublicAddressBlock _Nonnull)block { + if (wallet != nil) { + uint32_t bAsset = (uint32_t)assetId; + uint64_t bAmount = round(amount * Rules::Coin); + + auto func = NewTokenGeneratedFunc(); + func.block = ^(std::string token) { + NSString *sToken = [NSString stringWithUTF8String:token.c_str()]; + block(sToken); + }; + wallet->getAsync()->generateToken(TokenType::RegularOldStyle, bAmount, bAsset, std::string(BEAM_LIB_VERSION), false, func); + } +} + +-(void)generatePublicOfflineAddress:(NSString*_Nonnull)walletId assetId:(int)assetId amount:(double)amount result:(PublicAddressBlock _Nonnull)block { + if (wallet != nil) { + uint32_t bAsset = (uint32_t)assetId; + uint64_t bAmount = round(amount * Rules::Coin); + + auto func = NewTokenGeneratedFunc(); + func.block = ^(std::string token) { + NSString *sToken = [NSString stringWithUTF8String:token.c_str()]; + block(sToken); + }; + wallet->getAsync()->generateToken(TokenType::Public, bAmount, bAsset, std::string(BEAM_LIB_VERSION), false, func); + } +} + +-(int)offlinePaymentsCountForWalletId:(NSString*_Nonnull)walletId { + NSNumber *cached = self.offlinePaymentsByWalletId[walletId]; + return cached != nil ? cached.intValue : -1; +} + +-(void)requestOfflinePaymentsCountForWalletId:(NSString*_Nonnull)walletId { + if (wallet == nil || walletId.length == 0) return; + if (self.offlinePaymentsByWalletId[walletId] != nil) return; + WalletID wid(Zero); + if (!wid.FromHex(walletId.string)) return; + wallet->getAsync()->getAddress(wid); +} + -(void)getAssetInfoAsync:(int)assetId { wallet->getAsync()->getAssetInfo((uint)assetId); } +-(void)loadFullAssetsList { + if (wallet == nullptr) return; + if (self.didLoadFullAssetsList) return; + self.didLoadFullAssetsList = YES; + wallet->getAsync()->loadFullAssetsList(); +} + #pragma mark - Addresses @@ -1569,12 +1808,14 @@ -(void)generateNewWalletAddressWithBlockAndAmount:(int)assetId amount:(double)am auto func = NewTokenGeneratedFunc(); func.block = ^(std::string token) { [AppModel sharedManager].addressGeneratedID = @""; - + NSString *sToken = [NSString stringWithUTF8String:token.c_str()]; auto pParams = beam::wallet::ParseParameters(token); - + BMAddress *address = [[BMAddress alloc] init]; address.address = sToken; + address.walletId = @""; + address.label = @""; if (pParams) { beam::wallet::WalletID pid; @@ -1583,16 +1824,16 @@ -(void)generateNewWalletAddressWithBlockAndAmount:(int)assetId amount:(double)am } } self.generatedNewAddressBlock(address, nil); - + self->wallet->getAsync()->getAddresses(true); }; - wallet->getAsync()->generateToken(TokenType::RegularNewStyle, bAmount, bAsset, std::string(BEAM_LIB_VERSION), func); + wallet->getAsync()->generateToken(TokenType::RegularNewStyle, bAmount, bAsset, std::string(BEAM_LIB_VERSION), true, func); } } -(void)generateNewWalletAddressWithBlock:(NewAddressGeneratedBlock _Nonnull )block{ self.generatedNewAddressBlock = block; - + if (wallet!=nil) { uint64_t amount = 0; auto asset = beam::Asset::ID(0); @@ -1602,9 +1843,11 @@ -(void)generateNewWalletAddressWithBlock:(NewAddressGeneratedBlock _Nonnull )blo NSString *sToken = [NSString stringWithUTF8String:token.c_str()]; auto pParams = beam::wallet::ParseParameters(token); - + BMAddress *address = [[BMAddress alloc] init]; address.address = sToken; + address.walletId = @""; + address.label = @""; if (pParams) { beam::wallet::WalletID pid; @@ -1614,8 +1857,8 @@ -(void)generateNewWalletAddressWithBlock:(NewAddressGeneratedBlock _Nonnull )blo } self.generatedNewAddressBlock(address, nil); }; - - wallet->getAsync()->generateToken(TokenType::RegularNewStyle, amount, asset, std::string(BEAM_LIB_VERSION), func); + + wallet->getAsync()->generateToken(TokenType::RegularNewStyle, amount, asset, std::string(BEAM_LIB_VERSION), false, func); // wallet->getAsync()->generateNewAddress(); } } @@ -1930,16 +2173,16 @@ - (NSUInteger)indexOfDelegate:(id)delegate { -(void)addDelegate:(id_Nullable) delegate { [_delegates compact]; - + void * objPtr = (__bridge void *)delegate; [_delegates addPointer:objPtr]; } -(void)removeDelegate:(id_Nullable) delegate { [_delegates compact]; - + void * objPtr = (__bridge void *)delegate; - + for(NSUInteger i = 0; i < _delegates.count; i++) { void * ptr = [_delegates pointerAtIndex:i]; if (ptr == objPtr) { @@ -2230,7 +2473,7 @@ -(void)prepareSend:(double)amount fee:(double)fee assetId:(int)assetId to:(NSStr transaction.assetId = assetId; [_preparedTransactions addObject:transaction]; - + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; for(id delegate in delegates) { @@ -2240,6 +2483,24 @@ -(void)prepareSend:(double)amount fee:(double)fee assetId:(int)assetId to:(NSStr } } +-(void)splitCoins:(int)assetId outputGroths:(NSArray*_Nonnull)groths fee:(double)fee { + if (wallet == nil || groths.count == 0) { + return; + } + + AmountList amountList; + amountList.reserve(groths.count); + for (NSNumber *n in groths) { + amountList.push_back((Amount)n.unsignedLongLongValue); + } + + auto params = CreateSplitTransactionParameters(amountList); + params.SetParameter(TxParameterID::Fee, (Amount)fee) + .SetParameter(TxParameterID::AssetID, beam::Asset::ID((uint32_t)assetId)); + + wallet->getAsync()->startTransaction(std::move(params)); +} + -(NSString*_Nonnull)allAmount:(double)fee assetId:(int)assetId { Amount available = [[AssetsManager sharedManager] getAsset:assetId].available; if(assetId == 0) { @@ -2426,7 +2687,7 @@ -(void)createLogger { [self clearLogs]; - static auto logger = beam::Logger::create(LOG_LEVEL_DEBUG,LOG_LEVEL_DEBUG,LOG_LEVEL_DEBUG,@"beam_".string, dataPath.string); + static auto logger = beam::Logger::create(BEAM_LOG_LEVEL_DEBUG,BEAM_LOG_LEVEL_DEBUG,BEAM_LOG_LEVEL_DEBUG,@"beam_".string, dataPath.string); auto path = logger->get_current_file_name(); pathLog = [NSString stringWithUTF8String:path.c_str()]; @@ -2442,12 +2703,12 @@ -(void)createLogger { NSString *appVersion = [NSString stringWithFormat:@"APP VERSION: %@ BUILD %@",version, build]; NSString *langCode = [NSString stringWithFormat:@"LANGUAGE CODE: %@",[[NSLocale currentLocale] languageCode]]; - LOG_INFO() << "Application has started"; - LOG_INFO() << ios.string; - LOG_INFO() << model.string; - LOG_INFO() << modelID.string; - LOG_INFO() << appVersion.string; - LOG_INFO() << langCode.string; + BEAM_LOG_INFO() << "Application has started"; + BEAM_LOG_INFO() << ios.string; + BEAM_LOG_INFO() << model.string; + BEAM_LOG_INFO() << modelID.string; + BEAM_LOG_INFO() << appVersion.string; + BEAM_LOG_INFO() << langCode.string; } @@ -2885,7 +3146,7 @@ bool IsValidTimeStamp(Timestamp currentBlockTime_s) if (currentTime_s > currentBlockTime_s) { - LOG_INFO() << "It seems that node is not up to date"; + BEAM_LOG_INFO() << "It seems that node is not up to date"; return false; } return true; @@ -3180,9 +3441,42 @@ -(void)enableBodyRequests:(BOOL)value { } -(void)sendDAOApiResult:(NSString*_Nonnull)json { + // Route store / publishers / IPFS responses (id >= 1_000_000) to the + // wallet-owned client. Anything else belongs to the DApp running in + // DAOViewController, which uses the per-DApp AppsApiUI context. + if ([walletAPIClient routeResult:json]) { + return; + } [daoViewController sendDAOApiResultWithJson:json]; } +-(WalletAPIClient*_Nullable)walletAPIClient { + return walletAPIClient; +} + +-(void)configureEmbeddedIPFS { +#ifdef BEAM_IPFS_SUPPORT + if (!wallet) { + return; + } + // Drop the embedded go-ipfs node's repo under Documents/. The swarm key + // is auto-derived from beam::Rules::Network in ipfs_imp.cpp, so the node + // joins the right private swarm without explicit configuration — + // dappnet/testnet/mainnet/masternet each have their own pre-shared key + // baked into the BEAM core. We don't call startIPFSNode here: that's + // triggered as part of AppsApi creation (ipfsnode=true in createApi), + // which queues IWThread_startIPFSNode on the same wallet-client thread + // *after* this setIPFSConfig has run. + asio_ipfs::config cfg(asio_ipfs::config::Mode::Desktop); + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths firstObject]; + NSString *repoPath = [documentsDirectory stringByAppendingPathComponent:@"ipfs-repo"]; + cfg.repo_root = repoPath.string; + + wallet->getAsync()->setIPFSConfig(std::move(cfg)); +#endif +} + -(void)approveContractInfo:(NSString*_Nonnull)json info:(NSString*_Nonnull)info amounts:(NSString*_Nonnull)amounts { @@ -3194,19 +3488,24 @@ -(void)stopDAO { } -(void)startApp:(UIViewController*_Nonnull)controller app:(BMApp*)app { + [self startApp:controller app:app installedRoot:nil]; +} + +-(void)startApp:(UIViewController*_Nonnull)controller app:(BMApp*)app installedRoot:(NSURL* _Nullable)installedRoot { if (daoManager == nil) { daoManager = [[DAOManager alloc] initWithWallet:wallet]; } BOOL isSupported = [daoManager appSupported:app]; - + __weak typeof(self) weakSelf = self; if (isSupported) { [daoManager launchApp:app]; - + daoViewController = [[DAOViewController alloc] init]; daoViewController.app = app; + daoViewController.installedRoot = installedRoot; daoViewController.onRejected = ^(NSString * _Nonnull json) { __strong typeof(self) strongSelf = weakSelf; [strongSelf->daoManager contractInfoRejected:json]; @@ -3380,11 +3679,11 @@ -(BMApp*_Nonnull)daoGalleryApp { return bApp; } } - + BMApp *app = [BMApp new]; app.name = @"NFT Gallery"; app.api_version = @"current"; - + if ([Settings sharedManager].target == Testnet) { app.url = @"https://apps-testnet.beam.mw/app/dao-core-app/index.html"; } @@ -3398,4 +3697,263 @@ -(BMApp*_Nonnull)daoGalleryApp { return app; } +#pragma mark - Messenger + +static bool parseMessengerWalletID(NSString *input, beam::wallet::WalletID &out) { + if (input == nil || input.length == 0) return false; + std::string s = input.string; + if (out.FromHex(s)) return true; + auto params = beam::wallet::ParseParameters(s); + if (params) { + beam::wallet::WalletID parsed; + if (params->GetParameter(beam::wallet::TxParameterID::PeerAddr, parsed)) { + out = parsed; + return true; + } + } + return false; +} + +-(NSString*_Nonnull)resolvedPeerWalletId:(NSString*_Nonnull)peerWalletId { + WalletID peerID; + if (parseMessengerWalletID(peerWalletId, peerID)) { + return [NSString stringWithUTF8String:to_string(peerID).c_str()]; + } + return peerWalletId; +} + +-(void)addChatStub:(NSString*_Nonnull)peerWalletId contactName:(NSString*_Nullable)contactName myWalletId:(NSString*_Nullable)myWalletId { + // UI-only entry point (called from MessengerNewChatViewController on the + // main thread). The reactor-thread paths in WalletModel.mm now also hop + // to main before mutating self.chats / self.messagesByPeer, so all writes + // serialize on main. + NSString *resolved = [self resolvedPeerWalletId:peerWalletId]; + BOOL changed = NO; + BMChat *existing = nil; + for (BMChat *chat in self.chats) { + if ([chat.peerWalletId isEqualToString:resolved]) { + existing = chat; + break; + } + } + if (existing != nil) { + if (contactName.length > 0 && existing.contactName.length == 0) { + existing.contactName = contactName; + changed = YES; + } + if (myWalletId.length > 0 && existing.myWalletId.length == 0) { + existing.myWalletId = myWalletId; + changed = YES; + } + } else { + BMChat *chat = [[BMChat alloc] init]; + chat.peerWalletId = resolved; + chat.contactName = contactName; + chat.myWalletId = myWalletId; + chat.hasUnread = NO; + chat.lastMessagePreview = @""; + chat.lastMessageTimestamp = (UInt64)[[NSDate date] timeIntervalSince1970]; + [self.chats insertObject:chat atIndex:0]; + changed = YES; + } + if (changed) { + NSArray *delegates = self.delegates.allObjects; + for (id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onChatListChanged)]) { + [delegate onChatListChanged]; + } + } + } +} + +-(void)requestChats { + if (wallet == nil) return; + wallet->getAsync()->getChats(); +} + +-(void)requestMessagesForPeer:(NSString*_Nonnull)peerWalletId { + if (wallet == nil) return; + WalletID peerID; + if (!parseMessengerWalletID(peerWalletId, peerID)) return; + wallet->getAsync()->getInstantMessages(peerID); +} + +-(void)sendInstantMessage:(NSString*_Nonnull)peerWalletId + fromAddress:(NSString*_Nonnull)myWalletId + message:(NSString*_Nonnull)message { + if (wallet == nil) { + NSLog(@"[Messenger] sendInstantMessage skipped — wallet not running"); + return; + } + WalletID peerID; + WalletID myID; + if (!parseMessengerWalletID(peerWalletId, peerID)) { + NSLog(@"[Messenger] sendInstantMessage failed to parse peer=%@", peerWalletId); + return; + } + if (!parseMessengerWalletID(myWalletId, myID)) { + NSLog(@"[Messenger] sendInstantMessage failed to parse my=%@", myWalletId); + return; + } + + std::string text = message.string; + ByteBuffer buffer(text.begin(), text.end()); + NSLog(@"[Messenger] sendInstantMessage peer=%@ from=%@ len=%lu", peerWalletId, myWalletId, (unsigned long)text.length()); + wallet->getAsync()->sendInstantMessage(peerID, myID, std::move(buffer)); +} + +-(void)markChatAsRead:(NSString*_Nonnull)peerWalletId { + // Always invoked from the UI thread (no reactor-side caller); pairs with + // the dispatch-on-main writes in WalletModel.mm so cache mutations remain + // serialized on the main queue. + if (wallet == nil) return; + WalletID peerID; + if (!parseMessengerWalletID(peerWalletId, peerID)) return; + + NSString *resolved = [NSString stringWithUTF8String:to_string(peerID).c_str()]; + std::vector> ims; + NSArray *cached = self.messagesByPeer[resolved]; + for (BMInstantMessage *msg in cached) { + if (msg.isIncome && !msg.isRead) { + ims.push_back(std::make_pair((Timestamp)msg.timestamp, peerID)); + msg.isRead = YES; + } + } + if (!ims.empty()) { + wallet->getAsync()->markIMsasRead(std::move(ims)); + } + + for (BMChat *chat in self.chats) { + if ([chat.peerWalletId isEqualToString:resolved]) { + chat.hasUnread = NO; + } + } +} + +-(void)removeChat:(NSString*_Nonnull)peerWalletId { + if (wallet == nil) return; + WalletID peerID; + if (!parseMessengerWalletID(peerWalletId, peerID)) return; + wallet->getAsync()->removeChat(peerID); +} + +-(NSArray*_Nonnull)cachedMessagesForPeer:(NSString*_Nonnull)peerWalletId { + NSString *resolved = [self resolvedPeerWalletId:peerWalletId]; + NSArray *messages = self.messagesByPeer[resolved]; + return messages ?: @[]; +} + +-(NSString*_Nullable)lastMyAddressForPeer:(NSString*_Nonnull)peerWalletId { + NSString *resolved = [self resolvedPeerWalletId:peerWalletId]; + NSArray *messages = self.messagesByPeer[resolved]; + for (BMInstantMessage *msg in messages.reverseObjectEnumerator) { + if (!msg.isIncome && msg.myWalletId.length > 0) { + return msg.myWalletId; + } + } + for (BMInstantMessage *msg in messages) { + if (msg.myWalletId.length > 0) { + return msg.myWalletId; + } + } + return nil; +} + +#pragma mark - Asset Swaps (DEX) + +-(void)requestDexOrders { +#ifdef BEAM_ASSET_SWAP_SUPPORT + if (wallet == nil) return; + wallet->getAsync()->getDexOrders(); +#endif +} + +-(BOOL)publishDexOrderWithSendAsset:(UInt32)sendAssetId + sendAmount:(UInt64)sendAmount + receiveAsset:(UInt32)receiveAssetId + receiveAmount:(UInt64)receiveAmount + expirationMinutes:(UInt32)expirationMinutes { +#ifdef BEAM_ASSET_SWAP_SUPPORT + if (wallet == nil || walletDb == nil) { + NSLog(@"[AssetSwap] publishDexOrder skipped — wallet not running"); + return NO; + } + if (sendAmount == 0 || receiveAmount == 0 || sendAssetId == receiveAssetId) { + return NO; + } + + static const UInt32 kMaxExpirationMinutes = 720; // 12h — SBBS message TTL + if (expirationMinutes == 0 || expirationMinutes > kMaxExpirationMinutes) { + expirationMinutes = kMaxExpirationMinutes; + } + + BMAsset *sendAsset = [[AssetsManager sharedManager] getAsset:(int)sendAssetId]; + BMAsset *receiveAsset = [[AssetsManager sharedManager] getAsset:(int)receiveAssetId]; + std::string sendSname = sendAsset.unitName.length > 0 ? sendAsset.unitName.string : ""; + std::string receiveSname = receiveAsset.unitName.length > 0 ? receiveAsset.unitName.string : ""; + + WalletAddress sbbsAddress; + sbbsAddress.m_label = "asset_swap"; + walletDb->createAddress(sbbsAddress); + sbbsAddress.m_duration = WalletAddress::AddressExpiration24h; + walletDb->saveAddress(sbbsAddress); + + DexOrder order(DexOrderID::generate(), + sbbsAddress.m_BbsAddr, + sbbsAddress.m_OwnID, + (Asset::ID)sendAssetId, + (Amount)sendAmount, + sendSname, + (Asset::ID)receiveAssetId, + (Amount)receiveAmount, + receiveSname, + expirationMinutes * 60); + + wallet->getAsync()->publishDexOrder(order); + return YES; +#else + return NO; +#endif +} + +-(void)cancelDexOrderWithID:(NSString*_Nonnull)hexOrderID { +#ifdef BEAM_ASSET_SWAP_SUPPORT + if (wallet == nil) return; + DexOrderID orderId; + if (!orderId.FromHex(hexOrderID.string)) return; + wallet->getAsync()->cancelDexOrder(orderId); +#endif +} + +-(BOOL)acceptDexOrder:(BMDexOrder*_Nonnull)order { +#ifdef BEAM_ASSET_SWAP_SUPPORT + if (wallet == nil) return NO; + + WalletID peerID(Zero); + if (!peerID.FromHex(order.sbbsID.string)) { + return NO; + } + DexOrderID orderId; + if (!orderId.FromHex(order.orderID.string)) { + return NO; + } + + Amount fee = (Amount)[self getDefaultFeeInGroth]; + + auto params = CreateDexTransactionParams( + orderId, + peerID, + (Asset::ID)order.receiveAssetId, + (Amount)order.receiveAmount, + (Asset::ID)order.sendAssetId, + (Amount)order.sendAmount, + fee); + + wallet->getAsync()->startTransaction(std::move(params)); + return YES; +#else + return NO; +#endif +} + @end diff --git a/BeamWallet/BeamSDK/AssetsManager.h b/BeamWallet/BeamSDK/AssetsManager.h index 3fcf25e5..9372b778 100644 --- a/BeamWallet/BeamSDK/AssetsManager.h +++ b/BeamWallet/BeamSDK/AssetsManager.h @@ -2,7 +2,7 @@ // AssetsManager.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/AssetsManager.mm b/BeamWallet/BeamSDK/AssetsManager.mm index b2a28ddf..2d24f6be 100644 --- a/BeamWallet/BeamSDK/AssetsManager.mm +++ b/BeamWallet/BeamSDK/AssetsManager.mm @@ -2,7 +2,7 @@ // AssetsManager.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/DAO/AppsApiUI.h b/BeamWallet/BeamSDK/DAO/AppsApiUI.h index 608ac57d..25add7a6 100644 --- a/BeamWallet/BeamSDK/DAO/AppsApiUI.h +++ b/BeamWallet/BeamSDK/DAO/AppsApiUI.h @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 31.08.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #include "wallet/api/i_wallet_api.h" diff --git a/BeamWallet/BeamSDK/DAO/AppsApiUI.mm b/BeamWallet/BeamSDK/DAO/AppsApiUI.mm index 87e79d51..692ba510 100644 --- a/BeamWallet/BeamSDK/DAO/AppsApiUI.mm +++ b/BeamWallet/BeamSDK/DAO/AppsApiUI.mm @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 31.08.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "AppsApiUI.h" @@ -37,37 +37,37 @@ void AppsApiUI::sendApproved(const std::string& request) { - LOG_INFO() << "Contract send approved: " << getAppName() << ", " << getAppId() << ", " << request; + BEAM_LOG_INFO() << "Contract send approved: " << getAppName() << ", " << getAppId() << ", " << request; AnyThread_callWalletApiDirectly(request); } void AppsApiUI::sendRejected(const std::string& request) { - LOG_INFO() << "Contract send rejected: " << getAppName() << ", " << getAppId() << ", " << request; + BEAM_LOG_INFO() << "Contract send rejected: " << getAppName() << ", " << getAppId() << ", " << request; AnyThread_sendApiError(request, beam::wallet::ApiError::UserRejected, std::string()); } void AppsApiUI::contractInfoApproved(const std::string& request) { - LOG_INFO() << "Contract tx approved: " << getAppName() << ", " << getAppId() << ", " << request; + BEAM_LOG_INFO() << "Contract tx approved: " << getAppName() << ", " << getAppId() << ", " << request; AnyThread_callWalletApiDirectly(request); } void AppsApiUI::contractInfoRejected(const std::string& request) { - LOG_INFO() << "Contract tx rejected: " << getAppName() << ", " << getAppId() << ", " << request; + BEAM_LOG_INFO() << "Contract tx rejected: " << getAppName() << ", " << getAppId() << ", " << request; AnyThread_sendApiError(request, beam::wallet::ApiError::UserRejected, std::string()); } void AppsApiUI::callWalletApi(const std::string& request) { - LOG_INFO() << "Call Wallet Api: " << getAppName() << ", " << getAppId() << ", " << request; + BEAM_LOG_INFO() << "Call Wallet Api: " << getAppName() << ", " << getAppId() << ", " << request; AnyThread_callWalletApiChecked(request); } void AppsApiUI::AnyThread_sendApiResponse(std::string&& result) { - LOG_INFO() << "Send Api Response: " << getAppName() << ", " << getAppId() << ", " << result; + BEAM_LOG_INFO() << "Send Api Response: " << getAppName() << ", " << getAppId() << ", " << result; NSString *json = [NSString stringWithUTF8String:result.c_str()]; [[AppModel sharedManager] sendDAOApiResult:json]; } @@ -77,7 +77,7 @@ // auto str = result.dump(); // NSString *json = [NSString stringWithUTF8String:str.c_str()]; // -// LOG_INFO() << "Send Api Response: " << getAppName() << ", " << getAppId() << ", " << json; +// BEAM_LOG_INFO() << "Send Api Response: " << getAppName() << ", " << getAppId() << ", " << json; // // [[AppModel sharedManager] sendDAOApiResult:json]; //} diff --git a/BeamWallet/BeamSDK/DAO/DAOManager.h b/BeamWallet/BeamSDK/DAO/DAOManager.h index 41fc6c02..39c21f8c 100644 --- a/BeamWallet/BeamSDK/DAO/DAOManager.h +++ b/BeamWallet/BeamSDK/DAO/DAOManager.h @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 01.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import diff --git a/BeamWallet/BeamSDK/DAO/DAOManager.mm b/BeamWallet/BeamSDK/DAO/DAOManager.mm index a84a9587..93a89845 100644 --- a/BeamWallet/BeamSDK/DAO/DAOManager.mm +++ b/BeamWallet/BeamSDK/DAO/DAOManager.mm @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 01.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "DAOManager.h" diff --git a/BeamWallet/BeamSDK/DAO/Public.h b/BeamWallet/BeamSDK/DAO/Public.h index c547ffcf..4d39bfbe 100644 --- a/BeamWallet/BeamSDK/DAO/Public.h +++ b/BeamWallet/BeamSDK/DAO/Public.h @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 31.08.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #include "wallet/api/i_wallet_api.h" diff --git a/BeamWallet/BeamSDK/DAO/WalletAPIClient+Internal.h b/BeamWallet/BeamSDK/DAO/WalletAPIClient+Internal.h new file mode 100644 index 00000000..88c25137 --- /dev/null +++ b/BeamWallet/BeamSDK/DAO/WalletAPIClient+Internal.h @@ -0,0 +1,17 @@ +// +// WalletAPIClient+Internal.h +// BeamWallet +// +// C++-only init for WalletAPIClient. Keep this out of the bridging header — +// it pulls in WalletModel which transitively requires C++ standard headers. +// Import this only from Objective-C++ translation units. +// + +#import "WalletAPIClient.h" +#import "WalletModel.h" + +@interface WalletAPIClient () + +- (instancetype _Nonnull)initWithWallet:(WalletModel::Ptr)wallet; + +@end diff --git a/BeamWallet/BeamSDK/DAO/WalletAPIClient.h b/BeamWallet/BeamSDK/DAO/WalletAPIClient.h new file mode 100644 index 00000000..3990e849 --- /dev/null +++ b/BeamWallet/BeamSDK/DAO/WalletAPIClient.h @@ -0,0 +1,42 @@ +// +// WalletAPIClient.h +// BeamWallet +// +// Wallet-owned AppsApiUI bridge used by the DApp Store browser, publishers list, +// and IPFS / contract calls made from native UI (not from a running DApp WebView). +// Call IDs start at WALLET_API_BASE_CALL_ID (1_000_000) so that responses for +// these requests don't collide with the per-DApp IDs handled by DAOViewController. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern const NSInteger WalletAPIClientBaseCallID; + +typedef void (^WalletAPICompletion)(NSDictionary * _Nonnull result); + +@interface WalletAPIClient : NSObject + +/// Lazily creates the AppsApiUI on the wallet thread. Safe to call repeatedly. +- (void)ensureApi; + +/// Tear down the underlying AppsApiUI and clear pending callbacks. +- (void)destroy; + +/// Encode `{jsonrpc:"2.0", id, method, params}` and dispatch to the wallet API. +/// `completion` is invoked once on the main queue with the parsed result map or +/// `{"error": ...}` on failure. +- (NSInteger)callMethod:(NSString *)method + params:(NSDictionary *)params + completion:(nullable WalletAPICompletion)completion + NS_SWIFT_NAME(call(method:params:completion:)); + +/// Inspect a JSON response from the C++ API. If its `id` belongs to this client +/// (`>= WalletAPIClientBaseCallID`), invokes the matching callback and returns YES. +/// Otherwise returns NO so the caller can forward the response elsewhere. +- (BOOL)routeResult:(NSString *)json; + +@end + +NS_ASSUME_NONNULL_END diff --git a/BeamWallet/BeamSDK/DAO/WalletAPIClient.mm b/BeamWallet/BeamSDK/DAO/WalletAPIClient.mm new file mode 100644 index 00000000..4e24c649 --- /dev/null +++ b/BeamWallet/BeamSDK/DAO/WalletAPIClient.mm @@ -0,0 +1,185 @@ +// +// WalletAPIClient.mm +// BeamWallet +// + +#import "WalletAPIClient+Internal.h" +#import "WebAPICreator.h" +#import "AppsApiUI.h" +#import "Public.h" +#import "StringStd.h" + +#include "wallet/api/i_wallet_api.h" +#include "utility/logger.h" + +const NSInteger WalletAPIClientBaseCallID = 1000000; + +static NSString *const kWalletAPIClientAppName = @"Beam-Wallet"; + +@implementation WalletAPIClient { + WebAPICreator _creator; + NSInteger _counter; + NSMutableDictionary *_callbacks; + dispatch_queue_t _callbacksQueue; + BOOL _apiCreationRequested; +} + +- (instancetype)initWithWallet:(WalletModel::Ptr)wallet { + self = [super init]; + if (self) { + _creator = WebAPICreator(); + _creator._walletModel = wallet; + _counter = WalletAPIClientBaseCallID; + _callbacks = [NSMutableDictionary dictionary]; + _callbacksQueue = dispatch_queue_create("com.beam.WalletAPIClient.callbacks", DISPATCH_QUEUE_SERIAL); + _apiCreationRequested = NO; + } + return self; +} + +- (void)dealloc { + _creator.destroyApi(); + _creator._walletModel.reset(); +} + +- (void)ensureApi { + if (_apiCreationRequested) { + return; + } + _apiCreationRequested = YES; + try { + _creator.createApi("current", "", kWalletAPIClientAppName.string, ""); + } catch (NSException *ex) { + NSLog(@"WalletAPIClient.ensureApi: %@", ex); + _apiCreationRequested = NO; + } +} + +- (void)destroy { + _creator.destroyApi(); + _apiCreationRequested = NO; + dispatch_sync(_callbacksQueue, ^{ + [_callbacks removeAllObjects]; + }); +} + +- (NSInteger)callMethod:(NSString *)method + params:(NSDictionary *)params + completion:(WalletAPICompletion)completion { + [self ensureApi]; + + NSInteger callId = ++_counter; + + if (completion) { + dispatch_sync(_callbacksQueue, ^{ + _callbacks[@(callId)] = [completion copy]; + }); + } + + NSDictionary *payload = @{ + @"jsonrpc": @"2.0", + @"id": @(callId), + @"method": method, + @"params": params ?: @{}, + }; + + NSError *err = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&err]; + if (!data) { + [self resolveCallId:callId withResult:@{@"error": @{@"message": err.localizedDescription ?: @"JSON encode failed"}}]; + return callId; + } + NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + if (_creator._api == nil) { + // API not yet created; report error so caller can retry later. + [self resolveCallId:callId withResult:@{@"error": @{@"message": @"Wallet API not ready"}}]; + return callId; + } + + try { + _creator._api->callWalletApi(jsonString.string); + } catch (...) { + [self resolveCallId:callId withResult:@{@"error": @{@"message": @"callWalletApi threw"}}]; + } + return callId; +} + +- (BOOL)routeResult:(NSString *)json { + if (json.length == 0) return NO; + + NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; + if (!data) return NO; + NSError *err = nil; + id parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err]; + if (![parsed isKindOfClass:[NSDictionary class]]) return NO; + NSDictionary *dict = parsed; + id rawId = dict[@"id"]; + if (![rawId isKindOfClass:[NSNumber class]]) return NO; + NSInteger callId = [rawId integerValue]; + if (callId < WalletAPIClientBaseCallID) return NO; + + NSDictionary *result = [self parseAnswer:dict]; + [self resolveCallId:callId withResult:result]; + return YES; +} + +- (NSDictionary *)parseAnswer:(NSDictionary *)answer { + if (answer[@"error"] != nil) { + id err = answer[@"error"]; + if ([err isKindOfClass:[NSDictionary class]]) { + return @{@"error": err}; + } else if ([err isKindOfClass:[NSString class]]) { + return @{@"error": @{@"message": err}}; + } + return @{@"error": @{@"message": @"Unknown error"}}; + } + + id rawResult = answer[@"result"]; + if ([rawResult isKindOfClass:[NSDictionary class]]) { + NSDictionary *resDict = rawResult; + // Shader invoke_contract responses carry their payload as a JSON string in "output". + NSString *output = resDict[@"output"]; + if ([output isKindOfClass:[NSString class]]) { + NSData *outData = [output dataUsingEncoding:NSUTF8StringEncoding]; + NSError *outErr = nil; + id outParsed = outData ? [NSJSONSerialization JSONObjectWithData:outData options:0 error:&outErr] : nil; + if ([outParsed isKindOfClass:[NSDictionary class]]) { + NSDictionary *outDict = outParsed; + if (outDict[@"error"] != nil) { + return @{@"error": outDict[@"error"]}; + } + NSMutableDictionary *merged = [outDict mutableCopy]; + if (resDict[@"raw_data"] != nil) { + merged[@"raw_data"] = resDict[@"raw_data"]; + } + return merged; + } + } + return resDict; + } + if ([rawResult isKindOfClass:[NSString class]]) { + return @{@"address": rawResult}; + } + if ([rawResult isKindOfClass:[NSArray class]]) { + return @{@"messages": rawResult}; + } + return @{}; +} + +- (void)resolveCallId:(NSInteger)callId withResult:(NSDictionary *)result { + __block WalletAPICompletion cb = nil; + dispatch_sync(_callbacksQueue, ^{ + cb = _callbacks[@(callId)]; + if (cb) { + [_callbacks removeObjectForKey:@(callId)]; + } + }); + if (cb) { + dispatch_async(dispatch_get_main_queue(), ^{ + cb(result); + }); + } +} + +@end diff --git a/BeamWallet/BeamSDK/DAO/WebAPICreator.h b/BeamWallet/BeamSDK/DAO/WebAPICreator.h index cbea7d51..3570db23 100644 --- a/BeamWallet/BeamSDK/DAO/WebAPICreator.h +++ b/BeamWallet/BeamSDK/DAO/WebAPICreator.h @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 31.08.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "WalletModel.h" diff --git a/BeamWallet/BeamSDK/DAO/WebAPICreator.mm b/BeamWallet/BeamSDK/DAO/WebAPICreator.mm index e289966f..2eddb1b1 100644 --- a/BeamWallet/BeamSDK/DAO/WebAPICreator.mm +++ b/BeamWallet/BeamSDK/DAO/WebAPICreator.mm @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 31.08.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "WebAPICreator.h" @@ -12,6 +12,7 @@ #include "wallet/api/i_wallet_api.h" #include "wallet/core/common.h" #include "bvm/invoke_data.h" +#include "utility/logger.h" #include "Public.h" @@ -44,16 +45,22 @@ auto guard = this; - AppsApiUI::ClientThread_Create(_walletModel.get(), version, appid, appName, 0 , false, + // `ipfsnode=true` makes ClientThread_Create call IWThread_startIPFSNode on + // the wallet thread and stash the IPFS service handle on the resulting + // ApiInitData. Without it, `ipfs_get` on this API instance returns + // ApiError::NotSupported even when BEAM_IPFS_SUPPORT is on. Callers must + // have set the IPFS config (repo_root, swarm key) on the wallet client + // before invoking createApi — AppModel does this at wallet-open time. + AppsApiUI::ClientThread_Create(_walletModel.get(), version, appid, appName, 0, true, [this, guard, version, appName, appid] (AppsApiUI::Ptr api) { if (guard) { _api = std::move(api); - LOG_INFO() << "API created: " << version << ", " << appName << ", " << appid; + BEAM_LOG_INFO() << "API created: " << version << ", " << appName << ", " << appid; } else { - LOG_INFO() << "WebAPICreator destroyed before api created:" << version << ", " << appName << ", " << appid; + BEAM_LOG_INFO() << "WebAPICreator destroyed before api created:" << version << ", " << appName << ", " << appid; } }); } diff --git a/BeamWallet/BeamSDK/DiskStatusManager.h b/BeamWallet/BeamSDK/DiskStatusManager.h index a4729e76..d4ae9319 100644 --- a/BeamWallet/BeamSDK/DiskStatusManager.h +++ b/BeamWallet/BeamSDK/DiskStatusManager.h @@ -2,7 +2,7 @@ // DiskStatusManager.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/DiskStatusManager.m b/BeamWallet/BeamSDK/DiskStatusManager.m index 1579d548..71d2343e 100644 --- a/BeamWallet/BeamSDK/DiskStatusManager.m +++ b/BeamWallet/BeamSDK/DiskStatusManager.m @@ -2,7 +2,7 @@ // DiskStatusManager.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ExchangeManager.h b/BeamWallet/BeamSDK/ExchangeManager.h index c5678a5e..ff30a137 100644 --- a/BeamWallet/BeamSDK/ExchangeManager.h +++ b/BeamWallet/BeamSDK/ExchangeManager.h @@ -2,7 +2,7 @@ // ExchangeManager.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ExchangeManager.mm b/BeamWallet/BeamSDK/ExchangeManager.mm index 82cbb89a..821b122a 100644 --- a/BeamWallet/BeamSDK/ExchangeManager.mm +++ b/BeamWallet/BeamSDK/ExchangeManager.mm @@ -2,7 +2,7 @@ // ExchangeManager.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/MnemonicModel.h b/BeamWallet/BeamSDK/MnemonicModel.h index dcd6356e..19cc93ac 100644 --- a/BeamWallet/BeamSDK/MnemonicModel.h +++ b/BeamWallet/BeamSDK/MnemonicModel.h @@ -2,7 +2,7 @@ // MnemonicModel.h // BeamTest // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/MnemonicModel.mm b/BeamWallet/BeamSDK/MnemonicModel.mm index 3b240e4c..2b888c26 100644 --- a/BeamWallet/BeamSDK/MnemonicModel.mm +++ b/BeamWallet/BeamSDK/MnemonicModel.mm @@ -2,7 +2,7 @@ // MnemonicModel.m // BeamTest // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/Color.h b/BeamWallet/BeamSDK/ObjcExtensions/Color.h index 9e92493b..9b85a844 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/Color.h +++ b/BeamWallet/BeamSDK/ObjcExtensions/Color.h @@ -2,7 +2,7 @@ // Color.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/Color.m b/BeamWallet/BeamSDK/ObjcExtensions/Color.m index 25be7f4e..5ba2c906 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/Color.m +++ b/BeamWallet/BeamSDK/ObjcExtensions/Color.m @@ -2,7 +2,7 @@ // Color.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.h b/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.h index 5640305e..ae52108c 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.h +++ b/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.h @@ -2,7 +2,7 @@ // CurrencyFormatter.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.m b/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.m index b887e6c8..0eaecb43 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.m +++ b/BeamWallet/BeamSDK/ObjcExtensions/CurrencyFormatter.m @@ -2,7 +2,7 @@ // CurrencyFormatter.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/Image.h b/BeamWallet/BeamSDK/ObjcExtensions/Image.h index 6ac9d983..98ad5fd2 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/Image.h +++ b/BeamWallet/BeamSDK/ObjcExtensions/Image.h @@ -2,7 +2,7 @@ // Image.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/Image.m b/BeamWallet/BeamSDK/ObjcExtensions/Image.m index 89c6bbb3..5fd9d9c3 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/Image.m +++ b/BeamWallet/BeamSDK/ObjcExtensions/Image.m @@ -2,7 +2,7 @@ // Image.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.h b/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.h index e5d4a467..ca0afda4 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.h +++ b/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.h @@ -2,7 +2,7 @@ // StringLocalize.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.m b/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.m index 54598e49..dcadaec9 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.m +++ b/BeamWallet/BeamSDK/ObjcExtensions/StringLocalize.m @@ -2,7 +2,7 @@ // StringLocalize.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/StringStd.h b/BeamWallet/BeamSDK/ObjcExtensions/StringStd.h index 42415935..f475e1a2 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/StringStd.h +++ b/BeamWallet/BeamSDK/ObjcExtensions/StringStd.h @@ -1,7 +1,7 @@ // // String.h // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/ObjcExtensions/StringStd.mm b/BeamWallet/BeamSDK/ObjcExtensions/StringStd.mm index 69bcf1dd..912aee6d 100644 --- a/BeamWallet/BeamSDK/ObjcExtensions/StringStd.mm +++ b/BeamWallet/BeamSDK/ObjcExtensions/StringStd.mm @@ -1,7 +1,7 @@ // // String.m // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMAddress.h b/BeamWallet/BeamSDK/Objects/BMAddress.h index 419b4c73..4e666bf4 100644 --- a/BeamWallet/BeamSDK/Objects/BMAddress.h +++ b/BeamWallet/BeamSDK/Objects/BMAddress.h @@ -2,7 +2,7 @@ // BMAddress.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -40,6 +40,8 @@ typedef int BMAddressType; @property (nonatomic,strong, getter = getWalletId) NSString * _Nonnull walletId; @property (nonatomic,strong) NSString * _Nullable offlineToken; @property (nonatomic,strong) NSString * _Nullable maxPrivacyToken; +@property (nonatomic,strong) NSString * _Nullable sbbsToken; +@property (nonatomic,strong) NSString * _Nullable publicOfflineToken; @property (nonatomic,strong) NSString * _Nullable address; @property (nonatomic,strong) NSString * _Nonnull label; diff --git a/BeamWallet/BeamSDK/Objects/BMAddress.mm b/BeamWallet/BeamSDK/Objects/BMAddress.mm index 99586602..d16e5e32 100644 --- a/BeamWallet/BeamSDK/Objects/BMAddress.mm +++ b/BeamWallet/BeamSDK/Objects/BMAddress.mm @@ -2,7 +2,7 @@ // BMAddress.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,8 +35,9 @@ +(BMAddress*_Nonnull)emptyAddress{ +(BMAddress*_Nonnull)fromAddress:(BMAddress*_Nonnull)address{ BMAddress *copied = [BMAddress new]; - copied.walletId = [NSString stringWithString:address.walletId]; - copied.label = [NSString stringWithString:address.label]; + copied.walletId = address.walletId ? [NSString stringWithString:address.walletId] : @""; + copied.label = address.label ? [NSString stringWithString:address.label] : @""; + copied.address = address.address ? [NSString stringWithString:address.address] : @""; copied.duration = address.duration; copied.createTime = address.createTime; return copied; diff --git a/BeamWallet/BeamSDK/Objects/BMApp.h b/BeamWallet/BeamSDK/Objects/BMApp.h index a64a650f..c1d16fc9 100644 --- a/BeamWallet/BeamSDK/Objects/BMApp.h +++ b/BeamWallet/BeamSDK/Objects/BMApp.h @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 01.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import diff --git a/BeamWallet/BeamSDK/Objects/BMApp.m b/BeamWallet/BeamSDK/Objects/BMApp.m index a7a72ce7..c695398c 100644 --- a/BeamWallet/BeamSDK/Objects/BMApp.m +++ b/BeamWallet/BeamSDK/Objects/BMApp.m @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 01.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "BMApp.h" diff --git a/BeamWallet/BeamSDK/Objects/BMAsset.h b/BeamWallet/BeamSDK/Objects/BMAsset.h index 8e42cc77..d392c506 100644 --- a/BeamWallet/BeamSDK/Objects/BMAsset.h +++ b/BeamWallet/BeamSDK/Objects/BMAsset.h @@ -2,7 +2,7 @@ // BMAsset.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMAsset.m b/BeamWallet/BeamSDK/Objects/BMAsset.m index d4c9654d..0bb587e8 100644 --- a/BeamWallet/BeamSDK/Objects/BMAsset.m +++ b/BeamWallet/BeamSDK/Objects/BMAsset.m @@ -2,7 +2,7 @@ // BMAsset.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -79,32 +79,30 @@ -(id)initWithCoder:(NSCoder *)decoder self = [super init]; if(self) { - self.unitName = [decoder decodeObjectForKey: @"unitName"]; - self.nthUnitName = [decoder decodeObjectForKey: @"nthUnitName"]; - self.shortName = [decoder decodeObjectForKey: @"shortName"]; - self.shortDesc = [decoder decodeObjectForKey: @"shortDesc"]; - self.longDesc = [decoder decodeObjectForKey: @"longDesc"]; - self.name = [decoder decodeObjectForKey: @"name"]; - self.color = [decoder decodeObjectForKey: @"color"]; -// self.site = [decoder decodeObjectForKey: @"site"]; - // self.paper = [decoder decodeObjectForKey: @"paper"]; + self.unitName = [decoder decodeObjectOfClass:[NSString class] forKey: @"unitName"]; + self.nthUnitName = [decoder decodeObjectOfClass:[NSString class] forKey: @"nthUnitName"]; + self.shortName = [decoder decodeObjectOfClass:[NSString class] forKey: @"shortName"]; + self.shortDesc = [decoder decodeObjectOfClass:[NSString class] forKey: @"shortDesc"]; + self.longDesc = [decoder decodeObjectOfClass:[NSString class] forKey: @"longDesc"]; + self.name = [decoder decodeObjectOfClass:[NSString class] forKey: @"name"]; + self.color = [decoder decodeObjectOfClass:[NSString class] forKey: @"color"]; self.site = @""; self.paper = @""; - self.available = [[decoder decodeObjectForKey: @"available"] longLongValue]; - self.receiving = [[decoder decodeObjectForKey: @"receiving"] longLongValue]; - self.sending = [[decoder decodeObjectForKey: @"sending"] longLongValue]; - self.assetId = [[decoder decodeObjectForKey: @"assetId"] longLongValue]; - self.shielded = [[decoder decodeObjectForKey: @"shielded"] longLongValue]; - self.maturing = [[decoder decodeObjectForKey: @"maturing"] longLongValue]; - self.maxPrivacy = [[decoder decodeObjectForKey: @"maxPrivacy"] longLongValue]; - - self.realAmount = [[decoder decodeObjectForKey: @"realAmount"] doubleValue]; - self.realReceiving = [[decoder decodeObjectForKey: @"realReceiving"] doubleValue]; - self.realSending = [[decoder decodeObjectForKey: @"realSending"] doubleValue]; - self.realMaturing = [[decoder decodeObjectForKey: @"realMaturing"] doubleValue]; - self.realShielded = [[decoder decodeObjectForKey: @"realShielded"] doubleValue]; - self.realMaxPrivacy = [[decoder decodeObjectForKey: @"realMaxPrivacy"] doubleValue]; + self.available = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"available"] longLongValue]; + self.receiving = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"receiving"] longLongValue]; + self.sending = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"sending"] longLongValue]; + self.assetId = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"assetId"] longLongValue]; + self.shielded = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"shielded"] longLongValue]; + self.maturing = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"maturing"] longLongValue]; + self.maxPrivacy = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"maxPrivacy"] longLongValue]; + + self.realAmount = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realAmount"] doubleValue]; + self.realReceiving = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realReceiving"] doubleValue]; + self.realSending = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realSending"] doubleValue]; + self.realMaturing = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realMaturing"] doubleValue]; + self.realShielded = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realShielded"] doubleValue]; + self.realMaxPrivacy = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realMaxPrivacy"] doubleValue]; } return self; } diff --git a/BeamWallet/BeamSDK/Objects/BMAvailableDApp.h b/BeamWallet/BeamSDK/Objects/BMAvailableDApp.h new file mode 100644 index 00000000..6d8d81ea --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMAvailableDApp.h @@ -0,0 +1,24 @@ +// +// BMAvailableDApp.h +// BeamWallet +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BMAvailableDApp : NSObject + +@property (nonatomic, strong) NSString *guid; +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *desc; +@property (nonatomic, strong) NSString *version; +@property (nonatomic, strong) NSString *ipfsCid; +@property (nonatomic, strong) NSString *publisher; +@property (nonatomic, strong) NSString *icon; +@property (nonatomic, strong) NSString *bundledAsset; +@property (nonatomic, strong) NSString *publisherName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/BeamWallet/BeamSDK/Objects/BMAvailableDApp.m b/BeamWallet/BeamSDK/Objects/BMAvailableDApp.m new file mode 100644 index 00000000..c9b7bb2b --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMAvailableDApp.m @@ -0,0 +1,26 @@ +// +// BMAvailableDApp.m +// BeamWallet +// + +#import "BMAvailableDApp.h" + +@implementation BMAvailableDApp + +- (instancetype)init { + self = [super init]; + if (self) { + _guid = @""; + _name = @""; + _desc = @""; + _version = @"1.0.0"; + _ipfsCid = @""; + _publisher = @""; + _icon = @""; + _bundledAsset = @""; + _publisherName = @""; + } + return self; +} + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMChat.h b/BeamWallet/BeamSDK/Objects/BMChat.h new file mode 100644 index 00000000..b8914c9c --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMChat.h @@ -0,0 +1,33 @@ +// +// BMChat.h +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +@interface BMChat : NSObject + +@property (nonatomic, strong) NSString * _Nonnull peerWalletId; +@property (nonatomic, strong) NSString * _Nullable contactName; +@property (nonatomic, strong) NSString * _Nullable myWalletId; +@property (nonatomic, assign) BOOL hasUnread; +@property (nonatomic, strong) NSString * _Nullable lastMessagePreview; +@property (nonatomic, assign) UInt64 lastMessageTimestamp; + +-(NSString * _Nonnull)displayName; + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMChat.mm b/BeamWallet/BeamSDK/Objects/BMChat.mm new file mode 100644 index 00000000..a365824e --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMChat.mm @@ -0,0 +1,44 @@ +// +// BMChat.mm +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 "BMChat.h" + +@implementation BMChat + +-(instancetype)init { + self = [super init]; + if (self) { + _peerWalletId = @""; + } + return self; +} + +-(NSString * _Nonnull)displayName { + if (self.contactName.length > 0) { + return self.contactName; + } + if (self.peerWalletId.length > 12) { + return [NSString stringWithFormat:@"%@…%@", + [self.peerWalletId substringToIndex:6], + [self.peerWalletId substringFromIndex:self.peerWalletId.length - 6]]; + } + return self.peerWalletId; +} + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMContact.h b/BeamWallet/BeamSDK/Objects/BMContact.h index a0f9890a..6b89c6f8 100644 --- a/BeamWallet/BeamSDK/Objects/BMContact.h +++ b/BeamWallet/BeamSDK/Objects/BMContact.h @@ -2,7 +2,7 @@ // BMContact.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMContact.m b/BeamWallet/BeamSDK/Objects/BMContact.m index bf293f51..24ae47d9 100644 --- a/BeamWallet/BeamSDK/Objects/BMContact.m +++ b/BeamWallet/BeamSDK/Objects/BMContact.m @@ -2,7 +2,7 @@ // BMContact.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMCurrency.h b/BeamWallet/BeamSDK/Objects/BMCurrency.h index 3522163f..3588cac8 100644 --- a/BeamWallet/BeamSDK/Objects/BMCurrency.h +++ b/BeamWallet/BeamSDK/Objects/BMCurrency.h @@ -2,7 +2,7 @@ // BMCurrency.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMCurrency.m b/BeamWallet/BeamSDK/Objects/BMCurrency.m index 79edb977..91b76028 100644 --- a/BeamWallet/BeamSDK/Objects/BMCurrency.m +++ b/BeamWallet/BeamSDK/Objects/BMCurrency.m @@ -2,7 +2,7 @@ // BMCurrency.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMDexOrder.h b/BeamWallet/BeamSDK/Objects/BMDexOrder.h new file mode 100644 index 00000000..2d5f9b00 --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMDexOrder.h @@ -0,0 +1,54 @@ +// +// BMDexOrder.h +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +@interface BMDexOrder : NSObject + +@property (nonatomic, strong) NSString * _Nonnull orderID; +@property (nonatomic, strong) NSString * _Nonnull sbbsID; + +@property (nonatomic, assign) UInt32 sendAssetId; +@property (nonatomic, assign) UInt32 receiveAssetId; + +@property (nonatomic, strong) NSString * _Nonnull sendAssetSName; +@property (nonatomic, strong) NSString * _Nonnull receiveAssetSName; + +@property (nonatomic, assign) UInt64 sendAmount; +@property (nonatomic, assign) UInt64 receiveAmount; + +@property (nonatomic, assign) UInt64 createTimestamp; +@property (nonatomic, assign) UInt64 expireTimestamp; + +@property (nonatomic, assign) BOOL isMine; +@property (nonatomic, assign) BOOL isAccepted; +@property (nonatomic, assign) BOOL isCanceled; +@property (nonatomic, assign) BOOL isCompleted; + +-(BOOL)isExpired; +-(BOOL)isActive; + +-(NSString * _Nonnull)displayCreatedDate; +-(NSString * _Nonnull)displayExpiresIn; +-(NSString * _Nonnull)displaySendAmount; +-(NSString * _Nonnull)displayReceiveAmount; +-(NSString * _Nonnull)displayRate; +-(NSString * _Nonnull)displayStatus; + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMDexOrder.mm b/BeamWallet/BeamSDK/Objects/BMDexOrder.mm new file mode 100644 index 00000000..a310a35b --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMDexOrder.mm @@ -0,0 +1,105 @@ +// +// BMDexOrder.mm +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 "BMDexOrder.h" + +static NSString *FormatGroth(UInt64 amount) { + double beam = (double)amount / 100000000.0; + NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + formatter.numberStyle = NSNumberFormatterDecimalStyle; + formatter.minimumFractionDigits = 0; + formatter.maximumFractionDigits = 8; + return [formatter stringFromNumber:@(beam)] ?: @"0"; +} + +@implementation BMDexOrder + +-(instancetype)init { + self = [super init]; + if (self) { + _orderID = @""; + _sbbsID = @""; + _sendAssetSName = @""; + _receiveAssetSName = @""; + } + return self; +} + +-(BOOL)isExpired { + if (self.expireTimestamp == 0) return NO; + return self.expireTimestamp < (UInt64)[[NSDate date] timeIntervalSince1970]; +} + +-(BOOL)isActive { + return !self.isExpired && !self.isCanceled && !self.isCompleted && !self.isAccepted; +} + +-(NSString * _Nonnull)displayCreatedDate { + if (self.createTimestamp == 0) return @""; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.createTimestamp]; + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateStyle = NSDateFormatterMediumStyle; + formatter.timeStyle = NSDateFormatterShortStyle; + return [formatter stringFromDate:date]; +} + +-(NSString * _Nonnull)displayExpiresIn { + if (self.expireTimestamp == 0) return @"—"; + NSDate *expire = [NSDate dateWithTimeIntervalSince1970:self.expireTimestamp]; + NSTimeInterval delta = [expire timeIntervalSinceDate:[NSDate date]]; + if (delta <= 0) return @"expired"; + + NSInteger seconds = (NSInteger)delta; + NSInteger days = seconds / 86400; + NSInteger hours = (seconds % 86400) / 3600; + NSInteger minutes = (seconds % 3600) / 60; + + if (days > 0) return [NSString stringWithFormat:@"%ldd %ldh", (long)days, (long)hours]; + if (hours > 0) return [NSString stringWithFormat:@"%ldh %ldm", (long)hours, (long)minutes]; + return [NSString stringWithFormat:@"%ldm", (long)minutes]; +} + +-(NSString * _Nonnull)displaySendAmount { + return [NSString stringWithFormat:@"%@ %@", FormatGroth(self.sendAmount), self.sendAssetSName]; +} + +-(NSString * _Nonnull)displayReceiveAmount { + return [NSString stringWithFormat:@"%@ %@", FormatGroth(self.receiveAmount), self.receiveAssetSName]; +} + +-(NSString * _Nonnull)displayRate { + if (self.sendAmount == 0) return @"—"; + double rate = (double)self.receiveAmount / (double)self.sendAmount; + NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + formatter.numberStyle = NSNumberFormatterDecimalStyle; + formatter.maximumFractionDigits = 8; + formatter.minimumFractionDigits = 0; + NSString *rateString = [formatter stringFromNumber:@(rate)] ?: @"0"; + return [NSString stringWithFormat:@"1 %@ = %@ %@", self.sendAssetSName, rateString, self.receiveAssetSName]; +} + +-(NSString * _Nonnull)displayStatus { + if (self.isCompleted) return @"completed"; + if (self.isCanceled) return @"canceled"; + if (self.isAccepted) return @"accepted"; + if ([self isExpired]) return @"expired"; + return @"open"; +} + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMDuration.h b/BeamWallet/BeamSDK/Objects/BMDuration.h index 6765c44f..ca555502 100644 --- a/BeamWallet/BeamSDK/Objects/BMDuration.h +++ b/BeamWallet/BeamSDK/Objects/BMDuration.h @@ -2,7 +2,7 @@ // BMDuration.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMDuration.m b/BeamWallet/BeamSDK/Objects/BMDuration.m index 4386e1f2..c7b64392 100644 --- a/BeamWallet/BeamSDK/Objects/BMDuration.m +++ b/BeamWallet/BeamSDK/Objects/BMDuration.m @@ -2,7 +2,7 @@ // BMDuration.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMInstalledDApp.h b/BeamWallet/BeamSDK/Objects/BMInstalledDApp.h new file mode 100644 index 00000000..df6abc11 --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMInstalledDApp.h @@ -0,0 +1,25 @@ +// +// BMInstalledDApp.h +// BeamWallet +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BMInstalledDApp : NSObject + +@property (nonatomic, strong) NSString *guid; +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *desc; +@property (nonatomic, strong) NSString *version; +@property (nonatomic, strong) NSString *localPath; +@property (nonatomic, strong) NSString *icon; +@property (nonatomic, strong) NSString *url; + +- (NSDictionary *)toDictionary; ++ (instancetype)fromDictionary:(NSDictionary *)dict; + +@end + +NS_ASSUME_NONNULL_END diff --git a/BeamWallet/BeamSDK/Objects/BMInstalledDApp.m b/BeamWallet/BeamSDK/Objects/BMInstalledDApp.m new file mode 100644 index 00000000..9af1cd24 --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMInstalledDApp.m @@ -0,0 +1,59 @@ +// +// BMInstalledDApp.m +// BeamWallet +// + +#import "BMInstalledDApp.h" + +@interface BMInstalledDApp () ++ (NSString *)stringFromDictionary:(NSDictionary *)dict key:(NSString *)key defaultValue:(NSString *)def; +@end + +@implementation BMInstalledDApp + +- (instancetype)init { + self = [super init]; + if (self) { + _guid = @""; + _name = @""; + _desc = @""; + _version = @"1.0"; + _localPath = @""; + _icon = @""; + _url = @"app/index.html"; + } + return self; +} + +- (NSDictionary *)toDictionary { + return @{ + @"guid": _guid ?: @"", + @"name": _name ?: @"", + @"description": _desc ?: @"", + @"version": _version ?: @"1.0", + @"localPath": _localPath ?: @"", + @"icon": _icon ?: @"", + @"url": _url ?: @"app/index.html", + }; +} + ++ (instancetype)fromDictionary:(NSDictionary *)dict { + BMInstalledDApp *d = [[BMInstalledDApp alloc] init]; + d.guid = [self stringFromDictionary:dict key:@"guid" defaultValue:@""]; + d.name = [self stringFromDictionary:dict key:@"name" defaultValue:@""]; + d.desc = [self stringFromDictionary:dict key:@"description" defaultValue:@""]; + d.version = [self stringFromDictionary:dict key:@"version" defaultValue:@"1.0"]; + d.localPath = [self stringFromDictionary:dict key:@"localPath" defaultValue:@""]; + d.icon = [self stringFromDictionary:dict key:@"icon" defaultValue:@""]; + d.url = [self stringFromDictionary:dict key:@"url" defaultValue:@"app/index.html"]; + return d; +} + ++ (NSString *)stringFromDictionary:(NSDictionary *)dict key:(NSString *)key defaultValue:(NSString *)def { + id value = dict[key]; + if ([value isKindOfClass:[NSString class]]) return value; + if (value == nil || value == [NSNull null]) return def; + return [NSString stringWithFormat:@"%@", value]; +} + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMInstantMessage.h b/BeamWallet/BeamSDK/Objects/BMInstantMessage.h new file mode 100644 index 00000000..027589d3 --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMInstantMessage.h @@ -0,0 +1,33 @@ +// +// BMInstantMessage.h +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +@interface BMInstantMessage : NSObject + +@property (nonatomic, assign) UInt64 timestamp; +@property (nonatomic, strong) NSString * _Nonnull peerWalletId; +@property (nonatomic, strong) NSString * _Nonnull myWalletId; +@property (nonatomic, strong) NSString * _Nonnull message; +@property (nonatomic, assign) BOOL isIncome; +@property (nonatomic, assign) BOOL isRead; + +-(NSString * _Nonnull)formattedTime; + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMInstantMessage.mm b/BeamWallet/BeamSDK/Objects/BMInstantMessage.mm new file mode 100644 index 00000000..6374258b --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMInstantMessage.mm @@ -0,0 +1,42 @@ +// +// BMInstantMessage.mm +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 "BMInstantMessage.h" + +@implementation BMInstantMessage + +-(instancetype)init { + self = [super init]; + if (self) { + _peerWalletId = @""; + _myWalletId = @""; + _message = @""; + } + return self; +} + +-(NSString * _Nonnull)formattedTime { + NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.timestamp]; + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateStyle = NSDateFormatterNoStyle; + formatter.timeStyle = NSDateFormatterShortStyle; + return [formatter stringFromDate:date]; +} + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMLanguage.h b/BeamWallet/BeamSDK/Objects/BMLanguage.h index 6cf67470..25d6850b 100644 --- a/BeamWallet/BeamSDK/Objects/BMLanguage.h +++ b/BeamWallet/BeamSDK/Objects/BMLanguage.h @@ -2,7 +2,7 @@ // BMLanguage.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMLanguage.m b/BeamWallet/BeamSDK/Objects/BMLanguage.m index 4380a85e..32864565 100644 --- a/BeamWallet/BeamSDK/Objects/BMLanguage.m +++ b/BeamWallet/BeamSDK/Objects/BMLanguage.m @@ -2,7 +2,7 @@ // BMLanguage.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMLockScreenValue.h b/BeamWallet/BeamSDK/Objects/BMLockScreenValue.h index e36bf16e..7b1b9532 100644 --- a/BeamWallet/BeamSDK/Objects/BMLockScreenValue.h +++ b/BeamWallet/BeamSDK/Objects/BMLockScreenValue.h @@ -2,7 +2,7 @@ // BMLockScreenValue.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMLockScreenValue.m b/BeamWallet/BeamSDK/Objects/BMLockScreenValue.m index 5d5aeb20..1b497db8 100644 --- a/BeamWallet/BeamSDK/Objects/BMLockScreenValue.m +++ b/BeamWallet/BeamSDK/Objects/BMLockScreenValue.m @@ -2,7 +2,7 @@ // BMLockScreenValue.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMLogValue.h b/BeamWallet/BeamSDK/Objects/BMLogValue.h index 187225ca..3b473955 100644 --- a/BeamWallet/BeamSDK/Objects/BMLogValue.h +++ b/BeamWallet/BeamSDK/Objects/BMLogValue.h @@ -2,7 +2,7 @@ // BMLogValue.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMLogValue.m b/BeamWallet/BeamSDK/Objects/BMLogValue.m index 6543e20f..b68d90ce 100644 --- a/BeamWallet/BeamSDK/Objects/BMLogValue.m +++ b/BeamWallet/BeamSDK/Objects/BMLogValue.m @@ -2,7 +2,7 @@ // BMLogValue.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.h b/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.h index 6d8e2ba8..a1774357 100644 --- a/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.h +++ b/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.h @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 14.12.2020. -// Copyright © 2020 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import diff --git a/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.m b/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.m index 4e198a21..2a77d602 100644 --- a/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.m +++ b/BeamWallet/BeamSDK/Objects/BMMaxPrivacyLock.m @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 14.12.2020. -// Copyright © 2020 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "BMMaxPrivacyLock.h" diff --git a/BeamWallet/BeamSDK/Objects/BMNotification.h b/BeamWallet/BeamSDK/Objects/BMNotification.h index 69fa862e..0bcc3123 100644 --- a/BeamWallet/BeamSDK/Objects/BMNotification.h +++ b/BeamWallet/BeamSDK/Objects/BMNotification.h @@ -2,7 +2,7 @@ // BMNotification.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMNotification.m b/BeamWallet/BeamSDK/Objects/BMNotification.m index 685a3d42..d087d11f 100644 --- a/BeamWallet/BeamSDK/Objects/BMNotification.m +++ b/BeamWallet/BeamSDK/Objects/BMNotification.m @@ -2,7 +2,7 @@ // BMNotification.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMPaymentInfo.h b/BeamWallet/BeamSDK/Objects/BMPaymentInfo.h index 6516f4ec..3a20e63d 100644 --- a/BeamWallet/BeamSDK/Objects/BMPaymentInfo.h +++ b/BeamWallet/BeamSDK/Objects/BMPaymentInfo.h @@ -2,7 +2,7 @@ // BMPaymentInfo.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMPaymentInfo.m b/BeamWallet/BeamSDK/Objects/BMPaymentInfo.m index 2841857c..82ec4572 100644 --- a/BeamWallet/BeamSDK/Objects/BMPaymentInfo.m +++ b/BeamWallet/BeamSDK/Objects/BMPaymentInfo.m @@ -2,7 +2,7 @@ // BMPaymentInfo.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMPaymentProof.h b/BeamWallet/BeamSDK/Objects/BMPaymentProof.h index b352aa9c..e813e989 100644 --- a/BeamWallet/BeamSDK/Objects/BMPaymentProof.h +++ b/BeamWallet/BeamSDK/Objects/BMPaymentProof.h @@ -2,7 +2,7 @@ // BMPaymentProof.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMPaymentProof.m b/BeamWallet/BeamSDK/Objects/BMPaymentProof.m index c2b16400..6695c154 100644 --- a/BeamWallet/BeamSDK/Objects/BMPaymentProof.m +++ b/BeamWallet/BeamSDK/Objects/BMPaymentProof.m @@ -2,7 +2,7 @@ // BMPaymentProof.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.h b/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.h index 639e2246..642f7fa4 100644 --- a/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.h +++ b/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.h @@ -2,7 +2,7 @@ // BMPreparedTransaction.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.m b/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.m index f16368f1..f90a4dab 100644 --- a/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.m +++ b/BeamWallet/BeamSDK/Objects/BMPreparedTransaction.m @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 5/29/19. -// Copyright © 2019 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "BMPreparedTransaction.h" diff --git a/BeamWallet/BeamSDK/Objects/BMPublisher.h b/BeamWallet/BeamSDK/Objects/BMPublisher.h new file mode 100644 index 00000000..fe772c53 --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMPublisher.h @@ -0,0 +1,24 @@ +// +// BMPublisher.h +// BeamWallet +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BMPublisher : NSObject + +@property (nonatomic, strong) NSString *pubkey; +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *aboutMe; +@property (nonatomic, strong) NSString *website; +@property (nonatomic, strong) NSString *twitter; +@property (nonatomic, strong) NSString *linkedin; +@property (nonatomic, strong) NSString *instagram; +@property (nonatomic, strong) NSString *telegram; +@property (nonatomic, strong) NSString *discord; + +@end + +NS_ASSUME_NONNULL_END diff --git a/BeamWallet/BeamSDK/Objects/BMPublisher.m b/BeamWallet/BeamSDK/Objects/BMPublisher.m new file mode 100644 index 00000000..8a704b3f --- /dev/null +++ b/BeamWallet/BeamSDK/Objects/BMPublisher.m @@ -0,0 +1,26 @@ +// +// BMPublisher.m +// BeamWallet +// + +#import "BMPublisher.h" + +@implementation BMPublisher + +- (instancetype)init { + self = [super init]; + if (self) { + _pubkey = @""; + _name = @""; + _aboutMe = @""; + _website = @""; + _twitter = @""; + _linkedin = @""; + _instagram = @""; + _telegram = @""; + _discord = @""; + } + return self; +} + +@end diff --git a/BeamWallet/BeamSDK/Objects/BMTransaction.h b/BeamWallet/BeamSDK/Objects/BMTransaction.h index 93e0ba10..2bddb52e 100644 --- a/BeamWallet/BeamSDK/Objects/BMTransaction.h +++ b/BeamWallet/BeamSDK/Objects/BMTransaction.h @@ -2,7 +2,7 @@ // BMTransaction.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMTransaction.m b/BeamWallet/BeamSDK/Objects/BMTransaction.m index 9ca480aa..578bcb33 100644 --- a/BeamWallet/BeamSDK/Objects/BMTransaction.m +++ b/BeamWallet/BeamSDK/Objects/BMTransaction.m @@ -2,7 +2,7 @@ // BMTransaction.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMTransactionParameters.h b/BeamWallet/BeamSDK/Objects/BMTransactionParameters.h index b9418e8a..8b9f5e4f 100644 --- a/BeamWallet/BeamSDK/Objects/BMTransactionParameters.h +++ b/BeamWallet/BeamSDK/Objects/BMTransactionParameters.h @@ -2,7 +2,7 @@ // BMTransactionParameters.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMTransactionParameters.m b/BeamWallet/BeamSDK/Objects/BMTransactionParameters.m index 466480da..4e735d77 100644 --- a/BeamWallet/BeamSDK/Objects/BMTransactionParameters.m +++ b/BeamWallet/BeamSDK/Objects/BMTransactionParameters.m @@ -2,7 +2,7 @@ // BMTransactionParameters.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMUTXO.h b/BeamWallet/BeamSDK/Objects/BMUTXO.h index cc41124c..3e846353 100644 --- a/BeamWallet/BeamSDK/Objects/BMUTXO.h +++ b/BeamWallet/BeamSDK/Objects/BMUTXO.h @@ -2,7 +2,7 @@ // BMUTXO.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMUTXO.m b/BeamWallet/BeamSDK/Objects/BMUTXO.m index efd432d7..a146506f 100644 --- a/BeamWallet/BeamSDK/Objects/BMUTXO.m +++ b/BeamWallet/BeamSDK/Objects/BMUTXO.m @@ -2,7 +2,7 @@ // BMUTXO.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMWalletStatus.h b/BeamWallet/BeamSDK/Objects/BMWalletStatus.h index 51a3abb5..fb266030 100644 --- a/BeamWallet/BeamSDK/Objects/BMWalletStatus.h +++ b/BeamWallet/BeamSDK/Objects/BMWalletStatus.h @@ -2,7 +2,7 @@ // BMWalletStatus.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMWalletStatus.m b/BeamWallet/BeamSDK/Objects/BMWalletStatus.m index a9135ed7..784b0fc5 100644 --- a/BeamWallet/BeamSDK/Objects/BMWalletStatus.m +++ b/BeamWallet/BeamSDK/Objects/BMWalletStatus.m @@ -2,7 +2,7 @@ // BMWalletStatus.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -70,23 +70,23 @@ -(id)initWithCoder:(NSCoder *)decoder self = [super init]; if(self) { - self.currentHeight = [decoder decodeObjectForKey: @"currentHeight"]; - self.currentStateHash = [decoder decodeObjectForKey: @"currentStateHash"]; - self.currentStateFullHash = [decoder decodeObjectForKey: @"currentStateFullHash"]; + self.currentHeight = [decoder decodeObjectOfClass:[NSString class] forKey: @"currentHeight"]; + self.currentStateHash = [decoder decodeObjectOfClass:[NSString class] forKey: @"currentStateHash"]; + self.currentStateFullHash = [decoder decodeObjectOfClass:[NSString class] forKey: @"currentStateFullHash"]; - self.available = [[decoder decodeObjectForKey: @"available"] longLongValue]; - self.receiving = [[decoder decodeObjectForKey: @"receiving"] longLongValue]; - self.sending = [[decoder decodeObjectForKey: @"sending"] longLongValue]; - self.maturing = [[decoder decodeObjectForKey: @"maturing"] longLongValue]; - self.shielded = [[decoder decodeObjectForKey: @"shielded"] longLongValue]; - self.maxPrivacy = [[decoder decodeObjectForKey: @"maxPrivacy"] longValue]; + self.available = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"available"] longLongValue]; + self.receiving = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"receiving"] longLongValue]; + self.sending = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"sending"] longLongValue]; + self.maturing = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"maturing"] longLongValue]; + self.shielded = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"shielded"] longLongValue]; + self.maxPrivacy = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"maxPrivacy"] longValue]; - self.realAmount = [[decoder decodeObjectForKey: @"realAmount"] doubleValue]; - self.realReceiving = [[decoder decodeObjectForKey: @"realReceiving"] doubleValue]; - self.realSending = [[decoder decodeObjectForKey: @"realSending"] doubleValue]; - self.realMaturing = [[decoder decodeObjectForKey: @"realMaturing"] doubleValue]; - self.realShielded = [[decoder decodeObjectForKey: @"realShielded"] doubleValue]; - self.realMaxPrivacy = [[decoder decodeObjectForKey: @"realMaxPrivacy"] doubleValue]; + self.realAmount = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realAmount"] doubleValue]; + self.realReceiving = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realReceiving"] doubleValue]; + self.realSending = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realSending"] doubleValue]; + self.realMaturing = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realMaturing"] doubleValue]; + self.realShielded = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realShielded"] doubleValue]; + self.realMaxPrivacy = [[decoder decodeObjectOfClass:[NSNumber class] forKey: @"realMaxPrivacy"] doubleValue]; } return self; } diff --git a/BeamWallet/BeamSDK/Objects/BMWord.h b/BeamWallet/BeamSDK/Objects/BMWord.h index 18721a93..49208107 100644 --- a/BeamWallet/BeamSDK/Objects/BMWord.h +++ b/BeamWallet/BeamSDK/Objects/BMWord.h @@ -2,7 +2,7 @@ // BMWord.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/Objects/BMWord.m b/BeamWallet/BeamSDK/Objects/BMWord.m index 31ab6f0c..84338b60 100644 --- a/BeamWallet/BeamSDK/Objects/BMWord.m +++ b/BeamWallet/BeamSDK/Objects/BMWord.m @@ -2,7 +2,7 @@ // BMWord.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/BeamSDK/RecoveryProgress.h b/BeamWallet/BeamSDK/RecoveryProgress.h index fe21e74f..ebfd061b 100644 --- a/BeamWallet/BeamSDK/RecoveryProgress.h +++ b/BeamWallet/BeamSDK/RecoveryProgress.h @@ -2,7 +2,7 @@ // RecoveryProgress.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -64,6 +64,11 @@ private: std::unique_ptr m_estimateFilter; private: long avgTime = 0; private: long start = 0; +// BEAM core fires OnProgress thousands of times per second (sub-1KB granularity). +// Forward to delegates only when integer percent advances or recovery completes, +// so the iOS side doesn't drown the main queue with redundant UI updates. +private: int m_lastReportedPercent = -1; + private: double getWindowedBps(); double getWholeTimeBps(); diff --git a/BeamWallet/BeamSDK/RecoveryProgress.mm b/BeamWallet/BeamSDK/RecoveryProgress.mm index 2c6da47f..8611f52b 100644 --- a/BeamWallet/BeamSDK/RecoveryProgress.mm +++ b/BeamWallet/BeamSDK/RecoveryProgress.mm @@ -2,7 +2,7 @@ // RecoveryProgress.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -183,27 +183,43 @@ } bool RecoveryProgress::OnProgress(uint64_t done, uint64_t total) { - NSLog(@"RecoveryProgress %llu - %llu",done, total); - + // Throttle: BEAM fires this thousands of times per second. Skip everything + // unless integer percent advanced or we just hit completion — otherwise the + // dispatch_async-per-callback on the iOS side accumulates blocks on the main + // queue faster than UIKit can drain them, blowing past the highwater limit. + if (total == 0) { + // No useful 0/0 update to forward — would just push a meaningless + // delegate callback before any real progress has been reported. + return [AppModel sharedManager].isRestoreFlow; + } + int percent = (int)((done * 100) / total); + bool atCompletion = (done >= total); + if (percent == m_lastReportedPercent && !atCompletion) { + return [AppModel sharedManager].isRestoreFlow; + } + m_lastReportedPercent = percent; + + NSLog(@"RecoveryProgress %llu / %llu (%d%%)", done, total, percent); + if (!m_isStart) { m_isStart = true; - + m_bpsWholeTimeFilter = std::make_unique(kFilterRange); m_bpsWindowedFilter = std::make_unique(kFilterRange*3); m_estimateFilter = std::make_unique(kFilterRange); - + m_startTimestamp = beam::getTimestamp(); } - + m_previousUpdateTimestamp = m_lastUpdateTimestamp; m_lastUpdateTimestamp = beam::getTimestamp(); m_lastDone = m_done; m_done = done; m_total = total; - + auto wbps = getWindowedBps(); auto bps = (getWholeTimeBps() + wbps) / 2; - + if (fabs(bps) < std::numeric_limits::epsilon()) { m_estimate = 0; @@ -218,7 +234,7 @@ [delegate onRecoveryProgressUpdated:(int)done total:(int)total time:(int)m_estimate]; } } - + return [AppModel sharedManager].isRestoreFlow; } diff --git a/BeamWallet/BeamSDK/Settings.h b/BeamWallet/BeamSDK/Settings.h index 8ed457fa..0592756d 100644 --- a/BeamWallet/BeamSDK/Settings.h +++ b/BeamWallet/BeamSDK/Settings.h @@ -2,7 +2,7 @@ // Settings.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -68,6 +68,7 @@ typedef enum Target : NSUInteger { @property (nonatomic, assign) int logDays; @property (nonatomic, assign) BOOL isDarkMode; @property (nonatomic, assign) BMCurrencyType currency; +@property (nonatomic, assign) BOOL isOracleEnabled; @property (nonatomic, assign) BOOL isNotificationWalletON; @property (nonatomic, assign) BOOL isNotificationNewsON; @property (nonatomic, assign) BOOL isNotificationTransactionON; diff --git a/BeamWallet/BeamSDK/Settings.m b/BeamWallet/BeamSDK/Settings.m index 3a318f44..853c4abe 100644 --- a/BeamWallet/BeamSDK/Settings.m +++ b/BeamWallet/BeamSDK/Settings.m @@ -2,7 +2,7 @@ // Settings.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ @implementation Settings static NSString *notificationsAddressKey = @"notificationsAddressKey"; static NSString *nodeProtocolKey = @"nodeProtocolKey"; static NSString *randomDBIdKey = @"randomDBIdKey"; +static NSString *oracleEnabledKey = @"oracleEnabledKey"; @@ -238,7 +239,14 @@ -(id)init { else{ _isNodeProtocolEnabled = NO; } - + + if ([[NSUserDefaults standardUserDefaults] objectForKey:oracleEnabledKey]) { + _isOracleEnabled = [[[NSUserDefaults standardUserDefaults] objectForKey:oracleEnabledKey] boolValue]; + } + else { + _isOracleEnabled = NO; + } + return self; } @@ -327,14 +335,26 @@ -(void)setConnectToRandomNode:(BOOL)connectToRandomNode { } -(void)setDefaultDarkMode:(BOOL)isSystemMode { - if (![[NSUserDefaults standardUserDefaults] objectForKey:isSetDarkModeKey]) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + // Pre-isSetDarkModeKey installs already had darkModeKey persisted; if we + // see one without the marker, lock the existing preference in place so + // the fresh-install branch below doesn't retroactively flip them. + if ([defaults objectForKey:darkModeKey] != nil && + [defaults objectForKey:isSetDarkModeKey] == nil) { + [defaults setObject:@"1" forKey:isSetDarkModeKey]; + [defaults synchronize]; + _isDarkMode = [[defaults objectForKey:darkModeKey] boolValue]; + return; + } + + if (![defaults objectForKey:isSetDarkModeKey]) { self.isDarkMode = isSystemMode; - - [[NSUserDefaults standardUserDefaults] setObject:@"1" forKey:isSetDarkModeKey]; - [[NSUserDefaults standardUserDefaults] synchronize]; + + [defaults setObject:@"1" forKey:isSetDarkModeKey]; + [defaults synchronize]; } else{ - _isDarkMode = [[[NSUserDefaults standardUserDefaults] objectForKey:darkModeKey] boolValue]; + _isDarkMode = [[defaults objectForKey:darkModeKey] boolValue]; } } @@ -468,6 +488,20 @@ -(void)setNodeAddress:(NSString *_Nonnull)nodeAddress { } } +-(void)setIsOracleEnabled:(BOOL)isOracleEnabled { + _isOracleEnabled = isOracleEnabled; + + [[NSUserDefaults standardUserDefaults] setBool:_isOracleEnabled forKey:oracleEnabledKey]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for(id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onExchangeRatesChange)]) { + [delegate onExchangeRatesChange]; + } + } +} + -(void)setIsNotificationNewsON:(BOOL)isNotificationNewsON { _isNotificationNewsON = isNotificationNewsON; [[NSUserDefaults standardUserDefaults] setBool:_isNotificationNewsON forKey:notificationsNewsKey]; diff --git a/BeamWallet/BeamSDK/StringManager.h b/BeamWallet/BeamSDK/StringManager.h index be31238d..ac658165 100644 --- a/BeamWallet/BeamSDK/StringManager.h +++ b/BeamWallet/BeamSDK/StringManager.h @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 09.06.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import diff --git a/BeamWallet/BeamSDK/StringManager.m b/BeamWallet/BeamSDK/StringManager.m index 38ec3aaa..4033f4ad 100644 --- a/BeamWallet/BeamSDK/StringManager.m +++ b/BeamWallet/BeamSDK/StringManager.m @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 09.06.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // #import "StringManager.h" diff --git a/BeamWallet/BeamSDK/WalletModel.h b/BeamWallet/BeamSDK/WalletModel.h index 34a65975..66d937b9 100644 --- a/BeamWallet/BeamSDK/WalletModel.h +++ b/BeamWallet/BeamSDK/WalletModel.h @@ -2,7 +2,7 @@ // WalletModel.h // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -42,6 +42,8 @@ class WalletModel : public beam::wallet::WalletClient std::map shieldedCoins; private: + std::set m_pendingAssetInfo; + NSString *GetAddressTo(beam::wallet::TxDescription transaction); NSString *GetAddressFrom(beam::wallet::TxDescription transaction); NSString *GetErrorString(beam::wallet::ErrorType type); @@ -84,5 +86,14 @@ class WalletModel : public beam::wallet::WalletClient void onAddressesChanged(beam::wallet::ChangeAction, const std::vector& addresses) override; void onPublicAddress(const std::string& publicAddr) override; void onAssetInfo(beam::Asset::ID assetId, const beam::wallet::WalletAsset&) override; + void onFullAssetsListLoaded() override; void onCoinsSelected(const beam::wallet::CoinsSelectionInfo&) override; + void onInstantMessage(beam::Timestamp time, const beam::wallet::WalletID& counterpart, const std::string& message, bool isIncome) override; + void onGetChatList(const std::vector>& chats) override; + void onGetChatMessages(const std::vector& messages) override; + void onChatRemoved(const beam::wallet::WalletID& counterpart) override; +#ifdef BEAM_ASSET_SWAP_SUPPORT + void onDexOrdersChanged(beam::wallet::ChangeAction action, const std::vector& orders) override; + void onFindDexOrder(const beam::wallet::DexOrder& order) override; +#endif }; diff --git a/BeamWallet/BeamSDK/WalletModel.mm b/BeamWallet/BeamSDK/WalletModel.mm index 5dc832b3..e1a59802 100644 --- a/BeamWallet/BeamSDK/WalletModel.mm +++ b/BeamWallet/BeamSDK/WalletModel.mm @@ -2,7 +2,7 @@ // WalletModel.m // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -29,6 +29,11 @@ #include "utility/helpers.h" #include "utility/common.h" #include "wallet/core/strings_resources.h" +#ifdef BEAM_ASSET_SWAP_SUPPORT +#include "wallet/client/extensions/dex_board/dex_board.h" +#include "wallet/client/extensions/dex_board/dex_order.h" +#endif +#import "BMDexOrder.h" #import "StringStd.h" @@ -37,6 +42,16 @@ using namespace beam::wallet; using namespace std; +// Defined in AppModel.mm — the fully-configured Rules singleton. +extern beam::Rules* g_pConfiguredRules; + +// Install s_pInstance on whichever thread calls this first. +// s_pInstance is thread_local; each new thread starts with nullptr. +static void ensureRulesOnCurrentThread() { + if (!beam::Rules::s_pInstance && g_pConfiguredRules) { + beam::Rules::s_pInstance = g_pConfiguredRules; + } +} NSString *const AppErrorDomain = @"beam.mw"; NSTimer *timer; @@ -65,6 +80,7 @@ void WalletModel::onStatus(const WalletStatus& status) { + ensureRulesOnCurrentThread(); NSLog(@"onStatus"); auto beamStatus = status.GetBeamStatus(); @@ -146,9 +162,9 @@ for (int i=0;i<[[AssetsManager sharedManager]assets].count; i++) { BMAsset *asset = [[[AssetsManager sharedManager]assets] objectAtIndex:i]; if (asset.name == nil || asset.name.isEmpty) { - NSLog(@"GET ASSET %d",(uint)asset.assetId); if(asset.assetId == 0) { asset.assetId = 0; + asset.name = @"BEAM"; asset.nthUnitName = @"BEAM"; asset.unitName = @"BEAM"; asset.color = @"#00F6D2"; @@ -159,7 +175,8 @@ asset.paper = @""; [[[AssetsManager sharedManager]assets] replaceObjectAtIndex:i withObject:asset]; } - else { + else if (m_pendingAssetInfo.insert((beam::Asset::ID)asset.assetId).second) { + NSLog(@"GET ASSET %d",(uint)asset.assetId); this->getAsync()->getAssetInfo((uint)asset.assetId); } } @@ -411,7 +428,15 @@ } } } - +#ifdef BEAM_ASSET_SWAP_SUPPORT + else if(item.m_txType == wallet::TxType::DexSimpleSwap) { + NSString *swapPrefix = NSLocalizedString(@"asset_swaps", nil); + transaction.status = [NSString stringWithFormat:@"%@ — %@", + swapPrefix, + [GetTransactionStatusString(item, transaction.isIncome) lowercaseString]]; + } +#endif + auto rate = item.getExchangeRate(currenCurrency, transaction.assetId); transaction.realRate = int64_t(rate); @@ -508,6 +533,7 @@ void WalletModel::onSyncProgressUpdated(int done, int total) { + ensureRulesOnCurrentThread(); NSLog(@"onSyncProgressUpdated %d/%d",done, total); [AppModel sharedManager].isUpdating = (done != total); @@ -779,6 +805,7 @@ void WalletModel::onNodeConnectionChanged(bool isNodeConnected) { + ensureRulesOnCurrentThread(); NSLog(@"onNodeConnectionChanged %d",isNodeConnected); if (isNodeConnected) { @@ -879,6 +906,7 @@ void WalletModel::FailedToStartWallet() { + ensureRulesOnCurrentThread(); if([AppModel sharedManager].isLoggedin) { dispatch_async(dispatch_get_main_queue(), ^{ [[AppModel sharedManager] restartWallet]; @@ -974,10 +1002,327 @@ void WalletModel::onPostFunctionToClientContext(MessageFunction&& func) { NSLog(@"onPostFunctionToClientContext"); - + doFunction(func); } +#pragma mark - Messenger callbacks + +void WalletModel::onInstantMessage(beam::Timestamp time, const beam::wallet::WalletID& counterpart, const std::string& message, bool isIncome) { + NSString *peer = [NSString stringWithUTF8String:to_string(counterpart).c_str()]; + NSString *text = [[NSString alloc] initWithBytes:message.data() length:message.size() encoding:NSUTF8StringEncoding]; + if (text == nil) { + text = [[NSString alloc] initWithBytes:message.data() length:message.size() encoding:NSISOLatin1StringEncoding] ?: @""; + } + NSLog(@"[Messenger] onInstantMessage peer=%@ income=%d len=%lu", peer, isIncome ? 1 : 0, (unsigned long)text.length); + + UInt64 timestamp = (UInt64)time; + BOOL isIncomeFlag = isIncome ? YES : NO; + + // Mutate the messagesByPeer / chats caches and broadcast on main; UI reads + // these unguarded (NSMutableArray / NSMutableDictionary) so reactor-thread + // writes will eventually crash with `collection mutated while being + // enumerated`. Mirrors the dispatch-on-main pattern used by the DEX path. + dispatch_async(dispatch_get_main_queue(), ^{ + BMInstantMessage *bmMessage = [[BMInstantMessage alloc] init]; + bmMessage.timestamp = timestamp; + bmMessage.peerWalletId = peer; + bmMessage.message = text; + bmMessage.isIncome = isIncomeFlag; + bmMessage.isRead = isIncomeFlag ? NO : YES; + bmMessage.myWalletId = @""; + + NSMutableDictionary *byPeer = [AppModel sharedManager].messagesByPeer; + NSMutableArray *list = byPeer[peer]; + if (list == nil) { + list = [NSMutableArray new]; + byPeer[peer] = list; + } + [list addObject:bmMessage]; + + NSMutableArray *chats = [AppModel sharedManager].chats; + BMChat *existing = nil; + for (BMChat *c in chats) { + if ([c.peerWalletId isEqualToString:peer]) { + existing = c; + break; + } + } + if (existing == nil) { + existing = [[BMChat alloc] init]; + existing.peerWalletId = peer; + BMContact *contact = [[AppModel sharedManager] getContactFromId:peer]; + if (contact != nil) { + existing.contactName = contact.name; + } + [chats addObject:existing]; + } + existing.lastMessagePreview = text; + existing.lastMessageTimestamp = timestamp; + if (isIncomeFlag) { + existing.hasUnread = YES; + } + + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for (id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onInstantMessageReceived:)]) { + [delegate onInstantMessageReceived:bmMessage]; + } + if ([delegate respondsToSelector:@selector(onChatListChanged)]) { + [delegate onChatListChanged]; + } + } + }); +} + +void WalletModel::onGetChatList(const std::vector>& chats) { + // Decode WalletIDs into ObjC strings up-front; the rest of the work + // (cache mutation + delegate broadcast) hops to main to avoid racing UI + // reads of `messagesByPeer` / `chats`. + NSMutableArray *entries = [NSMutableArray arrayWithCapacity:chats.size()]; + for (const auto& entry : chats) { + NSString *peer = [NSString stringWithUTF8String:to_string(entry.first).c_str()]; + [entries addObject:@{ @"peer": peer, @"unread": @(entry.second) }]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableDictionary*> *byPeer = [AppModel sharedManager].messagesByPeer; + + NSArray *previousChats = [[AppModel sharedManager].chats copy]; + NSMutableSet *peersFromDB = [NSMutableSet new]; + + NSMutableArray *result = [NSMutableArray new]; + + for (NSDictionary *entry in entries) { + NSString *peer = entry[@"peer"]; + BOOL unread = [entry[@"unread"] boolValue]; + [peersFromDB addObject:peer]; + + BMChat *chat = [[BMChat alloc] init]; + chat.peerWalletId = peer; + chat.hasUnread = unread ? YES : NO; + + BMContact *contact = [[AppModel sharedManager] getContactFromId:peer]; + if (contact != nil) { + chat.contactName = contact.name; + } + for (BMChat *prev in previousChats) { + if ([prev.peerWalletId isEqualToString:peer]) { + if (prev.contactName.length > 0 && chat.contactName.length == 0) { + chat.contactName = prev.contactName; + } + if (prev.myWalletId.length > 0) { + chat.myWalletId = prev.myWalletId; + } + break; + } + } + + NSArray *cached = byPeer[peer]; + BMInstantMessage *latest = cached.lastObject; + if (latest != nil) { + chat.lastMessagePreview = latest.message; + chat.lastMessageTimestamp = latest.timestamp; + } + + [result addObject:chat]; + } + + for (BMChat *prev in previousChats) { + if (![peersFromDB containsObject:prev.peerWalletId]) { + [result addObject:prev]; + } + } + + [result sortUsingComparator:^NSComparisonResult(BMChat *a, BMChat *b) { + if (a.lastMessageTimestamp == b.lastMessageTimestamp) return NSOrderedSame; + return a.lastMessageTimestamp < b.lastMessageTimestamp ? NSOrderedDescending : NSOrderedAscending; + }]; + + [[AppModel sharedManager].chats removeAllObjects]; + [[AppModel sharedManager].chats addObjectsFromArray:result]; + + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for (id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onChatListChanged)]) { + [delegate onChatListChanged]; + } + } + }); +} + +void WalletModel::onGetChatMessages(const std::vector& messages) { + if (messages.empty()) return; + + NSString *peer = [NSString stringWithUTF8String:to_string(messages.front().m_counterpart).c_str()]; + + // Build the BMInstantMessage list off-main (no shared state touched), then + // assign the result and broadcast on main so UI readers of messagesByPeer + // never observe a half-mutated container. + NSMutableArray *result = [NSMutableArray new]; + for (const auto& im : messages) { + BMInstantMessage *bmMessage = [[BMInstantMessage alloc] init]; + bmMessage.timestamp = (UInt64)im.m_timestamp; + bmMessage.peerWalletId = [NSString stringWithUTF8String:to_string(im.m_counterpart).c_str()]; + bmMessage.myWalletId = [NSString stringWithUTF8String:to_string(im.m_mySbbs).c_str()]; + NSString *body = [[NSString alloc] initWithBytes:im.m_message.data() length:im.m_message.size() encoding:NSUTF8StringEncoding]; + if (body == nil) { + body = [[NSString alloc] initWithBytes:im.m_message.data() length:im.m_message.size() encoding:NSISOLatin1StringEncoding] ?: @""; + } + bmMessage.message = body; + bmMessage.isIncome = im.m_is_income ? YES : NO; + bmMessage.isRead = im.m_is_read ? YES : NO; + [result addObject:bmMessage]; + } + + [result sortUsingComparator:^NSComparisonResult(BMInstantMessage *a, BMInstantMessage *b) { + if (a.timestamp == b.timestamp) return NSOrderedSame; + return a.timestamp < b.timestamp ? NSOrderedAscending : NSOrderedDescending; + }]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [AppModel sharedManager].messagesByPeer[peer] = result; + + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for (id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onChatMessagesLoaded:messages:)]) { + [delegate onChatMessagesLoaded:peer messages:result]; + } + } + }); +} + +void WalletModel::onChatRemoved(const beam::wallet::WalletID& counterpart) { + NSString *peer = [NSString stringWithUTF8String:to_string(counterpart).c_str()]; + + // Mutate chats / messagesByPeer on main; UI iterates these unguarded. + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray *chats = [AppModel sharedManager].chats; + NSMutableIndexSet *indexes = [NSMutableIndexSet new]; + for (NSUInteger i = 0; i < chats.count; i++) { + if ([chats[i].peerWalletId isEqualToString:peer]) { + [indexes addIndex:i]; + } + } + [chats removeObjectsAtIndexes:indexes]; + [[AppModel sharedManager].messagesByPeer removeObjectForKey:peer]; + + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for (id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onChatRemoved:)]) { + [delegate onChatRemoved:peer]; + } + if ([delegate respondsToSelector:@selector(onChatListChanged)]) { + [delegate onChatListChanged]; + } + } + }); +} + +#ifdef BEAM_ASSET_SWAP_SUPPORT +static BMDexOrder *MakeBMDexOrder(const beam::wallet::DexOrder& order) { + BMDexOrder *bmo = [[BMDexOrder alloc] init]; + bmo.orderID = [NSString stringWithUTF8String:order.getID().to_string().c_str()]; + bmo.sbbsID = [NSString stringWithUTF8String:to_string(order.getSBBSID()).c_str()]; + bmo.sendAssetId = (UInt32)order.getSendAssetId(); + bmo.receiveAssetId = (UInt32)order.getReceiveAssetId(); + bmo.sendAssetSName = [NSString stringWithUTF8String:order.getSendAssetSName().c_str()]; + bmo.receiveAssetSName = [NSString stringWithUTF8String:order.getReceiveAssetSName().c_str()]; + bmo.sendAmount = (UInt64)order.getSendAmount(); + bmo.receiveAmount = (UInt64)order.getReceiveAmount(); + bmo.createTimestamp = (UInt64)order.getCreation(); + bmo.expireTimestamp = (UInt64)order.getExpiration(); + bmo.isMine = order.isMine(); + bmo.isAccepted = order.isAccepted(); + bmo.isCanceled = order.isCanceled(); + return bmo; +} + +void WalletModel::onDexOrdersChanged(beam::wallet::ChangeAction action, const std::vector& orders) { + // Snapshot the C++ vector into BMDexOrder objects on the reactor thread + // (DexOrder is reactor-thread-only), then mutate the shared `dexOrders` + // cache and broadcast on main. UI reads `dexOrders` unguarded, so the + // cache mutation must not race with main-thread enumeration. Mirrors the + // dispatch-on-main pattern used by the messenger callbacks. + NSMutableArray *converted = [NSMutableArray arrayWithCapacity:orders.size()]; + for (const auto& order : orders) { + [converted addObject:MakeBMDexOrder(order)]; + } + BOOL isReset = (action == beam::wallet::ChangeAction::Reset); + BOOL isRemoved = (action == beam::wallet::ChangeAction::Removed); + + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray *cache = [AppModel sharedManager].dexOrders; + + if (isReset) { + [cache removeAllObjects]; + } + + for (BMDexOrder *bmo in converted) { + NSUInteger existingIdx = NSNotFound; + for (NSUInteger i = 0; i < cache.count; i++) { + if ([cache[i].orderID isEqualToString:bmo.orderID]) { + existingIdx = i; + break; + } + } + + if (isRemoved) { + if (existingIdx != NSNotFound) { + [cache removeObjectAtIndex:existingIdx]; + } + continue; + } + + if (existingIdx != NSNotFound) { + [cache replaceObjectAtIndex:existingIdx withObject:bmo]; + } else { + [cache addObject:bmo]; + } + } + + NSArray *snapshot = [cache copy]; + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for (id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onDexOrdersChanged:)]) { + [delegate onDexOrdersChanged:snapshot]; + } + } + }); +} + +void WalletModel::onFindDexOrder(const beam::wallet::DexOrder& order) { + // Build the BMDexOrder off-main (no shared state touched), then mutate + // the `dexOrders` cache and broadcast on main. UI reads the cache + // unguarded, so the mutation must not race with main-thread enumeration. + BMDexOrder *bmo = MakeBMDexOrder(order); + + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray *cache = [AppModel sharedManager].dexOrders; + NSUInteger existingIdx = NSNotFound; + for (NSUInteger i = 0; i < cache.count; i++) { + if ([cache[i].orderID isEqualToString:bmo.orderID]) { + existingIdx = i; + break; + } + } + if (existingIdx != NSNotFound) { + [cache replaceObjectAtIndex:existingIdx withObject:bmo]; + } else { + [cache addObject:bmo]; + } + + NSArray *snapshot = [cache copy]; + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for (id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onDexOrdersChanged:)]) { + [delegate onDexOrdersChanged:snapshot]; + } + } + }); +} +#endif + void WalletModel::onExportTxHistoryToCsv(const std::string& data) { NSString *csv = [NSString stringWithUTF8String:data.c_str()]; @@ -1199,6 +1544,12 @@ currency.realValue = double(int64_t(rate.m_rate)) / Rules::Coin; if (rate.m_to == Currency::USD() && rate.m_from == Currency::BEAM()) { + // OraclePriceManager owns BEAM/USD when the on-chain feed is on. + // Drop the remote tick so the oracle's value isn't clobbered between + // its 60s refreshes. + if ([Settings sharedManager].isOracleEnabled) { + continue; + } currency.type = BMCurrencyUSD; currency.maximumFractionDigits = 2; currency.code = @"USD"; @@ -1469,16 +1820,32 @@ } void WalletModel::onGetAddress(const beam::wallet::WalletID& wid, const boost::optional& address, size_t offlinePayments) { - + NSLog(@"onGetAddress: %d", (int)offlinePayments); - - NSArray *delegates = [AppModel sharedManager].delegates.allObjects; - for(id delegate in delegates) - { - if ([delegate respondsToSelector:@selector(onMaxPrivacyTokensLeft:)]) { - [delegate onMaxPrivacyTokensLeft:(int)offlinePayments]; + + NSString *walletIdStr = [NSString stringWithUTF8String:to_string(wid).c_str()]; + int newCount = (int)offlinePayments; + + // offlinePaymentsByWalletId is read on main from + // AppModel.offlinePaymentsCountForWalletId:; mutate + broadcast on main + // so the dictionary is never written from the reactor thread while UI + // enumerates it. + dispatch_async(dispatch_get_main_queue(), ^{ + NSNumber *prev = [AppModel sharedManager].offlinePaymentsByWalletId[walletIdStr]; + BOOL changed = (prev == nil || prev.intValue != newCount); + [AppModel sharedManager].offlinePaymentsByWalletId[walletIdStr] = @(newCount); + + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for(id delegate in delegates) + { + if ([delegate respondsToSelector:@selector(onMaxPrivacyTokensLeft:)]) { + [delegate onMaxPrivacyTokensLeft:newCount]; + } + if (changed && [delegate respondsToSelector:@selector(onOfflinePaymentsCountForWalletId:count:)]) { + [delegate onOfflinePaymentsCountForWalletId:walletIdStr count:newCount]; + } } - } + }); } void WalletModel::onShieldedCoinChanged(beam::wallet::ChangeAction action, const std::vector& items) { @@ -1634,7 +2001,9 @@ void WalletModel::onAssetInfo(Asset::ID assetId, const WalletAsset& asset) { NSLog(@"onAssetInfo :%d", assetId); - + + m_pendingAssetInfo.erase(assetId); + auto info = WalletAssetMeta(asset); NSString *name = [NSString stringWithUTF8String:info.GetName().c_str()]; @@ -1664,6 +2033,7 @@ } bmAsset.assetId = UInt64(assetId); + bmAsset.name = name; bmAsset.nthUnitName = nthName; bmAsset.unitName = unitName; bmAsset.shortName = shortName; @@ -1701,6 +2071,15 @@ } } +void WalletModel::onFullAssetsListLoaded() { + NSArray *delegates = [AppModel sharedManager].delegates.allObjects; + for(id delegate in delegates) { + if ([delegate respondsToSelector:@selector(onAssetInfoChange)]) { + [delegate onAssetInfoChange]; + } + } +} + NSString* WalletModel::GetErrorString(beam::wallet::ErrorType type) { switch (type) diff --git a/BeamWallet/BeamWallet-Bridging-Header.h b/BeamWallet/BeamWallet-Bridging-Header.h index 2e9cf34b..64ddd88c 100644 --- a/BeamWallet/BeamWallet-Bridging-Header.h +++ b/BeamWallet/BeamWallet-Bridging-Header.h @@ -4,6 +4,10 @@ #import "AppModel.h" #import "MnemonicModel.h" +#import "BMInstalledDApp.h" +#import "BMAvailableDApp.h" +#import "BMPublisher.h" +#import "WalletAPIClient.h" #import #import "LGSideMenuController.h" #import "UIViewController+LGSideMenuController.h" diff --git a/BeamWallet/Controls/AssetIconView.swift b/BeamWallet/Controls/AssetIconView.swift index 56c04ebe..b8c7a6a0 100644 --- a/BeamWallet/Controls/AssetIconView.swift +++ b/BeamWallet/Controls/AssetIconView.swift @@ -2,7 +2,7 @@ // AssetIconView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,71 +23,113 @@ class AssetIconView: BMGradientView { private let imageView = UIImageView(image: UIImage(named: "ic_asset")) private let verify = UIImageView(image: UIImage(named: "ic_verify-1")) - + public var isBig = false - + + private var glyphReferenceSize: CGSize? + private var glyphFillsBounds = false + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + override func layoutSubviews() { super.layoutSubviews() - + verify.frame = CGRect(x: self.width-10, y: -3, width: 13, height: 13) + layoutImageView() } - + override func awakeFromNib() { super.awakeFromNib() - + commonInit() + } + + private func commonInit() { self.cornerRadius = self.frame.width/2 - - self.addSubview(imageView) + if imageView.superview !== self { + self.addSubview(imageView) + } } - + public func setAsset(_ asset:BMAsset) { verify.removeFromSuperview() - + if isBig { if asset.isBeamX() { addSubview(verify) - imageView.frame = self.bounds imageView.image = UIImage(named: "assetbeamx") + glyphReferenceSize = nil + glyphFillsBounds = true } else if asset.isBeam() { imageView.image = UIImage(named: "ic_asset_beam_big") - imageView.frame = CGRect(x:12, y: 11, width: 24, height: 20) + glyphReferenceSize = CGSize(width: 24, height: 20) + glyphFillsBounds = false } else { imageView.image = UIImage(named: "ic_asset_big") - imageView.frame = CGRect(x:12, y: 11, width: 23, height: 19) + glyphReferenceSize = CGSize(width: 23, height: 19) + glyphFillsBounds = false } } else { if asset.isBeamX() { addSubview(verify) - imageView.frame = self.bounds imageView.image = UIImage(named: "assetbeamx") + glyphReferenceSize = nil + glyphFillsBounds = true } else if asset.isBeam() { imageView.image = UIImage(named: "ic_asset_beam") - imageView.frame = CGRect(x:6, y: 5, width: 15, height: 13) + glyphReferenceSize = CGSize(width: 15, height: 13) + glyphFillsBounds = false } else { imageView.image = UIImage(named: "ic_asset") - imageView.frame = CGRect(x:7, y: 7, width: 12, height: 10) + glyphReferenceSize = CGSize(width: 12, height: 10) + glyphFillsBounds = false } - + self.gradientLayer.type = .radial self.gradientLayer.colors = [ UIColor(hexString: asset.color).withAlphaComponent(0.7).cgColor, UIColor.black] self.gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) self.gradientLayer.endPoint = CGPoint(x: 1, y: 1) - + self.borderWidth = 2 self.borderColor = UIColor(hexString: asset.color) - + if asset.isBeamX() { self.borderWidth = 0 self.gradientLayer.colors = nil } } + + setNeedsLayout() + } + + private func layoutImageView() { + if glyphFillsBounds { + imageView.frame = bounds + return + } + guard let refSize = glyphReferenceSize, bounds.width > 0 else { return } + let referenceFrame: CGFloat = isBig ? 48 : 28 + let scale = bounds.width / referenceFrame + let size = CGSize(width: refSize.width * scale, height: refSize.height * scale) + imageView.frame = CGRect( + x: (bounds.width - size.width) / 2, + y: (bounds.height - size.height) / 2, + width: size.width, + height: size.height + ) } } diff --git a/BeamWallet/Controls/BMAlertViewController.swift b/BeamWallet/Controls/BMAlertViewController.swift new file mode 100644 index 00000000..33873f66 --- /dev/null +++ b/BeamWallet/Controls/BMAlertViewController.swift @@ -0,0 +1,314 @@ +// +// BMAlertViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class BMAlertViewController: UIViewController { + + enum ActionStyle { + case `default` + case destructive + case cancel + } + + struct Action { + let title: String + let style: ActionStyle + let handler: (() -> Void)? + + init(title: String, style: ActionStyle, handler: (() -> Void)? = nil) { + self.title = title + self.style = style + self.handler = handler + } + } + + private let titleText: String? + private let messageText: String? + private let actions: [Action] + + private let backdrop = UIView() + private let card = UIView() + private let titleLabel = UILabel() + private let messageLabel = UILabel() + private let buttonStack = UIStackView() + + private let horizontalInset: CGFloat = 32 + private let cardCornerRadius: CGFloat = 18 + private let cardPadding: CGFloat = 24 + private let buttonHeight: CGFloat = 48 + + init(title: String?, message: String?, actions: [Action]) { + self.titleText = title + self.messageText = message + self.actions = actions + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .overFullScreen + modalTransitionStyle = .crossDissolve + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear + + setupBackdrop() + setupCard() + setupTitle() + setupMessage() + setupButtons() + setupConstraints() + } + + private func setupBackdrop() { + backdrop.backgroundColor = UIColor.black.withAlphaComponent(0.6) + backdrop.alpha = 0 + backdrop.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(backdrop) + } + + private func setupCard() { + card.backgroundColor = BMAlertViewController.cardBackgroundColor + card.layer.cornerRadius = cardCornerRadius + card.layer.borderWidth = 1 + card.layer.borderColor = UIColor.white.withAlphaComponent(0.06).cgColor + card.layer.shadowColor = UIColor.black.cgColor + card.layer.shadowOpacity = 0.35 + card.layer.shadowRadius = 24 + card.layer.shadowOffset = CGSize(width: 0, height: 12) + card.alpha = 0 + card.transform = CGAffineTransform(scaleX: 0.92, y: 0.92) + card.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(card) + } + + private func setupTitle() { + titleLabel.translatesAutoresizingMaskIntoConstraints = false + guard let titleText = titleText, !titleText.isEmpty else { + titleLabel.isHidden = true + return + } + titleLabel.text = titleText + titleLabel.font = BoldFont(size: 18) + titleLabel.textColor = UIColor.white + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 0 + card.addSubview(titleLabel) + } + + private func setupMessage() { + messageLabel.translatesAutoresizingMaskIntoConstraints = false + guard let messageText = messageText, !messageText.isEmpty else { + messageLabel.isHidden = true + return + } + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .center + paragraph.lineSpacing = 3 + messageLabel.attributedText = NSAttributedString( + string: messageText, + attributes: [ + .font: RegularFont(size: 14), + .foregroundColor: UIColor.white.withAlphaComponent(0.72), + .paragraphStyle: paragraph + ] + ) + messageLabel.numberOfLines = 0 + card.addSubview(messageLabel) + } + + private func setupButtons() { + buttonStack.axis = .vertical + buttonStack.spacing = 10 + buttonStack.translatesAutoresizingMaskIntoConstraints = false + card.addSubview(buttonStack) + + let indexed = Array(actions.enumerated()) + let ordered = indexed.filter { $0.element.style != .cancel } + indexed.filter { $0.element.style == .cancel } + + for (originalIndex, action) in ordered { + let button = makeButton(for: action, originalIndex: originalIndex) + buttonStack.addArrangedSubview(button) + button.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true + } + } + + private func makeButton(for action: Action, originalIndex: Int) -> UIButton { + let button = UIButton(type: .custom) + button.titleLabel?.font = BoldFont(size: 14) + button.titleLabel?.adjustsFontSizeToFitWidth = true + button.titleLabel?.minimumScaleFactor = 0.7 + button.layer.cornerRadius = buttonHeight / 2 + button.layer.masksToBounds = true + button.setTitle(action.title, for: .normal) + button.tag = originalIndex + button.addTarget(self, action: #selector(onButtonTap(_:)), for: .touchUpInside) + + switch action.style { + case .default: + button.setTitleColor(UIColor.main.marineOriginal, for: .normal) + button.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + button.setBackgroundColor(color: UIColor.main.brightTeal, forState: .normal) + button.setBackgroundColor(color: UIColor.main.brightTeal.withAlphaComponent(0.7), forState: .highlighted) + case .destructive: + button.setTitleColor(UIColor.white, for: .normal) + button.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted) + button.setBackgroundColor(color: UIColor.main.failed, forState: .normal) + button.setBackgroundColor(color: UIColor.main.failed.withAlphaComponent(0.7), forState: .highlighted) + case .cancel: + button.setTitleColor(UIColor.white, for: .normal) + button.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted) + button.setBackgroundColor(color: UIColor.white.withAlphaComponent(0.08), forState: .normal) + button.setBackgroundColor(color: UIColor.white.withAlphaComponent(0.04), forState: .highlighted) + } + + return button + } + + private func setupConstraints() { + var constraints: [NSLayoutConstraint] = [ + backdrop.topAnchor.constraint(equalTo: view.topAnchor), + backdrop.bottomAnchor.constraint(equalTo: view.bottomAnchor), + backdrop.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backdrop.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + card.centerXAnchor.constraint(equalTo: view.centerXAnchor), + card.centerYAnchor.constraint(equalTo: view.centerYAnchor), + card.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: horizontalInset), + card.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -horizontalInset), + + buttonStack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: cardPadding), + buttonStack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -cardPadding), + buttonStack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -cardPadding) + ] + + let widthCap = card.widthAnchor.constraint(equalToConstant: 320) + widthCap.priority = .defaultHigh + constraints.append(widthCap) + + let hasTitle = !(titleText?.isEmpty ?? true) + let hasMessage = !(messageText?.isEmpty ?? true) + + if hasTitle { + constraints.append(contentsOf: [ + titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: cardPadding), + titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: cardPadding), + titleLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -cardPadding) + ]) + } + + if hasMessage { + constraints.append(contentsOf: [ + messageLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: cardPadding), + messageLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -cardPadding) + ]) + if hasTitle { + constraints.append(messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10)) + } else { + constraints.append(messageLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: cardPadding)) + } + constraints.append(buttonStack.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 22)) + } else if hasTitle { + constraints.append(buttonStack.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 22)) + } else { + constraints.append(buttonStack.topAnchor.constraint(equalTo: card.topAnchor, constant: cardPadding)) + } + + NSLayoutConstraint.activate(constraints) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + // Set an explicit shadowPath so iOS doesn't recompute the shadow off the layer's + // alpha channel each frame during the spring + dismiss animations. + card.layer.shadowPath = UIBezierPath(roundedRect: card.bounds, cornerRadius: cardCornerRadius).cgPath + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIView.animate(withDuration: 0.18, delay: 0, options: [.curveEaseOut], animations: { + self.backdrop.alpha = 1 + }) + UIView.animate( + withDuration: 0.32, + delay: 0, + usingSpringWithDamping: 0.85, + initialSpringVelocity: 0.2, + options: [.allowUserInteraction], + animations: { + self.card.alpha = 1 + self.card.transform = .identity + } + ) + } + + @objc private func onButtonTap(_ sender: UIButton) { + guard actions.indices.contains(sender.tag) else { return } + let action = actions[sender.tag] + view.isUserInteractionEnabled = false + UIView.animate( + withDuration: 0.18, + animations: { + self.backdrop.alpha = 0 + self.card.alpha = 0 + self.card.transform = CGAffineTransform(scaleX: 0.96, y: 0.96) + }, + completion: { _ in + self.dismiss(animated: false) { + action.handler?() + } + } + ) + } + + private static var cardBackgroundColor: UIColor { + if Settings.sharedManager().isDarkMode { + return UIColor(red: 36 / 255, green: 36 / 255, blue: 38 / 255, alpha: 1) + } + if Settings.sharedManager().target == Testnet { + return UIColor(red: 40 / 255, green: 34 / 255, blue: 54 / 255, alpha: 1) + } + if Settings.sharedManager().target == Masternet { + return UIColor(red: 28 / 255, green: 28 / 255, blue: 30 / 255, alpha: 1) + } + return UIColor(red: 23 / 255, green: 57 / 255, blue: 97 / 255, alpha: 1) + } +} + +extension BMAlertViewController { + + static func present( + from presenter: UIViewController?, + title: String?, + message: String?, + actions: [Action] + ) { + guard let presenter = presenter else { return } + if presenter.presentedViewController is BMAlertViewController { return } + if presenter.presentedViewController is UIAlertController { return } + + presenter.view.endEditing(true) + + let alert = BMAlertViewController(title: title, message: message, actions: actions) + presenter.present(alert, animated: false, completion: nil) + } +} diff --git a/BeamWallet/Controls/BMAutoSecurityScreen.swift b/BeamWallet/Controls/BMAutoSecurityScreen.swift index 32afc839..b5834a0f 100755 --- a/BeamWallet/Controls/BMAutoSecurityScreen.swift +++ b/BeamWallet/Controls/BMAutoSecurityScreen.swift @@ -2,7 +2,7 @@ // BMAutoSecurityScreen.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -68,9 +68,9 @@ public class BMAutoSecurityScreen { private func removeBlurEffect() { UIView.animate(withDuration: 0.3, animations: { self.blurView.alpha = 0 - }) { (_ ) in + }, completion: { _ in self.blurView.removeFromSuperview() - } + }) } } diff --git a/BeamWallet/Controls/BMButton.swift b/BeamWallet/Controls/BMButton.swift index 00e4899e..8b3e4aec 100644 --- a/BeamWallet/Controls/BMButton.swift +++ b/BeamWallet/Controls/BMButton.swift @@ -3,7 +3,7 @@ // BeamWallet // // 2/28/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMCategoryColorsView.swift b/BeamWallet/Controls/BMCategoryColorsView.swift index fb527faf..4b1c2f7f 100644 --- a/BeamWallet/Controls/BMCategoryColorsView.swift +++ b/BeamWallet/Controls/BMCategoryColorsView.swift @@ -2,7 +2,7 @@ // BMCategoryColorsView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -124,7 +124,7 @@ class BMCategoryColorsView: UIView { } } - public func colorsWidht()->CGFloat { + public func colorsWidht() -> CGFloat { if self.subviews.count > 0 { if let last = self.subviews.last { return last.frame.origin.x + last.frame.size.width diff --git a/BeamWallet/Controls/BMCopyLabel.swift b/BeamWallet/Controls/BMCopyLabel.swift index 3b4c64cd..7084fde6 100644 --- a/BeamWallet/Controls/BMCopyLabel.swift +++ b/BeamWallet/Controls/BMCopyLabel.swift @@ -2,7 +2,7 @@ // BMCopyLabel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,9 +32,7 @@ class BMCopyLabel: UILabel { public var copiedText:String? override public var canBecomeFirstResponder: Bool { - get { - return true - } + return true } override init(frame: CGRect) { diff --git a/BeamWallet/Controls/BMCountdownView.swift b/BeamWallet/Controls/BMCountdownView.swift index 98f60048..745f6571 100644 --- a/BeamWallet/Controls/BMCountdownView.swift +++ b/BeamWallet/Controls/BMCountdownView.swift @@ -2,7 +2,7 @@ // BMCountdownView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMDataPickerViewController.swift b/BeamWallet/Controls/BMDataPickerViewController.swift index e750c10f..23a6a113 100644 --- a/BeamWallet/Controls/BMDataPickerViewController.swift +++ b/BeamWallet/Controls/BMDataPickerViewController.swift @@ -2,7 +2,7 @@ // BMDataPickerViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ class BMDataPickerViewController: BaseTableViewController { case sendCurrency case max_privacy_lock case confirmations + case address_type } private var type: DataType! @@ -90,6 +91,8 @@ class BMDataPickerViewController: BaseTableViewController { case .confirmations: title = Localizable.shared.strings.confirmations tableView.tableFooterView = footerView(text: Localizable.shared.strings.confirmations_hint) + case .address_type: + title = Localizable.shared.strings.address_type default: title = String.empty() } @@ -109,7 +112,7 @@ class BMDataPickerViewController: BaseTableViewController { tableView.backgroundColor = UIColor.main.marine } - private func footerView(text:String)-> UIView { + private func footerView(text:String) -> UIView { let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 0)) let infoLabel = UILabel() @@ -119,7 +122,7 @@ class BMDataPickerViewController: BaseTableViewController { infoLabel.font = ItalicFont(size: 14) infoLabel.textAlignment = .center if Settings.sharedManager().isDarkMode { - infoLabel.textColor = UIColor.main.steel; + infoLabel.textColor = UIColor.main.steel } else { infoLabel.textColor = UIColor.main.blueyGrey @@ -140,20 +143,18 @@ class BMDataPickerViewController: BaseTableViewController { if type == .clear { var deleted = [String]() - for item in values { - if item.arrowType == BMPickerData.ArrowType.selected { - switch item.unique as! Int { - case 1: - deleted.append(Localizable.shared.strings.addresses.lowercased()) - case 2: - deleted.append(Localizable.shared.strings.contacts.lowercased()) - case 3: - deleted.append(Localizable.shared.strings.transactions.lowercased()) - case 4: - deleted.append(Localizable.shared.strings.categories.lowercased()) - default: - break - } + for item in values where item.arrowType == BMPickerData.ArrowType.selected { + switch item.unique as! Int { + case 1: + deleted.append(Localizable.shared.strings.addresses.lowercased()) + case 2: + deleted.append(Localizable.shared.strings.contacts.lowercased()) + case 3: + deleted.append(Localizable.shared.strings.transactions.lowercased()) + case 4: + deleted.append(Localizable.shared.strings.categories.lowercased()) + default: + break } } @@ -169,31 +170,27 @@ class BMDataPickerViewController: BaseTableViewController { } confirmAlert(title: Localizable.shared.strings.clear_data, message: Localizable.shared.strings.delete_data_text(str: str), cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.delete, cancelHandler: { _ in - - }) { _ in - for item in self.values { - if item.arrowType == BMPickerData.ArrowType.selected { - switch item.unique as! Int { - case 1: - AppModel.sharedManager().clearAllAddresses() - case 2: - AppModel.sharedManager().clearAllContacts() - case 3: - AppModel.sharedManager().clearAllTransactions() - default: - break - } + + }, confirmHandler: { _ in + for item in self.values where item.arrowType == BMPickerData.ArrowType.selected { + switch item.unique as! Int { + case 1: + AppModel.sharedManager().clearAllAddresses() + case 2: + AppModel.sharedManager().clearAllContacts() + case 3: + AppModel.sharedManager().clearAllTransactions() + default: + break } } self.back() - } + }) } else if type == .export_data { var selectedValues = [String]() - for item in values { - if item.arrowType == BMPickerData.ArrowType.selected { - selectedValues.append(item.unique as! String) - } + for item in values where item.arrowType == BMPickerData.ArrowType.selected { + selectedValues.append(item.unique as! String) } if type == .export_data { @@ -209,7 +206,7 @@ class BMDataPickerViewController: BaseTableViewController { try data.write(to: fileURL, atomically: false, encoding: .utf8) let vc = UIActivityViewController(activityItems: [fileURL], applicationActivities: []) - vc.completionWithItemsHandler = { (activityType, completed:Bool, returnedItems:[Any]?, error: Error?) in + vc.completionWithItemsHandler = { (_, completed:Bool, _:[Any]?, _: Error?) in if completed { self.back() } @@ -277,8 +274,10 @@ class BMDataPickerViewController: BaseTableViewController { eth.type = BMCurrencyType(BMCurrencyETH) values.append(BMPickerData(title: usd.currencyLongName(), detail: nil, titleColor: UIColor.white, arrowType: (usd.type == Settings.sharedManager().currency) ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: usd.type)) - values.append(BMPickerData(title: btc.currencyLongName(), detail: nil, titleColor: UIColor.white, arrowType: (btc.type == Settings.sharedManager().currency) ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: btc.type)) - values.append(BMPickerData(title: eth.currencyLongName(), detail: nil, titleColor: UIColor.white, arrowType: (eth.type == Settings.sharedManager().currency) ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: eth.type)) + if !Settings.sharedManager().isOracleEnabled { + values.append(BMPickerData(title: btc.currencyLongName(), detail: nil, titleColor: UIColor.white, arrowType: (btc.type == Settings.sharedManager().currency) ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: btc.type)) + values.append(BMPickerData(title: eth.currencyLongName(), detail: nil, titleColor: UIColor.white, arrowType: (eth.type == Settings.sharedManager().currency) ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: eth.type)) + } case .notifications: values.append(BMPickerData(title: Localizable.shared.strings.wallet_updates, detail: nil, titleColor: UIColor.white, arrowType: Settings.sharedManager().isNotificationWalletON ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: Localizable.shared.strings.wallet_updates, multiplie: false, isSwitch: true)) @@ -295,6 +294,18 @@ class BMDataPickerViewController: BaseTableViewController { let value = UInt32(n) values.append(BMPickerData(title: "\(n)", detail: nil, titleColor: UIColor.white, arrowType: (value == Settings.sharedManager().minConfirmations) ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: value)) } + case .address_type: + let selected = (selectedValue as? Int) ?? 0 + let entries: [(Int, String)] = [ + (0, Localizable.shared.strings.sbbs_address), + (1, Localizable.shared.strings.regular_address), + (2, Localizable.shared.strings.max_privacy_address), + (3, Localizable.shared.strings.offline_address), + (4, Localizable.shared.strings.public_offline_address) + ] + for (raw, label) in entries { + values.append(BMPickerData(title: label, detail: nil, titleColor: UIColor.white, arrowType: (raw == selected) ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: raw)) + } default: break @@ -333,10 +344,8 @@ class BMDataPickerViewController: BaseTableViewController { data.arrowType = BMPickerData.ArrowType.selected } var isAllDisabled = true - for item in values { - if item.arrowType == BMPickerData.ArrowType.selected { - isAllDisabled = false - } + for item in values where item.arrowType == BMPickerData.ArrowType.selected { + isAllDisabled = false } enableRightButton(enabled: !isAllDisabled) case .currency: @@ -358,7 +367,10 @@ class BMDataPickerViewController: BaseTableViewController { case .confirmations: completion?(data.unique) back() - + case .address_type: + completion?(data.unique) + back() + default: break } diff --git a/BeamWallet/Controls/BMDoubleAuthViewController.swift b/BeamWallet/Controls/BMDoubleAuthViewController.swift index 06bf9cb2..487e5a25 100644 --- a/BeamWallet/Controls/BMDoubleAuthViewController.swift +++ b/BeamWallet/Controls/BMDoubleAuthViewController.swift @@ -2,7 +2,7 @@ // BMDoubleAuthViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMEmptyView.swift b/BeamWallet/Controls/BMEmptyView.swift index 773e782d..a061074c 100644 --- a/BeamWallet/Controls/BMEmptyView.swift +++ b/BeamWallet/Controls/BMEmptyView.swift @@ -2,7 +2,7 @@ // BMEmptyView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMField.swift b/BeamWallet/Controls/BMField.swift index 27303e6a..d2b6db7c 100644 --- a/BeamWallet/Controls/BMField.swift +++ b/BeamWallet/Controls/BMField.swift @@ -2,7 +2,7 @@ // BMField.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -107,6 +107,12 @@ class BMField: BMClearField { _defaultHeight = newValue } } + + // TODO: stored only to satisfy KVC for legacy xibs that still reference these + // keys; the values are not rendered. Do not remove the properties without + // also stripping the bindings from every xib that uses BMField. + @IBInspectable var lineColor: UIColor? + @IBInspectable var lineHeight: CGFloat = 0 private var isInFocus = false { didSet { diff --git a/BeamWallet/Controls/BMGradientView.swift b/BeamWallet/Controls/BMGradientView.swift index 31d9cc18..02ab2a87 100644 --- a/BeamWallet/Controls/BMGradientView.swift +++ b/BeamWallet/Controls/BMGradientView.swift @@ -2,7 +2,7 @@ // BMGradientView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMInputCopyBar.swift b/BeamWallet/Controls/BMInputCopyBar.swift index 832905d9..74feb52b 100644 --- a/BeamWallet/Controls/BMInputCopyBar.swift +++ b/BeamWallet/Controls/BMInputCopyBar.swift @@ -2,7 +2,7 @@ // BMInputCopyBar.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,12 +31,12 @@ class BMInputCopyBar: UIView { addSubview(view) let toolbar = UIToolbar(frame: view.bounds) - toolbar.autoresizingMask = .flexibleWidth; - toolbar.isUserInteractionEnabled = false; + toolbar.autoresizingMask = .flexibleWidth + toolbar.isUserInteractionEnabled = false view.addSubview(toolbar) let separator = UIView(frame: CGRect(x:0, y:43.5, width:view.frame.size.width, height:0.5)) - separator.autoresizingMask = .flexibleWidth; + separator.autoresizingMask = .flexibleWidth separator.backgroundColor = UIColor.init(white: 0, alpha: 0.2) view.addSubview(separator) diff --git a/BeamWallet/Controls/BMInputViewController.swift b/BeamWallet/Controls/BMInputViewController.swift index 67973b15..dbea7dbc 100644 --- a/BeamWallet/Controls/BMInputViewController.swift +++ b/BeamWallet/Controls/BMInputViewController.swift @@ -2,7 +2,7 @@ // BMInputViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMLockScreen.swift b/BeamWallet/Controls/BMLockScreen.swift index 98511490..d36405bc 100644 --- a/BeamWallet/Controls/BMLockScreen.swift +++ b/BeamWallet/Controls/BMLockScreen.swift @@ -2,7 +2,7 @@ // LockScreen.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMNetworkStatusView.swift b/BeamWallet/Controls/BMNetworkStatusView.swift index 602123a3..fb1eddb4 100644 --- a/BeamWallet/Controls/BMNetworkStatusView.swift +++ b/BeamWallet/Controls/BMNetworkStatusView.swift @@ -2,7 +2,7 @@ // BMStatusView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -134,22 +134,20 @@ class BMNetworkStatusView: UIView { @objc private func onChangeNode() { if let top = UIApplication.getTopMostViewController() { let vc = SelectNodeViewController() - //SettingsViewController(type: SettingsViewModel.SettingsType.node) + // SettingsViewController(type: SettingsViewModel.SettingsType.node) top.pushViewController(vc: vc) } } private var onlineString:String { - get { - if Settings.sharedManager().isNodeProtocolEnabled { - return Localizable.shared.strings.online_mobile_node.lowercased() - } - else if !Settings.sharedManager().connectToRandomNode { - return Localizable.shared.strings.online.lowercased() - } - else { - return Localizable.shared.strings.online.lowercased() - } + if Settings.sharedManager().isNodeProtocolEnabled { + return Localizable.shared.strings.online_mobile_node.lowercased() + } + else if !Settings.sharedManager().connectToRandomNode { + return Localizable.shared.strings.online.lowercased() + } + else { + return Localizable.shared.strings.online.lowercased() } } @@ -366,7 +364,7 @@ extension BMNetworkStatusView: WalletModelDelegate { func onNetwotkStartConnecting(_ connecting: Bool) { DispatchQueue.main.async { if connecting { - //https://github.com/BeamMW/ios-wallet/issues/194 + // https://github.com/BeamMW/ios-wallet/issues/194 self.onNetwotkStatusChange(true) } } diff --git a/BeamWallet/Controls/BMNotificationView.swift b/BeamWallet/Controls/BMNotificationView.swift index d6af8a0e..6285038f 100644 --- a/BeamWallet/Controls/BMNotificationView.swift +++ b/BeamWallet/Controls/BMNotificationView.swift @@ -2,7 +2,7 @@ // BMNotificationView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ class BMNotificationView: UIView { weak var delegate: BMNotificationViewDelegate? - private static func showNews (notification: BMNotification, delegate:BMNotificationViewDelegate?) { + private static func showNews(notification: BMNotification, delegate:BMNotificationViewDelegate?) { DispatchQueue.main.async { let detail = NSMutableAttributedString(string: Localizable.shared.strings.new_notifications_text) detail.addAttribute(NSAttributedString.Key.font, value: BoldFont(size: 14) , range: NSRange(location: 0, length: Localizable.shared.strings.new_notifications_text.count)) @@ -46,14 +46,14 @@ class BMNotificationView: UIView { } } - private static func showAddress (notification: BMNotification, delegate:BMNotificationViewDelegate) { + private static func showAddress(notification: BMNotification, delegate:BMNotificationViewDelegate) { DispatchQueue.main.async { let view = BMNotificationView(title: Localizable.shared.strings.address_expired_notif, detail: nil, icon: IconNotifictionsExpired()?.maskWithColor(color: UIColor.main.marineOriginal), id: notification.pId, delegate: delegate) view.display() } } - private static func showVersion (notification: BMNotification, delegate:BMNotificationViewDelegate?) { + private static func showVersion(notification: BMNotification, delegate:BMNotificationViewDelegate?) { DispatchQueue.main.async { let detail = NSMutableAttributedString(string: Localizable.shared.strings.new_version_available_notif_detail) detail.addAttribute(NSAttributedString.Key.font, value: BoldFont(size: 14) , range: NSRange(location: 0, length: Localizable.shared.strings.new_version_available_notif_detail.count)) @@ -62,7 +62,7 @@ class BMNotificationView: UIView { } } - static func showTransaction (transaction: BMTransaction, delegate:BMNotificationViewDelegate?, delay:Double = 2.0) { + static func showTransaction(transaction: BMTransaction, delegate:BMNotificationViewDelegate?, delay:Double = 2.0) { DispatchQueue.main.async { if transaction.enumStatus == BMTransactionStatusInProgress && transaction.isDapps { return @@ -208,7 +208,7 @@ class BMNotificationView: UIView { } } - static func showTransaction (notification: BMNotification, delegate:BMNotificationViewDelegate) { + static func showTransaction(notification: BMNotification, delegate:BMNotificationViewDelegate) { DispatchQueue.main.async { if let transaction = AppModel.sharedManager().transaction(byId: notification.pId) { guard let asset = transaction.asset else { @@ -306,7 +306,7 @@ class BMNotificationView: UIView { } } - public static func show (notification: BMNotification, delegate:BMNotificationViewDelegate) { + public static func show(notification: BMNotification, delegate:BMNotificationViewDelegate) { if(BMNotificationView.notificationView == nil) { if(notification.type == ADDRESS) { showAddress(notification: notification, delegate: delegate) @@ -323,7 +323,7 @@ class BMNotificationView: UIView { } } - public static func show (title: String?, detail: NSMutableAttributedString?, icon: UIImage?, id: String, delegate:BMNotificationViewDelegate) { + public static func show(title: String?, detail: NSMutableAttributedString?, icon: UIImage?, id: String, delegate:BMNotificationViewDelegate) { DispatchQueue.main.async { if(BMNotificationView.notificationView == nil) { let view = BMNotificationView(title: title, detail: detail, icon: icon, id: id, delegate: delegate) @@ -376,7 +376,7 @@ class BMNotificationView: UIView { self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap))) self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onPan))) - BMNotificationView.notificationView = self; + BMNotificationView.notificationView = self } fileprivate func display(delay:Double = 2.0) { diff --git a/BeamWallet/Controls/BMOptionPickerViewController.swift b/BeamWallet/Controls/BMOptionPickerViewController.swift new file mode 100644 index 00000000..3c23ec50 --- /dev/null +++ b/BeamWallet/Controls/BMOptionPickerViewController.swift @@ -0,0 +1,210 @@ +// +// BMOptionPickerViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class BMOptionPickerViewController: UIViewController { + + struct Option { + let title: String + let value: Int + } + + private let titleText: String + private let options: [Option] + private let selectedValue: Int? + private let onSelect: (Option) -> Void + + private let backdrop = UIView() + private let card = UIView() + private let titleLabel = UILabel() + private let stack = UIStackView() + private let cancelButton = UIButton(type: .system) + private var cardBottomConstraint: NSLayoutConstraint! + + private let rowHeight: CGFloat = 56 + private let horizontalInset: CGFloat = 16 + private let cardCornerRadius: CGFloat = 16 + + init(title: String, options: [Option], selectedValue: Int?, onSelect: @escaping (Option) -> Void) { + self.titleText = title + self.options = options + self.selectedValue = selectedValue + self.onSelect = onSelect + super.init(nibName: nil, bundle: nil) + self.modalPresentationStyle = .overFullScreen + self.modalTransitionStyle = .crossDissolve + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear + + backdrop.backgroundColor = UIColor.black.withAlphaComponent(0.55) + backdrop.alpha = 0 + backdrop.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(backdrop) + + let tap = UITapGestureRecognizer(target: self, action: #selector(onCancel)) + backdrop.addGestureRecognizer(tap) + + card.backgroundColor = UIColor.main.peacockBlue + card.layer.cornerRadius = cardCornerRadius + card.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + card.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(card) + + titleLabel.text = titleText.uppercased() + titleLabel.font = BoldFont(size: 14) + titleLabel.textColor = UIColor.main.blueyGrey + titleLabel.letterSpacing = 2 + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + card.addSubview(titleLabel) + + stack.axis = .vertical + stack.spacing = 0 + stack.translatesAutoresizingMaskIntoConstraints = false + card.addSubview(stack) + + for (index, option) in options.enumerated() { + stack.addArrangedSubview(makeRow(for: option, index: index)) + if index < options.count - 1 { + stack.addArrangedSubview(makeSeparator()) + } + } + + cancelButton.setTitle(Localizable.shared.strings.cancel.lowercased(), for: .normal) + cancelButton.titleLabel?.font = BoldFont(size: 14) + cancelButton.setTitleColor(UIColor.white, for: .normal) + cancelButton.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted) + cancelButton.backgroundColor = UIColor.white.withAlphaComponent(0.08) + cancelButton.layer.cornerRadius = 12 + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.addTarget(self, action: #selector(onCancel), for: .touchUpInside) + card.addSubview(cancelButton) + + cardBottomConstraint = card.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 600) + + NSLayoutConstraint.activate([ + backdrop.topAnchor.constraint(equalTo: view.topAnchor), + backdrop.bottomAnchor.constraint(equalTo: view.bottomAnchor), + backdrop.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backdrop.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + card.leadingAnchor.constraint(equalTo: view.leadingAnchor), + card.trailingAnchor.constraint(equalTo: view.trailingAnchor), + cardBottomConstraint, + + titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: horizontalInset), + titleLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -horizontalInset), + + stack.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 14), + stack.leadingAnchor.constraint(equalTo: card.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: card.trailingAnchor), + + cancelButton.topAnchor.constraint(equalTo: stack.bottomAnchor, constant: 16), + cancelButton.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: horizontalInset), + cancelButton.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -horizontalInset), + cancelButton.heightAnchor.constraint(equalToConstant: 50), + cancelButton.bottomAnchor.constraint(equalTo: card.safeAreaLayoutGuide.bottomAnchor, constant: -16), + ]) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + view.layoutIfNeeded() + cardBottomConstraint.constant = 0 + UIView.animate(withDuration: 0.28, delay: 0, options: [.curveEaseOut], animations: { + self.backdrop.alpha = 1 + self.view.layoutIfNeeded() + }) + } + + private func makeRow(for option: Option, index: Int) -> UIControl { + let row = OptionRow() + row.translatesAutoresizingMaskIntoConstraints = false + row.heightAnchor.constraint(equalToConstant: rowHeight).isActive = true + row.tag = index + + let isSelected = (selectedValue != nil && option.value == selectedValue) + let label = UILabel() + label.text = option.title + label.font = isSelected ? BoldFont(size: 16) : RegularFont(size: 16) + label.textColor = isSelected ? UIColor.main.brightTeal : UIColor.white + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + row.addSubview(label) + + NSLayoutConstraint.activate([ + label.centerYAnchor.constraint(equalTo: row.centerYAnchor), + label.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: horizontalInset), + label.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -horizontalInset), + ]) + + row.addTarget(self, action: #selector(onRowTapped(_:)), for: .touchUpInside) + return row + } + + private func makeSeparator() -> UIView { + let line = UIView() + line.backgroundColor = UIColor.white.withAlphaComponent(0.08) + line.translatesAutoresizingMaskIntoConstraints = false + line.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale).isActive = true + return line + } + + @objc private func onRowTapped(_ sender: UIControl) { + let option = options[sender.tag] + dismissAnimated { [weak self] in + self?.onSelect(option) + } + } + + @objc private func onCancel() { + dismissAnimated(completion: nil) + } + + private func dismissAnimated(completion: (() -> Void)?) { + cardBottomConstraint.constant = card.bounds.height + UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseIn], animations: { + self.backdrop.alpha = 0 + self.view.layoutIfNeeded() + }, completion: { _ in + self.dismiss(animated: false, completion: completion) + }) + } +} + +private class OptionRow: UIControl { + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override var isHighlighted: Bool { + didSet { + backgroundColor = isHighlighted ? UIColor.white.withAlphaComponent(0.06) : .clear + } + } +} diff --git a/BeamWallet/Controls/BMOverlayTimerView.swift b/BeamWallet/Controls/BMOverlayTimerView.swift index 5a5a74e1..4ec4b9a9 100644 --- a/BeamWallet/Controls/BMOverlayTimerView.swift +++ b/BeamWallet/Controls/BMOverlayTimerView.swift @@ -2,7 +2,7 @@ // BMOverlayTimerView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ class BMOverlayTimerView: UIView, BMCountdownViewDelegate { private var seconds = 0 private var url:URL! - public static func show (text:String, link:URL) { + public static func show(text:String, link:URL) { let view = BMOverlayTimerView(text: text, link: link) view.display() } diff --git a/BeamWallet/Controls/BMPagingViewController.swift b/BeamWallet/Controls/BMPagingViewController.swift index 997c17c2..5f242733 100644 --- a/BeamWallet/Controls/BMPagingViewController.swift +++ b/BeamWallet/Controls/BMPagingViewController.swift @@ -2,7 +2,7 @@ // BMPagingViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMPopoverMenu.swift b/BeamWallet/Controls/BMPopoverMenu.swift index ee825144..74ece2d8 100644 --- a/BeamWallet/Controls/BMPopoverMenu.swift +++ b/BeamWallet/Controls/BMPopoverMenu.swift @@ -2,7 +2,7 @@ // BMPopoverMenu.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -53,10 +53,7 @@ extension BMPopoverMenu { rootVC.present(alert, animated: true) - if #available(iOS 13, *) { - - } - else { + if #unavailable(iOS 13) { if(Settings.sharedManager().isDarkMode) { alert.setBackgroundColor(color: UIColor.black) } @@ -106,8 +103,8 @@ class BMPopoverMenu: NSObject { var name: String var icon: String? var action: BMPopoverMenuItemAction - var selected:Bool? = nil - var id:Int? = nil + var selected:Bool? + var id:Int? init(name: String, icon: String?, action: BMPopoverMenuItemAction) { self.name = name @@ -180,7 +177,7 @@ class BMPopoverMenu: NSObject { animations: { self.popOverMenu.alpha = 0 self.popOverMenu.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) - }) { isFinished in + }, completion: { isFinished in if isFinished { self.backgroundView.removeFromSuperview() if selectedItem == nil { @@ -194,7 +191,7 @@ class BMPopoverMenu: NSObject { } } } - } + }) } fileprivate func showForSenderAssets(sender: UIView?, or senderFrame: CGRect?, with menuItems: [BMPopoverMenuItem]!, done: @escaping (BMPopoverMenuItem?) -> Void, cancel: (() -> Void)? = nil) { diff --git a/BeamWallet/Controls/BMRippleCell.swift b/BeamWallet/Controls/BMRippleCell.swift index 57fdb23a..4da148ff 100644 --- a/BeamWallet/Controls/BMRippleCell.swift +++ b/BeamWallet/Controls/BMRippleCell.swift @@ -2,7 +2,7 @@ // BMRippleCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMSearchView.swift b/BeamWallet/Controls/BMSearchView.swift index 210cbf59..54361c92 100644 --- a/BeamWallet/Controls/BMSearchView.swift +++ b/BeamWallet/Controls/BMSearchView.swift @@ -2,7 +2,7 @@ // BMSearchView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMSegmentView.swift b/BeamWallet/Controls/BMSegmentView.swift index b314cb0b..7e8ec6fc 100644 --- a/BeamWallet/Controls/BMSegmentView.swift +++ b/BeamWallet/Controls/BMSegmentView.swift @@ -2,7 +2,7 @@ // BMSegmentView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMSlider.swift b/BeamWallet/Controls/BMSlider.swift index d2589b27..e11fc9a0 100644 --- a/BeamWallet/Controls/BMSlider.swift +++ b/BeamWallet/Controls/BMSlider.swift @@ -2,7 +2,7 @@ // BMSlider.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMSnackBar.swift b/BeamWallet/Controls/BMSnackBar.swift index 863cc787..629640bf 100644 --- a/BeamWallet/Controls/BMSnackBar.swift +++ b/BeamWallet/Controls/BMSnackBar.swift @@ -2,7 +2,7 @@ // BMSnackBar.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,12 +32,6 @@ class BMSnackBar: UIView, BMCountdownViewDelegate { var type:SnackType! var id:String! var title:String! - - init(type:SnackType!, id:String!, title:String!) { - self.type = type - self.id = id - self.title = title - } } private var data:SnackData! @@ -129,7 +123,7 @@ extension BMSnackBar { private static var snack: BMSnackBar? - public static func show (data:SnackData!, done: @escaping (SnackData?) -> Void, ended: @escaping (SnackData?) -> Void) { + public static func show(data:SnackData!, done: @escaping (SnackData?) -> Void, ended: @escaping (SnackData?) -> Void) { if snack != nil && snack?.data.id == data.id { return diff --git a/BeamWallet/Controls/BMStepView.swift b/BeamWallet/Controls/BMStepView.swift index c6d18ccc..b2390e32 100644 --- a/BeamWallet/Controls/BMStepView.swift +++ b/BeamWallet/Controls/BMStepView.swift @@ -3,7 +3,7 @@ // BeamWallet // // 3/1/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ class BMStepView: UIView { super.layoutSubviews() let separate:CGFloat = 5.0 - let stepWidth = round((self.frame.size.width - separate * CGFloat(totalStep)) / CGFloat(totalStep)); + let stepWidth = round((self.frame.size.width - separate * CGFloat(totalStep)) / CGFloat(totalStep)) if views.count > 0 { @@ -49,13 +49,13 @@ class BMStepView: UIView { if view.tag < currentStep { UIView.animate(withDuration: 0.2) { view.layer.borderColor = UIColor.clear.cgColor - view.backgroundColor = self.finishedStepColor; + view.backgroundColor = self.finishedStepColor } } else{ UIView.animate(withDuration: 0.2) { view.layer.borderColor = UIColor.main.darkSlateBlue.cgColor - view.backgroundColor = UIColor.clear; + view.backgroundColor = UIColor.clear } } } @@ -65,8 +65,8 @@ class BMStepView: UIView { let view = UIView(frame: CGRect(x: CGFloat(i) * (stepWidth + separate), y: 0, width: stepWidth, height: self.frame.size.height)) view.layer.cornerRadius = view.frame.size.height/2 view.layer.borderColor = UIColor.main.darkSlateBlue.cgColor - view.backgroundColor = UIColor.clear; - view.layer.borderWidth = 1; + view.backgroundColor = UIColor.clear + view.layer.borderWidth = 1 view.tag = i views.append(view) self.addSubview(view) diff --git a/BeamWallet/Controls/BMTableHeaderTitleView.swift b/BeamWallet/Controls/BMTableHeaderTitleView.swift index 13935898..eef02511 100644 --- a/BeamWallet/Controls/BMTableHeaderTitleView.swift +++ b/BeamWallet/Controls/BMTableHeaderTitleView.swift @@ -2,7 +2,7 @@ // BMTableHeaderTitleView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMTextView.swift b/BeamWallet/Controls/BMTextView.swift index 768194de..ab94c697 100644 --- a/BeamWallet/Controls/BMTextView.swift +++ b/BeamWallet/Controls/BMTextView.swift @@ -3,7 +3,7 @@ // BeamWallet // // 3/1/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMToast.swift b/BeamWallet/Controls/BMToast.swift index b4e1391d..9e9d546b 100644 --- a/BeamWallet/Controls/BMToast.swift +++ b/BeamWallet/Controls/BMToast.swift @@ -2,7 +2,7 @@ // BMToast.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/BMWordField.swift b/BeamWallet/Controls/BMWordField.swift index 9a145ba3..bc56d23d 100644 --- a/BeamWallet/Controls/BMWordField.swift +++ b/BeamWallet/Controls/BMWordField.swift @@ -3,7 +3,7 @@ // BeamWallet // // 3/1/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -43,15 +43,12 @@ class BMWordField: BMField { case .empty?: self.status = .normal self.textColor = UIColor.white - break case .error?: self.status = .error self.textColor = errorColor - break case .correct?: self.status = .normal self.textColor = UIColor.white - break case .none: break } @@ -73,8 +70,8 @@ class BMWordField: BMField { accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44)) let toolbar = UIToolbar(frame: accessoryView.bounds) - toolbar.autoresizingMask = .flexibleWidth; - toolbar.isUserInteractionEnabled = false; + toolbar.autoresizingMask = .flexibleWidth + toolbar.isUserInteractionEnabled = false accessoryView.addSubview(toolbar) let width_3 = toolbar.frame.size.width / CGFloat(maxWords) @@ -90,7 +87,7 @@ class BMWordField: BMField { } let separator = UIView(frame: CGRect(x:0, y:43.5, width:accessoryView.frame.size.width, height:0.5)) - separator.autoresizingMask = .flexibleWidth; + separator.autoresizingMask = .flexibleWidth separator.backgroundColor = UIColor.init(white: 0, alpha: 0.2) accessoryView.addSubview(separator) @@ -143,17 +140,15 @@ class BMWordField: BMField { if !recommendFirstWord && words.count != 0 && words[0].hasPrefix(prefix) { - var hasPrefix = false; + var hasPrefix = false - for i in 1...words.count-1 { - if words[i].hasPrefix(prefix) { - hasPrefix = true; - break - } + for i in 1...words.count-1 where words[i].hasPrefix(prefix) { + hasPrefix = true + break } if !hasPrefix { - recommendFirstWord = true; + recommendFirstWord = true } } @@ -189,13 +184,13 @@ class BMWordField: BMField { } if recommendFirstWord { - let button = accessoryOptions[0]; + let button = accessoryOptions[0] button.layer.removeAllAnimations() let animate = CABasicAnimation(keyPath: "backgroundColor") animate.fromValue = UIColor.black.cgColor animate.toValue = UIColor.clear.cgColor - animate.duration = 1.0; + animate.duration = 1.0 button.layer.add(animate, forKey: "recommend") } diff --git a/BeamWallet/Controls/Cell/BMAddressCell.swift b/BeamWallet/Controls/Cell/BMAddressCell.swift index c668db0e..464b991f 100644 --- a/BeamWallet/Controls/Cell/BMAddressCell.swift +++ b/BeamWallet/Controls/Cell/BMAddressCell.swift @@ -2,7 +2,7 @@ // BMAddressCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -113,5 +113,15 @@ extension BMAddressCell: Configurable { } expiredLabel.isHidden = true + + if !options.address.isContact { + let walletId = options.address.walletId + AppModel.sharedManager().requestOfflinePaymentsCount(forWalletId: walletId) + let count = AppModel.sharedManager().offlinePaymentsCount(forWalletId: walletId) + if count > 0 { + expiredLabel.text = String(format: Localizable.shared.strings.offline_left_address, count) + expiredLabel.isHidden = false + } + } } } diff --git a/BeamWallet/Controls/Cell/BMAmountCell.swift b/BeamWallet/Controls/Cell/BMAmountCell.swift index 62cc17a9..d67ab8b1 100644 --- a/BeamWallet/Controls/Cell/BMAmountCell.swift +++ b/BeamWallet/Controls/Cell/BMAmountCell.swift @@ -2,7 +2,7 @@ // BMAmountCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ class BMAmountCell: BaseCell { } } - public var hideNameLabel:Bool? = nil + public var hideNameLabel:Bool? { didSet { if let hide = hideNameLabel { @@ -78,37 +78,45 @@ class BMAmountCell: BaseCell { } } + public var allowAllAssets: Bool = false { + didSet { updateCurrencyInteraction() } + } + + private func updateCurrencyInteraction() { + guard selectedAssetId != nil else { return } + if allowAllAssets || AssetsManager.shared().getAssetsWithBalance().count >= 1 { + currencyView.isUserInteractionEnabled = true + currencyArrow.isHidden = false + } else { + currencyView.isUserInteractionEnabled = false + currencyArrow.isHidden = true + } + } + public var selectedAssetId:Int? { didSet{ - + if selectedAssetId != nil { let asset = AssetsManager.shared().getAsset(Int32(selectedAssetId ?? 0)) currencyLabel.letterSpacing = 2 - + let id = "(\(selectedAssetId ?? 0))" let text = asset?.unitName ?? "" let fullString = text + " " + id - + let attributedString = NSMutableAttributedString(string: fullString) let range = (fullString as NSString).range(of: id) attributedString.addAttribute(.foregroundColor, value: UIColor.white.withAlphaComponent(0.5), range: range) - + currencyLabel.attributedText = attributedString - + if let asset = asset { currencyIcon.setAsset(asset) } - - if AssetsManager.shared().getAssetsWithBalance().count >= 1 { - currencyView.isUserInteractionEnabled = true - currencyArrow.isHidden = false - } - else { - currencyView.isUserInteractionEnabled = false - currencyArrow.isHidden = true - } + + updateCurrencyInteraction() } else { currencyView.isUserInteractionEnabled = false @@ -164,8 +172,8 @@ class BMAmountCell: BaseCell { if Settings.sharedManager().isDarkMode { secondCurrencyLabel.textColor = UIColor.main.steel - nameLabel.textColor = UIColor.main.steel; - maxAmountErrorLabel.textColor = UIColor.main.steel; + nameLabel.textColor = UIColor.main.steel + maxAmountErrorLabel.textColor = UIColor.main.steel } } @@ -215,7 +223,6 @@ extension BMAmountCell: Configurable { case 1: textField.textColor = UIColor.main.brightTeal textField.setNormalColor(color: UIColor.main.brightTeal) - break default: return } diff --git a/BeamWallet/Controls/Cell/BMCellProtocol.swift b/BeamWallet/Controls/Cell/BMCellProtocol.swift index 1265e477..a5f36e4e 100644 --- a/BeamWallet/Controls/Cell/BMCellProtocol.swift +++ b/BeamWallet/Controls/Cell/BMCellProtocol.swift @@ -2,7 +2,7 @@ // BMCellProtocol.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ protocol GeneralInfoCellDelegate: AnyObject { @objc optional func onClickShare() @objc optional func onClickSave() @objc optional func onClickCopy() + @objc optional func onClickCopyAndClose() @objc optional func onRightButton(_ sender: UITableViewCell) diff --git a/BeamWallet/Controls/Cell/BMCopyCell.swift b/BeamWallet/Controls/Cell/BMCopyCell.swift index 05b045fa..4e90d6ac 100644 --- a/BeamWallet/Controls/Cell/BMCopyCell.swift +++ b/BeamWallet/Controls/Cell/BMCopyCell.swift @@ -2,7 +2,7 @@ // BMMultiLinesCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/Cell/BMDetailAmountCell.swift b/BeamWallet/Controls/Cell/BMDetailAmountCell.swift index 74b09b8e..a27e6609 100644 --- a/BeamWallet/Controls/Cell/BMDetailAmountCell.swift +++ b/BeamWallet/Controls/Cell/BMDetailAmountCell.swift @@ -2,7 +2,7 @@ // BMMultiLinesCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/Cell/BMDetailCell.swift b/BeamWallet/Controls/Cell/BMDetailCell.swift index b19cd8bf..2aae08aa 100644 --- a/BeamWallet/Controls/Cell/BMDetailCell.swift +++ b/BeamWallet/Controls/Cell/BMDetailCell.swift @@ -2,7 +2,7 @@ // BMDetailCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class BMDetailCell: BaseCell { arrowIcon.highlightedImage = IconNextArrow()?.maskWithColor(color: UIColor.main.steelGrey) if Settings.sharedManager().isDarkMode { - nameLabel.textColor = UIColor.main.steel; + nameLabel.textColor = UIColor.main.steel } } diff --git a/BeamWallet/Controls/Cell/BMEmptyCell.swift b/BeamWallet/Controls/Cell/BMEmptyCell.swift index 7db57613..64a9a1b0 100644 --- a/BeamWallet/Controls/Cell/BMEmptyCell.swift +++ b/BeamWallet/Controls/Cell/BMEmptyCell.swift @@ -2,7 +2,7 @@ // BMEmptyCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/Cell/BMExpandCell.swift b/BeamWallet/Controls/Cell/BMExpandCell.swift index f3c676a1..e32e3722 100644 --- a/BeamWallet/Controls/Cell/BMExpandCell.swift +++ b/BeamWallet/Controls/Cell/BMExpandCell.swift @@ -2,7 +2,7 @@ // BMExpandCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ class BMExpandCell: BaseCell { selectedBackgroundView = selectedView if Settings.sharedManager().isDarkMode { - nameLabel.textColor = UIColor.main.steel; + nameLabel.textColor = UIColor.main.steel } } diff --git a/BeamWallet/Controls/Cell/BMFieldCell.swift b/BeamWallet/Controls/Cell/BMFieldCell.swift index f9f1421c..d81b9df0 100644 --- a/BeamWallet/Controls/Cell/BMFieldCell.swift +++ b/BeamWallet/Controls/Cell/BMFieldCell.swift @@ -2,7 +2,7 @@ // BMFieldCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -47,15 +47,15 @@ class BMFieldCell: BaseCell { } } - public var titleTextColor: UIColor? = nil { + public var titleTextColor: UIColor? { didSet { if let color = titleTextColor { - nameLabel.textColor = color; + nameLabel.textColor = color } } } - public var hideNameLabel:Bool? = nil + public var hideNameLabel:Bool? { didSet { if let hide = hideNameLabel { @@ -67,9 +67,15 @@ class BMFieldCell: BaseCell { } } + override func prepareForReuse() { + super.prepareForReuse() + copyText = nil + keyboardType = .default + } + override func awakeFromNib() { super.awakeFromNib() - + selectionStyle = .none nameLabel.isUserInteractionEnabled = true @@ -81,7 +87,7 @@ class BMFieldCell: BaseCell { // textField.placeHolderColor = UIColor.white.withAlphaComponent(0.2) if Settings.sharedManager().isDarkMode { - nameLabel.textColor = UIColor.main.steel; + nameLabel.textColor = UIColor.main.steel } } @@ -131,6 +137,15 @@ class BMFieldCell: BaseCell { } } } + + public var keyboardType: UIKeyboardType { + get { return textField.keyboardType } + set { textField.keyboardType = newValue } + } + + public func setText(_ value: String?) { + textField.text = value + } } extension BMFieldCell : UITextFieldDelegate { @@ -142,13 +157,10 @@ extension BMFieldCell : UITextFieldDelegate { } func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - textField.inputAccessoryView = nil - if let copy = copyText { let inputBar = BMInputCopyBar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44), copy:copy) - - inputBar.completion = { - (obj : String?) -> Void in + + inputBar.completion = { (obj : String?) in if let text = obj { self.textField.text = text self.delegate?.textValueDidChange?(self, text, false) @@ -157,6 +169,9 @@ extension BMFieldCell : UITextFieldDelegate { } textField.inputAccessoryView = inputBar } + else if textField.keyboardType != .numberPad && textField.keyboardType != .decimalPad && textField.returnKeyType != .next { + textField.inputAccessoryView = nil + } return true } diff --git a/BeamWallet/Controls/Cell/BMGroupedCell.swift b/BeamWallet/Controls/Cell/BMGroupedCell.swift index 2df1222a..a927a1f3 100644 --- a/BeamWallet/Controls/Cell/BMGroupedCell.swift +++ b/BeamWallet/Controls/Cell/BMGroupedCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 7/19/19. -// Copyright © 2019 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit diff --git a/BeamWallet/Controls/Cell/BMMultiLinesCell.swift b/BeamWallet/Controls/Cell/BMMultiLinesCell.swift index ad5fb92b..42015f7b 100644 --- a/BeamWallet/Controls/Cell/BMMultiLinesCell.swift +++ b/BeamWallet/Controls/Cell/BMMultiLinesCell.swift @@ -2,7 +2,7 @@ // BMMultiLinesCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -225,10 +225,10 @@ extension BMMultiLinesCell: Configurable { let imageString = NSAttributedString(attachment: imageAttachment) let nameString = NSMutableAttributedString(string:address.label) - nameString.addAttribute(NSAttributedString.Key.font, value: BoldFont(size: 16 + fontSizeOffset), range: NSMakeRange(0, nameString.string.count)) + nameString.addAttribute(NSAttributedString.Key.font, value: BoldFont(size: 16 + fontSizeOffset), range: NSRange(location: 0, length: nameString.string.count)) let detailString = NSMutableAttributedString(string:item.detail!) - detailString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style2, range: NSMakeRange(0, detailString.string.count)) + detailString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style2, range: NSRange(location: 0, length: detailString.string.count)) attributedString.append(imageString) attributedString.append(NSAttributedString(string: " ")) @@ -236,7 +236,7 @@ extension BMMultiLinesCell: Configurable { attributedString.append(NSAttributedString(string: "\n")) attributedString.append(detailString) - attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSMakeRange(0, nameString.string.count)) + attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: nameString.string.count)) copyButton.imageEdgeInsets = UIEdgeInsets(top: 30, left: 0, bottom: 0, right: 0) } diff --git a/BeamWallet/Controls/Cell/BMMultiLinesCell2.swift b/BeamWallet/Controls/Cell/BMMultiLinesCell2.swift index 8c4d66fb..4b63dfba 100644 --- a/BeamWallet/Controls/Cell/BMMultiLinesCell2.swift +++ b/BeamWallet/Controls/Cell/BMMultiLinesCell2.swift @@ -2,7 +2,7 @@ // BMMultiLinesCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/Cell/BMPickedAddressCell.swift b/BeamWallet/Controls/Cell/BMPickedAddressCell.swift index 0980d954..c0369e74 100644 --- a/BeamWallet/Controls/Cell/BMPickedAddressCell.swift +++ b/BeamWallet/Controls/Cell/BMPickedAddressCell.swift @@ -2,7 +2,7 @@ // BMPickedAddressCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ class BMPickedAddressCell: BaseCell { selectionStyle = .none if Settings.sharedManager().isDarkMode { - nameLabel.textColor = UIColor.main.steel; + nameLabel.textColor = UIColor.main.steel } } diff --git a/BeamWallet/Controls/Cell/BMPickerCell.swift b/BeamWallet/Controls/Cell/BMPickerCell.swift index 9ba96027..95220281 100644 --- a/BeamWallet/Controls/Cell/BMPickerCell.swift +++ b/BeamWallet/Controls/Cell/BMPickerCell.swift @@ -2,7 +2,7 @@ // BMPickerCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/Cell/BMThreeLineCell.swift b/BeamWallet/Controls/Cell/BMThreeLineCell.swift index 96acd395..33df6140 100644 --- a/BeamWallet/Controls/Cell/BMThreeLineCell.swift +++ b/BeamWallet/Controls/Cell/BMThreeLineCell.swift @@ -2,7 +2,7 @@ // BMThreeLineCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/Cell/PopoverAssetCell.swift b/BeamWallet/Controls/Cell/PopoverAssetCell.swift index 713a57f0..97a1e6f6 100644 --- a/BeamWallet/Controls/Cell/PopoverAssetCell.swift +++ b/BeamWallet/Controls/Cell/PopoverAssetCell.swift @@ -2,7 +2,7 @@ // PopoverAssetCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,15 +23,4 @@ class PopoverAssetCell: UITableViewCell { @IBOutlet weak var label:UILabel! @IBOutlet weak var iconView:AssetIconView! - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - } diff --git a/BeamWallet/Controls/Cell/PopoverCell.swift b/BeamWallet/Controls/Cell/PopoverCell.swift index 6c0aee37..f494a921 100644 --- a/BeamWallet/Controls/Cell/PopoverCell.swift +++ b/BeamWallet/Controls/Cell/PopoverCell.swift @@ -2,7 +2,7 @@ // PopoverCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,17 +23,5 @@ class PopoverCell: UITableViewCell { @IBOutlet weak var label:UILabel! @IBOutlet weak var iconView:UIImageView! - - - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } } diff --git a/BeamWallet/Controls/Cell/SingleCenterCell.swift b/BeamWallet/Controls/Cell/SingleCenterCell.swift index edf36a24..bb318231 100644 --- a/BeamWallet/Controls/Cell/SingleCenterCell.swift +++ b/BeamWallet/Controls/Cell/SingleCenterCell.swift @@ -2,7 +2,7 @@ // SingleCenterCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,11 +31,5 @@ class SingleCenterCell: UITableViewCell { } selectionStyle = .none } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } } diff --git a/BeamWallet/Controls/NonDisappearingView.swift b/BeamWallet/Controls/NonDisappearingView.swift index 2f34ac7d..6b000a79 100644 --- a/BeamWallet/Controls/NonDisappearingView.swift +++ b/BeamWallet/Controls/NonDisappearingView.swift @@ -2,7 +2,7 @@ // NonDisappearingView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Controls/QRCodeView.swift b/BeamWallet/Controls/QRCodeView.swift index b046a595..d07fdb8a 100644 --- a/BeamWallet/Controls/QRCodeView.swift +++ b/BeamWallet/Controls/QRCodeView.swift @@ -2,7 +2,7 @@ // QRCodeView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/AlertController.swift b/BeamWallet/Extensions/AlertController.swift index b9114952..7b45a16d 100644 --- a/BeamWallet/Extensions/AlertController.swift +++ b/BeamWallet/Extensions/AlertController.swift @@ -2,7 +2,7 @@ // AlertController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Application.swift b/BeamWallet/Extensions/Application.swift index 769ee015..b6d6c1c9 100644 --- a/BeamWallet/Extensions/Application.swift +++ b/BeamWallet/Extensions/Application.swift @@ -2,7 +2,7 @@ // Application.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Array.swift b/BeamWallet/Extensions/Array.swift index dee0e5b2..2be2419e 100644 --- a/BeamWallet/Extensions/Array.swift +++ b/BeamWallet/Extensions/Array.swift @@ -2,7 +2,7 @@ // Array.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import Foundation -func rearrange(array: Array, fromIndex: Int, toIndex: Int) -> Array{ +func rearrange(array: [T], fromIndex: Int, toIndex: Int) -> [T]{ var arr = array let element = arr.remove(at: fromIndex) arr.insert(element, at: toIndex) diff --git a/BeamWallet/Extensions/BackButtonItem.swift b/BeamWallet/Extensions/BackButtonItem.swift index 350466e6..af326b33 100644 --- a/BeamWallet/Extensions/BackButtonItem.swift +++ b/BeamWallet/Extensions/BackButtonItem.swift @@ -2,7 +2,7 @@ // BackButtonItem.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Button.swift b/BeamWallet/Extensions/Button.swift index fc080084..16e0c2b6 100644 --- a/BeamWallet/Extensions/Button.swift +++ b/BeamWallet/Extensions/Button.swift @@ -2,7 +2,7 @@ // Button.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Color.swift b/BeamWallet/Extensions/Color.swift index 7c48b19d..baacfd01 100644 --- a/BeamWallet/Extensions/Color.swift +++ b/BeamWallet/Extensions/Color.swift @@ -2,7 +2,7 @@ // Color.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Device.swift b/BeamWallet/Extensions/Device.swift index 476210c7..81be1cea 100644 --- a/BeamWallet/Extensions/Device.swift +++ b/BeamWallet/Extensions/Device.swift @@ -2,7 +2,7 @@ // Device.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Double.swift b/BeamWallet/Extensions/Double.swift index fce88b2f..6648d6b6 100644 --- a/BeamWallet/Extensions/Double.swift +++ b/BeamWallet/Extensions/Double.swift @@ -2,7 +2,7 @@ // Double.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Field.swift b/BeamWallet/Extensions/Field.swift index 0f546a6b..8a2f029e 100644 --- a/BeamWallet/Extensions/Field.swift +++ b/BeamWallet/Extensions/Field.swift @@ -3,7 +3,7 @@ // BeamWallet // // 3/2/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import Foundation import UIKit -//extension UITextField { +// extension UITextField { // // var clearButtonTintColor: UIColor? { // get { @@ -30,11 +30,11 @@ import UIKit // set { // } // } -//} +// } extension UITextField { - public func disablePasswordAutoFill () { + public func disablePasswordAutoFill() { if #available(iOS 12, *) { // iOS 12: Not the best solution, but it works. self.textContentType = .oneTimeCode @@ -69,7 +69,7 @@ extension UITextField { } } - @IBInspectable var placeHolderFont: UIFont? { + var placeHolderFont: UIFont? { get { return self.placeHolderFont } diff --git a/BeamWallet/Extensions/Image.swift b/BeamWallet/Extensions/Image.swift index 281ca424..e0bce301 100644 --- a/BeamWallet/Extensions/Image.swift +++ b/BeamWallet/Extensions/Image.swift @@ -2,7 +2,7 @@ // Image.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/Label.swift b/BeamWallet/Extensions/Label.swift index 7b741c47..6b490fbd 100644 --- a/BeamWallet/Extensions/Label.swift +++ b/BeamWallet/Extensions/Label.swift @@ -3,7 +3,7 @@ // BeamWallet // // 3/2/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Extensions/String.swift b/BeamWallet/Extensions/String.swift index 23979a1e..5d133e1f 100644 --- a/BeamWallet/Extensions/String.swift +++ b/BeamWallet/Extensions/String.swift @@ -2,7 +2,7 @@ // String.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -210,9 +210,9 @@ extension String { func isValidIp() -> Bool { if #available(iOS 12.0, *) { - if let _ = IPv4Address(self) { + if IPv4Address(self) != nil { return true - } else if let _ = IPv6Address(self) { + } else if IPv6Address(self) != nil { return true } else { return false diff --git a/BeamWallet/Extensions/TableView.swift b/BeamWallet/Extensions/TableView.swift index 8518aef7..74fc70c9 100644 --- a/BeamWallet/Extensions/TableView.swift +++ b/BeamWallet/Extensions/TableView.swift @@ -2,7 +2,7 @@ // TableView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ extension UITableView { extension UITableView { - func performUpdate(_ update: ()->Void, completion: (()->Void)?) { + func performUpdate(_ update: () -> Void, completion: (() -> Void)?) { CATransaction.begin() CATransaction.setCompletionBlock(completion) @@ -68,35 +68,28 @@ extension UITableView { } public func findPath(_ row:AnyClass) -> IndexPath? { - for cell in visibleCells { - if cell.isKind(of: row) { - return self.indexPath(for: cell) - } + for cell in visibleCells where cell.isKind(of: row) { + return self.indexPath(for: cell) } return nil } - + public func findCell(_ row:AnyClass) -> UITableViewCell? { - for cell in visibleCells { - if cell.isKind(of: row) { - return cell - } + for cell in visibleCells where cell.isKind(of: row) { + return cell } return nil } - + public func reloadRow(_ row: AnyClass, animated:Bool = true) { - - for cell in visibleCells { - - if cell.isKind(of: row) { - - if let path = indexPath(for: cell) { - - reloadRows(at: [path], with: (animated ? .fade : .none)) - - return - } + + for cell in visibleCells where cell.isKind(of: row) { + + if let path = indexPath(for: cell) { + + reloadRows(at: [path], with: (animated ? .fade : .none)) + + return } } @@ -147,7 +140,7 @@ extension UICollectionView { extension UITableView { func rowsHeight() -> CGFloat { - var cellsHeight:CGFloat = 0; + var cellsHeight:CGFloat = 0 let sections = numberOfSections for section in 0.. Void)? = nil) { - if (self.presentedViewController as? UIAlertController) != nil { - return - } - - self.addBlur() - - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - if Settings.sharedManager().isDarkMode { - alertController.setBackgroundColor(color: UIColor.main.twilightBlue2) - alertController.setValue(NSAttributedString(string: title, attributes: [ - NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedTitle") - alertController.setValue(NSAttributedString(string: message, attributes: [ - NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedMessage") - } - let OKAction = UIAlertAction(title: button, style: .default) { action in - if handler != nil { - handler!(action) - } - self.view.viewWithTag(BLUR_TAG)?.removeFromSuperview() - } - alertController.addAction(OKAction) - self.present(alertController, animated: true, completion: nil) + BMAlertViewController.present( + from: self, + title: title, + message: message, + actions: [makeAlertAction(title: button, isConfirm: true, handler: handler)] + ) } - + func alert(title: String = "", message: String, handler: ((UIAlertAction) -> Void)? = nil) { - if (self.presentedViewController as? UIAlertController) != nil { - return - } - - self.addBlur() - - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - if Settings.sharedManager().isDarkMode { - alertController.setBackgroundColor(color: UIColor.main.twilightBlue2) - alertController.setValue(NSAttributedString(string: title, attributes: [ - NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedTitle") - alertController.setValue(NSAttributedString(string: message, attributes: [ - NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedMessage") - } - let OKAction = UIAlertAction(title: "OK", style: .default) { action in - if handler != nil { - handler!(action) - } - self.view.viewWithTag(BLUR_TAG)?.removeFromSuperview() - } - alertController.addAction(OKAction) - self.present(alertController, animated: true, completion: nil) + alert(title: title, message: message, button: "OK", handler: handler) } - + + // `cancelTitle` is a meaningful choice (e.g. "Don't save"), not a dismiss — it stays `.default` + // styled. The third action is the actual dismiss/cancel. func confirmAndSkipAlert(title: String, message: String, cancelTitle: String, confirmTitle: String, cancelHandler: @escaping ((UIAlertAction) -> Void), confirmHandler: @escaping ((UIAlertAction) -> Void)) { - if (self.presentedViewController as? UIAlertController) != nil { - return - } - - self.addBlur() - - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - if Settings.sharedManager().isDarkMode { - alertController.setBackgroundColor(color: UIColor.main.twilightBlue2) - alertController.setValue(NSAttributedString(string: title, attributes: [ - NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedTitle") - alertController.setValue(NSAttributedString(string: message, attributes: [ - NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedMessage") - } - let confirmAction = UIAlertAction(title: confirmTitle, style: .default) { action in - confirmHandler(action) - self.view.viewWithTag(BLUR_TAG)?.removeFromSuperview() - } - alertController.addAction(confirmAction) - - let cancelAction = UIAlertAction(title: cancelTitle, style: .default) { action in - cancelHandler(action) - self.view.viewWithTag(BLUR_TAG)?.removeFromSuperview() - } - alertController.addAction(cancelAction) - - let skipAction = UIAlertAction(title: Localizable.shared.strings.cancel, style: .default) { _ in - self.view.viewWithTag(BLUR_TAG)?.removeFromSuperview() - } - alertController.addAction(skipAction) - - alertController.preferredAction = confirmAction - - self.present(alertController, animated: true, completion: nil) + BMAlertViewController.present( + from: self, + title: title, + message: message, + actions: [ + makeAlertAction(title: confirmTitle, isConfirm: true, handler: confirmHandler), + makeAlertAction(title: cancelTitle, isConfirm: true, handler: cancelHandler), + BMAlertViewController.Action(title: Localizable.shared.strings.cancel, style: .cancel, handler: nil) + ] + ) } - + func confirmAlert(title: String, message: String, cancelTitle: String, confirmTitle: String, cancelHandler: @escaping ((UIAlertAction) -> Void), confirmHandler: @escaping ((UIAlertAction) -> Void)) { - if (self.presentedViewController as? UIAlertController) != nil { - return - } - - self.addBlur() - - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: cancelTitle, style: .default) { action in - cancelHandler(action) - self.view.viewWithTag(BLUR_TAG)?.removeFromSuperview() - } - alertController.addAction(cancelAction) - - let confirmAction = UIAlertAction(title: confirmTitle, style: (confirmTitle == Localizable.shared.strings.delete ? .destructive : .default)) { action in - confirmHandler(action) - self.view.viewWithTag(BLUR_TAG)?.removeFromSuperview() - } - alertController.addAction(confirmAction) - alertController.preferredAction = confirmAction - - self.present(alertController, animated: true, completion: nil) - - if #available(iOS 13, *) { + BMAlertViewController.present( + from: self, + title: title, + message: message, + actions: [ + makeAlertAction(title: confirmTitle, isConfirm: true, handler: confirmHandler), + makeAlertAction(title: cancelTitle, isConfirm: false, handler: cancelHandler) + ] + ) + } - } - else { - if Settings.sharedManager().isDarkMode { - alertController.setBackgroundColor(color: UIColor.main.twilightBlue2) - alertController.setValue(NSAttributedString(string: title, attributes: [ - NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedTitle") - alertController.setValue(NSAttributedString(string: message, attributes: [ - NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15), - NSAttributedString.Key.foregroundColor: UIColor.white - ]), forKey: "attributedMessage") - } - } + private func makeAlertAction(title: String, isConfirm: Bool, handler: ((UIAlertAction) -> Void)?) -> BMAlertViewController.Action { + let style: BMAlertViewController.ActionStyle + if !isConfirm { + style = .cancel + } else if BMAlertViewController.isDestructiveTitle(title) { + style = .destructive + } else { + style = .default + } + let bridged: (() -> Void)? = handler.map { cb in + { cb(UIAlertAction(title: title, style: .default, handler: nil)) } + } + return BMAlertViewController.Action(title: title, style: style, handler: bridged) + } +} + +extension BMAlertViewController { + static func isDestructiveTitle(_ title: String) -> Bool { + let destructive: [String] = [ + Localizable.shared.strings.delete, + Localizable.shared.strings.remove_wallet, + Localizable.shared.strings.dapps_uninstall, + Localizable.shared.strings.asset_swap_cancel_order + ] + return destructive.contains { $0.caseInsensitiveCompare(title) == .orderedSame } } } @@ -244,7 +171,7 @@ extension UIViewController { func hideKeyboardWhenTappedAround() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.hideKeyboard)) - tapGesture.cancelsTouchesInView = true + tapGesture.cancelsTouchesInView = false view.addGestureRecognizer(tapGesture) } diff --git a/BeamWallet/Manager/AppStoreReviewManager.swift b/BeamWallet/Manager/AppStoreReviewManager.swift index 8b830f42..1aa17a26 100644 --- a/BeamWallet/Manager/AppStoreReviewManager.swift +++ b/BeamWallet/Manager/AppStoreReviewManager.swift @@ -2,7 +2,7 @@ // AppStoreReviewManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Manager/BackgroundDownloader.swift b/BeamWallet/Manager/BackgroundDownloader.swift index ba878eb9..f58c8a84 100644 --- a/BeamWallet/Manager/BackgroundDownloader.swift +++ b/BeamWallet/Manager/BackgroundDownloader.swift @@ -2,7 +2,7 @@ // BackgroundDownloader.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import Foundation class BackgroundDownloader: NSObject { - typealias ProgressHandler = (Float?,Error?, String?, String?) -> () + typealias ProgressHandler = (Float?,Error?, String?, String?) -> Void var onProgress : ProgressHandler? @@ -32,10 +32,10 @@ class BackgroundDownloader: NSObject { private var destinationURLForFile:URL! private var task:URLSessionDownloadTask? - private var start = Date.timeIntervalSinceReferenceDate; + private var start = Date.timeIntervalSinceReferenceDate public func startDownloading(_ url:URL, _ destinationUrl:URL) { - + if FileManager.default.fileExists(atPath: destinationUrl.path) { do { @@ -45,9 +45,9 @@ class BackgroundDownloader: NSObject { print(error) } } - + self.destinationURLForFile = destinationUrl - + let config = URLSessionConfiguration.background(withIdentifier: "com.beam.background" + url.lastPathComponent) let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue()) @@ -68,9 +68,9 @@ extension BackgroundDownloader: URLSessionTaskDelegate, URLSessionDownloadDelega func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { if totalBytesExpectedToWrite > 0 { - + let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) - + avgTime = avgTime + 1 let speed = Double(totalBytesWritten) / Double((Date.timeIntervalSinceReferenceDate - self.start)) @@ -99,16 +99,16 @@ extension BackgroundDownloader: URLSessionTaskDelegate, URLSessionDownloadDelega func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { print("Download finished: \(location)") - + do { try FileManager.default.moveItem(at: location, to: destinationURLForFile) onProgress?(nil, nil, destinationURLForFile.path, nil) - + }catch{ onProgress?(nil, error, nil, nil) } } - + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let reason = error { let code = (reason as NSError).code diff --git a/BeamWallet/Manager/BiometricAuthorization.swift b/BeamWallet/Manager/BiometricAuthorization.swift index 9526deef..23b17219 100644 --- a/BeamWallet/Manager/BiometricAuthorization.swift +++ b/BeamWallet/Manager/BiometricAuthorization.swift @@ -2,7 +2,7 @@ // BiometricAuthorization.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,34 +20,39 @@ import Foundation import LocalAuthentication -public typealias AuthorizationSuccess = (() -> ()) +public typealias AuthorizationSuccess = (() -> Void) -public typealias AuthorizationFailure = (() -> ()) +public typealias AuthorizationFailure = (() -> Void) -public typealias AuthorizationRetry = (() -> ()) +public typealias AuthorizationRetry = (() -> Void) + +@objc public enum BiometricFailureReason: Int { + case canceled + case notEnrolled + case lockout + case notAvailable + case authenticationFailed + case other +} class BiometricAuthorization: NSObject { - + public static let shared = BiometricAuthorization() public var isAuthorizationProccess = false + public private(set) var lastFailureReason: BiometricFailureReason? + /// The LAContext that was just authenticated. Pass this to + /// `KeychainManager.getPassword(context:)` inside the success callback + /// to read the password without triggering a second biometric prompt. + /// Becomes nil on the next authentication attempt. + public private(set) var lastAuthenticatedContext: LAContext? private var mechanism = "" - + public func canAuthenticate() -> Bool { - var isBiometricAuthenticationAvailable = false - var error: NSError? = nil - - if LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) { - isBiometricAuthenticationAvailable = (error == nil) - } - - if error?.code == -8 - { - return true - } - - return isBiometricAuthenticationAvailable + var error: NSError? + let canEvaluate = LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + return canEvaluate && error == nil } - + public func faceIDAvailable() -> Bool { if #available(iOS 11.0, *) { let context = LAContext() @@ -55,36 +60,47 @@ class BiometricAuthorization: NSObject { } return false } - + public func touchIDAvailable() -> Bool { - + let context = LAContext() var error: NSError? - + let canEvaluate = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) if #available(iOS 11.0, *) { return canEvaluate && context.biometryType == .touchID } return canEvaluate } - + + public func failureMessage(for reason: BiometricFailureReason) -> String { + let strings = Localizable.shared.strings + switch reason { + case .lockout: + return strings.auth_bio_locked.replacingOccurrences(of: "(Mechanism)", with: mechanism) + case .notEnrolled: + return strings.auth_bio_not_enrolled.replacingOccurrences(of: "(Mechanism)", with: mechanism) + case .notAvailable: + return strings.auth_bio_unavailable.replacingOccurrences(of: "(Mechanism)", with: mechanism) + case .authenticationFailed, .other: + return strings.auth_bio_failed.replacingOccurrences(of: "(Mechanism)", with: mechanism) + case .canceled: + return "" + } + } + public func authenticateWithBioMetrics(success successBlock: @escaping AuthorizationSuccess, failure failureBlock: @escaping AuthorizationFailure, retry retryBlock: @escaping AuthorizationRetry, reasonText:String? = nil) { - + isAuthorizationProccess = true - + lastFailureReason = nil + lastAuthenticatedContext = nil + if mechanism.isEmpty { mechanism = BiometricAuthorization.shared.faceIDAvailable() ? Localizable.shared.strings.face_id : Localizable.shared.strings.touch_id } - var reason = "" - - if let value = reasonText { - reason = value - } - else{ - reason = faceIDAvailable() ? Localizable.shared.strings.auth_face_confirm : Localizable.shared.strings.auth_touch_confirm - } - + let reason = reasonText ?? (faceIDAvailable() ? Localizable.shared.strings.auth_face_confirm : Localizable.shared.strings.auth_touch_confirm) + let context = LAContext() context.localizedFallbackTitle = "" context.touchIDAuthenticationAllowableReuseDuration = 0 @@ -92,37 +108,37 @@ class BiometricAuthorization: NSObject { context.evaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { (success, error) in DispatchQueue.main.async { if success { + self.lastFailureReason = nil + self.lastAuthenticatedContext = context successBlock() + } else { + self.lastFailureReason = Self.classify(error) + self.lastAuthenticatedContext = nil + failureBlock() } - else { - if error != nil && (error as NSError?)?.code == -8 - { - let context : LAContext = LAContext(); - - let reason:String = Localizable.shared.strings.auth_bio_failed.replacingOccurrences(of: "(Mechanism)", with: self.mechanism) - - context.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, - localizedReason: reason, - reply: { (success, error) in - - DispatchQueue.main.async { - if (success) { - retryBlock() - } - else{ - failureBlock() - } - - } - }) - } - else{ - failureBlock() - } - } - self.isAuthorizationProccess = false } } } + + private static func classify(_ error: Error?) -> BiometricFailureReason { + guard let nsError = error as NSError? else { return .other } + switch nsError.code { + case LAError.userCancel.rawValue, + LAError.systemCancel.rawValue, + LAError.appCancel.rawValue: + return .canceled + case LAError.biometryNotEnrolled.rawValue: + return .notEnrolled + case LAError.biometryLockout.rawValue: + return .lockout + case LAError.biometryNotAvailable.rawValue, + LAError.passcodeNotSet.rawValue: + return .notAvailable + case LAError.authenticationFailed.rawValue: + return .authenticationFailed + default: + return .other + } + } } diff --git a/BeamWallet/Manager/Crowdin/CrowdinManager.swift b/BeamWallet/Manager/Crowdin/CrowdinManager.swift index 1ea20acf..9c179b7d 100644 --- a/BeamWallet/Manager/Crowdin/CrowdinManager.swift +++ b/BeamWallet/Manager/Crowdin/CrowdinManager.swift @@ -2,7 +2,7 @@ // CrowdinManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,28 +23,24 @@ import Foundation class CrowdinManager : NSObject { fileprivate static var zipPath:URL { - get{ - let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) - let documentDirectoryPath:String = path[0] - let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/all.zip")) - return destinationURLForFile - } + let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) + let documentDirectoryPath:String = path[0] + let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/all.zip")) + return destinationURLForFile } - + static var localizationPath:URL { - get{ - let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) - let documentDirectoryPath:String = path[0] - let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/localization")) - return destinationURLForFile - } + let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) + let documentDirectoryPath:String = path[0] + let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/localization")) + return destinationURLForFile } static public func updateLocalizations() { let zipLink = URL(string: "https://api.crowdin.com/api/project/\(crowdinProject)/download/all.zip?key=\(crowdinKey)")! BackgroundDownloader.shared.startDownloading(zipLink, zipPath) - BackgroundDownloader.shared.onProgress = { (progress, error, filePath, time) in + BackgroundDownloader.shared.onProgress = { (_, error, filePath, _) in if filePath != nil { if FileManager.default.fileExists(atPath: localizationPath.path) @@ -59,7 +55,7 @@ class CrowdinManager : NSObject { SSZipArchive.unzipFile(atPath: filePath!, toDestination: localizationPath.path, overwrite:true, password: nil, progressHandler: { (_ , _ , _ , _ ) in - }, completionHandler: { (path, success, error ) in + }, completionHandler: { (_, success, error ) in if(success) { do { let items = try FileManager.default.contentsOfDirectory(atPath: localizationPath.path) diff --git a/BeamWallet/Manager/KeychainManager.swift b/BeamWallet/Manager/KeychainManager.swift index 0be0ef67..9b355db0 100644 --- a/BeamWallet/Manager/KeychainManager.swift +++ b/BeamWallet/Manager/KeychainManager.swift @@ -2,7 +2,7 @@ // KeychainManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ // import Foundation +import LocalAuthentication struct Credentials { var password: String @@ -32,87 +33,121 @@ enum KeychainError: Error { class KeychainManager { private static let passKey = "wallet" private static let seedKey = "seed" - private static let readLock = NSLock() - - public static func addSeed(seed:String) -> Bool { - _ = delete(seedKey) - - let password = seed.data(using: String.Encoding.utf8)! - let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, - kSecValueData as String: password, - kSecAttrAccount as String : seedKey] - - let status = SecItemAdd(query as CFDictionary, nil) - - return status == errSecSuccess + private static let lock = NSLock() + + public static func addSeed(seed: String) -> Bool { + return write(key: seedKey, value: seed, requireBiometry: false) } - - public static func addPassword(password:String) -> Bool { - _ = delete(passKey) - - let password = password.data(using: String.Encoding.utf8)! - let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, - kSecValueData as String: password, - kSecAttrAccount as String : passKey] - - let status = SecItemAdd(query as CFDictionary, nil) - - return status == errSecSuccess + + /// Stores the wallet password in the keychain when biometric unlock is on, + /// guarded by a `.biometryCurrentSet` access-control so reading the entry + /// requires fresh biometric authentication. When the toggle is off, any + /// existing entry is removed so the toggle is the single source of truth. + public static func addPassword(password: String) -> Bool { + if !Settings.sharedManager().isEnableBiometric { + _ = delete(passKey) + return true + } + return write(key: passKey, value: password, requireBiometry: true) } - + public static func getPassword() -> String? { - if let data = getData(passKey) { - - if let currentString = String(data: data, encoding: .utf8) { - return currentString - } - } - - return nil + return getPassword(context: nil) } - + + /// Reads the password. If `context` is supplied and already authenticated, + /// the OS skips the biometric prompt; otherwise SecItemCopyMatching shows + /// its own prompt because of the `.biometryCurrentSet` ACL on the entry. + public static func getPassword(context: LAContext?) -> String? { + guard let data = getData(passKey, context: context) else { return nil } + return String(data: data, encoding: .utf8) + } + public static func getSeed() -> String? { - if let data = getData(seedKey) { - - if let currentString = String(data: data, encoding: .utf8) { - return currentString + guard let data = getData(seedKey, context: nil) else { return nil } + return String(data: data, encoding: .utf8) + } + + @discardableResult + public static func deletePassword() -> Bool { + return delete(passKey) + } + + @discardableResult + public static func deleteSeed() -> Bool { + return delete(seedKey) + } + + // MARK: - Private + + private static func write(key: String, value: String, requireBiometry: Bool) -> Bool { + lock.lock() + defer { lock.unlock() } + + _ = deleteLocked(key) + + guard let data = value.data(using: .utf8) else { return false } + + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key, + kSecValueData as String: data, + ] + + if requireBiometry { + guard let access = SecAccessControlCreateWithFlags( + nil, + kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + .biometryCurrentSet, + nil) else { + // Refuse to store a password unprotected on a device that can't + // produce a biometry ACL (no biometry / no passcode). + return false } + query[kSecAttrAccessControl as String] = access + } else { + query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly } - - return nil + + let status = SecItemAdd(query as CFDictionary, nil) + return status == errSecSuccess } - + + @discardableResult private static func delete(_ key: String) -> Bool { + lock.lock() + defer { lock.unlock() } + return deleteLocked(key) + } + + private static func deleteLocked(_ key: String) -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String : key + kSecAttrAccount as String: key, ] - - let lastResultCode = SecItemDelete(query as CFDictionary) - - return lastResultCode == noErr + let status = SecItemDelete(query as CFDictionary) + return status == errSecSuccess || status == errSecItemNotFound } - - private static func getData(_ key: String) -> Data? { - readLock.lock() - defer { readLock.unlock() } - - let query: [String: Any] = [ + + private static func getData(_ key: String, context: LAContext?) -> Data? { + lock.lock() + defer { lock.unlock() } + + var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String : key, - kSecReturnData as String : kCFBooleanTrue as Any, - kSecMatchLimit as String : kSecMatchLimitOne + kSecAttrAccount as String: key, + kSecReturnData as String: kCFBooleanTrue as Any, + kSecMatchLimit as String: kSecMatchLimitOne, ] - - + if let context = context { + query[kSecUseAuthenticationContext as String] = context + } + var result: AnyObject? - - let lastResultCode = withUnsafeMutablePointer(to: &result) { + let status = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) } - - if lastResultCode == noErr { return result as? Data } - - return nil + guard status == errSecSuccess else { return nil } + return result as? Data } } diff --git a/BeamWallet/Manager/New Group/CrowdinCredentials.swift b/BeamWallet/Manager/New Group/CrowdinCredentials.swift index 9e129466..9ab558e9 100644 --- a/BeamWallet/Manager/New Group/CrowdinCredentials.swift +++ b/BeamWallet/Manager/New Group/CrowdinCredentials.swift @@ -2,7 +2,7 @@ // CrowdinCredentials.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Manager/NotificationManager.swift b/BeamWallet/Manager/NotificationManager.swift index 9172a7b7..31d4f169 100644 --- a/BeamWallet/Manager/NotificationManager.swift +++ b/BeamWallet/Manager/NotificationManager.swift @@ -2,7 +2,7 @@ // NotificationManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class NotificationManager : NSObject { AppModel.sharedManager().addDelegate(self) } - //MARK: - Registration + // MARK: - Registration public func isApnsEnabled(completion: @escaping ((Bool) -> Void)) { let current = UNUserNotificationCenter.current() @@ -61,8 +61,7 @@ class NotificationManager : NSObject { let options: UNAuthorizationOptions = [.alert, .sound, .badge] let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.requestAuthorization(options: options) { - (didAllow, error) in + notificationCenter.requestAuthorization(options: options) { (_, _) in } UIApplication.shared.registerForRemoteNotifications() @@ -77,7 +76,7 @@ class NotificationManager : NSObject { } - //MARK: - Send + // MARK: - Send public func scheduleNotification(notification: BMNotification){ print("-------- scheduleNotification ---------") @@ -248,12 +247,10 @@ extension NotificationManager: BMNotificationViewDelegate { if(id == NotificationManager.notificationID || id == NotificationManager.addressesID) { if let vc = UIApplication.getTopMostViewController() { var notificationFound = false - for v in vc.navigationController?.viewControllers ?? [] { - if v is NotificationsViewController { - notificationFound = true - vc.navigationController?.popToViewController(v, animated: true) - break - } + for v in vc.navigationController?.viewControllers ?? [] where v is NotificationsViewController { + notificationFound = true + vc.navigationController?.popToViewController(v, animated: true) + break } if !notificationFound { vc.navigationController?.pushViewController(NotificationsViewController(), animated: true) @@ -263,12 +260,10 @@ extension NotificationManager: BMNotificationViewDelegate { else if(id == NotificationManager.versionID) { if let vc = UIApplication.getTopMostViewController() { var notificationFound = false - for v in vc.navigationController?.viewControllers ?? [] { - if v is NotificationVersionViewController { - notificationFound = true - vc.navigationController?.popToViewController(v, animated: true) - break - } + for v in vc.navigationController?.viewControllers ?? [] where v is NotificationVersionViewController { + notificationFound = true + vc.navigationController?.popToViewController(v, animated: true) + break } if !notificationFound { if let notification = AppModel.sharedManager().getLastVersionNotification() { diff --git a/BeamWallet/Manager/OnboardManager.swift b/BeamWallet/Manager/OnboardManager.swift index 24215de5..9944985e 100644 --- a/BeamWallet/Manager/OnboardManager.swift +++ b/BeamWallet/Manager/OnboardManager.swift @@ -2,7 +2,7 @@ // RestoreManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,8 +24,9 @@ class OnboardManager: NSObject { private let faucetKey = "faucetKey" private let isSkipedSeedKey = "isSkipedSeedKey" - - static var shared = OnboardManager() + private let walletInitializedKey = "walletInitializedKey" + + @objc static var shared = OnboardManager() public var isCloseSecure = false public var isCloseFaucet = false @@ -33,11 +34,28 @@ class OnboardManager: NSObject { public func reset() { isCloseSecure = false isCloseFaucet = false - + UserDefaults.standard.removeObject(forKey: faucetKey) UserDefaults.standard.set(false, forKey: isSkipedSeedKey) + UserDefaults.standard.removeObject(forKey: walletInitializedKey) UserDefaults.standard.synchronize() } + + // MARK: Wallet integrity marker + + // Distinguishes a healthy wallet from a half-built DB file left by a crash + // mid-create; AppDelegate routing branches on this. + @objc public func markWalletInitialized() { + UserDefaults.standard.set(true, forKey: walletInitializedKey) + } + + @objc public func isWalletInitializedFlag() -> Bool { + return UserDefaults.standard.bool(forKey: walletInitializedKey) + } + + @objc public func clearWalletInitialized() { + UserDefaults.standard.removeObject(forKey: walletInitializedKey) + } // MARK: Faucet diff --git a/BeamWallet/Manager/PasswordManager.swift b/BeamWallet/Manager/PasswordManager.swift index 93583ccf..a4df2c93 100644 --- a/BeamWallet/Manager/PasswordManager.swift +++ b/BeamWallet/Manager/PasswordManager.swift @@ -2,7 +2,7 @@ // PasswordManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -62,11 +62,8 @@ class PasswordTestManager { let range = NSRange(location: 0, length: password.utf16.count) - for test in strengthTests { - if(test.exp.firstMatch(in: password, options: [], range: range) != nil) - { - state = test.state - } + for test in strengthTests where test.exp.firstMatch(in: password, options: [], range: range) != nil { + state = test.state } return state diff --git a/BeamWallet/Manager/RestoreManager.swift b/BeamWallet/Manager/RestoreManager.swift index 184e7c5d..5190167c 100644 --- a/BeamWallet/Manager/RestoreManager.swift +++ b/BeamWallet/Manager/RestoreManager.swift @@ -2,7 +2,7 @@ // RestoreManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,18 +27,14 @@ class RestoreManager: NSObject { private var progress : ((Error?, Float?, String?) -> Void)? var filePath:URL { - get{ - let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) - let documentDirectoryPath:String = path[0] - let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/node.bin")) - return destinationURLForFile - } + let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) + let documentDirectoryPath:String = path[0] + let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/node.bin")) + return destinationURLForFile } - + var isNeedReturnToRestore:Bool { - get { - return (FileManager.default.fileExists(atPath: filePath.path)) - } + return (FileManager.default.fileExists(atPath: filePath.path)) } public func cancelRestore() { @@ -56,13 +52,13 @@ class RestoreManager: NSObject { } public func startRestore(completion:@escaping ((Bool) -> Void), progress:@escaping ((Error?, Float?, String?) -> Void)) { - + self.cancelRestore() self.completion = completion self.progress = progress var url:URL? - + if Settings.sharedManager().target == Testnet { url = URL(string: "https://mobile-restore.beam.mw/testnet/testnet_recovery.bin") } @@ -74,7 +70,7 @@ class RestoreManager: NSObject { // url = URL(string: "https://mobile-restore.beam.mw/masternet/masternet_recovery.bin") } - + if let downloadUrl = url { BackgroundDownloader.shared.startDownloading(downloadUrl, filePath) BackgroundDownloader.shared.onProgress = { (progress, error, filePath, time) in diff --git a/BeamWallet/Manager/ShortcutManager.swift b/BeamWallet/Manager/ShortcutManager.swift index 05738f91..282d3975 100644 --- a/BeamWallet/Manager/ShortcutManager.swift +++ b/BeamWallet/Manager/ShortcutManager.swift @@ -2,7 +2,7 @@ // ShortcutManager.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Model/Address/AddressViewModel.swift b/BeamWallet/Model/Address/AddressViewModel.swift index 6cc1994d..1ef119be 100644 --- a/BeamWallet/Model/Address/AddressViewModel.swift +++ b/BeamWallet/Model/Address/AddressViewModel.swift @@ -2,7 +2,7 @@ // AddressViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,10 +25,10 @@ class AddressViewModel: NSObject { case expired = 1 case contacts = 2 } - + public var onDataChanged: (() -> Void)? public var onDataDeleted: ((IndexPath?, BMAddress) -> Void)? - + public var addresses = [BMAddress]() public var contacts = [BMContact]() public var selectedState: AddressesSelectedState = .active { @@ -36,6 +36,45 @@ class AddressViewModel: NSObject { filterAddresses() } } + + // Shared across instances so the asynchronous fetch only runs once per session. + private static var cachedPublicOfflineAddress: String? + private static var isFetchingPublicOfflineAddress = false + + public func isPublicOfflineEntry(_ address: BMAddress) -> Bool { + guard let cached = AddressViewModel.cachedPublicOfflineAddress, !cached.isEmpty else { + return false + } + return address.walletId == cached + } + + private func ensurePublicOfflineAddressLoaded() { + if AddressViewModel.cachedPublicOfflineAddress != nil { return } + if AddressViewModel.isFetchingPublicOfflineAddress { return } + AddressViewModel.isFetchingPublicOfflineAddress = true + AppModel.sharedManager().getPublicAddress { [weak self] address in + DispatchQueue.main.async { + AddressViewModel.isFetchingPublicOfflineAddress = false + AddressViewModel.cachedPublicOfflineAddress = address + self?.filterAddresses() + } + } + } + + private func makePublicOfflineAddress() -> BMAddress? { + guard let token = AddressViewModel.cachedPublicOfflineAddress, !token.isEmpty else { + return nil + } + let entry = BMAddress.empty() + entry.walletId = token + entry.address = token + entry.displayAddress = token + entry.label = Localizable.shared.strings.public_offline_address + entry.duration = 0 + entry.createTime = 0 + entry.isContact = true + return entry + } public var count: Int { return selectedState == .contacts ? contacts.count : addresses.count @@ -58,8 +97,9 @@ class AddressViewModel: NSObject { init(selected: AddressesSelectedState) { super.init() - + AppModel.sharedManager().addDelegate(self) + ensurePublicOfflineAddressLoaded() } init(address: BMAddress) { @@ -100,21 +140,26 @@ class AddressViewModel: NSObject { self.addresses = uniqueAddressesObjects self.contacts = uniqueContactsObjects - + let isOwn = AppModel.sharedManager().checkIsOwnNode() if !isOwn { for (index, element) in self.addresses.enumerated() { self.addresses[index].displayAddress = element.walletId } - + for (index, element) in self.contacts.enumerated() { let params = AppModel.sharedManager().getTransactionParameters(element.address.address ?? "") if params.isMaxPrivacy { self.contacts[index].address.displayAddress = element.address.address } } - } - + } + + if self.selectedState == .active, let publicOffline = self.makePublicOfflineAddress() { + self.addresses.removeAll { $0.walletId == publicOffline.walletId } + self.addresses.insert(publicOffline, at: 0) + } + self.onDataChanged?() } } @@ -187,7 +232,7 @@ class AddressViewModel: NSObject { return } } - }) {} + }, cancel: {}) } public func onEditAddress(address: BMAddress) { @@ -242,20 +287,26 @@ class AddressViewModel: NSObject { public func trailingSwipeActions(indexPath: IndexPath) -> UISwipeActionsConfiguration? { let address: BMAddress = selectedState == .contacts ? contacts[indexPath.row].address : addresses[indexPath.row] - - let delete = UIContextualAction(style: .normal, title: nil) { _, _, handler in - handler(true) - self.onDeleteAddress(address: address, indexPath: indexPath) - } - delete.image = IconRowDelete() - delete.backgroundColor = UIColor.main.coral - + let copy = UIContextualAction(style: .normal, title: nil) { _, _, handler in handler(true) self.onCopyAddress(address: address) } copy.image = IconRowCopy() copy.backgroundColor = UIColor.main.deepSeaBlue + + if isPublicOfflineEntry(address) { + let configuration = UISwipeActionsConfiguration(actions: [copy]) + configuration.performsFirstActionWithFullSwipe = false + return configuration + } + + let delete = UIContextualAction(style: .normal, title: nil) { _, _, handler in + handler(true) + self.onDeleteAddress(address: address, indexPath: indexPath) + } + delete.image = IconRowDelete() + delete.backgroundColor = UIColor.main.coral let edit = UIContextualAction(style: .normal, title: nil) { _, _, handler in handler(true) @@ -331,4 +382,10 @@ extension AddressViewModel: WalletModelDelegate { self.filterAddresses() } } + + func onOfflinePaymentsCount(forWalletId walletId: String, count: Int32) { + DispatchQueue.main.async { + self.onDataChanged?() + } + } } diff --git a/BeamWallet/Model/Address/DetailAddressViewModel.swift b/BeamWallet/Model/Address/DetailAddressViewModel.swift index 814cb259..db1ff37c 100644 --- a/BeamWallet/Model/Address/DetailAddressViewModel.swift +++ b/BeamWallet/Model/Address/DetailAddressViewModel.swift @@ -2,7 +2,7 @@ // DetailAddressViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,21 +25,23 @@ class DetailAddressViewModel: AddressViewModel { public var details = [BMMultiLineItem]() public var transactionViewModel:TransactionViewModel! public var transactions:[BMTransaction] { - get{ - return transactionViewModel.transactions - } + return transactionViewModel.transactions } override init(address: BMAddress) { super.init(address: address) - + isContact = (AppModel.sharedManager().getContactFromId(address.walletId) != nil) transactionViewModel = TransactionViewModel(address: address) transactionViewModel.onDataChanged = { [weak self] in self?.onDataChanged?() } - + + if !isContact { + AppModel.sharedManager().requestOfflinePaymentsCount(forWalletId: address.walletId) + } + fillDetails() } @@ -85,7 +87,16 @@ class DetailAddressViewModel: AddressViewModel { identityItem.copiedText = Localizable.shared.strings.copied_to_clipboard details.append(identityItem) } - + + if !isContact, let walletId = self.address?.walletId { + let count = AppModel.sharedManager().offlinePaymentsCount(forWalletId: walletId) + if count >= 0 { + let detail = String(format: Localizable.shared.strings.offline_left_address, count) + let item = BMMultiLineItem(title: Localizable.shared.strings.offline_address.uppercased(), detail: detail, detailFont: RegularFont(size: 16), detailColor: UIColor.white) + details.append(item) + } + } + if self.address?.identity != nil && self.address?.identity?.isEmpty == false { let detail = self.address!.identity if detail!.count > 1 { @@ -109,6 +120,15 @@ class DetailAddressViewModel: AddressViewModel { // } } + override func onOfflinePaymentsCount(forWalletId walletId: String, count: Int32) { + DispatchQueue.main.async { + if walletId == self.address?.walletId { + self.fillDetails() + self.onDataChanged?() + } + } + } + public func actionItems() -> [BMPopoverMenu.BMPopoverMenuItem] { var items = [BMPopoverMenu.BMPopoverMenuItem(name: Localizable.shared.strings.show_qr_code, icon: nil, action: .show_qr_code), BMPopoverMenu.BMPopoverMenuItem(name: (isContact ? Localizable.shared.strings.copy_contact : Localizable.shared.strings.copy_address), icon: nil, action:.copy_address), BMPopoverMenu.BMPopoverMenuItem(name: (isContact ? Localizable.shared.strings.edit_contact : Localizable.shared.strings.edit_address), icon: nil, action:.edit_address), BMPopoverMenu.BMPopoverMenuItem(name: (isContact ? Localizable.shared.strings.delete_contact : Localizable.shared.strings.delete_address), icon: nil, action:.delete_address)] diff --git a/BeamWallet/Model/Address/EditAddressViewModel.swift b/BeamWallet/Model/Address/EditAddressViewModel.swift index e751145f..01f40a53 100644 --- a/BeamWallet/Model/Address/EditAddressViewModel.swift +++ b/BeamWallet/Model/Address/EditAddressViewModel.swift @@ -2,7 +2,7 @@ // EditAddressViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Model/Address/ReceiveAddressViewModel.swift b/BeamWallet/Model/Address/ReceiveAddressViewModel.swift index 0e8f9abf..38aa742c 100644 --- a/BeamWallet/Model/Address/ReceiveAddressViewModel.swift +++ b/BeamWallet/Model/Address/ReceiveAddressViewModel.swift @@ -2,7 +2,7 @@ // ReceiveAddressViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,34 +25,110 @@ class ReceiveAddressViewModel: NSObject { case regular = 0 case privacy = 1 } - + + enum ReceiveTokenType: Int { + case sbbs = 0 + case regular = 1 + case maxPrivacy = 2 + case offline = 3 + case publicOffline = 4 + + var localizedName: String { + switch self { + case .sbbs: return Localizable.shared.strings.sbbs_address + case .regular: return Localizable.shared.strings.regular_address + case .maxPrivacy: return Localizable.shared.strings.max_privacy_address + case .offline: return Localizable.shared.strings.offline_address + case .publicOffline: return Localizable.shared.strings.public_offline_address + } + } + + var supportsVouchers: Bool { + switch self { + case .regular, .offline: return true + default: return false + } + } + + var supportsOnlineReceive: Bool { + switch self { + case .sbbs, .regular, .offline: return true + default: return false + } + } + + var defaultVouchers: Int { + switch self { + case .offline: return 10 + default: return 1 + } + } + } + + static let maxVouchersCount = 30 + enum ExpireOptions: Int { case oneTime = 0 case parmanent = 1 } - + enum ReceiveAddressViewModelSaveState: Int { case none = 0 case new = 1 case edit = 2 } - + public var needReloadButtons = false public var transactionComment = String.empty() - + public var onAddressCreated: ((Error?) -> Void)? public var onDataChanged: (() -> Void)? public var onShared: (() -> Void)? public var onAddressUpdate: ((Error?) -> Void)? public var address: BMAddress! - public var transaction = TransactionOptions.regular - { + public var selectedTokenType: ReceiveTokenType = .regular { didSet { if isSavedAddress { isShared = true } + suppressVouchersRegen = true + vouchersCount = selectedTokenType.defaultVouchers + suppressVouchersRegen = false + } + } + + public var transaction: TransactionOptions { + return selectedTokenType == .maxPrivacy ? .privacy : .regular + } + + private var suppressVouchersRegen = false + + private var _vouchersCount: Int = ReceiveTokenType.regular.defaultVouchers + public var vouchersCount: Int { + get { _vouchersCount } + set { + // Mirror the VC's 1...maxVouchersCount clamp so direct/programmatic + // writes can't bypass validation. + let clamped = max(1, min(ReceiveAddressViewModel.maxVouchersCount, newValue)) + guard clamped != _vouchersCount else { return } + _vouchersCount = clamped + if !suppressVouchersRegen && selectedTokenType.supportsVouchers { + generateTokens() + } + } + } + + public var currentToken: String { + let isOwn = AppModel.sharedManager().checkIsOwnNode() + if !isOwn { return address?.address ?? address?.walletId ?? "" } + switch selectedTokenType { + case .sbbs: return address?.sbbsToken ?? "" + case .regular: return address?.address ?? address?.walletId ?? "" + case .maxPrivacy: return address?.maxPrivacyToken ?? "" + case .offline: return address?.offlineToken ?? "" + case .publicOffline: return address?.publicOfflineToken ?? "" } } @@ -112,9 +188,7 @@ class ReceiveAddressViewModel: NSObject { } } public var selectedCurrencyString: String { - get { - return AssetsManager.shared().getAsset(Int32(selectedAssetId))?.unitName ?? "" - } + return AssetsManager.shared().getAsset(Int32(selectedAssetId))?.unitName ?? "" } override init() { @@ -127,36 +201,64 @@ class ReceiveAddressViewModel: NSObject { public func generateTokens() { let isOwn = AppModel.sharedManager().checkIsOwnNode() - + let bamount = Double(amount ?? "0") ?? 0 - - if isOwn { - AppModel.sharedManager().generateMaxPrivacyAddress(address._id, assetId: Int32(selectedAssetId), amount: bamount) { (token) in - self.address.maxPrivacyToken = token; + + let walletId = address._id + let assetId = Int32(selectedAssetId) + + if !isOwn { + let token = AppModel.sharedManager().generateRegularAddress(walletId, assetId: assetId, amount: bamount, isPermanentAddress: false) + address.address = token + DispatchQueue.main.async { + self.onAddressUpdate?(nil) } + return } - - if isOwn { - AppModel.sharedManager().generateOfflineAddress(address._id, assetId: Int32(selectedAssetId), amount: bamount) { (token) in + + switch selectedTokenType { + case .sbbs: + AppModel.sharedManager().generateSBBSAddress(walletId, assetId: assetId, amount: bamount) { [weak self] token in DispatchQueue.main.async { - if self.address.offlineToken != nil { - self.address.offlineToken = token; - self.onAddressUpdate?(nil) - } - else { - self.address.offlineToken = token; + guard let self = self else { return } + self.address.sbbsToken = token + self.onAddressUpdate?(nil) + } + } + case .regular: + AppModel.sharedManager().generateOfflineAddress(walletId, assetId: assetId, amount: bamount, offlineCount: UInt32(vouchersCount)) { [weak self] token in + DispatchQueue.main.async { + guard let self = self else { return } + self.address.address = token + self.onAddressUpdate?(nil) + } + } + case .maxPrivacy: + AppModel.sharedManager().generateMaxPrivacyAddress(walletId, assetId: assetId, amount: bamount) { [weak self] token in + DispatchQueue.main.async { + guard let self = self else { return } + self.address.maxPrivacyToken = token + self.onAddressUpdate?(nil) + } + } + case .offline: + AppModel.sharedManager().generateOfflineAddress(walletId, assetId: assetId, amount: bamount, offlineCount: UInt32(vouchersCount)) { [weak self] token in + DispatchQueue.main.async { + guard let self = self else { return } + let isFirst = (self.address.offlineToken == nil) + self.address.offlineToken = token + if isFirst { self.onAddressCreated?(nil) + } else { + self.onAddressUpdate?(nil) } } } - } - - if !isOwn { - AppModel.sharedManager().generateNewWalletAddress(withBlockAndAmount: Int32(selectedAssetId), amount: bamount) { address, error in - if let result = address { - self.address = result - } + case .publicOffline: + AppModel.sharedManager().generatePublicOfflineAddress(walletId, assetId: assetId, amount: bamount) { [weak self] token in DispatchQueue.main.async { + guard let self = self else { return } + self.address.publicOfflineToken = token self.onAddressUpdate?(nil) } } @@ -242,15 +344,13 @@ class ReceiveAddressViewModel: NSObject { if let token = self.address.offlineToken { self.showShareDialog(token) } - break case .share_pool_token: self.showShareDialog(self.address.walletId) - break default: return } } - }) {} + }, cancel: {}) } private func showShareDialog(_ token:String) { diff --git a/BeamWallet/Model/AssetSwaps/AssetSwapCreateViewModel.swift b/BeamWallet/Model/AssetSwaps/AssetSwapCreateViewModel.swift new file mode 100644 index 00000000..db6aa345 --- /dev/null +++ b/BeamWallet/Model/AssetSwaps/AssetSwapCreateViewModel.swift @@ -0,0 +1,101 @@ +// +// AssetSwapCreateViewModel.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class AssetSwapCreateViewModel: NSObject { + + public var sendAsset: BMAsset? + public var receiveAsset: BMAsset? + public var sendAmountString: String = "" + public var receiveAmountString: String = "" + public var expirationMinutes: UInt32 = 720 // 12 hours — SBBS message TTL cap + + public var onDataChanged: (() -> Void)? + + override init() { + super.init() + AppModel.sharedManager().loadFullAssetsList() + } + + public var validationError: String? { + guard let send = sendAsset else { + return Localizable.shared.strings.asset_swap_pick_send_asset + } + guard let receive = receiveAsset else { + return Localizable.shared.strings.asset_swap_pick_receive_asset + } + if send.assetId == receive.assetId { + return Localizable.shared.strings.asset_swap_same_asset_error + } + let sendGroth = grothFrom(sendAmountString) + let receiveGroth = grothFrom(receiveAmountString) + if sendGroth == 0 || receiveGroth == 0 { + return Localizable.shared.strings.amount_zero + } + if sendGroth > send.available { + return Localizable.shared.strings.asset_swap_insufficient_funds + } + return nil + } + + public var canSubmit: Bool { + return validationError == nil + } + + public func displayRate() -> String { + let s = grothFrom(sendAmountString) + let r = grothFrom(receiveAmountString) + guard s > 0, r > 0, + let sendName = sendAsset?.unitName, sendName.count > 0, + let receiveName = receiveAsset?.unitName, receiveName.count > 0 else { + return "—" + } + let rate = Double(r) / Double(s) + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 8 + let rateString = formatter.string(from: NSNumber(value: rate)) ?? "0" + return "1 \(sendName) = \(rateString) \(receiveName)" + } + + public func submit() -> Bool { + guard canSubmit, let send = sendAsset, let receive = receiveAsset else { + return false + } + // SBBS message TTL caps the order lifetime at 12h regardless of what + // the picker offers; clamp at the model boundary as defense in depth + // against any caller writing a larger value. + let clampedExpiration = min(expirationMinutes, 720) + return AppModel.sharedManager().publishDexOrder( + withSendAsset: UInt32(send.assetId), + sendAmount: grothFrom(sendAmountString), + receiveAsset: UInt32(receive.assetId), + receiveAmount: grothFrom(receiveAmountString), + expirationMinutes: clampedExpiration) + } + + private func grothFrom(_ string: String) -> UInt64 { + let normalized = string.replacingOccurrences(of: ",", with: ".") + guard let beam = Double(normalized), beam > 0 else { return 0 } + let scaled = beam * 100_000_000.0 + guard scaled.isFinite, scaled >= 0, scaled < Double(UInt64.max) else { return 0 } + return UInt64(scaled) + } +} diff --git a/BeamWallet/Model/AssetSwaps/AssetSwapDetailsViewModel.swift b/BeamWallet/Model/AssetSwaps/AssetSwapDetailsViewModel.swift new file mode 100644 index 00000000..b5e85bc6 --- /dev/null +++ b/BeamWallet/Model/AssetSwaps/AssetSwapDetailsViewModel.swift @@ -0,0 +1,82 @@ +// +// AssetSwapDetailsViewModel.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class AssetSwapDetailsViewModel: NSObject { + + public let order: BMDexOrder + + init(order: BMDexOrder) { + self.order = order + super.init() + } + + public var canCancel: Bool { + return order.isMine && order.isActive() + } + + public var canAccept: Bool { + return !order.isMine && order.isActive() + } + + public var sendAsset: BMAsset? { + return AssetsManager.shared().getAsset(Int32(order.sendAssetId)) + } + + public var receiveAsset: BMAsset? { + return AssetsManager.shared().getAsset(Int32(order.receiveAssetId)) + } + + public var statusColor: UIColor { + if order.isExpired() { + return UIColor.main.red + } + if order.isCompleted || order.isCanceled { + return UIColor.main.steelGrey + } + return UIColor.main.brightTeal + } + + public var isExpiringSoon: Bool { + guard order.isActive() else { return false } + let now = UInt64(Date().timeIntervalSince1970) + guard order.expireTimestamp > now else { return false } + return order.expireTimestamp - now < 3600 + } + + public var validationError: String? { + guard canAccept else { return nil } + guard let sendAsset = AssetsManager.shared().getAsset(Int32(order.sendAssetId)) else { + return Localizable.shared.strings.asset_swap_insufficient_funds + } + if sendAsset.available < order.sendAmount { + return Localizable.shared.strings.asset_swap_insufficient_funds + } + return nil + } + + public func cancel() { + AppModel.sharedManager().cancelDexOrder(withID: order.orderID) + } + + public func accept() -> Bool { + return AppModel.sharedManager().acceptDexOrder(order) + } +} diff --git a/BeamWallet/Model/AssetSwaps/AssetSwapsViewModel.swift b/BeamWallet/Model/AssetSwaps/AssetSwapsViewModel.swift new file mode 100644 index 00000000..0eb313f5 --- /dev/null +++ b/BeamWallet/Model/AssetSwaps/AssetSwapsViewModel.swift @@ -0,0 +1,70 @@ +// +// AssetSwapsViewModel.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +enum AssetSwapsTab: Int { + case open = 0 + case mine = 1 + case history = 2 +} + +class AssetSwapsViewModel: NSObject { + + public var onDataChanged: (() -> Void)? + public var selectedTab: AssetSwapsTab = .open + + private var allOrders: [BMDexOrder] { + return AppModel.sharedManager().dexOrders as? [BMDexOrder] ?? [] + } + + public var ordersForActiveTab: [BMDexOrder] { + switch selectedTab { + case .open: + return allOrders.filter { !$0.isMine && $0.isActive() } + case .mine: + return allOrders.filter { $0.isMine && $0.isActive() } + case .history: + return allOrders.filter { !$0.isActive() } + .sorted { $0.createTimestamp > $1.createTimestamp } + } + } + + override init() { + super.init() + AppModel.sharedManager().addDelegate(self) + AppModel.sharedManager().requestDexOrders() + } + + deinit { + AppModel.sharedManager().removeDelegate(self) + } + + func reload() { + AppModel.sharedManager().requestDexOrders() + } +} + +extension AssetSwapsViewModel: WalletModelDelegate { + func onDexOrdersChanged(_ orders: [BMDexOrder]) { + DispatchQueue.main.async { [weak self] in + self?.onDataChanged?() + } + } +} diff --git a/BeamWallet/Model/DAppManager.swift b/BeamWallet/Model/DAppManager.swift new file mode 100644 index 00000000..d1096aac --- /dev/null +++ b/BeamWallet/Model/DAppManager.swift @@ -0,0 +1,203 @@ +// +// DAppManager.swift +// BeamWallet +// +// Install / uninstall / list locally-installed DApps. +// + +import Foundation +// SSZipArchive is exposed to Swift through the bridging header. + +enum DAppManagerError: Error { + case invalidGUID + case missingManifestGUID + case unzipFailed + case bundledAssetMissing(String) +} + +@objc final class DAppManager: NSObject { + + @objc static let shared = DAppManager() + + static let defaultEntry = "app/index.html" + private let installedKey = "installed_dapps" + private let guidRegex = try! NSRegularExpression(pattern: "^[a-fA-F0-9]{32,64}$") + + private override init() { super.init() } + + private var dappsDirectory: URL { + let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + return base.appendingPathComponent("dapps", isDirectory: true) + } + + private func ensureDappsDirectory() throws { + let dir = dappsDirectory + if !FileManager.default.fileExists(atPath: dir.path) { + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + } + } + + // MARK: - Persistence + + @objc func installed() -> [BMInstalledDApp] { + guard let raw = UserDefaults.standard.string(forKey: installedKey), + let data = raw.data(using: .utf8), + let arr = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]] else { + return [] + } + return arr.compactMap { item in + let dapp = BMInstalledDApp.fromDictionary(item) + return dapp.guid.isEmpty ? nil : dapp + } + } + + private func saveInstalled(_ list: [BMInstalledDApp]) { + let serialised = list.map { $0.toDictionary() } + guard let data = try? JSONSerialization.data(withJSONObject: serialised, options: []), + let json = String(data: data, encoding: .utf8) else { return } + UserDefaults.standard.set(json, forKey: installedKey) + } + + // MARK: - Validation + + private func isValidGUID(_ guid: String) -> Bool { + let range = NSRange(guid.startIndex.. String? { + if url.isEmpty { return DAppManager.defaultEntry } + if url.contains("..") || url.contains("//") { return nil } + if url.hasPrefix("/") || url.hasPrefix("\\") { return nil } + // Strip the desktop `localapp/` prefix so the load resolves directly + // against the DApp directory. + if url.hasPrefix("localapp/") { + return String(url.dropFirst("localapp/".count)) + } + if url.contains("/") && !url.hasPrefix("app/") { + return nil + } + return url + } + + // MARK: - Install + + /// Installs a `.dapp` (ZIP) and returns the resulting record. The dapps + /// directory is keyed by the manifest's `guid` field — we don't know that + /// value until the archive is extracted, so the unzip is staged in a temp + /// directory and only then moved into place. + @objc func installFromZip(data: Data, + fallbackName: String, + fallbackIcon: String) throws -> BMInstalledDApp { + try ensureDappsDirectory() + + let staging = FileManager.default.temporaryDirectory + .appendingPathComponent("dapp-stage-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true) + var stagingValid = true + defer { if stagingValid { try? FileManager.default.removeItem(at: staging) } } + + let tmpZip = FileManager.default.temporaryDirectory + .appendingPathComponent("dapp-\(UUID().uuidString).zip") + try data.write(to: tmpZip) + defer { try? FileManager.default.removeItem(at: tmpZip) } + + guard SSZipArchive.unzipFile(atPath: tmpZip.path, toDestination: staging.path) else { + throw DAppManagerError.unzipFailed + } + + // Drop anything resembling path traversal — SSZipArchive accepts entries + // verbatim. Walk the extracted tree and delete files that resolved + // outside the staging directory (canonicalised paths help on iOS where + // /private/var aliases /var). + sanitizeExtractedTree(dir: staging) + + var manifest: [String: Any] = [:] + let manifestPath = staging.appendingPathComponent("manifest.json") + if let mData = try? Data(contentsOf: manifestPath), + let parsed = (try? JSONSerialization.jsonObject(with: mData)) as? [String: Any] { + manifest = parsed + } + + let manifestGuid = (manifest["guid"] as? String) ?? "" + guard !manifestGuid.isEmpty else { throw DAppManagerError.missingManifestGUID } + guard isValidGUID(manifestGuid) else { throw DAppManagerError.invalidGUID } + + let dir = dappsDirectory.appendingPathComponent(manifestGuid, isDirectory: true) + if FileManager.default.fileExists(atPath: dir.path) { + try? FileManager.default.removeItem(at: dir) + } + try FileManager.default.moveItem(at: staging, to: dir) + stagingValid = false + + let icon = DAppStore.firstIcon(in: dir) ?? fallbackIcon + let rawURL = (manifest["url"] as? String) ?? DAppManager.defaultEntry + let safeURL = validateManifestURL(rawURL) ?? DAppManager.defaultEntry + + let dapp = BMInstalledDApp() + dapp.guid = manifestGuid + dapp.name = (manifest["name"] as? String) ?? fallbackName + dapp.desc = (manifest["description"] as? String) ?? "" + dapp.version = (manifest["version"] as? String) ?? "1.0" + dapp.localPath = dir.path + dapp.icon = icon + dapp.url = safeURL + + var list = installed() + list.removeAll { $0.guid == manifestGuid } + list.append(dapp) + saveInstalled(list) + + return dapp + } + + /// Walks the extracted DApp directory and removes any file whose canonical + /// path escapes the root — SSZipArchive accepts traversal entries verbatim + /// on older versions, so we sanitise after the fact. + private func sanitizeExtractedTree(dir: URL) { + let fm = FileManager.default + let rootStandardized = dir.standardizedFileURL.path + guard let enumerator = fm.enumerator(at: dir, + includingPropertiesForKeys: [.isRegularFileKey], + options: []) else { return } + for case let url as URL in enumerator { + let resolved = url.standardizedFileURL.path + if !resolved.hasPrefix(rootStandardized) { + try? fm.removeItem(at: url) + } + } + } + + @objc func installFromBundle(assetName: String, + fallbackName: String) throws -> BMInstalledDApp { + guard let url = Bundle.main.url(forResource: (assetName as NSString).deletingPathExtension, + withExtension: (assetName as NSString).pathExtension) else { + throw DAppManagerError.bundledAssetMissing(assetName) + } + let data = try Data(contentsOf: url) + return try installFromZip(data: data, fallbackName: fallbackName, fallbackIcon: "") + } + + @objc func uninstall(guid: String) { + let dir = dappsDirectory.appendingPathComponent(guid) + try? FileManager.default.removeItem(at: dir) + var list = installed() + list.removeAll { $0.guid == guid } + saveInstalled(list) + } + + // MARK: - Launch + + /// Returns the file:// URL pointing at the DApp's entry HTML, clamped to + /// stay inside the DApp directory. + @objc func launchURL(for dapp: BMInstalledDApp) -> URL? { + let root = URL(fileURLWithPath: dapp.localPath, isDirectory: true).standardizedFileURL + let safeURL = validateManifestURL(dapp.url) ?? DAppManager.defaultEntry + let target = root.appendingPathComponent(safeURL).standardizedFileURL + if !target.path.hasPrefix(root.path) { + return root.appendingPathComponent(DAppManager.defaultEntry) + } + return target + } +} diff --git a/BeamWallet/Model/DAppStore.swift b/BeamWallet/Model/DAppStore.swift new file mode 100644 index 00000000..23bf84dc --- /dev/null +++ b/BeamWallet/Model/DAppStore.swift @@ -0,0 +1,369 @@ +// +// DAppStore.swift +// BeamWallet +// +// Queries the on-chain DApp Store contract for published DApps and publishers, +// caches the publisher list, downloads DApp ZIPs over IPFS via the wallet RPC. +// + +import Foundation + +@objc final class DAppStore: NSObject { + + @objc static let shared = DAppStore() + + static let storeCID = "e2d24b686e8d31a0fe97eade9cd23281e7059b74b5757bdb96c820ef9e2af41c" + static let iconCandidates = ["app/icon.svg", "app/appicon.svg", "app/logo.svg"] + private let ipfsTimeoutMS = 180_000 + private let unwantedPublishersKey = "unwanted_publishers" + + private let bundled: [BMAvailableDApp] = { + let core = BMAvailableDApp() + core.guid = "abcc470e12c6422291f360f83d79355e" + core.name = "BeamX DAO" + core.desc = "Governance, staking and voting" + core.version = "1.0.0" + core.bundledAsset = "dao-core-app.dapp" + + let voting = BMAvailableDApp() + voting.guid = "c26538f5ce9e410b89c1fd0dff783f97" + voting.name = "BeamX DAO Voting" + voting.desc = "Voting on Beam community proposals" + voting.version = "1.0.0" + voting.bundledAsset = "dao-voting-app.dapp" + + return [core, voting] + }() + + private var shaderBytes: [Int]? + private var cachedPublishers: [BMPublisher] = [] + + // MARK: - Shader + + private func loadShader() -> [Int]? { + if let bytes = shaderBytes { return bytes } + guard let url = Bundle.main.url(forResource: "dapps_store_app", withExtension: "wasm"), + let data = try? Data(contentsOf: url) else { + return nil + } + let bytes = data.map { Int($0) } + shaderBytes = bytes + return bytes + } + + // MARK: - Available DApps + + @objc func queryAvailableDApps(filterUnwanted: Bool = true, + completion: @escaping ([BMAvailableDApp]) -> Void) { + guard let shader = loadShader() else { + completion(bundledWithIcons()) + return + } + loadPublishers(shader: shader) { [weak self] in + self?.loadDApps(shader: shader, filterUnwanted: filterUnwanted, completion: completion) + } + } + + @objc func queryPublishers(completion: @escaping ([BMPublisher]) -> Void) { + if !cachedPublishers.isEmpty { + completion(cachedPublishers) + return + } + guard let shader = loadShader() else { + completion([]) + return + } + loadPublishers(shader: shader) { [weak self] in + completion(self?.cachedPublishers ?? []) + } + } + + private func loadPublishers(shader: [Int], onDone: @escaping () -> Void) { + if !cachedPublishers.isEmpty { onDone(); return } + guard let client = AppModel.sharedManager().walletAPIClient() else { + onDone() + return + } + let args = "action=view_publishers,cid=\(DAppStore.storeCID)" + let params: [String: Any] = [ + "args": args, + "create_tx": false, + "contract": shader, + ] + client.call(method: "invoke_contract", params: params) { [weak self] (result: [AnyHashable: Any]) in + guard let self = self else { onDone(); return } + if let publishers = result["publishers"] as? [[String: Any]] { + self.cachedPublishers = publishers.compactMap { p in + guard let pubkey = p["pubkey"] as? String else { return nil } + let name = Self.hexToString(p["name"] as? String ?? "") + if name.isEmpty { return nil } + let pub = BMPublisher() + pub.pubkey = pubkey + pub.name = name + pub.aboutMe = Self.hexToString(p["about_me"] as? String ?? "") + pub.website = Self.hexToString(p["website"] as? String ?? "") + pub.twitter = Self.hexToString(p["twitter"] as? String ?? "") + pub.linkedin = Self.hexToString(p["linkedin"] as? String ?? "") + pub.instagram = Self.hexToString(p["instagram"] as? String ?? "") + pub.telegram = Self.hexToString(p["telegram"] as? String ?? "") + pub.discord = Self.hexToString(p["discord"] as? String ?? "") + return pub + } + } + onDone() + } + } + + private func loadDApps(shader: [Int], + filterUnwanted: Bool, + completion: @escaping ([BMAvailableDApp]) -> Void) { + guard let client = AppModel.sharedManager().walletAPIClient() else { + completion(bundledWithIcons()) + return + } + let args = "action=view_dapps,cid=\(DAppStore.storeCID)" + let params: [String: Any] = [ + "args": args, + "create_tx": false, + "contract": shader, + ] + client.call(method: "invoke_contract", params: params) { [weak self] (result: [AnyHashable: Any]) in + guard let self = self else { return } + if result["error"] != nil { + completion(self.bundledWithIcons()) + return + } + guard let raw = result["dapps"] as? [[String: Any]] else { + completion(self.bundledWithIcons()) + return + } + let unwanted = filterUnwanted ? self.getUnwantedPublishers() : Set() + let publisherNames: [String: String] = Dictionary( + self.cachedPublishers.map { ($0.pubkey.lowercased(), $0.name) }, + uniquingKeysWith: { first, _ in first } + ) + let parsed: [BMAvailableDApp] = raw.compactMap { item in + guard let guid = item["id"] as? String else { return nil } + let name = Self.hexToString(item["name"] as? String ?? "") + if name.isEmpty { return nil } + let publisherPk = (item["publisher"] as? String) ?? "" + if !publisherPk.isEmpty && unwanted.contains(publisherPk.lowercased()) { + return nil + } + + let dapp = BMAvailableDApp() + dapp.guid = guid + dapp.name = name + dapp.desc = Self.hexToString(item["description"] as? String ?? "") + dapp.ipfsCid = (item["ipfs_id"] as? String) ?? "" + dapp.publisher = publisherPk + dapp.publisherName = publisherNames[publisherPk.lowercased()] ?? "" + + if let ver = item["version"] as? [String: Any] { + let major = (ver["major"] as? NSNumber)?.intValue ?? 1 + let minor = (ver["minor"] as? NSNumber)?.intValue ?? 0 + let release = (ver["release"] as? NSNumber)?.intValue ?? 0 + dapp.version = "\(major).\(minor).\(release)" + } else { + dapp.version = "1.0.0" + } + + let iconHex = (item["icon"] as? String) ?? "" + if !iconHex.isEmpty { + let decoded = Self.hexToString(iconHex).trimmingCharacters(in: .whitespacesAndNewlines) + let prefix = "data:image/svg+xml;utf8," + let svg = decoded.hasPrefix(prefix) ? String(decoded.dropFirst(prefix.count)) : decoded + if svg.hasPrefix(" [BMAvailableDApp] { + for template in bundled where template.icon.isEmpty && !template.bundledAsset.isEmpty { + template.icon = extractIcon(fromBundledAsset: template.bundledAsset) + } + return bundled + } + + private func extractIcon(fromBundledAsset assetName: String) -> String { + let nameNS = assetName as NSString + guard let url = Bundle.main.url(forResource: nameNS.deletingPathExtension, + withExtension: nameNS.pathExtension) else { return "" } + let workDir = FileManager.default.temporaryDirectory + .appendingPathComponent("bundled-icon-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: workDir) } + try? FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true) + guard SSZipArchive.unzipFile(atPath: url.path, toDestination: workDir.path) else { return "" } + return DAppStore.firstIcon(in: workDir) ?? "" + } + + static func firstIcon(in dir: URL) -> String? { + for name in iconCandidates { + let p = dir.appendingPathComponent(name) + guard let svg = try? String(contentsOf: p, encoding: .utf8) else { continue } + let trimmed = svg.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.hasPrefix(" Void) { + guard let client = AppModel.sharedManager().walletAPIClient() else { + completion(nil, "Wallet API not ready") + return + } + let params: [String: Any] = ["hash": cid, "timeout": ipfsTimeoutMS] + client.call(method: "ipfs_get", params: params) { (result: [AnyHashable: Any]) in + if let err = result["error"] { + let message: String + if let dict = err as? [String: Any], let m = dict["message"] as? String { message = m } + else if let s = err as? String { message = s } + else { message = "IPFS download failed" } + completion(nil, message) + return + } + guard let bytes = result["data"] as? [NSNumber] else { + completion(nil, "No data returned from IPFS") + return + } + var buffer = Data(count: bytes.count) + buffer.withUnsafeMutableBytes { raw in + guard let base = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return } + for (i, num) in bytes.enumerated() { + base[i] = UInt8(truncatingIfNeeded: num.intValue) + } + } + completion(buffer, nil) + } + } + + // MARK: - Version comparison + + private static func parseVersion(_ v: String) -> [Int] { + return v.split(separator: ".").compactMap { Int($0) } + } + + @objc static func isVersionOlder(_ a: String, than b: String) -> Bool { + let av = parseVersion(a) + let bv = parseVersion(b) + let maxLen = max(av.count, bv.count) + for i in 0.. Void) { + queryAvailableDApps { [weak self] available in + guard let self = self, + let match = available.first(where: { $0.guid == dapp.guid }), + DAppStore.isVersionOlder(dapp.version, than: match.version) else { + completion(false) + return + } + self.install(match) { installed, _ in completion(installed != nil) } + } + } + + // MARK: - Install dispatch + + /// Installs an available DApp from either its bundled asset or via IPFS. + @objc func install(_ available: BMAvailableDApp, + completion: @escaping (BMInstalledDApp?, String?) -> Void) { + if !available.bundledAsset.isEmpty { + do { + let installed = try DAppManager.shared.installFromBundle(assetName: available.bundledAsset, + fallbackName: available.name) + completion(installed, nil) + } catch { + completion(nil, "\(error)") + } + return + } + if available.ipfsCid.isEmpty { + completion(nil, "No download source") + return + } + downloadFromIpfs(cid: available.ipfsCid) { data, err in + guard let data = data else { + completion(nil, err ?? "Download failed") + return + } + do { + let installed = try DAppManager.shared.installFromZip(data: data, + fallbackName: available.name, + fallbackIcon: available.icon) + completion(installed, nil) + } catch { + completion(nil, "\(error)") + } + } + } + + // MARK: - Unwanted publishers + + @objc func getUnwantedPublishers() -> Set { + let arr = UserDefaults.standard.stringArray(forKey: unwantedPublishersKey) ?? [] + return Set(arr.map { $0.lowercased() }) + } + + @objc func addUnwantedPublisher(_ pubkey: String) { + var set = getUnwantedPublishers() + set.insert(pubkey.lowercased()) + UserDefaults.standard.set(Array(set), forKey: unwantedPublishersKey) + } + + @objc func removeUnwantedPublisher(_ pubkey: String) { + var set = getUnwantedPublishers() + set.remove(pubkey.lowercased()) + UserDefaults.standard.set(Array(set), forKey: unwantedPublishersKey) + } + + // MARK: - Hex decode + + private static func hexToString(_ hex: String) -> String { + if hex.isEmpty { return "" } + var bytes: [UInt8] = [] + bytes.reserveCapacity(hex.count / 2) + var idx = hex.startIndex + while idx < hex.endIndex { + let next = hex.index(idx, offsetBy: 2, limitedBy: hex.endIndex) ?? hex.endIndex + if next == idx { break } + let pair = hex[idx.. Void)? + + public var chats: [BMChat] { + return AppModel.sharedManager().chats as? [BMChat] ?? [] + } + + override init() { + super.init() + AppModel.sharedManager().addDelegate(self) + } + + deinit { + AppModel.sharedManager().removeDelegate(self) + } + + func reload() { + AppModel.sharedManager().requestChats() + } + + func remove(at index: Int) { + guard index >= 0, index < chats.count else { return } + let peer = chats[index].peerWalletId + AppModel.sharedManager().removeChat(peer) + } +} + +extension MessengerChatListViewModel: WalletModelDelegate { + func onChatListChanged() { + DispatchQueue.main.async { [weak self] in + self?.onDataChanged?() + } + } + + func onInstantMessageReceived(_ message: BMInstantMessage) { + DispatchQueue.main.async { [weak self] in + self?.onDataChanged?() + } + } + + func onChatRemoved(_ peerWalletId: String) { + DispatchQueue.main.async { [weak self] in + self?.onDataChanged?() + } + } +} diff --git a/BeamWallet/Model/Messenger/MessengerConversationViewModel.swift b/BeamWallet/Model/Messenger/MessengerConversationViewModel.swift new file mode 100644 index 00000000..634b3dd3 --- /dev/null +++ b/BeamWallet/Model/Messenger/MessengerConversationViewModel.swift @@ -0,0 +1,92 @@ +// +// MessengerConversationViewModel.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class MessengerConversationViewModel: NSObject { + + public var onDataChanged: (() -> Void)? + + public let peerWalletId: String + public var myWalletId: String? + public private(set) var messages: [BMInstantMessage] = [] + + init(peerWalletId: String, myWalletId: String? = nil) { + self.peerWalletId = peerWalletId + let resolvedMyId = myWalletId + ?? AppModel.sharedManager().lastMyAddress(forPeer: peerWalletId) + ?? (AppModel.sharedManager().chats as? [BMChat])?.first { $0.peerWalletId == peerWalletId }?.myWalletId + ?? (AppModel.sharedManager().walletAddresses as? [BMAddress])?.first { !$0.isExpired() }?.walletId + self.myWalletId = resolvedMyId + super.init() + + let cached = AppModel.sharedManager().cachedMessages(forPeer: peerWalletId) as? [BMInstantMessage] ?? [] + messages = cached.sorted { $0.timestamp < $1.timestamp } + + AppModel.sharedManager().addDelegate(self) + } + + deinit { + AppModel.sharedManager().removeDelegate(self) + } + + func load() { + AppModel.sharedManager().requestMessages(forPeer: peerWalletId) + } + + func send(_ text: String) { + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + guard let myId = myWalletId, !myId.isEmpty else { return } + AppModel.sharedManager().sendInstantMessage(peerWalletId, fromAddress: myId, message: trimmed) + } + + func markAsRead() { + AppModel.sharedManager().markChat(asRead: peerWalletId) + } + + func remove() { + AppModel.sharedManager().removeChat(peerWalletId) + } +} + +extension MessengerConversationViewModel: WalletModelDelegate { + + func onChatMessagesLoaded(_ peerWalletId: String, messages: [BMInstantMessage]) { + guard peerWalletId == self.peerWalletId else { return } + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.messages = messages.sorted { $0.timestamp < $1.timestamp } + if self.myWalletId == nil || self.myWalletId?.isEmpty == true { + self.myWalletId = AppModel.sharedManager().lastMyAddress(forPeer: peerWalletId) + } + self.onDataChanged?() + } + } + + func onInstantMessageReceived(_ message: BMInstantMessage) { + guard message.peerWalletId == peerWalletId else { return } + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + let cached = AppModel.sharedManager().cachedMessages(forPeer: self.peerWalletId) as? [BMInstantMessage] ?? [] + self.messages = cached.sorted { $0.timestamp < $1.timestamp } + self.onDataChanged?() + } + } +} diff --git a/BeamWallet/Model/Objects/BMMultiLineItem.swift b/BeamWallet/Model/Objects/BMMultiLineItem.swift index 23e0e07d..dee92bea 100644 --- a/BeamWallet/Model/Objects/BMMultiLineItem.swift +++ b/BeamWallet/Model/Objects/BMMultiLineItem.swift @@ -2,7 +2,7 @@ // BMMultiLineItem.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,9 +35,9 @@ struct BMThreeLineItem { public var hasArrow:Bool public var expand = true - public var accessoryName:String? = nil + public var accessoryName:String? - public var customObject:Any? = nil + public var customObject:Any? } class BMMultiLineItem { @@ -47,9 +47,9 @@ class BMMultiLineItem { public var detailColor:UIColor? public var detailAttributedString:NSMutableAttributedString? public var canCopy = false - public var copiedText: String? = nil - public var copyValue: String? = nil - public var showCopyButton: Bool? = nil + public var copiedText: String? + public var copyValue: String? + public var showCopyButton: Bool? required init(title:String!, detail:String?, detailFont:UIFont?, detailColor:UIColor?) { self.title = title diff --git a/BeamWallet/Model/Objects/BMPickerData.swift b/BeamWallet/Model/Objects/BMPickerData.swift index 28420948..bd113e7a 100644 --- a/BeamWallet/Model/Objects/BMPickerData.swift +++ b/BeamWallet/Model/Objects/BMPickerData.swift @@ -2,7 +2,7 @@ // BMPickerData.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Model/OraclePriceManager.swift b/BeamWallet/Model/OraclePriceManager.swift new file mode 100644 index 00000000..f882d237 --- /dev/null +++ b/BeamWallet/Model/OraclePriceManager.swift @@ -0,0 +1,212 @@ +// +// OraclePriceManager.swift +// BeamWallet +// +// Polls the on-chain Oracle2 contract for BEAM/USD when the user has opted +// out of the centralised price feed (Settings.isOracleEnabled). Writes the +// median value into ExchangeManager's USD/BEAM slot using the same shape +// WalletModel::onExchangeRates would have produced. +// + +import Foundation +import UIKit + +@objc final class OraclePriceManager: NSObject { + + @objc static let shared = OraclePriceManager() + + // Nephrite stablecoin's Oracle2 instance — single BEAM/USD median feed. + static let oracleCID = "4f160f01dcc6751e61d793279b803328d5332125fe8492e93ee8f3bfe9abe13b" + + // Oracle2 returns price * 1e9; BMCurrency.value is stored as price * Rules::Coin (1e8). + private static let oracleNormalization: Double = 1_000_000_000 + private static let beamCoin: Double = 100_000_000 + + private static let refreshInterval: TimeInterval = 60 + + private var shaderBytes: [Int]? + private var refreshTimer: Timer? + private var running = false + private var inflight = false + private var lastPrice: Double? + private var didFireStatusRedraw = false + + private override init() { + super.init() + NotificationCenter.default.addObserver( + self, + selector: #selector(handleForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + // MARK: - Lifecycle + + @objc func start() { + guard !running else { + fetchPriceNow() + return + } + running = true + fetchPriceNow() + refreshTimer = Timer.scheduledTimer(withTimeInterval: OraclePriceManager.refreshInterval, + repeats: true) { [weak self] _ in + self?.fetchPriceNow() + } + } + + @objc func stop() { + running = false + refreshTimer?.invalidate() + refreshTimer = nil + lastPrice = nil + didFireStatusRedraw = false + } + + @objc private func handleForeground() { + if running { fetchPriceNow() } + } + + // MARK: - Fetch + + @objc func fetchPriceNow() { + fetch(retriesLeft: 6) + } + + private func fetch(retriesLeft: Int) { + guard Settings.sharedManager().isOracleEnabled else { return } + guard !inflight else { return } + guard let shader = loadShader() else { return } + guard let client = AppModel.sharedManager().walletAPIClient() else { + scheduleRetry(retriesLeft: retriesLeft) + return + } + + inflight = true + let args = "role=manager,action=view_median,cid=\(OraclePriceManager.oracleCID)" + let params: [String: Any] = [ + "args": args, + "create_tx": false, + "contract": shader, + ] + client.call(method: "invoke_contract", params: params) { [weak self] (result: [AnyHashable: Any]) in + self?.inflight = false + self?.apply(result: result, retriesLeft: retriesLeft) + } + } + + private func apply(result: [AnyHashable: Any], retriesLeft: Int) { + guard Settings.sharedManager().isOracleEnabled else { return } + if Self.isApiNotReady(result) { + scheduleRetry(retriesLeft: retriesLeft) + return + } + guard let price = Self.parsePrice(from: result), price > 0 else { return } + if let last = lastPrice, last == price { return } + writeUSD(price: price) + lastPrice = price + } + + private func scheduleRetry(retriesLeft: Int) { + guard retriesLeft > 0 else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in + self?.fetch(retriesLeft: retriesLeft - 1) + } + } + + private static func isApiNotReady(_ result: [AnyHashable: Any]) -> Bool { + guard let err = result["error"] as? [String: Any], + let msg = err["message"] as? String else { return false } + return msg.localizedCaseInsensitiveContains("not ready") + } + + private static func parsePrice(from result: [AnyHashable: Any]) -> Double? { + // Oracle2's view_median emits a Beam-manager string in result["output"]: + // {"res": ["val": ,"hEnd": }]} + // — note the square brackets with key:value pairs, which is not valid + // JSON. Extract the "val" number directly. + if let raw = (result["val"] as? NSNumber)?.doubleValue { + return raw / oracleNormalization + } + if let s = result["val"] as? String, let raw = Double(s) { + return raw / oracleNormalization + } + guard let output = result["output"] as? String, + let raw = extractVal(from: output) else { + return nil + } + return raw / oracleNormalization + } + + private static func extractVal(from output: String) -> Double? { + let pattern = #""val"\s*:\s*([0-9]+)"# + guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } + let range = NSRange(output.startIndex..., in: output) + guard let match = regex.firstMatch(in: output, range: range), + match.numberOfRanges >= 2, + let valRange = Range(match.range(at: 1), in: output) else { + return nil + } + return Double(output[valRange]) + } + + private func writeUSD(price: Double) { + let currencies = ExchangeManager.shared().currencies + + let raw = UInt64((price * OraclePriceManager.beamCoin).rounded()) + let usd = BMCurrencyType(BMCurrencyUSD) + + var found = false + for case let currency as BMCurrency in currencies + where currency.type == usd && currency.name == "BEAM" && currency.assetId == 0 { + currency.value = raw + currency.realValue = price + currency.code = "USD" + currency.maximumFractionDigits = 2 + found = true + break + } + + if !found { + let currency = BMCurrency() + currency.type = usd + currency.value = raw + currency.realValue = price + currency.code = "USD" + currency.name = "BEAM" + currency.maximumFractionDigits = 2 + currencies.add(currency) + } + + ExchangeManager.shared().changeCurrencies() + notifyExchangeRatesChanged() + } + + private func notifyExchangeRatesChanged() { + let delegates = AppModel.sharedManager().delegates.allObjects + // BMNetworkStatusView only re-renders its "rate not received" suffix on + // network-status callbacks. We need to nudge it once to drop the suffix + // after the first successful oracle write, but firing it every 60s + // makes the status bar re-debounce and flicker. + let nudgeStatus = !didFireStatusRedraw && AppModel.sharedManager().isConnected + for case let d as WalletModelDelegate in delegates { + d.onExchangeRatesChange?() + if nudgeStatus { d.onNetwotkStatusChange?(true) } + } + if nudgeStatus { didFireStatusRedraw = true } + } + + // MARK: - Shader + + private func loadShader() -> [Int]? { + if let bytes = shaderBytes { return bytes } + guard let url = Bundle.main.url(forResource: "oracle2-app", withExtension: "wasm"), + let data = try? Data(contentsOf: url) else { + return nil + } + let bytes = data.map { Int($0) } + shaderBytes = bytes + return bytes + } +} diff --git a/BeamWallet/Model/Settings/SettingsViewModel.swift b/BeamWallet/Model/Settings/SettingsViewModel.swift index 42ac0ea6..e9cfb0ef 100644 --- a/BeamWallet/Model/Settings/SettingsViewModel.swift +++ b/BeamWallet/Model/Settings/SettingsViewModel.swift @@ -2,7 +2,7 @@ // SettingsViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -60,13 +60,27 @@ class SettingsViewModel: NSObject { case random_node = 28 case currency = 29 case notifications = 30 - case offline_address = 31 - case max_privacy_limit = 32 - case rescan = 33 - case mobile_node = 34 - case utxo = 35 - case blockchain = 36 - case confirmations = 37 + case max_privacy_limit = 31 + case rescan = 32 + case mobile_node = 33 + case utxo = 34 + case blockchain = 35 + case confirmations = 36 + case sign_message = 37 + case verify_message = 38 + case node_peers = 39 + case node_type = 40 + case price_oracle = 41 + } + + private static func currentNodeTypeLabel() -> String { + if Settings.sharedManager().isNodeProtocolEnabled { + return Localizable.shared.strings.mobile_node_title + } + if !Settings.sharedManager().connectToRandomNode { + return Localizable.shared.strings.own_node_title + } + return Localizable.shared.strings.random_node_title } class SettingsItem { @@ -177,6 +191,7 @@ class SettingsViewModel: NSObject { allItems.append(SettingsItem(title: Localizable.shared.strings.allow_open_link, detail: nil, isSwitch: Settings.sharedManager().isAllowOpenLink, type: .allow_open_link, hasArrow: false)) allItems.append(SettingsItem(title: Localizable.shared.strings.lock_screen, detail: Settings.sharedManager().currentLocedValue().shortName, isSwitch: nil, type: .lock_screen, hasArrow: true)) allItems.append(SettingsItem(title: Localizable.shared.strings.show_amounts_in, detail: Settings.sharedManager().currencyName(), isSwitch: nil, type: .currency, hasArrow: true)) + allItems.append(SettingsItem(title: Localizable.shared.strings.use_on_chain_price_oracle, detail: nil, isSwitch: Settings.sharedManager().isOracleEnabled, type: .price_oracle, hasArrow: false)) allItems.append(SettingsItem(title: Localizable.shared.strings.min_confirmations, detail: "\(Settings.sharedManager().minConfirmations)", isSwitch: nil, type: .confirmations, hasArrow: true)) allItems.append(SettingsItem(title: Localizable.shared.strings.clear_local_data, detail: nil, isSwitch: nil, type: .clear_data, hasArrow: true)) @@ -201,18 +216,21 @@ class SettingsViewModel: NSObject { allItems.append(SettingsItem(title: Localizable.shared.strings.change_password, detail: nil, isSwitch: nil, type: .change_password, hasArrow: true)) allItems.append(SettingsItem(title: Localizable.shared.strings.blockchain_height, detail: "\(AppModel.sharedManager().walletStatus?.currentHeight ?? "")", isSwitch: nil, type: .blockchain, hasArrow: false)) - allItems.append(SettingsItem(title: Localizable.shared.strings.export_wallet_data, detail: nil, isSwitch: nil, type: .export, hasArrow: true)) - allItems.append(SettingsItem(title: Localizable.shared.strings.import_wallet_data, detail: nil, isSwitch: nil, type: .imprt, hasArrow: true)) allItems.append(SettingsItem(title: Localizable.shared.strings.show_utxo, detail: nil, isSwitch: nil, type: .utxo, hasArrow: true)) - + if AppModel.sharedManager().checkIsOwnNode() { allItems.append(SettingsItem(title: Localizable.shared.strings.rescan, detail: nil, isSwitch: nil, type: .rescan, hasArrow: false)) } - - allItems.append(SettingsItem(title: Localizable.shared.strings.clear_wallet.capitalizingFirstLetter(), detail: nil, isSwitch: nil, type: .remove_wallet, hasArrow: false)) + allItems.append(SettingsItem(title: Localizable.shared.strings.payment_proof, detail: nil, isSwitch: nil, type: .payment_proof, hasArrow: true)) - allItems.append(SettingsItem(title: Localizable.shared.strings.show_public_offline, detail: nil, isSwitch: nil, type: .offline_address, hasArrow: false)) allItems.append(SettingsItem(title: Localizable.shared.strings.get_beam_faucet, detail: nil, isSwitch: nil, type: .faucet, hasArrow: false)) + + allItems.append(SettingsItem(title: Localizable.shared.strings.sign_message, detail: nil, isSwitch: nil, type: .sign_message, hasArrow: true)) + allItems.append(SettingsItem(title: Localizable.shared.strings.verify_message, detail: nil, isSwitch: nil, type: .verify_message, hasArrow: true)) + + allItems.append(SettingsItem(title: Localizable.shared.strings.export_wallet_data, detail: nil, isSwitch: nil, type: .export, hasArrow: true)) + allItems.append(SettingsItem(title: Localizable.shared.strings.import_wallet_data, detail: nil, isSwitch: nil, type: .imprt, hasArrow: true)) + allItems.append(SettingsItem(title: Localizable.shared.strings.clear_wallet.capitalizingFirstLetter(), detail: nil, isSwitch: nil, type: .remove_wallet, hasArrow: false)) } private func initItems() { @@ -237,10 +255,9 @@ class SettingsViewModel: NSObject { section_0.append(SettingsItem(title: Localizable.shared.strings.allow_open_link, detail: nil, isSwitch: Settings.sharedManager().isAllowOpenLink, type: .allow_open_link, hasArrow: false)) section_0.append(SettingsItem(title: Localizable.shared.strings.lock_screen, detail: Settings.sharedManager().currentLocedValue().shortName, isSwitch: nil, type: .lock_screen, hasArrow: true)) section_0.append(SettingsItem(title: Localizable.shared.strings.show_amounts_in, detail: Settings.sharedManager().currencyName(), isSwitch: nil, type: .currency, hasArrow: true)) + section_0.append(SettingsItem(title: Localizable.shared.strings.use_on_chain_price_oracle, detail: nil, isSwitch: Settings.sharedManager().isOracleEnabled, type: .price_oracle, hasArrow: false)) section_0.append(SettingsItem(title: Localizable.shared.strings.min_confirmations, detail: "\(Settings.sharedManager().minConfirmations)", isSwitch: nil, type: .confirmations, hasArrow: true)) - - section_0.append(SettingsItem(title: Localizable.shared.strings.clear_local_data, detail: nil, isSwitch: nil, type: .clear_data, hasArrow: true)) - + var section_1 = [SettingsItem]() if ENALBE_LANG == true { section_1.append(SettingsItem(title: Localizable.shared.strings.language, detail: Settings.sharedManager().languageName(), isSwitch: nil, type: .language, hasArrow: true)) @@ -250,27 +267,10 @@ class SettingsViewModel: NSObject { items.append(section_1) case .node: var section_0 = [SettingsItem]() - section_0.append(SettingsItem(title: Localizable.shared.strings.random_node, detail: nil, isSwitch: Settings.sharedManager().connectToRandomNode, type: .random_node, hasArrow: false)) - section_0.append(SettingsItem(title: Localizable.shared.strings.ip_port, detail: Settings.sharedManager().nodeAddress, isSwitch: nil, type: .ip_port, hasArrow: true)) + section_0.append(SettingsItem(title: Localizable.shared.strings.node_type, detail: SettingsViewModel.currentNodeTypeLabel(), isSwitch: nil, type: .node_type, hasArrow: true)) + section_0.append(SettingsItem(title: Localizable.shared.strings.node_peers, detail: nil, isSwitch: nil, type: .node_peers, hasArrow: true)) + section_0.append(SettingsItem(title: Localizable.shared.strings.show_owner_key, detail: nil, isSwitch: nil, type: .show_owner_key, hasArrow: true)) items.append(section_0) - - let detail = NSMutableAttributedString(string: "\(Localizable.shared.strings.mobile_node_title)\nspace\n\(Localizable.shared.strings.mobile_node_text)") - - let rangeDetail = (detail.string as NSString).range(of: String(Localizable.shared.strings.mobile_node_text)) - let spaceRange = (detail.string as NSString).range(of: String("space")) - - detail.addAttribute(NSAttributedString.Key.font, value: ItalicFont(size: 14), range: rangeDetail) - detail.addAttribute(NSAttributedString.Key.foregroundColor, value: Settings.sharedManager().isDarkMode ? UIColor.main.steel : UIColor.main.steelGrey, range: rangeDetail) - - detail.addAttribute(NSAttributedString.Key.font, value: LightFont(size: 5), range: spaceRange) - detail.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: spaceRange) - - let mobileItem = SettingsItem(title: Localizable.shared.strings.mobile_node_title, detail: nil, isSwitch: Settings.sharedManager().isNodeProtocolEnabled, type: .mobile_node, hasArrow: false) - mobileItem.titleAttributed = detail - - var section_1 = [SettingsItem]() - section_1.append(mobileItem) - items.append(section_1) case .privacy: var section_0 = [SettingsItem]() section_0.append(SettingsItem(title: Localizable.shared.strings.ask_password, detail: nil, isSwitch: Settings.sharedManager().isNeedaskPasswordForSend, type: .ask_password, hasArrow: false)) @@ -282,36 +282,30 @@ class SettingsViewModel: NSObject { if OnboardManager.shared.isSkipedSeed() == true { section_0.append(SettingsItem(title: Localizable.shared.strings.complete_seed_verification, detail: nil, isSwitch: nil, type: .verification, hasArrow: true)) } - section_0.append(SettingsItem(title: Localizable.shared.strings.show_owner_key, detail: nil, isSwitch: nil, type: .show_owner_key, hasArrow: true)) - // if OnboardManager.shared.isSkipedSeed() == true { - // section_0.append(SettingsItem(title: Localizable.shared.strings.show_seed_phrase, detail: nil, isSwitch: nil, type: .show_seed, hasArrow: true)) - // } section_0.append(SettingsItem(title: Localizable.shared.strings.change_password, detail: nil, isSwitch: nil, type: .change_password, hasArrow: true)) items.append(section_0) case .utilites: - -// var section_0 = [SettingsItem]() -// section_0.append(SettingsItem(title: Localizable.shared.strings.blockchain_height, detail: "\(AppModel.sharedManager().walletStatus?.currentHeight ?? "")", isSwitch: nil, type: .blockchain, hasArrow: false)) - var section_1 = [SettingsItem]() - section_1.append(SettingsItem(title: Localizable.shared.strings.export_wallet_data, detail: nil, isSwitch: nil, type: .export, hasArrow: true)) - section_1.append(SettingsItem(title: Localizable.shared.strings.import_wallet_data, detail: nil, isSwitch: nil, type: .imprt, hasArrow: true)) section_1.append(SettingsItem(title: Localizable.shared.strings.show_utxo, detail: nil, isSwitch: nil, type: .utxo, hasArrow: true)) if AppModel.sharedManager().checkIsOwnNode() { section_1.append(SettingsItem(title: Localizable.shared.strings.rescan, detail: nil, isSwitch: nil, type: .rescan, hasArrow: false)) } - section_1.append(SettingsItem(title: Localizable.shared.strings.clear_wallet.capitalizingFirstLetter(), detail: nil, isSwitch: nil, type: .remove_wallet, hasArrow: false)) + section_1.append(SettingsItem(title: Localizable.shared.strings.payment_proof, detail: nil, isSwitch: nil, type: .payment_proof, hasArrow: true)) + section_1.append(SettingsItem(title: Localizable.shared.strings.get_beam_faucet, detail: nil, isSwitch: nil, type: .faucet, hasArrow: false)) - var section_2 = [SettingsItem]() - section_2.append(SettingsItem(title: Localizable.shared.strings.payment_proof, detail: nil, isSwitch: nil, type: .payment_proof, hasArrow: true)) - section_2.append(SettingsItem(title: Localizable.shared.strings.show_public_offline, detail: nil, isSwitch: nil, type: .offline_address, hasArrow: false)) - section_2.append(SettingsItem(title: Localizable.shared.strings.get_beam_faucet, detail: nil, isSwitch: nil, type: .faucet, hasArrow: false)) - + section_2.append(SettingsItem(title: Localizable.shared.strings.sign_message, detail: nil, isSwitch: nil, type: .sign_message, hasArrow: true)) + section_2.append(SettingsItem(title: Localizable.shared.strings.verify_message, detail: nil, isSwitch: nil, type: .verify_message, hasArrow: true)) + + var section_3 = [SettingsItem]() + section_3.append(SettingsItem(title: Localizable.shared.strings.export_wallet_data, detail: nil, isSwitch: nil, type: .export, hasArrow: true)) + section_3.append(SettingsItem(title: Localizable.shared.strings.import_wallet_data, detail: nil, isSwitch: nil, type: .imprt, hasArrow: true)) + section_3.append(SettingsItem(title: Localizable.shared.strings.clear_local_data, detail: nil, isSwitch: nil, type: .clear_data, hasArrow: true)) + section_3.append(SettingsItem(title: Localizable.shared.strings.clear_wallet.capitalizingFirstLetter(), detail: nil, isSwitch: nil, type: .remove_wallet, hasArrow: false)) - // items.append(section_0) items.append(section_1) items.append(section_2) + items.append(section_3) default: break @@ -348,12 +342,12 @@ class SettingsViewModel: NSObject { public func didSelectItem(item: SettingsItem) { switch item.type { - case .general, .privacy, .tags, .utilites: + case .general, .privacy, .tags, .utilites, .node: if let top = UIApplication.getTopMostViewController() { let vc = SettingsViewController(type: SettingsType(rawValue: item.type.rawValue)!) top.pushViewController(vc: vc) } - case .node: + case .node_type: if let top = UIApplication.getTopMostViewController() { let vc = SelectNodeViewController() top.pushViewController(vc: vc) @@ -396,8 +390,6 @@ class SettingsViewModel: NSObject { onCurrencyScreen() case .notifications: onNotifications() - case .offline_address: - onOfflineAddress() case .max_privacy_limit: onLockLimit() case .rescan: @@ -406,6 +398,11 @@ class SettingsViewModel: NSObject { onShowUTXO() case .confirmations: onConfirmationsScreen() + case .node_peers: + if let top = UIApplication.getTopMostViewController() { + let vc = NodePeersViewController() + top.pushViewController(vc: vc) + } default: return } @@ -489,8 +486,7 @@ extension SettingsViewModel { func onLockLimit() { if let top = UIApplication.getTopMostViewController() { let vc = BMDataPickerViewController(type: .max_privacy_lock) - vc.completion = { [weak self] - _ in + vc.completion = { [weak self] _ in self?.items.removeAll() self?.initItems() self?.onDataChanged?() @@ -502,8 +498,7 @@ extension SettingsViewModel { func onConfirmationsScreen() { if let top = UIApplication.getTopMostViewController() { let vc = BMDataPickerViewController(type: .confirmations) - vc.completion = { [weak self] - value in + vc.completion = { [weak self] value in AppModel.sharedManager().setMinConfirmations(value as! UInt32) self?.items.removeAll() self?.initItems() @@ -516,16 +511,16 @@ extension SettingsViewModel { func onRescan() { if let top = UIApplication.getTopMostViewController() { top.confirmAlert(title: Localizable.shared.strings.rescan, message: Localizable.shared.strings.rescan_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.rescan, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in AppModel.sharedManager().rescan() - + let vc = OpenWalletProgressViewController(onlyConnect: true) vc.isRescan = true vc.cancelCallback = { } top.pushViewController(vc: vc) - } + }) } } @@ -535,8 +530,7 @@ extension SettingsViewModel { modalViewController.completion = { [weak self] obj in if obj { let vc = TrustedNodeViewController(event: .change) - vc.completion = { [weak self] - obj in + vc.completion = { [weak self] obj in if obj == true { self?.items[0][1].detail = Settings.sharedManager().nodeAddress } @@ -591,22 +585,6 @@ extension SettingsViewModel { } } - func onOfflineAddress() { - let isOwn = AppModel.sharedManager().checkIsOwnNode() - if isOwn { - if let top = UIApplication.getTopMostViewController() { - let vc = OfflineAddressViewController() - top.pushViewController(vc: vc) - } - } - else { - if let top = UIApplication.getTopMostViewController() { - top.alert(title: Localizable.shared.strings.show_public_offline, message: Localizable.shared.strings.connect_node_offline_public, handler: nil) - } - } - - } - func onLockScreen() { if let top = UIApplication.getTopMostViewController() { let vc = BMDataPickerViewController(type: .lock) @@ -644,14 +622,14 @@ extension SettingsViewModel { } func showSeed() { - if let _ = OnboardManager.shared.getSeed(), let top = UIApplication.getTopMostViewController() { + if OnboardManager.shared.getSeed() != nil, let top = UIApplication.getTopMostViewController() { let vc = BMDoubleAuthViewController(event: .seed) top.pushViewController(vc: vc) } } - + func makeSecure() { - if let _ = OnboardManager.shared.getSeed(), let top = UIApplication.getTopMostViewController() { + if OnboardManager.shared.getSeed() != nil, let top = UIApplication.getTopMostViewController() { let vc = BMDoubleAuthViewController(event: .verification) top.pushViewController(vc: vc) } @@ -669,10 +647,10 @@ extension SettingsViewModel { } else { top.confirmAlert(title: Localizable.shared.strings.external_link_title, message: Localizable.shared.strings.external_link_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.open, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in BMOverlayTimerView.show(text: Localizable.shared.strings.faucet_redirect_text, link: result) - } + }) } } } @@ -686,8 +664,8 @@ extension SettingsViewModel { } else { top.confirmAlert(title: Localizable.shared.strings.clear_wallet, message: Localizable.shared.strings.clear_wallet_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.remove_wallet, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in let modalViewController = UnlockPasswordPopover(event: .clear_wallet, allowBiometric: false) modalViewController.completion = { obj in if obj { @@ -698,7 +676,7 @@ extension SettingsViewModel { modalViewController.modalPresentationStyle = .overFullScreen modalViewController.modalTransitionStyle = .crossDissolve top.present(modalViewController, animated: true, completion: nil) - } + }) } } } @@ -737,8 +715,8 @@ extension SettingsViewModel: UIDocumentPickerDelegate, UINavigationControllerDel func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { if let topVC = UIApplication.getTopMostViewController() { topVC.confirmAlert(title: Localizable.shared.strings.import_data_title, message: Localizable.shared.strings.import_data_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.imprt, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in if let url = urls.first { do { let data = try String(contentsOf: url) @@ -756,7 +734,7 @@ extension SettingsViewModel: UIDocumentPickerDelegate, UINavigationControllerDel topVC.alert(title: Localizable.shared.strings.incorrect_file_title, message: Localizable.shared.strings.incorrect_file_text, handler: nil) } } - } + }) } } diff --git a/BeamWallet/Model/Transaction/DetailTransactionViewModel.swift b/BeamWallet/Model/Transaction/DetailTransactionViewModel.swift index 1f2c0ad0..ce80c376 100644 --- a/BeamWallet/Model/Transaction/DetailTransactionViewModel.swift +++ b/BeamWallet/Model/Transaction/DetailTransactionViewModel.swift @@ -2,7 +2,7 @@ // DetailTransactionViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -264,7 +264,7 @@ class DetailTransactionViewModel: TransactionViewModel { if let tr = self.transaction, let top = UIApplication.getTopMostViewController() { let activityItem: [String] = [tr.textDetails()] let vc = UIActivityViewController(activityItems: activityItem, applicationActivities: []) - vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in + vc.completionWithItemsHandler = {(_: UIActivity.ActivityType?, _: Bool, _: [Any]?, _: Error?) in } vc.excludedActivityTypes = [UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.print,UIActivity.ActivityType.openInIBooks] @@ -300,7 +300,7 @@ class DetailTransactionViewModel: TransactionViewModel { if let image = shareView.snapshot(scale: false) { let activityItem: [AnyObject] = [image] let vc = UIActivityViewController(activityItems: activityItem, applicationActivities: []) - vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in + vc.completionWithItemsHandler = {(_: UIActivity.ActivityType?, _: Bool, _: [Any]?, _: Error?) in } vc.excludedActivityTypes = [UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.print,UIActivity.ActivityType.openInIBooks] @@ -313,7 +313,7 @@ class DetailTransactionViewModel: TransactionViewModel { public func saveContact() { if let top = UIApplication.getTopMostViewController() { let transaction = self.transaction! - var address:String? = nil + var address:String? if transaction.isIncome { address = transaction.senderAddress diff --git a/BeamWallet/Model/Transaction/SendTransactionViewModel.swift b/BeamWallet/Model/Transaction/SendTransactionViewModel.swift index f1adf596..7c6a1647 100644 --- a/BeamWallet/Model/Transaction/SendTransactionViewModel.swift +++ b/BeamWallet/Model/Transaction/SendTransactionViewModel.swift @@ -2,7 +2,7 @@ // SendTransactionViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -62,9 +62,7 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { } public var isNeedDisplaySegmentCell:Bool { - get { - return addressType == BMAddressTypeShielded && isToken - } + return addressType == BMAddressTypeShielded && isToken } public var maxPrivacy:Bool = false @@ -72,29 +70,25 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { public var selectedAssetId = 0 public var selectedCurrencyString: String { - get { - return AssetsManager.shared().getAsset(Int32(selectedAssetId))?.unitName ?? "" - } + return AssetsManager.shared().getAsset(Int32(selectedAssetId))?.unitName ?? "" } - + public var secondAmount:String { - get { - return ExchangeManager.shared().exchangeValueAsset(Double(inputAmount) ?? 0, assetID: UInt64(selectedAssetId)) - } + return ExchangeManager.shared().exchangeValueAsset(Double(inputAmount) ?? 0, assetID: UInt64(selectedAssetId)) } public func calculateFee() { let isShielded = (addressType == BMAddressTypeOfflinePublic || addressType == BMAddressTypeMaxPrivacy || isSendOffline) - //addressType == BMAddressTypeShielded || + // addressType == BMAddressTypeShielded || var assetName = (AssetsManager.shared().getAsset(Int32(self.selectedAssetId))?.unitName ?? "") + " " if assetName == "assets" { assetName = "BEAM" } - AppModel.sharedManager().calculateFee(Double(amount) ?? 0, assetId: Int32(selectedAssetId), fee: (Double(fee) ?? 0), isShielded: isShielded) { (result, changed, shieldedInputsFee, max) in + AppModel.sharedManager().calculateFee(Double(amount) ?? 0, assetId: Int32(selectedAssetId), fee: (Double(fee) ?? 0), isShielded: isShielded) { (result, _, shieldedInputsFee, max) in DispatchQueue.main.async { self.shieldedInputsFee = shieldedInputsFee let current = UInt64(self.fee) ?? 0 @@ -204,7 +198,7 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { isToken = false - requestedMaxPrivacy = false; + requestedMaxPrivacy = false maxPrivacy = false calculateFee() } @@ -231,12 +225,12 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { } } public var amount: String { - set { - inputAmount = newValue - } get { return getAmount } + set { + inputAmount = newValue + } } public var getAmount: String { @@ -279,22 +273,20 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { } var isNeedFocus:Bool { - get { - if !isFocused && transaction == nil { - isFocused = true - - if let address = UIPasteboard.general.string { - if AppModel.sharedManager().isValidAddress(address) - { - copyAddress = address - - return true - } + if !isFocused && transaction == nil { + isFocused = true + + if let address = UIPasteboard.general.string { + if AppModel.sharedManager().isValidAddress(address) + { + copyAddress = address + + return true } } - - return false } + + return false } @@ -313,7 +305,7 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { } private func generateOutgoindAddress() { - AppModel.sharedManager().generateNewWalletAddress { (address, error) in + AppModel.sharedManager().generateNewWalletAddress { (address, _) in if let result = address { DispatchQueue.main.async { self.outgoindAdderss = result @@ -526,7 +518,7 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { public func isNeedSaveContact() -> Bool { var address = toAddress - var identity:String? = nil + var identity:String? if (AppModel.sharedManager().isToken(address)) { let params = AppModel.sharedManager().getTransactionParameters(address) @@ -549,7 +541,7 @@ class SendTransactionViewModel: NSObject, WalletModelDelegate { let isShielded = (addressType == BMAddressTypeShielded || addressType == BMAddressTypeOfflinePublic || addressType == BMAddressTypeMaxPrivacy || isSendOffline) - AppModel.sharedManager().calculateFee((Double(amount) ?? 0), assetId: Int32(selectedAssetId), fee: (Double(fee) ?? 0), isShielded: isShielded) { (fee, change, shieldedInputsFee, max) in + AppModel.sharedManager().calculateFee((Double(amount) ?? 0), assetId: Int32(selectedAssetId), fee: (Double(fee) ?? 0), isShielded: isShielded) { (_, change, _, _) in self.onCalculateChanged?(change) } } diff --git a/BeamWallet/Model/Transaction/TransactionViewModel.swift b/BeamWallet/Model/Transaction/TransactionViewModel.swift index 20fca9a1..a219c1ee 100644 --- a/BeamWallet/Model/Transaction/TransactionViewModel.swift +++ b/BeamWallet/Model/Transaction/TransactionViewModel.swift @@ -2,7 +2,7 @@ // TransactionViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ class TransactionViewModel: NSObject { } } - public var assetId:Int32? = nil { + public var assetId:Int32? { didSet { if let id = assetId { self.transactions = self.transactions.filter { tr in @@ -105,7 +105,7 @@ class TransactionViewModel: NSObject { AppModel.sharedManager().removeDelegate(self) } -//MARK: - Actions +// MARK: - Actions public func cancelTransation(indexPath:IndexPath?) { let transaction:BMTransaction = (indexPath == nil ? self.transaction! : transactions[indexPath!.row]) @@ -161,17 +161,17 @@ class TransactionViewModel: NSObject { public func trailingSwipeActions(indexPath:IndexPath) -> UISwipeActionsConfiguration? { let transaction = transactions[indexPath.row] - let cancel = UIContextualAction(style: .normal, title: nil) { (action, view, handler) in + let cancel = UIContextualAction(style: .normal, title: nil) { (_, _, handler) in handler(true) self.cancelTransation(indexPath: indexPath) } cancel.image = IconRowCancel() cancel.backgroundColor = UIColor.main.cerulean - var rep:UIContextualAction? = nil + var rep:UIContextualAction? if !transaction.isShielded { - rep = UIContextualAction(style: .normal, title: nil) { (action, view, handler) in + rep = UIContextualAction(style: .normal, title: nil) { (_, _, handler) in handler(true) self.repeatTransation(transaction: transaction) } @@ -179,7 +179,7 @@ class TransactionViewModel: NSObject { rep!.backgroundColor = UIColor.main.deepSeaBlue } - let delete = UIContextualAction(style: .normal, title: nil) { (action, view, handler) in + let delete = UIContextualAction(style: .normal, title: nil) { (_, _, handler) in handler(true) self.deleteTransation(indexPath: indexPath) } @@ -206,7 +206,7 @@ class TransactionViewModel: NSObject { } } -//MARK: - Delegate +// MARK: - Delegate extension TransactionViewModel : WalletModelDelegate { diff --git a/BeamWallet/Model/UTXO/AssetUTXOGroup.swift b/BeamWallet/Model/UTXO/AssetUTXOGroup.swift new file mode 100644 index 00000000..d8f49be4 --- /dev/null +++ b/BeamWallet/Model/UTXO/AssetUTXOGroup.swift @@ -0,0 +1,49 @@ +// +// AssetUTXOGroup.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 AssetUTXOGroup { + let asset: BMAsset + let utxos: [BMUTXO] + let largestUtxo: BMUTXO? + let totalAvailableGroth: UInt64 + + var assetId: Int32 { Int32(asset.assetId) } + + var concentrationRatio: Double { + guard totalAvailableGroth > 0, let largest = largestUtxo else { return 0 } + return Double(largest.amount) / Double(totalAvailableGroth) + } + + var isConcentrated: Bool { concentrationRatio > 0.7 } + + var canSplit: Bool { (largestUtxo?.amount ?? 0) >= 2 } + + static func make(asset: BMAsset, utxos: [BMUTXO]) -> AssetUTXOGroup { + let sorted = utxos.sorted { $0.amount > $1.amount } + let total = sorted.reduce(UInt64(0)) { $0 + $1.amount } + return AssetUTXOGroup( + asset: asset, + utxos: sorted, + largestUtxo: sorted.first, + totalAvailableGroth: total + ) + } +} diff --git a/BeamWallet/Model/UTXO/DetailUTXOViewModel.swift b/BeamWallet/Model/UTXO/DetailUTXOViewModel.swift index 87d9d881..d5cf960f 100644 --- a/BeamWallet/Model/UTXO/DetailUTXOViewModel.swift +++ b/BeamWallet/Model/UTXO/DetailUTXOViewModel.swift @@ -2,7 +2,7 @@ // DetailUTXOViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Model/UTXO/SplitCoinsViewModel.swift b/BeamWallet/Model/UTXO/SplitCoinsViewModel.swift new file mode 100644 index 00000000..b7650ceb --- /dev/null +++ b/BeamWallet/Model/UTXO/SplitCoinsViewModel.swift @@ -0,0 +1,274 @@ +// +// SplitCoinsViewModel.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +final class SplitCoinsViewModel: NSObject { + + enum Mode { + case split + case consolidate + } + + let group: AssetUTXOGroup + let mode: Mode + let allowedSplitCounts: [Int] = [2, 3, 5, 8, 10] + + var splitInto: Int { + didSet { + if splitInto != oldValue { + recalculateFee() + onDataChanged?() + } + } + } + + private(set) var feeGroth: UInt64 = 0 + + // When on, the operation is routed through a Lelantus push tx to a + // self-generated offline address instead of splitting/consolidating in + // the public pool — breaking input/output linkability and growing the + // shielded anonymity set. + var sendOffline: Bool = false { + didSet { + if sendOffline != oldValue { + recalculateFee() + onDataChanged?() + } + } + } + + var onDataChanged: (() -> Void)? + var onError: ((String) -> Void)? + var onSubmitted: (() -> Void)? + + init(group: AssetUTXOGroup, mode: Mode = .split) { + self.group = group + self.mode = mode + let suggested = SplitCoinsViewModel.suggestedSplitCount(for: group, allowed: [2, 3, 5, 8, 10]) + self.splitInto = (mode == .consolidate) ? 1 : (group.isConcentrated ? suggested : 3) + super.init() + feeGroth = UInt64(AppModel.sharedManager().getDefaultFeeInGroth()) + } + + // Lelantus push: applies to both modes. In split-offline the largest + // UTXO is pushed into the shielded pool; in consolidate-offline the + // entire available pool is pushed. Either way the on-chain result is a + // single shielded output, so the split-into selector is meaningless. + var isShielded: Bool { sendOffline } + + var assetId: Int32 { group.assetId } + var largestGroth: UInt64 { group.largestUtxo?.amount ?? 0 } + + // The pool we're rebalancing: largest UTXO for split, total available for + // consolidate (coin-selection picks the actual inputs in either case). + private var sourceGroth: UInt64 { + mode == .consolidate ? group.totalAvailableGroth : largestGroth + } + + var outputAmounts: [UInt64] { + guard sourceGroth > 0 else { return [] } + + // Shielded path is the same shape regardless of mode: a single + // shielded output equal to the source minus the BEAM fee (or the + // full source for non-BEAM assets, where the fee is paid separately). + if isShielded { + if assetId == 0 { + guard sourceGroth > feeGroth else { return [] } + return [sourceGroth - feeGroth] + } + return [sourceGroth] + } + + switch mode { + case .consolidate: + guard group.utxos.count >= 2 else { return [] } + if assetId == 0 { + guard sourceGroth > feeGroth else { return [] } + return [sourceGroth - feeGroth] + } + return [sourceGroth] + + case .split: + guard splitInto > 0 else { return [] } + let count = UInt64(splitInto) + let base = sourceGroth / count + let remainder = sourceGroth - base * count + var arr = Array(repeating: base, count: splitInto) + if !arr.isEmpty { + arr[arr.count - 1] += remainder + } + // For BEAM (assetId == 0) the fee is paid in BEAM from the same UTXO + // we are splitting. If we keep the slices summing to sourceGroth the + // wallet has no headroom for the fee; coin-selection then silently + // fails. Subtract the fee from the last slice. Caller is gated on + // validationError, which already guards `sourceGroth > feeGroth`, so + // the subtraction is safe; the > 0 guard is belt-and-braces. + if assetId == 0 && !arr.isEmpty && feeGroth > 0 { + let last = arr[arr.count - 1] + guard last > feeGroth else { return [] } + arr[arr.count - 1] = last - feeGroth + } + return arr + } + } + + var perOutputGroth: UInt64 { outputAmounts.first ?? 0 } + + var concentrationWarning: String? { + guard mode == .split, !isShielded, group.isConcentrated else { return nil } + let percent = Int((group.concentrationRatio * 100).rounded()) + let suggested = SplitCoinsViewModel.suggestedSplitCount(for: group, allowed: allowedSplitCounts) + return Localizable.shared.strings.split_concentration_warning_format + .replacingOccurrences(of: "(percent)", with: "\(percent)") + .replacingOccurrences(of: "(count)", with: "\(suggested)") + } + + var validationError: String? { + if isShielded { + if assetId == 0 && sourceGroth <= feeGroth { + return Localizable.shared.strings.asset_swap_insufficient_funds + } + return nil + } + if mode == .consolidate { + if group.utxos.count < 2 { return "" } + if assetId == 0 && sourceGroth <= feeGroth { + return Localizable.shared.strings.asset_swap_insufficient_funds + } + return nil + } + if sourceGroth < UInt64(splitInto) { + return Localizable.shared.strings.split_too_small + } + if assetId == 0 && sourceGroth <= feeGroth { + return Localizable.shared.strings.asset_swap_insufficient_funds + } + // For BEAM the fee comes off the last slice (see outputAmounts); if + // the per-split base + remainder doesn't leave room for the fee, the + // resulting tx would be invalid. + if assetId == 0 && splitInto > 0 { + let count = UInt64(splitInto) + let base = sourceGroth / count + let remainder = sourceGroth - base * count + if base + remainder <= feeGroth { + return Localizable.shared.strings.asset_swap_insufficient_funds + } + } + return nil + } + + var canSubmit: Bool { + guard validationError == nil, sourceGroth > 0 else { return false } + return mode == .consolidate ? group.utxos.count >= 2 : group.canSplit + } + + func recalculateFee() { + let realAmount = Double(sourceGroth) / 100_000_000 + let app = AppModel.sharedManager() + let defaultFee = isShielded + ? Double(app.getMinMaxPrivacyFeeInGroth()) + : Double(app.getDefaultFeeInGroth()) + app.calculateFee( + realAmount, + assetId: assetId, + fee: defaultFee, + isShielded: isShielded + ) { [weak self] fee, _, _, _ in + DispatchQueue.main.async { + guard let self = self else { return } + self.feeGroth = fee + self.onDataChanged?() + } + } + } + + func submit() { + guard canSubmit else { + onError?(validationError ?? "") + return + } + if isShielded { + submitShielded() + return + } + let groths = outputAmounts.map { NSNumber(value: $0) } + AppModel.sharedManager().splitCoins( + assetId, + outputGroths: groths, + fee: Double(feeGroth) + ) + onSubmitted?() + } + + private func submitShielded() { + let outGroth = perOutputGroth + guard outGroth > 0 else { + onError?(validationError ?? Localizable.shared.strings.error) + return + } + let realAmount = Double(outGroth) / 100_000_000 + let assetIdCopy = assetId + let feeReal = Double(feeGroth) + + // Reuse the wallet's default own address (created on demand by + // generateToken / getDefaultAddressAlways). `_id` is the hex WalletID + // that generateOfflineAddress expects; the public `walletId` getter + // returns the token string and would fail FromHex parsing. + AppModel.sharedManager().generateNewWalletAddress { [weak self] address, _ in + DispatchQueue.main.async { + guard let self = self, + let walletIdHex = address?._id, !walletIdHex.isEmpty else { + self?.onError?(Localizable.shared.strings.error) + return + } + AppModel.sharedManager().generateOfflineAddress( + walletIdHex, + assetId: assetIdCopy, + amount: realAmount, + offlineCount: 1 + ) { token in + DispatchQueue.main.async { + guard !token.isEmpty else { + self.onError?(Localizable.shared.strings.error) + return + } + AppModel.sharedManager().send( + realAmount, + fee: feeReal, + assetId: assetIdCopy, + to: token, + from: walletIdHex, + comment: "", + isOffline: true + ) + self.onSubmitted?() + } + } + } + } + } + + private static func suggestedSplitCount(for group: AssetUTXOGroup, allowed: [Int]) -> Int { + guard group.totalAvailableGroth > 0 else { return allowed.first ?? 2 } + let ratio = group.concentrationRatio + let target = max(2, Int((ratio / 0.3).rounded(.up))) + return allowed.last(where: { $0 <= target }) ?? allowed.last ?? 5 + } +} diff --git a/BeamWallet/Model/UTXO/UTXOViewModel.swift b/BeamWallet/Model/UTXO/UTXOViewModel.swift index 50d99170..b6228c00 100644 --- a/BeamWallet/Model/UTXO/UTXOViewModel.swift +++ b/BeamWallet/Model/UTXO/UTXOViewModel.swift @@ -2,7 +2,7 @@ // UTXOViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -41,7 +41,25 @@ class UTXOViewModel: NSObject { public var onStatusChanged : (() -> Void)? public var utxos = [BMUTXO]() - + + public func groupedByAsset() -> [AssetUTXOGroup] { + var pool = [BMUTXO]() + if let utxos = AppModel.sharedManager().utxos { + pool.append(contentsOf: utxos as! [BMUTXO]) + } + if let utxos = AppModel.sharedManager().shildedUtxos { + pool.append(contentsOf: utxos as! [BMUTXO]) + } + let available = pool.filter { $0.status == BMUTXOAvailable } + let byAsset = Dictionary(grouping: available, by: { $0.assetId }) + + return byAsset.compactMap { (assetId, utxos) -> AssetUTXOGroup? in + guard let asset = AssetsManager.shared().getAsset(Int32(assetId)) else { return nil } + return AssetUTXOGroup.make(asset: asset, utxos: utxos) + } + .sorted { $0.asset.assetId < $1.asset.assetId } + } + override init() { super.init() @@ -54,8 +72,7 @@ class UTXOViewModel: NSObject { private func filterUTXOS() { - DispatchQueue.main.async { - [weak self] in + DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } diff --git a/BeamWallet/Model/Wallet/AssetViewModel.swift b/BeamWallet/Model/Wallet/AssetViewModel.swift index 51a11811..b07b5486 100644 --- a/BeamWallet/Model/Wallet/AssetViewModel.swift +++ b/BeamWallet/Model/Wallet/AssetViewModel.swift @@ -2,7 +2,7 @@ // AssetViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -75,17 +75,14 @@ class AssetViewModel: NSObject { self.assets.sort { a1, a2 in return a1.dateUsed() > a2.dateUsed() } - break case .old_recent: self.assets.sort { a1, a2 in return a1.dateUsed() < a2.dateUsed() } - break case .amount_large_small: self.assets.sort { a1, a2 in return a1.realAmount > a2.realAmount } - break case .amount_small_large: self.assets.sort { a1, a2 in return a1.realAmount < a2.realAmount @@ -156,7 +153,7 @@ class AssetViewModel: NSObject { return result } - public func getAssetBalanceInfo(asset:BMAsset)-> [[BMThreeLineItem]] { + public func getAssetBalanceInfo(asset:BMAsset) -> [[BMThreeLineItem]] { var result = [[BMThreeLineItem]]() var section_1 = [BMThreeLineItem]() @@ -203,7 +200,7 @@ class AssetViewModel: NSObject { } } -//MARK: - Delegate +// MARK: - Delegate extension AssetViewModel : WalletModelDelegate { diff --git a/BeamWallet/Model/Wallet/StatusViewModel.swift b/BeamWallet/Model/Wallet/StatusViewModel.swift index 48f75f33..e34f04a6 100644 --- a/BeamWallet/Model/Wallet/StatusViewModel.swift +++ b/BeamWallet/Model/Wallet/StatusViewModel.swift @@ -2,7 +2,7 @@ // StatusViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Protocols/Configurable.swift b/BeamWallet/Protocols/Configurable.swift index 47b1a3ac..aa320ecd 100644 --- a/BeamWallet/Protocols/Configurable.swift +++ b/BeamWallet/Protocols/Configurable.swift @@ -2,7 +2,7 @@ // Configurable.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Protocols/Fonts.swift b/BeamWallet/Protocols/Fonts.swift index 93c3a298..bbde8b91 100644 --- a/BeamWallet/Protocols/Fonts.swift +++ b/BeamWallet/Protocols/Fonts.swift @@ -2,7 +2,7 @@ // Fonts.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Protocols/Frames.swift b/BeamWallet/Protocols/Frames.swift index 019cac3f..c647446d 100644 --- a/BeamWallet/Protocols/Frames.swift +++ b/BeamWallet/Protocols/Frames.swift @@ -2,7 +2,7 @@ // Frames.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/Protocols/KeyboardListener.swift b/BeamWallet/Protocols/KeyboardListener.swift index ffc46325..3e1da5e0 100644 --- a/BeamWallet/Protocols/KeyboardListener.swift +++ b/BeamWallet/Protocols/KeyboardListener.swift @@ -2,7 +2,7 @@ // KeyboardListener.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ import Foundation -import Foundation - class KeyboardListener: NSObject { static let shared = KeyboardListener() diff --git a/BeamWallet/Protocols/LocalizableStrings.swift b/BeamWallet/Protocols/LocalizableStrings.swift index 3d9eeba9..a8d679c8 100644 --- a/BeamWallet/Protocols/LocalizableStrings.swift +++ b/BeamWallet/Protocols/LocalizableStrings.swift @@ -2,7 +2,7 @@ // Localizable.shared.strings.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -63,6 +63,46 @@ class LocalizableStrings : NSObject { var incorrect_password = "incorrect_password".localized var create_new_wallet = "create_new_wallet".localized var wallet = "wallet".localized + var messenger = "messenger".localized + var messenger_new_chat = "messenger_new_chat".localized + var messenger_no_chats = "messenger_no_chats".localized + var messenger_send_placeholder = "messenger_send_placeholder".localized + var messenger_delete_chat = "messenger_delete_chat".localized + var messenger_my_address = "messenger_my_address".localized + var messenger_peer_address = "messenger_peer_address".localized + var messenger_contact_name = "messenger_contact_name".localized + var messenger_start = "messenger_start".localized + var asset_swaps = "asset_swaps".localized + var asset_swap_new = "asset_swap_new".localized + var asset_swap_open_orders = "asset_swap_open_orders".localized + var asset_swap_my_orders = "asset_swap_my_orders".localized + var asset_swap_history = "asset_swap_history".localized + var asset_swap_send = "asset_swap_send".localized + var asset_swap_receive = "asset_swap_receive".localized + var asset_swap_send_amount = "asset_swap_send_amount".localized + var asset_swap_receive_amount = "asset_swap_receive_amount".localized + var asset_swap_expiration = "asset_swap_expiration".localized + var asset_swap_publish = "asset_swap_publish".localized + var asset_swap_accept = "asset_swap_accept".localized + var asset_swap_cancel_order = "asset_swap_cancel_order".localized + var asset_swap_no_orders = "asset_swap_no_orders".localized + var asset_swap_expired = "asset_swap_expired".localized + var asset_swap_completed = "asset_swap_completed".localized + var asset_swap_canceled = "asset_swap_canceled".localized + var asset_swap_details = "asset_swap_details".localized + var asset_swap_rate = "asset_swap_rate".localized + var asset_swap_status = "asset_swap_status".localized + var asset_swap_pick_send_asset = "asset_swap_pick_send_asset".localized + var asset_swap_pick_receive_asset = "asset_swap_pick_receive_asset".localized + var asset_swap_insufficient_funds = "asset_swap_insufficient_funds".localized + var asset_swap_same_asset_error = "asset_swap_same_asset_error".localized + var asset_swap_confirm_cancel = "asset_swap_confirm_cancel".localized + var asset_swap_created = "asset_swap_created".localized + var asset_swap_expires_in = "asset_swap_expires_in".localized + var asset_swap_peer_id = "asset_swap_peer_id".localized + var asset_swap_order_id = "asset_swap_order_id".localized + var asset_swap_show_details = "asset_swap_show_details".localized + var asset_swap_hide_details = "asset_swap_hide_details".localized var transactions = "transactions".localized var export = "export".localized var activate = "activate".localized @@ -89,6 +129,9 @@ class LocalizableStrings : NSObject { var error = "error".localized var wallet_not_created = "wallet_not_created".localized var wallet_not_opened = "wallet_not_opened".localized + var wallet_recovery_title = "wallet_recovery_title".localized + var wallet_recovery_message = "wallet_recovery_message".localized + var reset_and_retry = "reset_and_retry".localized var external_link_title = "external_link_title".localized var external_link_text = "external_link_text".localized var open = "open".localized @@ -262,6 +305,9 @@ class LocalizableStrings : NSObject { var auth_face_confirm = "auth_face_confirm".localized var auth_touch_confirm = "auth_touch_confirm".localized var auth_bio_failed = "auth_bio_failed".localized + var auth_bio_locked = "auth_bio_locked".localized + var auth_bio_not_enrolled = "auth_bio_not_enrolled".localized + var auth_bio_unavailable = "auth_bio_unavailable".localized var my_active_addresses = "my_active_addresses".localized var contacts_empty = "contacts_empty".localized var addresses_empty = "addresses_empty".localized @@ -294,6 +340,14 @@ class LocalizableStrings : NSObject { var manual_restore_warning = "manual_restore_warning".localized var search_transactions = "search_transactions".localized var random_node = "random_node".localized + var node_peers = "node_peers".localized + var node_type = "node_type".localized + var node_peer_active = "node_peer_active".localized + var node_peer_pool = "node_peer_pool".localized + var node_peer_connected = "node_peer_connected".localized + var node_peer_connecting = "node_peer_connecting".localized + var node_peer_disconnected = "node_peer_disconnected".localized + var node_peer_last_seen = "node_peer_last_seen".localized var restore_create_title = "restore_create_title".localized var restore_create_text = "restore_create_text".localized @@ -388,6 +442,7 @@ class LocalizableStrings : NSObject { var faucet_redirect_text = "faucet_redirect_text".localized var clear_wallet_transactions_text = "clear_wallet_transactions_text".localized var show_amounts_in = "show_amounts_in".localized + var use_on_chain_price_oracle = "use_on_chain_price_oracle".localized var second_currency = "second_currency".localized var notifications = "notifications".localized var news = "news".localized @@ -423,6 +478,7 @@ class LocalizableStrings : NSObject { var regular = "regular".localized var identity = "identity".localized var sbbs_address_new = "sbbs_address_new".localized + var sbbs_address_hint = "sbbs_address_hint".localized var for_pool = "for_pool".localized var withdraw = "withdraw".localized var withdraw_cofirm = "withdraw_cofirm".localized @@ -432,17 +488,19 @@ class LocalizableStrings : NSObject { var shielded_pool = "shielded_pool".localized var min_fee_error_offline = "min_fee_error_offline".localized var maturity = "maturity".localized - var show_public_offline = "show_public_offline".localized + var sign_message = "sign_message".localized + var verify_message = "verify_message".localized var offline_address = "offline_address".localized var public_offline_address_info = "public_offline_address_info".localized var public_offline = "public_offline".localized + var public_offline_address = "public_offline_address".localized var max_privacy = "max_privacy".localized + var max_privacy_address = "max_privacy_address".localized var tr_sent_public_offline = "tr_sent_public_offline".localized var tr_sent_offline = "tr_sent_offline".localized var tr_received_public_offline = "tr_received_public_offline".localized var tr_received_offline = "tr_received_offline".localized var address_details = "address_details".localized - var connect_node_offline_public = "connect_node_offline_public".localized var send_address_placholder = "send_address_placholder".localized var local_comment = "local_comment".localized var choose_currency = "choose_currency".localized @@ -480,6 +538,10 @@ class LocalizableStrings : NSObject { var connect_to_mobilenode = "connect_to_mobilenode".localized var syncing_with_blockchain = "syncing_with_blockchain".localized + var sync_phase_connecting = "sync_phase_connecting".localized + var sync_phase_reconnecting = "sync_phase_reconnecting".localized + var sync_phase_almost_done = "sync_phase_almost_done".localized + var sync_phase_finalizing = "sync_phase_finalizing".localized var please_no_lock = "please_no_lock".localized var wallet_connected_to_mobile_node = "wallet_connected_to_mobile_node".localized var disconnect = "disconnect".localized @@ -488,8 +550,9 @@ class LocalizableStrings : NSObject { var senf_offline_notice = "senf_offline_notice".localized var regular_online_only = "regular_online_only".localized var regular_offline_only = "regular_offline_only".localized - var sender_choice = "sender_choice".localized var assets = "assets".localized + var select_asset = "select_asset".localized + var search_assets = "search_assets".localized var usage_recent_old = "usage_recent_old".localized var usage_old_recent = "usage_old_recent".localized @@ -532,6 +595,9 @@ class LocalizableStrings : NSObject { var online_address = "online_address".localized var offline_left_address = "offline_left_address".localized var offline_left_address_warning = "offline_left_address_warning".localized + var vouchers_count = "vouchers_count".localized + var vouchers_count_hint = "vouchers_count_hint".localized + var vouchers_count_error = "vouchers_count_error".localized var send_offline_hint = "send_offline_hint".localized var send_max_privacy_title = "send_max_privacy_title".localized var send_notice_max_privacy = "send_notice_max_privacy".localized @@ -541,14 +607,69 @@ class LocalizableStrings : NSObject { var max_funds_error = "max_funds_error".localized var max_funds_hint = "max_funds_hint".localized var dAppStore = "dAppStore".localized + + // DApp Store / My DApps + var myDApps = "myDApps".localized + var dapps_empty_title = "dapps_empty_title".localized + var dapps_empty_subtitle = "dapps_empty_subtitle".localized + var dapps_empty_button = "dapps_empty_button".localized + var dapps_get_more = "dapps_get_more".localized + var dapps_uninstall = "dapps_uninstall".localized + var dapps_remove_confirm = "dapps_remove_confirm".localized + var dapps_install_from_file = "dapps_install_from_file".localized + var dapps_sideload_desc = "dapps_sideload_desc".localized + var dapps_sideload_invalid = "dapps_sideload_invalid".localized + var dapps_install_label = "dapps_install_label".localized + var dapps_installed_label = "dapps_installed_label".localized + var dapps_update = "dapps_update".localized + var dapps_installed_toast = "dapps_installed_toast".localized + var dapps_updated_toast = "dapps_updated_toast".localized + var dapps_install_complete = "dapps_install_complete".localized + var dapps_install_failed_msg = "dapps_install_failed_msg".localized + var dapps_no_download_source = "dapps_no_download_source".localized + var dapps_source_bundled = "dapps_source_bundled".localized + var dapps_source_ipfs = "dapps_source_ipfs".localized + var dapps_store_title = "dapps_store_title".localized + var dapps_store_subtitle = "dapps_store_subtitle".localized + var dapps_store_loading = "dapps_store_loading".localized + var dapps_store_no_dapps = "dapps_store_no_dapps".localized + var dapps_store_empty_hint = "dapps_store_empty_hint".localized + var dapps_publishers = "dapps_publishers".localized + var dapps_publishers_subtitle = "dapps_publishers_subtitle".localized + var dapps_no_publishers = "dapps_no_publishers".localized + var dapps_back_button = "dapps_back_button".localized + var dapps_sideloaded_name = "dapps_sideloaded_name".localized + var fee = "fee".localized + + var split = "split".localized + var split_coins_subtitle = "split_coins_subtitle".localized + var split_coins_cta = "split_coins_cta".localized + var regular_balance = "regular_balance".localized + var current_coins = "current_coins".localized + var coin_count_format = "coin_count_format".localized + var largest_coin_format = "largest_coin_format".localized + var more_coins_format = "more_coins_format".localized + var split_into = "split_into".localized + var preview = "preview".localized + var nx_equal_format = "nx_equal_format".localized + var split_concentration_warning_format = "split_concentration_warning_format".localized + var split_too_small = "split_too_small".localized + var split_started_message = "split_started_message".localized + + var consolidate = "consolidate".localized + var consolidate_coins_subtitle = "consolidate_coins_subtitle".localized + var consolidate_coins_cta = "consolidate_coins_cta".localized + var consolidate_started_message = "consolidate_started_message".localized + var consolidate_offline_toggle = "consolidate_offline_toggle".localized + var consolidate_offline_hint = "consolidate_offline_hint".localized + var confirm_accept = "confirm_accept".localized var will_take_funds = "will_take_funds".localized var will_send_funds = "will_send_funds".localized var dapp_anme = "dapp_anme".localized var app_shader_id = "app_shader_id".localized - var beamx_dao = "beamx_dao".localized var beam_faucet = "beam_faucet".localized var beam_gallery = "beam_gallery".localized @@ -622,7 +743,6 @@ class LocalizableStrings : NSObject { var search_settings = "search_settings".localized var no_search_settings = "no_search_settings".localized var online_own_not_conifg = "online_own_not_conifg".localized - var beamx_dao_dao_voting = "beamx_dao_dao_voting".localized public func new_version_available_title(version: String) -> String { diff --git a/BeamWallet/Protocols/Progress.swift b/BeamWallet/Protocols/Progress.swift index 9189ad28..5c74c8ff 100644 --- a/BeamWallet/Protocols/Progress.swift +++ b/BeamWallet/Protocols/Progress.swift @@ -2,7 +2,7 @@ // SVProgressHUDProtocol.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import Foundation -func ShowCopied (text:String? = nil) { +func ShowCopied(text:String? = nil) { if let s = text { BMToast.show(text: s) } diff --git a/BeamWallet/Protocols/Resources.swift b/BeamWallet/Protocols/Resources.swift index 19880592..fdb18ac5 100644 --- a/BeamWallet/Protocols/Resources.swift +++ b/BeamWallet/Protocols/Resources.swift @@ -2,7 +2,7 @@ // Resources.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -105,16 +105,16 @@ func IconDappStore() -> UIImage? { return UIImage(named: "iconDapps") } -func IconBeamXDAO() -> UIImage? { - return UIImage(named: "iconDao") +func IconMessenger() -> UIImage? { + return UIImage(named: "iconMessenger") } -func IconBeamFaucet() -> UIImage? { - return UIImage(named: "icon_faucet") +func IconAssetSwap() -> UIImage? { + return UIImage(named: "iconAssetSwap") } -func IconBeamXDAOVoting() -> UIImage? { - return UIImage(named: "iconDaoVote") +func IconBeamFaucet() -> UIImage? { + return UIImage(named: "icon_faucet") } func IconBeamGallery() -> UIImage? { diff --git a/BeamWallet/Protocols/ShareLogActivity.swift b/BeamWallet/Protocols/ShareLogActivity.swift index 96817623..5b3e10fb 100644 --- a/BeamWallet/Protocols/ShareLogActivity.swift +++ b/BeamWallet/Protocols/ShareLogActivity.swift @@ -2,7 +2,7 @@ // ShareLogActivity.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Base/BaseCell.swift b/BeamWallet/ViewControllers/Base/BaseCell.swift index 2a033008..f68e1111 100644 --- a/BeamWallet/ViewControllers/Base/BaseCell.swift +++ b/BeamWallet/ViewControllers/Base/BaseCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // 3/1/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Base/BaseNavigationController.swift b/BeamWallet/ViewControllers/Base/BaseNavigationController.swift index eea3ab53..1cbe975a 100644 --- a/BeamWallet/ViewControllers/Base/BaseNavigationController.swift +++ b/BeamWallet/ViewControllers/Base/BaseNavigationController.swift @@ -2,7 +2,7 @@ // BaseNavigationController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -53,17 +53,17 @@ class NavigationPopTransition: NSObject, UIViewControllerAnimatedTransitioning { delay: 0, options: .curveLinear, animations: { - + dimmingView.alpha = 0 toViewController.view.frame = transitionContext.finalFrame(for: toViewController) fromViewController.view.frame = CGRect(x: toViewController.view.frame.size.width, y: fromViewController.view.frame.origin.y, width: fromViewController.view.frame.size.width, height: fromViewController.view.frame.size.height) - - }) { finished in - + + }, completion: { _ in + dimmingView.removeFromSuperview() transitionContext.completeTransition(!transitionContext.transitionWasCancelled) - - } + + }) } } @@ -93,7 +93,7 @@ class BaseNavigationController: UINavigationController, UINavigationControllerDe } } - var gesture:UIPanGestureRecognizer? = nil + var gesture:UIPanGestureRecognizer? var interactivePopTransition: UIPercentDrivenInteractiveTransition! override func viewDidLoad() { @@ -129,29 +129,15 @@ class BaseNavigationController: UINavigationController, UINavigationControllerDe } func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if let top = UIApplication.getTopMostViewController() { - if top is WalletViewController { - return false - } - else if top is SettingsViewController { - return false - } - else if top is AddressesViewController { - return false - } - else if top is DAOAppsViewController { - return false - } - else if top is NotificationsViewController { - return false - } - else if top is DAOViewController { - if let dao = top as? DAOViewController { - if dao.app.name.uppercased() == "BEAMX DAO" { - return false - } - } - } + guard let top = UIApplication.getTopMostViewController() else { return true } + // No nav stack means there's nothing to pop — and a transient nil during + // a transition shouldn't be treated as "this is a root, allow swipe". + guard let nav = top.navigationController else { return false } + // Side-menu roots have nothing to pop to. + if nav.viewControllers.first === top { return false } + // BEAMX DAO embeds a WebView whose horizontal pan conflicts with the swipe. + if let dao = top as? DAOViewController, dao.app.name.uppercased() == "BEAMX DAO" { + return false } return true } diff --git a/BeamWallet/ViewControllers/Base/BaseTableViewController.swift b/BeamWallet/ViewControllers/Base/BaseTableViewController.swift index c9ae920a..69942d1a 100644 --- a/BeamWallet/ViewControllers/Base/BaseTableViewController.swift +++ b/BeamWallet/ViewControllers/Base/BaseTableViewController.swift @@ -2,7 +2,7 @@ // BaseTableViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,14 +33,32 @@ class BaseTableViewController: BaseViewController { var tableView: UITableView! var tableStyle = UITableView.Style.plain - + + /// Snapshot of `tableView.alwaysBounceVertical` taken before the keyboard + /// shows; restored on hide so subclasses that opt into bouncing aren't + /// silently flipped off whenever a field unfocuses. + private var savedAlwaysBounce: Bool? + + /// View pinned above the bottom safe area, outside the table. The table is + /// sized to fit the area above it, so content placed here never causes the + /// table to scroll. Subclasses set this from viewDidLoad. Toggle visibility + /// with `isHidden`; setting to nil removes it entirely. + var bottomAccessoryView: UIView? { + didSet { + oldValue?.removeFromSuperview() + if let v = bottomAccessoryView { view.addSubview(v) } + view.setNeedsLayout() + } + } + override func viewDidLoad() { super.viewDidLoad() tableView = UITableView(frame: self.view.bounds, style: tableStyle) tableView.backgroundColor = UIColor.main.marine tableView.separatorStyle = .none - + tableView.alwaysBounceVertical = false + self.view.addSubview(tableView) } @@ -60,18 +78,27 @@ class BaseTableViewController: BaseViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - - var offset:CGFloat = 0 - + + var offset: CGFloat = 0 + if !isGradient { - offset = 30 + offset = 30 } else if isGradient && !isAddStatusView { offset = 30 } - + let y = navigationBarOffset - offset - tableView.frame = CGRect(x: 0, y: y , width: self.view.bounds.width, height: self.view.bounds.size.height - y) + let bottomSafe = view.safeAreaInsets.bottom + var bottomReserved: CGFloat = bottomSafe + if let acc = bottomAccessoryView, !acc.isHidden { + let h = acc.frame.height + acc.frame = CGRect(x: 0, y: view.bounds.height - bottomSafe - h, + width: view.bounds.width, height: h) + bottomReserved = h + bottomSafe + } + tableView.frame = CGRect(x: 0, y: y, width: view.bounds.width, + height: view.bounds.height - y - bottomReserved) } // public func footerView(buttons:[FooterButton]) -> UIView { @@ -101,13 +128,25 @@ extension BaseTableViewController { if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { let keyboardRectangle = keyboardFrame.cgRectValue let keyboardHeight = keyboardRectangle.height - + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) + if savedAlwaysBounce == nil { + savedAlwaysBounce = tableView.alwaysBounceVertical + } + tableView.alwaysBounceVertical = true + + let lift = keyboardHeight - view.safeAreaInsets.bottom + if lift > 0 { + bottomAccessoryView?.transform = CGAffineTransform(translationX: 0, y: -lift) + } } } - + @objc func keyboardWillHide(notification: NSNotification) { tableView.contentInset = UIEdgeInsets.zero + tableView.alwaysBounceVertical = savedAlwaysBounce ?? false + savedAlwaysBounce = nil + bottomAccessoryView?.transform = .identity } } diff --git a/BeamWallet/ViewControllers/Base/BaseView.swift b/BeamWallet/ViewControllers/Base/BaseView.swift index d589f0c6..e97dd43a 100644 --- a/BeamWallet/ViewControllers/Base/BaseView.swift +++ b/BeamWallet/ViewControllers/Base/BaseView.swift @@ -2,7 +2,7 @@ // BaseView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Base/BaseViewController.swift b/BeamWallet/ViewControllers/Base/BaseViewController.swift index bfeb425e..951a3297 100644 --- a/BeamWallet/ViewControllers/Base/BaseViewController.swift +++ b/BeamWallet/ViewControllers/Base/BaseViewController.swift @@ -2,7 +2,7 @@ // BaseViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -389,11 +389,11 @@ class BaseViewController: UIViewController { if !Settings.sharedManager().isHideAmounts { if Settings.sharedManager().isAskForHideAmounts { self.confirmAlert(title: Localizable.shared.strings.activate_security_title, message: Localizable.shared.strings.activate_security_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.activate, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in Settings.sharedManager().isAskForHideAmounts = false Settings.sharedManager().isHideAmounts = !Settings.sharedManager().isHideAmounts - } + }) } else { Settings.sharedManager().isHideAmounts = !Settings.sharedManager().isHideAmounts diff --git a/BeamWallet/ViewControllers/Base/BaseWizardViewController.swift b/BeamWallet/ViewControllers/Base/BaseWizardViewController.swift index 66a735e8..d703cc47 100644 --- a/BeamWallet/ViewControllers/Base/BaseWizardViewController.swift +++ b/BeamWallet/ViewControllers/Base/BaseWizardViewController.swift @@ -3,7 +3,7 @@ // BeamWallet // // 3/1/19. -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Login/EnterWalletPasswordViewController.swift b/BeamWallet/ViewControllers/Login/EnterWalletPasswordViewController.swift index 657d8aea..d7b60dc8 100644 --- a/BeamWallet/ViewControllers/Login/EnterWalletPasswordViewController.swift +++ b/BeamWallet/ViewControllers/Login/EnterWalletPasswordViewController.swift @@ -2,7 +2,7 @@ // EnterWalletPasswordViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -96,13 +96,6 @@ class EnterWalletPasswordViewController: BaseWizardViewController { } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - -// passField.text = "1" -// onLogin(sender: UIButton()) - } - override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) @@ -118,13 +111,20 @@ class EnterWalletPasswordViewController: BaseWizardViewController { public func biometricAuthorization() { if BiometricAuthorization.shared.canAuthenticate(), Settings.sharedManager().isEnableBiometric { BiometricAuthorization.shared.authenticateWithBioMetrics(success: { - if let password = KeychainManager.getPassword() { + let context = BiometricAuthorization.shared.lastAuthenticatedContext + if let password = KeychainManager.getPassword(context: context) { self.passField.text = password self.onLogin(sender: UIButton()) } - + }, failure: { self.touchIdButton.tintColor = UIColor.white + if let reason = BiometricAuthorization.shared.lastFailureReason, reason != .canceled { + let message = BiometricAuthorization.shared.failureMessage(for: reason) + if !message.isEmpty { + BMToast.show(text: message) + } + } }, retry: { self.touchIdButton.tintColor = UIColor.white }) @@ -154,7 +154,7 @@ class EnterWalletPasswordViewController: BaseWizardViewController { if(!AppModel.sharedManager().isWalletRunning()) { AppModel.sharedManager().openWallet(pass) isLoggedin = true - AppModel.sharedManager().isLoggedin = isLoggedin; + AppModel.sharedManager().isLoggedin = isLoggedin AppModel.sharedManager().isConnected = true } if navigationController?.viewControllers.count == 1 { @@ -191,12 +191,12 @@ class EnterWalletPasswordViewController: BaseWizardViewController { @IBAction func onChangeWallet(sender: UIButton) { confirmAlert(title: Localizable.shared.strings.restore_create_title, message: Localizable.shared.strings.restore_create_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.proceed, cancelHandler: { _ in - - }) { _ in - AppModel.sharedManager().isLoggedin = false; + + }, confirmHandler: { _ in + AppModel.sharedManager().isLoggedin = false AppModel.sharedManager().startChangeWallet() self.pushViewController(vc: WellcomeViewController()) - } + }) } @IBAction func onExport(sender: UIButton) { @@ -235,8 +235,8 @@ class EnterWalletPasswordViewController: BaseWizardViewController { leftViewController: menuViewController, rightViewController: nil) - sideMenuController.leftViewWidth = UIScreen.main.bounds.size.width - 60; - sideMenuController.leftViewPresentationStyle = LGSideMenuPresentationStyle.slideAbove; + sideMenuController.leftViewWidth = UIScreen.main.bounds.size.width - 60 + sideMenuController.leftViewPresentationStyle = LGSideMenuPresentationStyle.slideAbove sideMenuController.rootViewLayerShadowRadius = 0 sideMenuController.rootViewLayerShadowColor = UIColor.clear sideMenuController.leftViewLayerShadowRadius = 0 diff --git a/BeamWallet/ViewControllers/Login/New Group/RestoreOptionsViewController.swift b/BeamWallet/ViewControllers/Login/New Group/RestoreOptionsViewController.swift index 2e1242b8..6a26352a 100644 --- a/BeamWallet/ViewControllers/Login/New Group/RestoreOptionsViewController.swift +++ b/BeamWallet/ViewControllers/Login/New Group/RestoreOptionsViewController.swift @@ -2,7 +2,7 @@ // RestoreOptionsViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -81,20 +81,20 @@ class RestoreOptionsViewController: BaseViewController { @IBAction func onNext(sender: UIButton) { if AppModel.sharedManager().restoreType == BMRestoreAutomatic { confirmAlert(title: Localizable.shared.strings.restore_wallet_title, message: Localizable.shared.strings.auto_restore_warning, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.understand, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in Settings.sharedManager().connectToRandomNode = true Settings.sharedManager().nodeAddress = AppModel.chooseRandomNode() AppModel.sharedManager().changeNodeAddress() let vc = OpenWalletProgressViewController(password: self.password, phrase: self.phrase) self.pushViewController(vc: vc) - } + }) } else { confirmAlert(title: Localizable.shared.strings.restore_wallet_title, message: Localizable.shared.strings.manual_restore_warning, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.understand, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in let created = AppModel.sharedManager().createWallet(self.phrase, pass: self.password) if !created { self.alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.wallet_not_created) { _ in @@ -114,16 +114,16 @@ class RestoreOptionsViewController: BaseViewController { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { AppModel.sharedManager().exportOwnerKey(self.password) { [weak self] key in SVProgressHUD.dismiss() - + guard let strongSelf = self else { return } - + let vc = OwnerKeyViewController() vc.ownerKey = key strongSelf.pushViewController(vc: vc) } } } - } + }) } } } diff --git a/BeamWallet/ViewControllers/Login/OpenWalletProgressViewController.swift b/BeamWallet/ViewControllers/Login/OpenWalletProgressViewController.swift index 2255572b..2cd321e3 100644 --- a/BeamWallet/ViewControllers/Login/OpenWalletProgressViewController.swift +++ b/BeamWallet/ViewControllers/Login/OpenWalletProgressViewController.swift @@ -2,7 +2,7 @@ // OpenWalletProgressViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,9 +44,22 @@ class OpenWalletProgressViewController: BaseViewController { public var isRescan = false private var backgroundTaskID: UIBackgroundTaskIdentifier! - + private let displayProgress = true + // Stabilization window: on the `onlyConnect` resume path the wallet often + // jumps from total=1,done=0 → total=1,done=1 in one tick, which renders as + // a useless 0% → 100% flash. Suppress the percent UI for the first 500 ms + // and decide once: either open the main page (if sync resolved or only a + // trivial number of requests remain) or commit to rendering progress. + private static let kStabilizationDelay: TimeInterval = 0.5 + private static let kTrivialSyncThreshold: Int32 = 2 + + private var stabilizationTimer: Timer? + private var pendingSnapshot: (done: Int32, total: Int32)? + private var hasConnected = false + private var isReconnecting = false + init(password:String, phrase:String?) { super.init(nibName: nil, bundle: nil) @@ -67,7 +80,7 @@ class OpenWalletProgressViewController: BaseViewController { super.viewDidLoad() let name = Bundle.main.bundleIdentifier! + ".task" - backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: name) { + backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: name) { print("END BG TASK") UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid @@ -115,23 +128,22 @@ class OpenWalletProgressViewController: BaseViewController { else { progressTitleLabel.text = Localizable.shared.strings.loading_wallet } - - if displayProgress { - progressValueLabel.text = Localizable.shared.strings.syncing_with_blockchain + " 0%" - } - else { - progressValueLabel.text = Localizable.shared.strings.syncing_with_blockchain + ": " - } - - + + // Hide the percent label until the stabilization window closes — + // otherwise we'd flash "0%" before either committing to a real + // progress render or skipping straight to the main page. + progressValueLabel.text = Localizable.shared.strings.sync_phase_connecting + progressValueLabel.isHidden = false + restotingInfoLabel.text = Localizable.shared.strings.please_no_lock - + progressTimeValueLabel.text = Localizable.shared.strings.calc_estimate_time progressTimeValueLabel.isHidden = false - - progressValueLabel.isHidden = false + cancelButton.isHidden = false restotingInfoLabel.isHidden = true + + startStabilizationWindow() } else if AppModel.sharedManager().isRestoreFlow { progressTitleLabel.text = Localizable.shared.strings.restoring_wallet @@ -165,8 +177,13 @@ class OpenWalletProgressViewController: BaseViewController { cancelButton.isHidden = true } else { + // Fresh-install random-node create. Use onTimeOut (not + // openMainPage directly) so a flaky network won't silently + // drop the user into a zero-balance wallet — onTimeOut gates + // on isSynced / isChangedNode / isPresented and otherwise + // surfaces an error. timeoutTimer?.invalidate() - timeoutTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(openMainPage), userInfo: nil, repeats: false) + timeoutTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(onTimeOut), userInfo: nil, repeats: false) } } @@ -203,8 +220,10 @@ class OpenWalletProgressViewController: BaseViewController { UIApplication.shared.endBackgroundTask(self.backgroundTaskID) self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid - + timeoutTimer?.invalidate() + stabilizationTimer?.invalidate() + stabilizationTimer = nil if isMovingFromParent { AppModel.sharedManager().removeDelegate(self) @@ -217,40 +236,44 @@ class OpenWalletProgressViewController: BaseViewController { } @objc private func openMainPage() { + // Don't remove the delegate before the early-return checks. Other + // callbacks (e.g. onNetwotkStatusChange after node-connect) call + // openMainPage while `isWaitingRestore` is still true; if we tore + // ourselves out of the delegate list first and then returned, the + // post-recovery sync events would never reach this VC and the screen + // would sit on "Synchronizing with node: 0%" forever. + if isWaitingRestore { + return + } + + if isPresented && !onlyConnect { + return + } + AppModel.sharedManager().removeDelegate(self) if onlyConnect { AppModel.sharedManager().refreshAddresses() - + var found = false if let controllers = self.navigationController?.viewControllers { - for vc in controllers { - if vc is WalletViewController { - found = true - self.navigationController?.popToViewController(vc, animated: true) - } + for vc in controllers where vc is WalletViewController { + found = true + self.navigationController?.popToViewController(vc, animated: true) } } - + if !found && !isPresented { isPresented = true - + onMainPage() - + if Settings.sharedManager().isNodeProtocolEnabled { BMToast.show(text: Localizable.shared.strings.wallet_connected_to_mobile_node) } } return } - - if isWaitingRestore { - return - } - - if isPresented { - return - } isPresented = true @@ -284,8 +307,8 @@ class OpenWalletProgressViewController: BaseViewController { leftViewController: menuViewController, rightViewController: nil) - sideMenuController.leftViewWidth = UIScreen.main.bounds.size.width - 60; - sideMenuController.leftViewPresentationStyle = LGSideMenuPresentationStyle.slideAbove; + sideMenuController.leftViewWidth = UIScreen.main.bounds.size.width - 60 + sideMenuController.leftViewPresentationStyle = LGSideMenuPresentationStyle.slideAbove sideMenuController.rootViewLayerShadowRadius = 0 sideMenuController.rootViewLayerShadowColor = UIColor.clear sideMenuController.leftViewLayerShadowRadius = 0 @@ -310,7 +333,7 @@ class OpenWalletProgressViewController: BaseViewController { self.restoreCompleted() } } - }) { (error, progress, time) in + }, progress: { (error, progress, time) in DispatchQueue.main.async { if let reason = error { self.alert(title: Localizable.shared.strings.error, message: reason.localizedDescription) { (_ ) in @@ -322,7 +345,7 @@ class OpenWalletProgressViewController: BaseViewController { else if let percent = progress { self.errorLabel.isHidden = true self.progressView.progress = percent - + if let remaining = time { self.progressValueLabel.text = Localizable.shared.strings.downloading + " " + "\(Int32(percent * 100))%" self.progressTimeValueLabel.text = remaining + Localizable.shared.strings.to_completion @@ -334,7 +357,7 @@ class OpenWalletProgressViewController: BaseViewController { } } } - } + }) } private func startCreateWallet() { @@ -351,6 +374,8 @@ class OpenWalletProgressViewController: BaseViewController { let created = AppModel.sharedManager().createWallet(phrase, pass: password!) if(!created) { + AppModel.sharedManager().abortCreateAndReset() + self.alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.wallet_not_created) { (_ ) in if AppModel.sharedManager().isInternetAvailable { self.navigationController?.popToRootViewController(animated: true) @@ -370,6 +395,8 @@ class OpenWalletProgressViewController: BaseViewController { let created = AppModel.sharedManager().createWallet(phrase, pass: password!) if(!created) { + AppModel.sharedManager().abortCreateAndReset() + self.alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.wallet_not_created) { (_ ) in if AppModel.sharedManager().isInternetAvailable { self.navigationController?.popToRootViewController(animated: true) @@ -412,7 +439,7 @@ class OpenWalletProgressViewController: BaseViewController { strongSelf.progressView.progress = 0 strongSelf.progressValueLabel.text = Localizable.shared.strings.restored + " \(0)%" } - + DispatchQueue.global(qos: .background).async { AppModel.sharedManager().restore(RestoreManager.shared.filePath.path) } @@ -420,9 +447,7 @@ class OpenWalletProgressViewController: BaseViewController { private func openNodeController() { let vc = TrustedNodeViewController(event: .change) - vc.completion = { [weak self] - obj in - + vc.completion = { [weak self] obj in if obj == true { AppModel.sharedManager().isConnecting = false @@ -438,8 +463,68 @@ class OpenWalletProgressViewController: BaseViewController { self.pushViewController(vc: vc) } + // MARK: Sync phase / stabilization + + private enum SyncPhase { + case connecting + case reconnecting + case almostDone + case finalizing + } + + private func resolveBoundaryPhase(done: Int32, total: Int32) -> SyncPhase? { + if !hasConnected { return .connecting } + if isReconnecting { return .reconnecting } + if total > 0 && done == total && !AppModel.sharedManager().isSynced() { + return .finalizing + } + if total > 0 && (total - done) <= OpenWalletProgressViewController.kTrivialSyncThreshold { + return .almostDone + } + return nil + } + + private func progressCopy(for phase: SyncPhase) -> String { + switch phase { + case .connecting: return Localizable.shared.strings.sync_phase_connecting + case .reconnecting: return Localizable.shared.strings.sync_phase_reconnecting + case .almostDone: return Localizable.shared.strings.sync_phase_almost_done + case .finalizing: return Localizable.shared.strings.sync_phase_finalizing + } + } + + private func startStabilizationWindow() { + stabilizationTimer?.invalidate() + stabilizationTimer = Timer.scheduledTimer( + timeInterval: OpenWalletProgressViewController.kStabilizationDelay, + target: self, + selector: #selector(onStabilizationFinished), + userInfo: nil, + repeats: false + ) + } + + @objc private func onStabilizationFinished() { + stabilizationTimer = nil + + if AppModel.sharedManager().isSynced() { + openMainPage() + return + } + if let snap = pendingSnapshot, + snap.total > 0, + snap.total - snap.done <= OpenWalletProgressViewController.kTrivialSyncThreshold { + openMainPage() + return + } + + if let snap = pendingSnapshot { + onSyncProgressUpdated(snap.done, total: snap.total) + } + } + // MARK: IBAction - + @IBAction func onCancel(sender :UIButton) { AppModel.sharedManager().removeDelegate(self) @@ -461,8 +546,39 @@ class OpenWalletProgressViewController: BaseViewController { } @objc private func onTimeOut() { - if Settings.sharedManager().isChangedNode() { - self.openMainPage() + // Don't surface an error if the underlying flow has already + // succeeded or is being handled elsewhere — `isPresented` means + // openMainPage already ran, `isSynced` means the wallet is good to + // go, and `isChangedNode` means the user is mid-node-swap and + // openMainPage is the legitimate next step. + if isPresented + || AppModel.sharedManager().isSynced() + || Settings.sharedManager().isChangedNode() { + openMainPage() + return + } + + // Watchdog fired without progress on a fresh open / random-node + // create. Previously we silently advanced into the wallet, which on + // a flaky network is indistinguishable from a wipe (zero balance, + // no error). Surface the failure and let the user retry from the + // login screen instead of guessing. + // TODO: dedicated "Connection timed out / Retry" copy. Reusing + // wallet_not_opened (already localized in every .lproj) until that + // string lands. + timeoutTimer?.invalidate() + errorLabel.isHidden = false + errorLabel.text = Localizable.shared.strings.no_internet + + self.alert(title: Localizable.shared.strings.error, + message: Localizable.shared.strings.wallet_not_opened) { [weak self] _ in + self?.navigationController?.popToRootViewController(animated: true) + } + } + + fileprivate func hideErrorIfOnline() { + if AppModel.sharedManager().isInternetAvailable { + errorLabel.isHidden = true } } } @@ -472,23 +588,53 @@ extension OpenWalletProgressViewController : WalletModelDelegate { func onNetwotkStatusChange(_ connected: Bool) { DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } - + + if connected { + strongSelf.errorLabel.isHidden = true + strongSelf.hasConnected = true + strongSelf.isReconnecting = false + } + if !strongSelf.onlyConnect && connected && !AppModel.sharedManager().isRestoreFlow && (strongSelf.phrase != nil && !Settings.sharedManager().isNodeProtocolEnabled){ strongSelf.progressView.progress = 1 - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { strongSelf.openMainPage() } } } } - + + func onNetwotkStartConnecting(_ connecting: Bool) { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return } + // Only show "Connecting…" before we've ever rendered a percent — + // once real progress is visible, flipping back to the connecting + // copy mid-sync would be misleading. + if connecting && !strongSelf.hasConnected && strongSelf.stabilizationTimer != nil { + strongSelf.progressValueLabel.text = Localizable.shared.strings.sync_phase_connecting + } + } + } + + func onNetwotkStartReconnecting() { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return } + strongSelf.isReconnecting = true + strongSelf.progressValueLabel.text = Localizable.shared.strings.sync_phase_reconnecting + } + } + func onNoInternetConnection() { DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } - if AppModel.sharedManager().isRestoreFlow && !strongSelf.onlyConnect { + if strongSelf.onlyConnect { + return + } + + if AppModel.sharedManager().isRestoreFlow || strongSelf.isWaitingRestore { strongSelf.errorLabel.isHidden = false strongSelf.errorLabel.text = Localizable.shared.strings.no_internet } @@ -496,12 +642,18 @@ extension OpenWalletProgressViewController : WalletModelDelegate { } func onRecoveryProgressUpdated(_ done: Int32, total: Int32, time: Int32) { - + DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } - - - strongSelf.errorLabel.isHidden = true + + // BEAM core often fires extra progress events at completion. Once we've + // already transitioned to the sync UI, ignore them — otherwise the text + // update below would overwrite "Synchronizing with node: 0%" back to + // "Restored 100%" (oldProgress is reset to 0 during the transition, + // so the next callback always passes the != check). + if strongSelf.stopRestore { return } + + strongSelf.hideErrorIfOnline() strongSelf.progressView.progress = Float(Float(done)/Float(total)) let progress_100 = Int32(strongSelf.progressView.progress * 100) @@ -530,25 +682,31 @@ extension OpenWalletProgressViewController : WalletModelDelegate { if done == total || percent >= 99.9 { if !strongSelf.stopRestore { strongSelf.stopRestore = true - strongSelf.isWaitingRestore = false - - let deadlineTime = DispatchTime.now() + .seconds(4) - DispatchQueue.main.asyncAfter(deadline: deadlineTime) { - AppModel.sharedManager().isRestoreFlow = false - RestoreManager.shared.cancelRestore() - - if !AppModel.sharedManager().isInternetAvailable { - strongSelf.alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.no_internet) { (_ ) in - - AppModel.sharedManager().resetWallet(false) - - strongSelf.navigationController?.setViewControllers( [EnterWalletPasswordViewController()], animated: true) - } - } - else{ - strongSelf.openMainPage() + + AppModel.sharedManager().isRestoreFlow = false + RestoreManager.shared.cancelRestore() + + if !AppModel.sharedManager().isInternetAvailable { + strongSelf.alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.no_internet) { (_ ) in + + AppModel.sharedManager().resetWallet(false) + + strongSelf.navigationController?.setViewControllers( [EnterWalletPasswordViewController()], animated: true) } } + else { + // Recovery is done; keep the screen up and show node-sync + // progress until it hits 100%. onSyncProgressUpdated will + // call openMainPage() once total == done. + strongSelf.isWaitingRestore = true + strongSelf.progressTitleLabel.text = Localizable.shared.strings.loading_wallet + strongSelf.restotingInfoLabel.isHidden = true + strongSelf.progressView.progress = 0 + strongSelf.oldProgress = 0 + strongSelf.progressValueLabel.text = "\(Localizable.shared.strings.sync_with_node): 0%." + strongSelf.progressTimeValueLabel.text = Localizable.shared.strings.calc_estimate_time + strongSelf.progressTimeValueLabel.isHidden = false + } } } @@ -558,8 +716,17 @@ extension OpenWalletProgressViewController : WalletModelDelegate { func onSyncProgressUpdated(_ done: Int32, total: Int32) { DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } - strongSelf.errorLabel.isHidden = true - + strongSelf.hideErrorIfOnline() + + // Stabilization: on the resume path, suppress percent rendering + // until either the timer fires or the workload is non-trivial. + // Recovery (`isWaitingRestore`) and the seed-restore flows are + // unaffected — they have real work to display from frame one. + if strongSelf.onlyConnect && strongSelf.stabilizationTimer != nil && !strongSelf.isWaitingRestore { + strongSelf.pendingSnapshot = (done, total) + return + } + if done > 0 { if (strongSelf.onlyConnect || strongSelf.isRescan || strongSelf.phrase == nil) || (strongSelf.phrase != nil){ @@ -579,28 +746,33 @@ extension OpenWalletProgressViewController : WalletModelDelegate { } } - if total == done && !strongSelf.isPresented && !AppModel.sharedManager().isRestoreFlow { - - } - else{ + if total > 0 { strongSelf.progressView.progress = Float(Float(done)/Float(total)) - if strongSelf.isWaitingRestore { - let progress_100 = Int32(strongSelf.progressView.progress * 100) - strongSelf.progressValueLabel.text = "\(Localizable.shared.strings.sync_with_node): \(progress_100)%." + } + if strongSelf.isWaitingRestore { + let progress_100 = Int32(strongSelf.progressView.progress * 100) + strongSelf.progressValueLabel.text = "\(Localizable.shared.strings.sync_with_node): \(progress_100)%." + } + else if strongSelf.onlyConnect || (strongSelf.phrase != nil && (Settings.sharedManager().isNodeProtocolEnabled || !Settings.sharedManager().connectToRandomNode)) { + let progress_100 = Int32(strongSelf.progressView.progress * 100) + + if strongSelf.displayProgress { + strongSelf.progressValueLabel.text = "\(Localizable.shared.strings.syncing_with_blockchain) \(progress_100)%" } - else if strongSelf.onlyConnect || (strongSelf.phrase != nil && (Settings.sharedManager().isNodeProtocolEnabled || !Settings.sharedManager().connectToRandomNode)) { - let progress_100 = Int32(strongSelf.progressView.progress * 100) - - if strongSelf.displayProgress { - strongSelf.progressValueLabel.text = "\(Localizable.shared.strings.syncing_with_blockchain) \(progress_100)%" - } - else { - strongSelf.progressValueLabel.text = "\(Localizable.shared.strings.syncing_with_blockchain): " - } + else { + strongSelf.progressValueLabel.text = "\(Localizable.shared.strings.syncing_with_blockchain): " } } - if total == done && strongSelf.isWaitingRestore { + // Phase-aware overlay: replace the generic percent copy with a more + // descriptive line at the boundaries of sync (reconnecting, almost + // done, finalizing). Steady-state (resolver returns nil) keeps the + // percent visible during the bulk of the work. + if let phase = strongSelf.resolveBoundaryPhase(done: done, total: total) { + strongSelf.progressValueLabel.text = strongSelf.progressCopy(for: phase) + } + + if strongSelf.isWaitingRestore && (total == done || AppModel.sharedManager().isSynced()) { strongSelf.isWaitingRestore = false strongSelf.openMainPage() } @@ -626,8 +798,7 @@ extension OpenWalletProgressViewController : WalletModelDelegate { } func onWalletError(_ _error: Error) { - DispatchQueue.main.async { - [weak self] in + DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } let error = _error as NSError @@ -640,10 +811,8 @@ extension OpenWalletProgressViewController : WalletModelDelegate { } else if !strongSelf.isPresented { if let controllers = strongSelf.navigationController?.viewControllers { - for vc in controllers { - if vc is TrustedNodeViewController { - return - } + for vc in controllers where vc is TrustedNodeViewController { + return } } strongSelf.alert(title: Localizable.shared.strings.error, message: error.localizedDescription, handler: { (_ ) in diff --git a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionButtonFooter.swift b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionButtonFooter.swift index 780e9639..4f60ce5a 100644 --- a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionButtonFooter.swift +++ b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionButtonFooter.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 10/18/19. -// Copyright © 2019 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import Foundation @@ -38,7 +38,6 @@ class CollectionButtonFooter: UICollectionReusableView { btn2.isHidden = true case .display, .onlyDisplay: btn2.setTitle(Localizable.shared.strings.i_will_later, for: .normal) - break } if Settings.sharedManager().isDarkMode { diff --git a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionTextHeader.swift b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionTextHeader.swift index b8c1b3df..50381e86 100644 --- a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionTextHeader.swift +++ b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/CollectionTextHeader.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 10/18/19. -// Copyright © 2019 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit diff --git a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/InputWordCell.swift b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/InputWordCell.swift index cd764c25..c2c15691 100644 --- a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/InputWordCell.swift +++ b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/InputWordCell.swift @@ -2,7 +2,7 @@ // InputWordCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/SeedIntroCell.swift b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/SeedIntroCell.swift index 09e64653..75996491 100644 --- a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/SeedIntroCell.swift +++ b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/SeedIntroCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 10/18/19. -// Copyright © 2019 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit diff --git a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/WordCell.swift b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/WordCell.swift index 339e524e..6dc4ed37 100644 --- a/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/WordCell.swift +++ b/BeamWallet/ViewControllers/Login/PhraseViewController/Cells/WordCell.swift @@ -2,7 +2,7 @@ // WordCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Login/PhraseViewController/SeedPhraseViewController.swift b/BeamWallet/ViewControllers/Login/PhraseViewController/SeedPhraseViewController.swift index 3defe4cf..c0614acf 100644 --- a/BeamWallet/ViewControllers/Login/PhraseViewController/SeedPhraseViewController.swift +++ b/BeamWallet/ViewControllers/Login/PhraseViewController/SeedPhraseViewController.swift @@ -1,7 +1,7 @@ // // SeedPhraseViewController.swift // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -94,10 +94,8 @@ class SeedPhraseViewController: BaseViewController { if let index = words.firstIndex(of: word) { var added = false - for inWord in inputWords { - if inWord.index == index { - added = true - } + for inWord in inputWords where inWord.index == index { + added = true } if !added { @@ -148,18 +146,17 @@ class SeedPhraseViewController: BaseViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - + + // swiftlint:disable:next notification_center_detachment NotificationCenter.default.removeObserver(self) } @objc private func didTakeScreenshot() { if event == .restore { var shouldDisplayAlert = false - for i in 0 ... confirmCountWords - 1 { - if inputWords[i].correct { - shouldDisplayAlert = true - break - } + for i in 0 ... confirmCountWords - 1 where inputWords[i].correct { + shouldDisplayAlert = true + break } if shouldDisplayAlert { alert(message: Localizable.shared.strings.seed_capture_warning) @@ -179,12 +176,12 @@ class SeedPhraseViewController: BaseViewController { } else { confirmAlert(title: Localizable.shared.strings.save_seed_title, message: Localizable.shared.strings.save_seed_info, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.done, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in let vc = SeedPhraseViewController(event: .confirm, words: self.words) vc.increaseSecutirty = self.increaseSecutirty self.pushViewController(vc: vc) - } + }) } } else if event == .confirm { @@ -203,6 +200,11 @@ class SeedPhraseViewController: BaseViewController { } } else { + // Terminal "confirm seed" path. Clear any stale skip flag a + // prior session may have left behind so isSkipedSeed reflects + // this final action only. + OnboardManager.shared.onSkipSeed(isSkiped: false) + let vc = CreateWalletPasswordViewController().withPhrase(phrase: words.joined(separator: ";")) pushViewController(vc: vc) } @@ -276,10 +278,10 @@ class SeedPhraseViewController: BaseViewController { @objc private func onNavigationBack() { confirmAlert(title: Localizable.shared.strings.seed_back_title, message: Localizable.shared.strings.seed_back_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.generate, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in self.back() - } + }) } @objc private func handleLongPress(sender: UIGestureRecognizer) { diff --git a/BeamWallet/ViewControllers/Login/WellcomeViewController.swift b/BeamWallet/ViewControllers/Login/WellcomeViewController.swift index 0e354610..08e45769 100644 --- a/BeamWallet/ViewControllers/Login/WellcomeViewController.swift +++ b/BeamWallet/ViewControllers/Login/WellcomeViewController.swift @@ -2,7 +2,7 @@ // WellcomeViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ class WellcomeViewController: BaseViewController { languageButton.setTitle(Settings.sharedManager().shortLanguageName(), for: .normal) } - //MARK: IBAction + // MARK: IBAction @IBAction func onRestoreWallet(sender :UIButton) { // AppModel.sharedManager().resetWallet(true) @@ -68,7 +68,7 @@ class WellcomeViewController: BaseViewController { @IBAction func onCreateWallet(sender :UIButton) { // AppModel.sharedManager().resetWallet(true) - AppModel.sharedManager().isRestoreFlow = false; + AppModel.sharedManager().isRestoreFlow = false pushViewController(vc: SeedPhraseViewController(event: .intro, words: nil)) } diff --git a/BeamWallet/ViewControllers/Main/Addresses/AddressTableView.swift b/BeamWallet/ViewControllers/Main/Addresses/AddressTableView.swift index abc84326..002cd5a6 100755 --- a/BeamWallet/ViewControllers/Main/Addresses/AddressTableView.swift +++ b/BeamWallet/ViewControllers/Main/Addresses/AddressTableView.swift @@ -2,7 +2,7 @@ // AddressTableView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -51,9 +51,7 @@ class AddressTableView: UITableViewController { strongSelf.tableView.reloadData() } - viewModel.onDataDeleted = { [weak self] - indexPath, address in - + viewModel.onDataDeleted = { [weak self] indexPath, address in guard let strongSelf = self else { return } if let path = indexPath { @@ -149,10 +147,15 @@ class AddressTableView: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - + if viewModel.count > 0 { let address = viewModel.selectedState == .contacts ? viewModel.contacts[indexPath.row].address : viewModel.addresses[indexPath.row] - + + if viewModel.isPublicOfflineEntry(address) { + pushViewController(vc: OfflineAddressViewController()) + return + } + let vc = AddressViewController(address: address) pushViewController(vc: vc) } diff --git a/BeamWallet/ViewControllers/Main/Addresses/AddressViewController.swift b/BeamWallet/ViewControllers/Main/Addresses/AddressViewController.swift index 532b9fea..70d4d83f 100644 --- a/BeamWallet/ViewControllers/Main/Addresses/AddressViewController.swift +++ b/BeamWallet/ViewControllers/Main/Addresses/AddressViewController.swift @@ -2,7 +2,7 @@ // AddressViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -51,26 +51,18 @@ class AddressViewController: BaseTableViewController { subscribeToUpdates() } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - } - private func subscribeToUpdates() { addressViewModel.onDataChanged = { [weak self] in self?.tableView.reloadData() } - addressViewModel.onDataDeleted = { [weak self] - indexPath, address in - + addressViewModel.onDataDeleted = { [weak self] _, address in AppModel.sharedManager().prepareDelete(address, removeTransactions: address.isNeedRemoveTransactions) self?.back() } - addressViewModel.transactionViewModel.onDataDeleted = { [weak self] - indexPath, transaction in - + addressViewModel.transactionViewModel.onDataDeleted = { [weak self] indexPath, transaction in if let path = indexPath { self?.tableView.performUpdate({ self?.tableView.deleteRows(at: [path], with: .left) @@ -80,9 +72,7 @@ class AddressViewController: BaseTableViewController { } } - addressViewModel.transactionViewModel.onDataUpdated = { [weak self] - indexPath, transaction in - + addressViewModel.transactionViewModel.onDataUpdated = { [weak self] indexPath, transaction in if let path = indexPath { self?.tableView.performUpdate({ self?.tableView.reloadRows(at: [path], with: .fade) diff --git a/BeamWallet/ViewControllers/Main/Addresses/AddressesViewController.swift b/BeamWallet/ViewControllers/Main/Addresses/AddressesViewController.swift index 2d9033b5..05375a2c 100644 --- a/BeamWallet/ViewControllers/Main/Addresses/AddressesViewController.swift +++ b/BeamWallet/ViewControllers/Main/Addresses/AddressesViewController.swift @@ -2,7 +2,7 @@ // AddressesViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Addresses/Cell/AddressExpiresCell.swift b/BeamWallet/ViewControllers/Main/Addresses/Cell/AddressExpiresCell.swift index aec89db5..f58a6420 100644 --- a/BeamWallet/ViewControllers/Main/Addresses/Cell/AddressExpiresCell.swift +++ b/BeamWallet/ViewControllers/Main/Addresses/Cell/AddressExpiresCell.swift @@ -2,7 +2,7 @@ // AddressExpireCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Addresses/EditAddressViewController.swift b/BeamWallet/ViewControllers/Main/Addresses/EditAddressViewController.swift index e82f67bd..1d884f7b 100644 --- a/BeamWallet/ViewControllers/Main/Addresses/EditAddressViewController.swift +++ b/BeamWallet/ViewControllers/Main/Addresses/EditAddressViewController.swift @@ -2,7 +2,7 @@ // EditAddressViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -87,9 +87,7 @@ class EditAddressViewController: BaseTableViewController { tableView.register([AddressExpiresCell.self, BMMultiLinesCell.self, BMFieldCell.self, BMDetailCell.self, BMGroupedCell.self]) - viewModel.onDataDeleted = { [weak self] - indexPath, address in - + viewModel.onDataDeleted = { [weak self] _, address in AppModel.sharedManager().prepareDelete(address, removeTransactions: address.isNeedRemoveTransactions) self?.navigationController?.popViewControllers(viewsToPop: 2) @@ -117,6 +115,34 @@ class EditAddressViewController: BaseTableViewController { back() } + private func presentExpiryPicker() { + guard !viewModel.isContact else { return } + if viewModel.newAddress.isExpired() && !viewModel.newAddress.isNowActive { return } + + let current: Int32 = (viewModel.newAddress.isNowActive ? viewModel.newAddress.isNowActiveDuration : viewModel.newAddress.duration) == 0 ? 0 : 24 + let picker = BMDataPickerViewController(type: .address_expire, selectedValue: current) + picker.completion = { [weak self] selected in + guard let self = self, let hours = selected as? Int32 else { return } + let duration: UInt64 = hours == 0 ? 0 : UInt64(Settings.sharedManager().maxAddressDurationSeconds) + + self.isNeverExpired = hours == 0 + self.canExtend = false + + self.viewModel.newAddress.createTime = UInt64(Date().timeIntervalSince1970) + self.viewModel.newAddress.duration = duration + self.viewModel.newAddress.isNowActiveDuration = duration + self.viewModel.newAddress.isNowActive = true + self.viewModel.newAddress.isNowExpired = false + + self.expireChanged = true + self.buttonSave.isEnabled = self.expireChanged || self.canSave + + self.fillAddressOptions() + self.tableView.reloadData() + } + pushViewController(vc: picker) + } + @objc private func fillAddressOptions() { hasActiveTransactions = AppModel.sharedManager().hasActiveTransactions(from: self.viewModel.address!) @@ -152,7 +178,7 @@ extension EditAddressViewController : UITableViewDelegate { tableView.deselectRow(at: indexPath, animated: true) if (indexPath.section == 0 && indexPath.row == 2) { - + presentExpiryPicker() } else if indexPath.row == addressOptions.count { @@ -246,17 +272,9 @@ extension EditAddressViewController : UITableViewDataSource { cell.topOffset?.constant = 20 return cell case 2: - - var detail = viewModel.newAddress.formattedDate() - - if viewModel.newAddress.isExpired() || viewModel.newAddress.isNowExpired { - detail = Localizable.shared.strings.this_address_expired - } - let cell = tableView - .dequeueReusableCell(withType: BMMultiLinesCell.self, for: indexPath) - .configured(with: BMMultiLineItem(title: Localizable.shared.strings.expires_on.uppercased(), detail:detail, detailFont: RegularFont(size: 16), detailColor: UIColor.white)) - cell.increaseSpace = true + .dequeueReusableCell(withType: AddressExpiresCell.self, for: indexPath) + .configured(with: viewModel.newAddress) return cell default: return BaseCell() diff --git a/BeamWallet/ViewControllers/Main/Addresses/ShowTokenViewController.swift b/BeamWallet/ViewControllers/Main/Addresses/ShowTokenViewController.swift index 15166511..4ed5b189 100644 --- a/BeamWallet/ViewControllers/Main/Addresses/ShowTokenViewController.swift +++ b/BeamWallet/ViewControllers/Main/Addresses/ShowTokenViewController.swift @@ -2,7 +2,7 @@ // ShowTokenViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -79,10 +79,6 @@ class ShowTokenViewController: BaseTableViewController { buildItems() } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - } - @objc private func onCopy() { UIPasteboard.general.string = token ShowCopied(text: Localizable.shared.strings.address_copied) @@ -145,7 +141,7 @@ class ShowTokenViewController: BaseTableViewController { // } - //&& AppModel.sharedManager().checkIsOwnNode() + // && AppModel.sharedManager().checkIsOwnNode() if !params.address.isEmpty { items.append(BMMultiLineItem(title: "", detail: "", detailFont: nil, detailColor: nil)) diff --git a/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapCreateViewController.swift b/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapCreateViewController.swift new file mode 100644 index 00000000..d9d6828d --- /dev/null +++ b/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapCreateViewController.swift @@ -0,0 +1,331 @@ +// +// AssetSwapCreateViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class AssetSwapCreateViewController: BaseViewController { + + private let viewModel = AssetSwapCreateViewModel() + + private let scrollView = UIScrollView() + private let contentView = UIView() + + private let sendLabel = sectionLabel() + private let sendAssetField = BMField() + private let sendAmountField = BMField() + + private let receiveLabel = sectionLabel() + private let receiveAssetField = BMField() + private let receiveAmountField = BMField() + + private let expirationLabel = sectionLabel() + private let expirationField = BMField() + + private let rateValueLabel = UILabel() + private let errorLabel = UILabel() + + private var isSubmitting = false + + private lazy var publishButton: BMButton = { + let button = BMButton.defaultButton(frame: CGRect(x: 0, y: 0, width: 240, height: 44), color: UIColor.main.brightTeal) + button.setTitle(Localizable.shared.strings.asset_swap_publish.lowercased(), for: .normal) + button.setTitleColor(UIColor.main.marineOriginal, for: .normal) + button.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + button.titleLabel?.font = BoldFont(size: 14) + button.addTarget(self, action: #selector(onPublish), for: .touchUpInside) + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true) + title = Localizable.shared.strings.asset_swap_new + + configureLabels() + configureFields() + layoutForm() + + AppModel.sharedManager().addDelegate(self) + refresh() + } + + deinit { + AppModel.sharedManager().removeDelegate(self) + } + + private static func sectionLabel() -> UILabel { + let label = UILabel() + label.font = BoldFont(size: 14) + label.textColor = UIColor.main.blueyGrey + label.letterSpacing = 2 + label.adjustFontSize = true + return label + } + + private func configureLabels() { + sendLabel.text = Localizable.shared.strings.asset_swap_send.uppercased() + receiveLabel.text = Localizable.shared.strings.asset_swap_receive.uppercased() + expirationLabel.text = Localizable.shared.strings.asset_swap_expiration.uppercased() + + rateValueLabel.font = RegularFont(size: 14) + rateValueLabel.textColor = UIColor.main.steelGrey + rateValueLabel.textAlignment = .center + rateValueLabel.numberOfLines = 0 + + errorLabel.font = RegularFont(size: 12) + errorLabel.textColor = UIColor.main.red + errorLabel.textAlignment = .center + errorLabel.numberOfLines = 0 + } + + private func configureFields() { + sendAssetField.placeholder = Localizable.shared.strings.asset_swap_pick_send_asset + sendAmountField.placeholder = Localizable.shared.strings.asset_swap_send_amount + receiveAssetField.placeholder = Localizable.shared.strings.asset_swap_pick_receive_asset + receiveAmountField.placeholder = Localizable.shared.strings.asset_swap_receive_amount + + for field in [sendAssetField, sendAmountField, receiveAssetField, receiveAmountField, expirationField] { + field.font = RegularFont(size: 16) + field.tintColor = UIColor.white + field.textColor = UIColor.white + field.adjustFontSize = true + field.spellCheckingType = .no + field.autocorrectionType = .no + field.awakeFromNib() + } + + sendAssetField.delegate = self + sendAssetField.clearButtonMode = .never + + sendAmountField.keyboardType = .decimalPad + sendAmountField.addTarget(self, action: #selector(onAmountChanged), for: .editingChanged) + + receiveAssetField.delegate = self + receiveAssetField.clearButtonMode = .never + + receiveAmountField.keyboardType = .decimalPad + receiveAmountField.addTarget(self, action: #selector(onAmountChanged), for: .editingChanged) + + expirationField.delegate = self + expirationField.clearButtonMode = .never + expirationField.text = expirationDisplayString(viewModel.expirationMinutes) + } + + private func layoutForm() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.keyboardDismissMode = .interactive + view.addSubview(scrollView) + + contentView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) + + NSLayoutConstraint.activate([ + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: navigationBarOffset), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor), + ]) + + let topInset: CGFloat = 16 + let labelToFieldSpacing: CGFloat = 8 + let fieldHeight: CGFloat = 45 + let groupSpacing: CGFloat = 14 + let sectionSpacing: CGFloat = 26 + let labelHeight: CGFloat = 20 + + var previousAnchor: NSLayoutYAxisAnchor = contentView.topAnchor + var topConstant: CGFloat = topInset + + let sections: [(UILabel, [BMField])] = [ + (sendLabel, [sendAssetField, sendAmountField]), + (receiveLabel, [receiveAssetField, receiveAmountField]), + (expirationLabel, [expirationField]), + ] + + for (label, fields) in sections { + label.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(label) + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: defaultX), + label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -defaultX), + label.topAnchor.constraint(equalTo: previousAnchor, constant: topConstant), + label.heightAnchor.constraint(equalToConstant: labelHeight), + ]) + previousAnchor = label.bottomAnchor + topConstant = labelToFieldSpacing + + for field in fields { + field.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(field) + NSLayoutConstraint.activate([ + field.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: defaultX), + field.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -defaultX), + field.topAnchor.constraint(equalTo: previousAnchor, constant: topConstant), + field.heightAnchor.constraint(equalToConstant: fieldHeight), + ]) + previousAnchor = field.bottomAnchor + topConstant = groupSpacing + } + + topConstant = sectionSpacing + } + + rateValueLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(rateValueLabel) + NSLayoutConstraint.activate([ + rateValueLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: defaultX), + rateValueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -defaultX), + rateValueLabel.topAnchor.constraint(equalTo: previousAnchor, constant: 18), + ]) + + errorLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(errorLabel) + NSLayoutConstraint.activate([ + errorLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: defaultX), + errorLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -defaultX), + errorLabel.topAnchor.constraint(equalTo: rateValueLabel.bottomAnchor, constant: 6), + ]) + + publishButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(publishButton) + NSLayoutConstraint.activate([ + publishButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + publishButton.topAnchor.constraint(equalTo: errorLabel.bottomAnchor, constant: 30), + publishButton.widthAnchor.constraint(equalToConstant: 240), + publishButton.heightAnchor.constraint(equalToConstant: 44), + publishButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30), + ]) + } + + private func refresh() { + sendAssetField.text = viewModel.sendAsset.map { "\($0.unitName) (\($0.assetId))" } ?? "" + receiveAssetField.text = viewModel.receiveAsset.map { "\($0.unitName) (\($0.assetId))" } ?? "" + expirationField.text = expirationDisplayString(viewModel.expirationMinutes) + rateValueLabel.text = "\(Localizable.shared.strings.asset_swap_rate): \(viewModel.displayRate())" + errorLabel.text = viewModel.validationError ?? "" + publishButton.isEnabled = viewModel.canSubmit + publishButton.alpha = viewModel.canSubmit ? 1.0 : 0.5 + } + + private func expirationDisplayString(_ minutes: UInt32) -> String { + switch minutes { + case 60: return "1h" + case 360: return "6h" + case 720: return "12h" + default: + if minutes >= 60 { return "\(minutes / 60)h" } + return "\(minutes)m" + } + } + + @objc private func onAmountChanged() { + viewModel.sendAmountString = sendAmountField.text ?? "" + viewModel.receiveAmountString = receiveAmountField.text ?? "" + rateValueLabel.text = "\(Localizable.shared.strings.asset_swap_rate): \(viewModel.displayRate())" + errorLabel.text = viewModel.validationError ?? "" + publishButton.isEnabled = viewModel.canSubmit + publishButton.alpha = viewModel.canSubmit ? 1.0 : 0.5 + } + + private func presentAssetPicker(currentId: Int, completion: @escaping (BMAsset) -> Void) { + let picker = AssetSearchViewController(selectedAssetId: currentId) + picker.completion = completion + let nav = BaseNavigationController.navigationController(rootViewController: picker) + nav.modalPresentationStyle = .fullScreen + present(nav, animated: true) + } + + @objc private func onPickSendAsset() { + let currentId = Int(viewModel.sendAsset?.assetId ?? 0) + presentAssetPicker(currentId: currentId) { [weak self] asset in + self?.viewModel.sendAsset = asset + self?.refresh() + } + } + + @objc private func onPickReceiveAsset() { + let currentId = Int(viewModel.receiveAsset?.assetId ?? 0) + presentAssetPicker(currentId: currentId) { [weak self] asset in + self?.viewModel.receiveAsset = asset + self?.refresh() + } + } + + @objc private func onPickExpiration() { + let options: [BMOptionPickerViewController.Option] = [ + .init(title: "1 hour", value: 60), + .init(title: "6 hours", value: 360), + .init(title: "12 hours", value: 720), + ] + let picker = BMOptionPickerViewController( + title: Localizable.shared.strings.asset_swap_expiration, + options: options, + selectedValue: Int(viewModel.expirationMinutes) + ) { [weak self] selected in + self?.viewModel.expirationMinutes = UInt32(selected.value) + self?.refresh() + } + present(picker, animated: true) + } + + @objc private func onPublish() { + guard !isSubmitting else { return } + guard viewModel.canSubmit else { return } + isSubmitting = true + publishButton.isEnabled = false + if viewModel.submit() { + navigationController?.popViewController(animated: true) + } else { + errorLabel.text = Localizable.shared.strings.error + isSubmitting = false + publishButton.isEnabled = true + } + } +} + +extension AssetSwapCreateViewController: UITextFieldDelegate { + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + if textField === sendAssetField { + onPickSendAsset() + return false + } + if textField === receiveAssetField { + onPickReceiveAsset() + return false + } + if textField === expirationField { + onPickExpiration() + return false + } + return true + } +} + +extension AssetSwapCreateViewController: WalletModelDelegate { + func onAssetInfoChange() { + refresh() + } +} diff --git a/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapDetailsViewController.swift b/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapDetailsViewController.swift new file mode 100644 index 00000000..10ece11f --- /dev/null +++ b/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapDetailsViewController.swift @@ -0,0 +1,677 @@ +// +// AssetSwapDetailsViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class AssetSwapDetailsViewController: BaseViewController { + + private let viewModel: AssetSwapDetailsViewModel + + private let scrollView = UIScrollView() + private let contentView = UIView() + private let footerContainer = UIView() + + private let sendIconView = AssetIconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) + private let receiveIconView = AssetIconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) + + private let detailsCard = UIView() + private let technicalContainer = UIView() + private let disclosureLabel = UILabel() + private let disclosureChevron = UIImageView(image: UIImage(named: "iconDownArrow")) + + private var technicalExpanded = false + + private var isSubmitting = false + private weak var acceptButton: BMButton? + + init(order: BMDexOrder) { + self.viewModel = AssetSwapDetailsViewModel(order: order) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true) + title = Localizable.shared.strings.asset_swap_details + + layoutFooter() + layoutScrollView() + + let header = makeSwapHeaderCard() + let rateStrip = makeRateStrip() + let details = makeDetailsCard() + + let stack = UIStackView(arrangedSubviews: [header, rateStrip, details]) + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .vertical + stack.spacing = 18 + stack.alignment = .fill + contentView.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: defaultX), + stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -defaultX), + stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24), + stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) + ]) + } + + // MARK: - Footer (sticky, pinned to safe area) + + private func layoutFooter() { + footerContainer.translatesAutoresizingMaskIntoConstraints = false + footerContainer.backgroundColor = view.backgroundColor + view.addSubview(footerContainer) + + NSLayoutConstraint.activate([ + footerContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), + footerContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), + footerContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + + populateFooter(footerContainer) + } + + // MARK: - Scroll container + + private func layoutScrollView() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + contentView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) + + NSLayoutConstraint.activate([ + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: navigationBarOffset), + scrollView.bottomAnchor.constraint(equalTo: footerContainer.topAnchor), + + contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) + ]) + } + + // MARK: - Swap header card (you send / you receive) + + private func makeSwapHeaderCard() -> UIView { + let card = UIView() + card.translatesAutoresizingMaskIntoConstraints = false + card.backgroundColor = UIColor.main.cellBackgroundColor + card.layer.cornerRadius = 12 + card.layer.masksToBounds = true + + let sendRow = makeAssetRow( + iconView: sendIconView, + asset: viewModel.sendAsset, + caption: Localizable.shared.strings.asset_swap_send, + amount: viewModel.order.displaySendAmount(), + assetSymbol: "\(viewModel.order.sendAssetSName) (\(viewModel.order.sendAssetId))" + ) + + let receiveRow = makeAssetRow( + iconView: receiveIconView, + asset: viewModel.receiveAsset, + caption: Localizable.shared.strings.asset_swap_receive, + amount: viewModel.order.displayReceiveAmount(), + assetSymbol: "\(viewModel.order.receiveAssetSName) (\(viewModel.order.receiveAssetId))" + ) + + let separator = UIView() + separator.translatesAutoresizingMaskIntoConstraints = false + separator.backgroundColor = UIColor.main.marineOriginal.withAlphaComponent(0.4) + + let arrow = UIView() + arrow.translatesAutoresizingMaskIntoConstraints = false + arrow.backgroundColor = UIColor.main.marineOriginal + arrow.layer.cornerRadius = 16 + arrow.layer.borderWidth = 2 + arrow.layer.borderColor = UIColor.main.cellBackgroundColor.cgColor + + let arrowGlyph = UILabel() + arrowGlyph.translatesAutoresizingMaskIntoConstraints = false + arrowGlyph.text = "↓" + arrowGlyph.font = BoldFont(size: 18) + arrowGlyph.textColor = UIColor.main.brightTeal + arrowGlyph.textAlignment = .center + arrow.addSubview(arrowGlyph) + + card.addSubview(sendRow) + card.addSubview(separator) + card.addSubview(receiveRow) + card.addSubview(arrow) + + NSLayoutConstraint.activate([ + sendRow.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), + sendRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16), + sendRow.topAnchor.constraint(equalTo: card.topAnchor, constant: 18), + + separator.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), + separator.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16), + separator.topAnchor.constraint(equalTo: sendRow.bottomAnchor, constant: 18), + separator.heightAnchor.constraint(equalToConstant: 1), + + receiveRow.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16), + receiveRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16), + receiveRow.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: 18), + receiveRow.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -18), + + arrow.centerXAnchor.constraint(equalTo: card.centerXAnchor), + arrow.centerYAnchor.constraint(equalTo: separator.centerYAnchor), + arrow.widthAnchor.constraint(equalToConstant: 32), + arrow.heightAnchor.constraint(equalToConstant: 32), + + arrowGlyph.centerXAnchor.constraint(equalTo: arrow.centerXAnchor), + arrowGlyph.centerYAnchor.constraint(equalTo: arrow.centerYAnchor, constant: -1) + ]) + + return card + } + + private func makeAssetRow(iconView: AssetIconView, asset: BMAsset?, caption: String, amount: String, assetSymbol: String) -> UIView { + let row = UIView() + row.translatesAutoresizingMaskIntoConstraints = false + + iconView.translatesAutoresizingMaskIntoConstraints = false + if let asset = asset { + iconView.setAsset(asset) + } + row.addSubview(iconView) + + let captionLabel = UILabel() + captionLabel.translatesAutoresizingMaskIntoConstraints = false + captionLabel.font = RegularFont(size: 11) + captionLabel.textColor = UIColor.main.steelGrey + captionLabel.numberOfLines = 1 + captionLabel.attributedText = NSAttributedString(string: caption.uppercased(), attributes: [.kern: 1.0]) + captionLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + captionLabel.setContentHuggingPriority(.required, for: .horizontal) + row.addSubview(captionLabel) + + let symbolLabel = UILabel() + symbolLabel.translatesAutoresizingMaskIntoConstraints = false + symbolLabel.font = SemiboldFont(size: 15) + symbolLabel.textColor = UIColor.white + symbolLabel.numberOfLines = 1 + symbolLabel.text = assetSymbol + symbolLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + symbolLabel.setContentHuggingPriority(.required, for: .horizontal) + row.addSubview(symbolLabel) + + let amountLabel = UILabel() + amountLabel.translatesAutoresizingMaskIntoConstraints = false + amountLabel.font = BoldFont(size: 22) + amountLabel.textColor = UIColor.white + amountLabel.textAlignment = .right + amountLabel.numberOfLines = 1 + amountLabel.adjustsFontSizeToFitWidth = true + amountLabel.minimumScaleFactor = 0.6 + amountLabel.text = amount + amountLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + amountLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + row.addSubview(amountLabel) + + NSLayoutConstraint.activate([ + iconView.leadingAnchor.constraint(equalTo: row.leadingAnchor), + iconView.centerYAnchor.constraint(equalTo: row.centerYAnchor), + iconView.widthAnchor.constraint(equalToConstant: 48), + iconView.heightAnchor.constraint(equalToConstant: 48), + + captionLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12), + captionLabel.topAnchor.constraint(equalTo: iconView.topAnchor, constant: 4), + captionLabel.trailingAnchor.constraint(lessThanOrEqualTo: amountLabel.leadingAnchor, constant: -8), + + symbolLabel.leadingAnchor.constraint(equalTo: captionLabel.leadingAnchor), + symbolLabel.topAnchor.constraint(equalTo: captionLabel.bottomAnchor, constant: 4), + symbolLabel.trailingAnchor.constraint(lessThanOrEqualTo: amountLabel.leadingAnchor, constant: -8), + + amountLabel.trailingAnchor.constraint(equalTo: row.trailingAnchor), + amountLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor), + + row.heightAnchor.constraint(greaterThanOrEqualToConstant: 48) + ]) + + return row + } + + // MARK: - Rate strip + + private func makeRateStrip() -> UIView { + let strip = UIView() + strip.translatesAutoresizingMaskIntoConstraints = false + + let captionLabel = UILabel() + captionLabel.translatesAutoresizingMaskIntoConstraints = false + captionLabel.font = RegularFont(size: 11) + captionLabel.textColor = UIColor.main.steelGrey + captionLabel.attributedText = NSAttributedString( + string: Localizable.shared.strings.asset_swap_rate.uppercased(), + attributes: [.kern: 1.0] + ) + strip.addSubview(captionLabel) + + let valueLabel = UILabel() + valueLabel.translatesAutoresizingMaskIntoConstraints = false + valueLabel.font = SemiboldFont(size: 13) + valueLabel.textColor = UIColor.main.brightTeal + valueLabel.textAlignment = .right + valueLabel.adjustsFontSizeToFitWidth = true + valueLabel.minimumScaleFactor = 0.7 + valueLabel.text = viewModel.order.displayRate() + strip.addSubview(valueLabel) + + NSLayoutConstraint.activate([ + captionLabel.leadingAnchor.constraint(equalTo: strip.leadingAnchor, constant: 4), + captionLabel.centerYAnchor.constraint(equalTo: strip.centerYAnchor), + + valueLabel.trailingAnchor.constraint(equalTo: strip.trailingAnchor, constant: -4), + valueLabel.centerYAnchor.constraint(equalTo: strip.centerYAnchor), + valueLabel.leadingAnchor.constraint(greaterThanOrEqualTo: captionLabel.trailingAnchor, constant: 12), + + strip.heightAnchor.constraint(equalToConstant: 24) + ]) + + return strip + } + + // MARK: - Details card (status / created / expires + collapsible technical) + + private func makeDetailsCard() -> UIView { + detailsCard.translatesAutoresizingMaskIntoConstraints = false + detailsCard.backgroundColor = UIColor.main.cellBackgroundColor + detailsCard.layer.cornerRadius = 12 + detailsCard.layer.masksToBounds = true + + let order = viewModel.order + + var visibleRows: [UIView] = [] + + let statusRow = makeDetailRow( + label: Localizable.shared.strings.asset_swap_status, + value: order.displayStatus().capitalized, + valueColor: viewModel.statusColor + ) + visibleRows.append(statusRow) + + let createdRow = makeDetailRow( + label: Localizable.shared.strings.asset_swap_created, + value: order.displayCreatedDate(), + valueColor: UIColor.white + ) + visibleRows.append(createdRow) + + if order.isActive() { + let expiresRow = makeDetailRow( + label: Localizable.shared.strings.asset_swap_expires_in, + value: order.displayExpiresIn(), + valueColor: viewModel.isExpiringSoon ? UIColor.main.coral : UIColor.white + ) + visibleRows.append(expiresRow) + } + + let primaryStack = UIStackView(arrangedSubviews: visibleRows) + primaryStack.translatesAutoresizingMaskIntoConstraints = false + primaryStack.axis = .vertical + primaryStack.spacing = 0 + addRowSeparators(to: primaryStack) + detailsCard.addSubview(primaryStack) + + // Disclosure + let disclosure = makeDisclosureRow() + detailsCard.addSubview(disclosure) + + // Technical section (initially hidden) + technicalContainer.translatesAutoresizingMaskIntoConstraints = false + technicalContainer.isHidden = true + technicalContainer.alpha = 0 + detailsCard.addSubview(technicalContainer) + + var techRows: [UIView] = [] + + if !order.sbbsID.isEmpty { + techRows.append(makeCopyableRow( + label: Localizable.shared.strings.asset_swap_peer_id, + value: order.sbbsID, + copyToast: Localizable.shared.strings.copied_to_clipboard + )) + } + techRows.append(makeCopyableRow( + label: Localizable.shared.strings.asset_swap_order_id, + value: order.orderID, + copyToast: Localizable.shared.strings.copied_to_clipboard + )) + + let techStack = UIStackView(arrangedSubviews: techRows) + techStack.translatesAutoresizingMaskIntoConstraints = false + techStack.axis = .vertical + techStack.spacing = 0 + addRowSeparators(to: techStack) + + let techTopSeparator = UIView() + techTopSeparator.translatesAutoresizingMaskIntoConstraints = false + techTopSeparator.backgroundColor = UIColor.main.marineOriginal.withAlphaComponent(0.4) + + technicalContainer.addSubview(techTopSeparator) + technicalContainer.addSubview(techStack) + + NSLayoutConstraint.activate([ + techTopSeparator.leadingAnchor.constraint(equalTo: technicalContainer.leadingAnchor, constant: 16), + techTopSeparator.trailingAnchor.constraint(equalTo: technicalContainer.trailingAnchor, constant: -16), + techTopSeparator.topAnchor.constraint(equalTo: technicalContainer.topAnchor), + techTopSeparator.heightAnchor.constraint(equalToConstant: 1), + + techStack.leadingAnchor.constraint(equalTo: technicalContainer.leadingAnchor), + techStack.trailingAnchor.constraint(equalTo: technicalContainer.trailingAnchor), + techStack.topAnchor.constraint(equalTo: techTopSeparator.bottomAnchor), + techStack.bottomAnchor.constraint(equalTo: technicalContainer.bottomAnchor) + ]) + + NSLayoutConstraint.activate([ + primaryStack.leadingAnchor.constraint(equalTo: detailsCard.leadingAnchor), + primaryStack.trailingAnchor.constraint(equalTo: detailsCard.trailingAnchor), + primaryStack.topAnchor.constraint(equalTo: detailsCard.topAnchor, constant: 6), + + disclosure.leadingAnchor.constraint(equalTo: detailsCard.leadingAnchor), + disclosure.trailingAnchor.constraint(equalTo: detailsCard.trailingAnchor), + disclosure.topAnchor.constraint(equalTo: primaryStack.bottomAnchor), + + technicalContainer.leadingAnchor.constraint(equalTo: detailsCard.leadingAnchor), + technicalContainer.trailingAnchor.constraint(equalTo: detailsCard.trailingAnchor), + technicalContainer.topAnchor.constraint(equalTo: disclosure.bottomAnchor), + technicalContainer.bottomAnchor.constraint(equalTo: detailsCard.bottomAnchor, constant: -6) + ]) + + return detailsCard + } + + private func makeDetailRow(label: String, value: String, valueColor: UIColor) -> UIView { + let row = UIView() + row.translatesAutoresizingMaskIntoConstraints = false + + let labelView = UILabel() + labelView.translatesAutoresizingMaskIntoConstraints = false + labelView.font = RegularFont(size: 13) + labelView.textColor = UIColor.main.steelGrey + labelView.text = label + row.addSubview(labelView) + + let valueView = UILabel() + valueView.translatesAutoresizingMaskIntoConstraints = false + valueView.font = SemiboldFont(size: 13) + valueView.textColor = valueColor + valueView.text = value + valueView.textAlignment = .right + valueView.adjustsFontSizeToFitWidth = true + valueView.minimumScaleFactor = 0.7 + row.addSubview(valueView) + + NSLayoutConstraint.activate([ + labelView.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 16), + labelView.centerYAnchor.constraint(equalTo: row.centerYAnchor), + + valueView.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -16), + valueView.centerYAnchor.constraint(equalTo: row.centerYAnchor), + valueView.leadingAnchor.constraint(greaterThanOrEqualTo: labelView.trailingAnchor, constant: 12), + + row.heightAnchor.constraint(equalToConstant: 44) + ]) + + return row + } + + private func makeCopyableRow(label: String, value: String, copyToast: String) -> UIView { + let row = UIView() + row.translatesAutoresizingMaskIntoConstraints = false + + let labelView = UILabel() + labelView.translatesAutoresizingMaskIntoConstraints = false + labelView.font = RegularFont(size: 13) + labelView.textColor = UIColor.main.steelGrey + labelView.text = label + row.addSubview(labelView) + + let valueView = UILabel() + valueView.translatesAutoresizingMaskIntoConstraints = false + valueView.font = RegularFont(size: 11) + valueView.textColor = UIColor.main.steelGrey + valueView.text = value + valueView.numberOfLines = 2 + valueView.lineBreakMode = .byTruncatingMiddle + valueView.textAlignment = .right + row.addSubview(valueView) + + let copyButton = UIButton(type: .system) + copyButton.translatesAutoresizingMaskIntoConstraints = false + copyButton.setImage(UIImage(named: "iconCopyBlue"), for: .normal) + copyButton.tintColor = UIColor.main.brightTeal + copyButton.contentEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + copyButton.accessibilityLabel = copyToast + copyButton.addAction(UIAction { _ in + UIPasteboard.general.string = value + BMToast.show(text: copyToast) + }, for: .touchUpInside) + row.addSubview(copyButton) + + NSLayoutConstraint.activate([ + labelView.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 16), + labelView.topAnchor.constraint(equalTo: row.topAnchor, constant: 12), + + copyButton.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -10), + copyButton.centerYAnchor.constraint(equalTo: labelView.centerYAnchor), + copyButton.widthAnchor.constraint(equalToConstant: 28), + copyButton.heightAnchor.constraint(equalToConstant: 28), + + valueView.leadingAnchor.constraint(equalTo: labelView.trailingAnchor, constant: 12), + valueView.trailingAnchor.constraint(equalTo: copyButton.leadingAnchor, constant: -8), + valueView.centerYAnchor.constraint(equalTo: labelView.centerYAnchor), + + row.bottomAnchor.constraint(equalTo: valueView.bottomAnchor, constant: 12), + row.heightAnchor.constraint(greaterThanOrEqualToConstant: 48) + ]) + + return row + } + + private func addRowSeparators(to stack: UIStackView) { + let rows = stack.arrangedSubviews + for index in 1.. UIView { + let row = UIControl() + row.translatesAutoresizingMaskIntoConstraints = false + row.addTarget(self, action: #selector(toggleTechnical), for: .touchUpInside) + + disclosureLabel.translatesAutoresizingMaskIntoConstraints = false + disclosureLabel.font = SemiboldFont(size: 12) + disclosureLabel.textColor = UIColor.main.brightTeal + disclosureLabel.text = Localizable.shared.strings.asset_swap_show_details.uppercased() + row.addSubview(disclosureLabel) + + disclosureChevron.translatesAutoresizingMaskIntoConstraints = false + disclosureChevron.tintColor = UIColor.main.brightTeal + disclosureChevron.contentMode = .scaleAspectFit + row.addSubview(disclosureChevron) + + NSLayoutConstraint.activate([ + disclosureLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 16), + disclosureLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor), + + disclosureChevron.leadingAnchor.constraint(equalTo: disclosureLabel.trailingAnchor, constant: 6), + disclosureChevron.centerYAnchor.constraint(equalTo: row.centerYAnchor), + disclosureChevron.widthAnchor.constraint(equalToConstant: 12), + disclosureChevron.heightAnchor.constraint(equalToConstant: 12), + + row.heightAnchor.constraint(equalToConstant: 44) + ]) + + return row + } + + @objc private func toggleTechnical() { + technicalExpanded.toggle() + + let strings = Localizable.shared.strings + let title = (technicalExpanded ? strings.asset_swap_hide_details : strings.asset_swap_show_details).uppercased() + + UIView.animate(withDuration: 0.25) { + self.disclosureLabel.text = title + self.disclosureChevron.transform = self.technicalExpanded + ? CGAffineTransform(rotationAngle: .pi) + : .identity + self.technicalContainer.isHidden = !self.technicalExpanded + self.technicalContainer.alpha = self.technicalExpanded ? 1 : 0 + self.view.layoutIfNeeded() + } + } + + // MARK: - Footer + + private func populateFooter(_ footer: UIView) { + if !viewModel.canAccept && !viewModel.canCancel { + return + } + + let separator = UIView() + separator.translatesAutoresizingMaskIntoConstraints = false + separator.backgroundColor = UIColor.main.marineOriginal.withAlphaComponent(0.4) + footer.addSubview(separator) + + let leftButton = makeOutlineButton(title: Localizable.shared.strings.cancel) + let rightButton: BMButton + if viewModel.canCancel { + rightButton = BMButton.defaultButton( + frame: CGRect(x: 0, y: 0, width: 0, height: 44), + color: UIColor.main.red + ) + rightButton.setTitle(Localizable.shared.strings.asset_swap_cancel_order, for: .normal) + rightButton.setTitleColor(UIColor.white, for: .normal) + rightButton.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted) + rightButton.titleLabel?.font = BoldFont(size: 14) + rightButton.addTarget(self, action: #selector(onCancel), for: .touchUpInside) + } else { + rightButton = BMButton.defaultButton( + frame: CGRect(x: 0, y: 0, width: 0, height: 44), + color: UIColor.main.brightTeal + ) + rightButton.setTitle(Localizable.shared.strings.asset_swap_accept, for: .normal) + rightButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) + rightButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + rightButton.titleLabel?.font = BoldFont(size: 14) + rightButton.addTarget(self, action: #selector(onAccept), for: .touchUpInside) + acceptButton = rightButton + } + + leftButton.addTarget(self, action: #selector(onLeftButton), for: .touchUpInside) + + leftButton.translatesAutoresizingMaskIntoConstraints = false + rightButton.translatesAutoresizingMaskIntoConstraints = false + footer.addSubview(leftButton) + footer.addSubview(rightButton) + + NSLayoutConstraint.activate([ + separator.leadingAnchor.constraint(equalTo: footer.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: footer.trailingAnchor), + separator.topAnchor.constraint(equalTo: footer.topAnchor), + separator.heightAnchor.constraint(equalToConstant: 1), + + leftButton.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: defaultX), + leftButton.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: 16), + leftButton.heightAnchor.constraint(equalToConstant: 44), + + rightButton.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -defaultX), + rightButton.topAnchor.constraint(equalTo: leftButton.topAnchor), + rightButton.heightAnchor.constraint(equalToConstant: 44), + + rightButton.leadingAnchor.constraint(equalTo: leftButton.trailingAnchor, constant: defaultX), + leftButton.widthAnchor.constraint(equalTo: rightButton.widthAnchor), + + footer.bottomAnchor.constraint(equalTo: leftButton.bottomAnchor, constant: 12) + ]) + } + + private func makeOutlineButton(title: String) -> BMButton { + let button = BMButton(frame: CGRect(x: 0, y: 0, width: 0, height: 44)) + button.cornerRadius = 22 + button.layer.borderWidth = 1 + button.layer.borderColor = UIColor.white.cgColor + button.setTitle(title, for: .normal) + button.setTitleColor(UIColor.white, for: .normal) + button.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted) + button.titleLabel?.font = BoldFont(size: 14) + button.adjustFontSize = true + button.titleEdgeInsets = .zero + button.imageEdgeInsets = .zero + return button + } + + // MARK: - Actions + + @objc private func onLeftButton() { + navigationController?.popViewController(animated: true) + } + + @objc private func onCancel() { + confirmAlert( + title: Localizable.shared.strings.asset_swap_confirm_cancel, + message: "", + cancelTitle: Localizable.shared.strings.cancel, + confirmTitle: Localizable.shared.strings.asset_swap_cancel_order, + cancelHandler: { _ in }, + confirmHandler: { [weak self] _ in + self?.viewModel.cancel() + self?.navigationController?.popViewController(animated: true) + } + ) + } + + @objc private func onAccept() { + guard !isSubmitting else { return } + if let error = viewModel.validationError { + alert(message: error) + return + } + isSubmitting = true + acceptButton?.isEnabled = false + if viewModel.accept() { + navigationController?.popViewController(animated: true) + } else { + isSubmitting = false + acceptButton?.isEnabled = true + } + } +} diff --git a/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapsViewController.swift b/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapsViewController.swift new file mode 100644 index 00000000..7ab68caa --- /dev/null +++ b/BeamWallet/ViewControllers/Main/AssetSwaps/AssetSwapsViewController.swift @@ -0,0 +1,143 @@ +// +// AssetSwapsViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class AssetSwapsViewController: BaseTableViewController { + + private let viewModel = AssetSwapsViewModel() + private let segmented = UISegmentedControl(items: [ + Localizable.shared.strings.asset_swap_open_orders, + Localizable.shared.strings.asset_swap_my_orders, + Localizable.shared.strings.asset_swap_history, + ]) + private let segmentedHeader = UIView() + private let emptyLabel = UILabel() + private let segmentedHeaderHeight: CGFloat = 60 + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true, menu: self.navigationController?.viewControllers.count == 1) + + title = Localizable.shared.strings.asset_swaps + + addRightButton(image: IconAdd(), target: self, selector: #selector(onNewSwap)) + + segmented.selectedSegmentIndex = 0 + segmented.addTarget(self, action: #selector(onSegmentChange), for: .valueChanged) + if #available(iOS 13.0, *) { + segmented.selectedSegmentTintColor = UIColor.main.brightTeal + segmented.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .normal) + segmented.setTitleTextAttributes([.foregroundColor: UIColor.main.marineOriginal], for: .selected) + } + + segmentedHeader.backgroundColor = UIColor.main.marine + segmentedHeader.addSubview(segmented) + view.addSubview(segmentedHeader) + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.register(AssetSwapOrderCell.self, forCellReuseIdentifier: "AssetSwapOrderCell") + tableView.addPullToRefresh(target: self, handler: #selector(refresh(_:))) + + emptyLabel.text = Localizable.shared.strings.asset_swap_no_orders + emptyLabel.textAlignment = .center + emptyLabel.numberOfLines = 0 + emptyLabel.font = RegularFont(size: 16) + emptyLabel.textColor = Settings.sharedManager().isDarkMode ? UIColor.main.steel : UIColor.main.blueyGrey + view.addSubview(emptyLabel) + + viewModel.onDataChanged = { [weak self] in + self?.tableView.stopRefreshing() + self?.tableView.reloadData() + self?.updateEmptyState() + } + + viewModel.reload() + updateEmptyState() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewModel.reload() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let tableTop = tableView.frame.origin.y + let width = view.bounds.width + segmentedHeader.frame = CGRect(x: 0, y: tableTop, width: width, height: segmentedHeaderHeight) + segmented.frame = CGRect(x: defaultX, y: 12, width: width - 2 * defaultX, height: 36) + view.bringSubviewToFront(segmentedHeader) + + tableView.frame = CGRect( + x: tableView.frame.origin.x, + y: tableTop + segmentedHeaderHeight, + width: tableView.frame.width, + height: tableView.frame.height - segmentedHeaderHeight + ) + + emptyLabel.frame = CGRect(x: 20, y: navigationBarOffset + 120, width: view.bounds.width - 40, height: 60) + } + + private func updateEmptyState() { + emptyLabel.isHidden = !viewModel.ordersForActiveTab.isEmpty + } + + @objc private func onNewSwap() { + pushViewController(vc: AssetSwapCreateViewController()) + } + + @objc private func onSegmentChange() { + viewModel.selectedTab = AssetSwapsTab(rawValue: segmented.selectedSegmentIndex) ?? .open + tableView.reloadData() + updateEmptyState() + } + + @objc private func refresh(_ sender: Any) { + viewModel.reload() + } +} + +extension AssetSwapsViewController: UITableViewDelegate, UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.ordersForActiveTab.count + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 80 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "AssetSwapOrderCell", for: indexPath) as! AssetSwapOrderCell + cell.configure(with: viewModel.ordersForActiveTab[indexPath.row], row: indexPath.row) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let order = viewModel.ordersForActiveTab[indexPath.row] + let vc = AssetSwapDetailsViewController(order: order) + pushViewController(vc: vc) + } +} diff --git a/BeamWallet/ViewControllers/Main/AssetSwaps/Cells/AssetSwapOrderCell.swift b/BeamWallet/ViewControllers/Main/AssetSwaps/Cells/AssetSwapOrderCell.swift new file mode 100644 index 00000000..abf39731 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/AssetSwaps/Cells/AssetSwapOrderCell.swift @@ -0,0 +1,119 @@ +// +// AssetSwapOrderCell.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class AssetSwapOrderCell: RippleCell { + + private let mainView = UIView() + private let amountsLabel = UILabel() + private let rateLabel = UILabel() + private let statusLabel = UILabel() + private let expiryLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + backgroundColor = .clear + contentView.backgroundColor = .clear + selectionStyle = .none + + mainView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(mainView) + + amountsLabel.translatesAutoresizingMaskIntoConstraints = false + amountsLabel.font = BoldFont(size: 15) + amountsLabel.textColor = UIColor.white + amountsLabel.numberOfLines = 0 + mainView.addSubview(amountsLabel) + + rateLabel.translatesAutoresizingMaskIntoConstraints = false + rateLabel.font = RegularFont(size: 13) + rateLabel.textColor = UIColor.main.steelGrey + mainView.addSubview(rateLabel) + + statusLabel.translatesAutoresizingMaskIntoConstraints = false + statusLabel.font = SemiboldFont(size: 12) + statusLabel.textColor = UIColor.main.brightTeal + statusLabel.textAlignment = .right + mainView.addSubview(statusLabel) + + expiryLabel.translatesAutoresizingMaskIntoConstraints = false + expiryLabel.font = RegularFont(size: 12) + expiryLabel.textColor = UIColor.main.steelGrey + expiryLabel.textAlignment = .right + mainView.addSubview(expiryLabel) + + NSLayoutConstraint.activate([ + mainView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + mainView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + mainView.topAnchor.constraint(equalTo: contentView.topAnchor), + mainView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + + amountsLabel.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: defaultX), + amountsLabel.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 14), + amountsLabel.trailingAnchor.constraint(lessThanOrEqualTo: statusLabel.leadingAnchor, constant: -8), + + statusLabel.trailingAnchor.constraint(equalTo: mainView.trailingAnchor, constant: -defaultX), + statusLabel.centerYAnchor.constraint(equalTo: amountsLabel.centerYAnchor), + statusLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 100), + + rateLabel.leadingAnchor.constraint(equalTo: amountsLabel.leadingAnchor), + rateLabel.topAnchor.constraint(equalTo: amountsLabel.bottomAnchor, constant: 6), + rateLabel.bottomAnchor.constraint(lessThanOrEqualTo: mainView.bottomAnchor, constant: -14), + + expiryLabel.trailingAnchor.constraint(equalTo: statusLabel.trailingAnchor), + expiryLabel.centerYAnchor.constraint(equalTo: rateLabel.centerYAnchor), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } + + func configure(with order: BMDexOrder, row: Int) { + let sendText = "\(order.displaySendAmount()) (\(order.sendAssetId))" + let receiveText = "\(order.displayReceiveAmount()) (\(order.receiveAssetId))" + amountsLabel.text = "\(sendText) → \(receiveText)" + rateLabel.text = order.displayRate() + + if order.isCompleted { + statusLabel.text = Localizable.shared.strings.asset_swap_completed + statusLabel.textColor = UIColor.main.steelGrey + expiryLabel.text = "" + } else if order.isCanceled { + statusLabel.text = Localizable.shared.strings.asset_swap_canceled + statusLabel.textColor = UIColor.main.steelGrey + expiryLabel.text = "" + } else if order.isExpired() { + statusLabel.text = Localizable.shared.strings.asset_swap_expired + statusLabel.textColor = UIColor.main.steelGrey + expiryLabel.text = "" + } else if order.isAccepted { + statusLabel.text = Localizable.shared.strings.in_progress + statusLabel.textColor = UIColor.main.brightTeal + expiryLabel.text = order.displayExpiresIn() + } else { + statusLabel.text = "" + expiryLabel.text = order.displayExpiresIn() + } + + mainView.backgroundColor = (row % 2 == 0) ? UIColor.main.cellBackgroundColor : UIColor.main.marine + } +} diff --git a/BeamWallet/ViewControllers/Main/Assets/AssetBalanceViewController.swift b/BeamWallet/ViewControllers/Main/Assets/AssetBalanceViewController.swift index a0827980..c9dfc8f6 100644 --- a/BeamWallet/ViewControllers/Main/Assets/AssetBalanceViewController.swift +++ b/BeamWallet/ViewControllers/Main/Assets/AssetBalanceViewController.swift @@ -2,7 +2,7 @@ // AssetInfoViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Assets/AssetDescViewController.swift b/BeamWallet/ViewControllers/Main/Assets/AssetDescViewController.swift index 0051b194..bba1f85f 100644 --- a/BeamWallet/ViewControllers/Main/Assets/AssetDescViewController.swift +++ b/BeamWallet/ViewControllers/Main/Assets/AssetDescViewController.swift @@ -2,7 +2,7 @@ // AssetDescViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Assets/AssetDetailViewController.swift b/BeamWallet/ViewControllers/Main/Assets/AssetDetailViewController.swift index 01e2f856..0059b600 100644 --- a/BeamWallet/ViewControllers/Main/Assets/AssetDetailViewController.swift +++ b/BeamWallet/ViewControllers/Main/Assets/AssetDetailViewController.swift @@ -2,7 +2,7 @@ // AssetDetailViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -94,9 +94,7 @@ class AssetDetailViewController: BaseTableViewController { } } - transactionViewModel.onDataDeleted = { [weak self] - indexPath, transaction in - + transactionViewModel.onDataDeleted = { [weak self] indexPath, transaction in guard let strongSelf = self else { return } if strongSelf.transactionViewModel.transactions.count == 0 { @@ -114,9 +112,7 @@ class AssetDetailViewController: BaseTableViewController { } } - transactionViewModel.onDataUpdated = { [weak self] - indexPath, transaction in - + transactionViewModel.onDataUpdated = { [weak self] indexPath, transaction in if let path = indexPath { self?.tableView.performUpdate({ self?.tableView.reloadRows(at: [path], with: .fade) @@ -396,7 +392,7 @@ extension AssetDetailViewController: UIContextMenuInteractionDelegate { return UIContextMenuConfiguration(identifier: indexPath as NSCopying, previewProvider: { return detailVC - }, actionProvider: { suggestedActions in + }, actionProvider: { _ in return self.makeContextMenu(transaction: self.transactionViewModel.transactions[indexPath.row]) }) } @@ -410,45 +406,45 @@ extension AssetDetailViewController: UIContextMenuInteractionDelegate { let viewModel = DetailTransactionViewModel(transaction: transaction) if transaction.canSaveContact() { - let action1 = UIAction(title: Localizable.shared.strings.save_contact_title, image: nil) { action in + let action1 = UIAction(title: Localizable.shared.strings.save_contact_title, image: nil) { _ in viewModel.saveContact() } array.append(action1) } - let action1 = UIAction(title: Localizable.shared.strings.share_details, image: nil) { action in + let action1 = UIAction(title: Localizable.shared.strings.share_details, image: nil) { _ in viewModel.share() } array.append(action1) - let action2 = UIAction(title: Localizable.shared.strings.copy_details, image: nil) { action in + let action2 = UIAction(title: Localizable.shared.strings.copy_details, image: nil) { _ in viewModel.copyDetails() } array.append(action2) if !transaction.isIncome && !transaction.isDapps { - let action3 = UIAction(title: Localizable.shared.strings.repeat_transaction, image: nil) { action in + let action3 = UIAction(title: Localizable.shared.strings.repeat_transaction, image: nil) { _ in viewModel.repeatTransation(transaction: viewModel.transaction!) } array.append(action3) } if transaction.canCancel && !transaction.isDapps { - let action4 = UIAction(title: Localizable.shared.strings.cancel_transaction, image: nil) { action in + let action4 = UIAction(title: Localizable.shared.strings.cancel_transaction, image: nil) { _ in viewModel.cancelTransation(indexPath: nil) } array.append(action4) } if transaction.canDelete { - let action5 = UIAction(title: Localizable.shared.strings.delete_transaction, image: nil) { action in + let action5 = UIAction(title: Localizable.shared.strings.delete_transaction, image: nil) { _ in viewModel.deleteTransationNew(indexPath: nil) } array.append(action5) } if transaction.isDapps { - let action6 = UIAction(title: Localizable.shared.strings.open_dapp, image: nil) { action in + let action6 = UIAction(title: Localizable.shared.strings.open_dapp, image: nil) { _ in viewModel.openDapp() } array.append(action6) diff --git a/BeamWallet/ViewControllers/Main/Assets/AssetInfoViewController.swift b/BeamWallet/ViewControllers/Main/Assets/AssetInfoViewController.swift index e95ca0d2..ec481ff6 100644 --- a/BeamWallet/ViewControllers/Main/Assets/AssetInfoViewController.swift +++ b/BeamWallet/ViewControllers/Main/Assets/AssetInfoViewController.swift @@ -2,7 +2,7 @@ // AssetInfoViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Assets/AssetsViewController.swift b/BeamWallet/ViewControllers/Main/Assets/AssetsViewController.swift index 959fbdb0..77eb70ab 100644 --- a/BeamWallet/ViewControllers/Main/Assets/AssetsViewController.swift +++ b/BeamWallet/ViewControllers/Main/Assets/AssetsViewController.swift @@ -2,7 +2,7 @@ // AssetsViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -47,10 +47,6 @@ class AssetsViewController: BaseTableViewController { subscribeToUpdates() } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - } deinit { Settings.sharedManager().removeDelegate(self) diff --git a/BeamWallet/ViewControllers/Main/Assets/Cells/AssetDropDownCell.swift b/BeamWallet/ViewControllers/Main/Assets/Cells/AssetDropDownCell.swift index 7ded51a8..e3dd1ba9 100644 --- a/BeamWallet/ViewControllers/Main/Assets/Cells/AssetDropDownCell.swift +++ b/BeamWallet/ViewControllers/Main/Assets/Cells/AssetDropDownCell.swift @@ -2,7 +2,7 @@ // AssetDropDownCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Assets/Cells/AssetExporerCell.swift b/BeamWallet/ViewControllers/Main/Assets/Cells/AssetExporerCell.swift index 8a3c5d4e..24c62aa3 100644 --- a/BeamWallet/ViewControllers/Main/Assets/Cells/AssetExporerCell.swift +++ b/BeamWallet/ViewControllers/Main/Assets/Cells/AssetExporerCell.swift @@ -2,7 +2,7 @@ // AssetExporerCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Assets/Cells/AssetSiteCell.swift b/BeamWallet/ViewControllers/Main/Assets/Cells/AssetSiteCell.swift index e971b6d2..45bdef38 100644 --- a/BeamWallet/ViewControllers/Main/Assets/Cells/AssetSiteCell.swift +++ b/BeamWallet/ViewControllers/Main/Assets/Cells/AssetSiteCell.swift @@ -2,7 +2,7 @@ // AssetSiteCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/DAO/Cell/DAOAppCell.swift b/BeamWallet/ViewControllers/Main/DAO/Cell/DAOAppCell.swift index 951e76c5..e0e5c2f3 100644 --- a/BeamWallet/ViewControllers/Main/DAO/Cell/DAOAppCell.swift +++ b/BeamWallet/ViewControllers/Main/DAO/Cell/DAOAppCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 08.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -19,24 +19,43 @@ class DAOAppCell: RippleCell { override func awakeFromNib() { super.awakeFromNib() - + selectionStyle = .none } + + override func prepareForReuse() { + super.prepareForReuse() + iconView.sd_cancelCurrentImageLoad() + iconView.image = nil + } } extension DAOAppCell: Configurable { - + func configure(with options: (row: Int, app:BMApp)) { - + iconMainView.backgroundColor = UIColor.main.marineThree mainView.backgroundColor = UIColor.white.withAlphaComponent(0.1) - + nameLabel.text = options.app.name + iconView.sd_cancelCurrentImageLoad() if options.app.name.lowercased() == "beamx dao" { iconView.image = UIImage(named: "dao_app_icon") } else { - iconView.sd_setImage(with: URL(string: options.app.icon), completed: nil) + // Force the SVG coder onto its bitmap path. The default vector path + // returns a UIImage backed by a live CGSVGDocumentRef, which CoreSVG + // re-renders on every draw — that crashes when scrolling on iOS 17+. + let pixelSize = CGSize(width: 96, height: 96) // 32pt @3x + let context: [SDWebImageContextOption: Any] = [ + .imageThumbnailPixelSize: NSValue(cgSize: pixelSize), + .imagePreserveAspectRatio: true, + ] + iconView.sd_setImage( + with: URL(string: options.app.icon), + placeholderImage: nil, + context: context + ) } if options.app.isSupported { diff --git a/BeamWallet/ViewControllers/Main/DAO/Cell/DAppStoreCells.swift b/BeamWallet/ViewControllers/Main/DAO/Cell/DAppStoreCells.swift new file mode 100644 index 00000000..d801c3b3 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/DAO/Cell/DAppStoreCells.swift @@ -0,0 +1,319 @@ +// +// DAppStoreCells.swift +// BeamWallet +// +// Cells used by the My DApps / DApp Store browse / Publishers screens. +// + +import UIKit +import SDWebImage + +private enum CellMetrics { + static let iconSize: CGFloat = 44 + static let inset: CGFloat = 16 +} + +// MARK: - Installed DApp row + +final class MyDAppCell: UITableViewCell { + + let iconView = UIImageView() + let nameLabel = UILabel() + let detailLabel = UILabel() + let removeButton = UIButton(type: .system) + var onRemoveTapped: (() -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + backgroundColor = .clear + selectionStyle = .none + + iconView.contentMode = .scaleAspectFit + iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.layer.cornerRadius = CellMetrics.iconSize / 2 + iconView.clipsToBounds = true + contentView.addSubview(iconView) + + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.font = SemiboldFont(size: 16) + nameLabel.textColor = .white + contentView.addSubview(nameLabel) + + detailLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.font = RegularFont(size: 13) + detailLabel.textColor = UIColor.white.withAlphaComponent(0.7) + detailLabel.numberOfLines = 2 + contentView.addSubview(detailLabel) + + removeButton.translatesAutoresizingMaskIntoConstraints = false + removeButton.setTitle("✕", for: .normal) + removeButton.titleLabel?.font = SemiboldFont(size: 20) + removeButton.tintColor = UIColor.white.withAlphaComponent(0.7) + removeButton.setTitleColor(UIColor.white.withAlphaComponent(0.7), for: .normal) + removeButton.addTarget(self, action: #selector(removeTapped), for: .touchUpInside) + contentView.addSubview(removeButton) + + NSLayoutConstraint.activate([ + iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: CellMetrics.inset), + iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconView.widthAnchor.constraint(equalToConstant: CellMetrics.iconSize), + iconView.heightAnchor.constraint(equalToConstant: CellMetrics.iconSize), + + removeButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -CellMetrics.inset), + removeButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + removeButton.widthAnchor.constraint(equalToConstant: 32), + removeButton.heightAnchor.constraint(equalToConstant: 32), + + nameLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12), + nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: removeButton.leadingAnchor, constant: -8), + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + + detailLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), + detailLabel.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor), + detailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 2), + detailLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -12), + ]) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func prepareForReuse() { + super.prepareForReuse() + iconView.image = nil + } + + @objc private func removeTapped() { onRemoveTapped?() } + + func configure(with dapp: BMInstalledDApp) { + nameLabel.text = dapp.name + detailLabel.text = dapp.desc.isEmpty ? "v\(dapp.version)" : "\(dapp.desc) • v\(dapp.version)" + DAppIconLoader.load(svg: dapp.icon, into: iconView) + } +} + +// MARK: - Store row (install / installed / update) + +final class DAppStoreCell: UITableViewCell { + + enum Action { + case install + case installed + case update + } + + let iconView = UIImageView() + let nameLabel = UILabel() + let detailLabel = UILabel() + let actionButton = UIButton(type: .system) + var onActionTapped: (() -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + backgroundColor = .clear + selectionStyle = .none + + iconView.contentMode = .scaleAspectFit + iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.layer.cornerRadius = CellMetrics.iconSize / 2 + iconView.clipsToBounds = true + contentView.addSubview(iconView) + + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.font = SemiboldFont(size: 16) + nameLabel.textColor = .white + contentView.addSubview(nameLabel) + + detailLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.font = RegularFont(size: 13) + detailLabel.textColor = UIColor.white.withAlphaComponent(0.7) + detailLabel.numberOfLines = 2 + contentView.addSubview(detailLabel) + + actionButton.translatesAutoresizingMaskIntoConstraints = false + actionButton.titleLabel?.font = SemiboldFont(size: 13) + actionButton.layer.cornerRadius = 14 + actionButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) + actionButton.addTarget(self, action: #selector(actionTapped), for: .touchUpInside) + contentView.addSubview(actionButton) + + NSLayoutConstraint.activate([ + iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: CellMetrics.inset), + iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconView.widthAnchor.constraint(equalToConstant: CellMetrics.iconSize), + iconView.heightAnchor.constraint(equalToConstant: CellMetrics.iconSize), + + actionButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -CellMetrics.inset), + actionButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + actionButton.heightAnchor.constraint(equalToConstant: 28), + + nameLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12), + nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: actionButton.leadingAnchor, constant: -8), + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + + detailLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), + detailLabel.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor), + detailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 2), + detailLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -12), + ]) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func prepareForReuse() { + super.prepareForReuse() + iconView.image = nil + } + + @objc private func actionTapped() { onActionTapped?() } + + func configure(with dapp: BMAvailableDApp, action: Action) { + nameLabel.text = dapp.name + var detail = dapp.desc + if !dapp.publisherName.isEmpty { + detail = detail.isEmpty ? dapp.publisherName : "\(detail) • \(dapp.publisherName)" + } + detailLabel.text = detail + + DAppIconLoader.load(svg: dapp.icon, into: iconView) + + // Reset borrowed-from-recycled-state styling — `.installed` sets a border + // that other actions don't, and would otherwise carry across reuse. + actionButton.layer.borderColor = nil + actionButton.layer.borderWidth = 0 + + switch action { + case .install: + actionButton.setTitle(Localizable.shared.strings.dapps_install_label, for: .normal) + actionButton.backgroundColor = UIColor.main.brightTeal + actionButton.setTitleColor(.black, for: .normal) + actionButton.isEnabled = true + case .installed: + actionButton.setTitle(Localizable.shared.strings.dapps_installed_label, for: .normal) + actionButton.backgroundColor = UIColor.clear + actionButton.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .normal) + actionButton.layer.borderColor = UIColor.white.withAlphaComponent(0.3).cgColor + actionButton.layer.borderWidth = 1 + actionButton.isEnabled = false + case .update: + actionButton.setTitle(Localizable.shared.strings.dapps_update, for: .normal) + actionButton.backgroundColor = UIColor.main.heliotrope + actionButton.setTitleColor(.black, for: .normal) + actionButton.isEnabled = true + } + } +} + +// MARK: - Sideload row + +final class SideloadCell: UITableViewCell { + + let titleLabel = UILabel() + let detailLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + backgroundColor = .clear + accessoryType = .disclosureIndicator + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = SemiboldFont(size: 15) + titleLabel.textColor = .white + titleLabel.text = Localizable.shared.strings.dapps_install_from_file + contentView.addSubview(titleLabel) + + detailLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.font = RegularFont(size: 12) + detailLabel.textColor = UIColor.white.withAlphaComponent(0.6) + detailLabel.numberOfLines = 2 + detailLabel.text = Localizable.shared.strings.dapps_sideload_desc + contentView.addSubview(detailLabel) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: CellMetrics.inset), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -CellMetrics.inset), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + + detailLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + detailLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -CellMetrics.inset), + detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), + detailLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12), + ]) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } +} + +// MARK: - Publisher row + +final class PublisherCell: UITableViewCell { + + let titleLabel = UILabel() + let detailLabel = UILabel() + let toggle = UISwitch() + var onToggleChanged: ((Bool) -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + backgroundColor = .clear + selectionStyle = .none + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = SemiboldFont(size: 15) + titleLabel.textColor = .white + contentView.addSubview(titleLabel) + + detailLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.font = RegularFont(size: 12) + detailLabel.textColor = UIColor.white.withAlphaComponent(0.6) + detailLabel.numberOfLines = 1 + contentView.addSubview(detailLabel) + + toggle.translatesAutoresizingMaskIntoConstraints = false + toggle.addTarget(self, action: #selector(toggleChanged), for: .valueChanged) + contentView.addSubview(toggle) + + NSLayoutConstraint.activate([ + toggle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -CellMetrics.inset), + toggle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: CellMetrics.inset), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: toggle.leadingAnchor, constant: -8), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + + detailLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + detailLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2), + detailLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -12), + ]) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + @objc private func toggleChanged() { + onToggleChanged?(toggle.isOn) + } + + func configure(with publisher: BMPublisher, allowed: Bool) { + titleLabel.text = publisher.name + detailLabel.text = publisher.pubkey + toggle.setOn(allowed, animated: false) + } +} + +// MARK: - Icon loader + +/// Bridges in-memory SVG bytes to a UIImageView via the project's bundled +/// SDImageSVGCoder. Falls back to the bundled DApp icon when decoding fails. +enum DAppIconLoader { + static func load(svg: String, into view: UIImageView) { + guard !svg.isEmpty, let data = svg.data(using: .utf8) else { + view.image = UIImage(named: "dao_app_icon") + return + } + if let image = SDImageSVGCoder.shared.decodedImage(with: data, options: nil) { + view.image = image + } else { + view.image = UIImage(named: "dao_app_icon") + } + } +} diff --git a/BeamWallet/ViewControllers/Main/DAO/DAOAppsViewController.swift b/BeamWallet/ViewControllers/Main/DAO/DAOAppsViewController.swift index d7e44760..3d76e6d7 100644 --- a/BeamWallet/ViewControllers/Main/DAO/DAOAppsViewController.swift +++ b/BeamWallet/ViewControllers/Main/DAO/DAOAppsViewController.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 08.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import Foundation diff --git a/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewController.swift b/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewController.swift index d41d2a8a..f5497ec7 100644 --- a/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewController.swift +++ b/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewController.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 06.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -54,7 +54,7 @@ class DAOConfirmViewController: BaseViewController { @IBOutlet private weak var passField:BMField! @IBOutlet private weak var viewWidth:NSLayoutConstraint! - private var hintText:String? = nil + private var hintText:String? override func viewDidLoad() { super.viewDidLoad() diff --git a/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewModel.swift b/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewModel.swift index 4e4d8535..af043559 100644 --- a/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewModel.swift +++ b/BeamWallet/ViewControllers/Main/DAO/DAOConfirmViewModel.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 06.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit diff --git a/BeamWallet/ViewControllers/Main/DAO/DAOListViewModel.swift b/BeamWallet/ViewControllers/Main/DAO/DAOListViewModel.swift index 009266d7..75c76a43 100644 --- a/BeamWallet/ViewControllers/Main/DAO/DAOListViewModel.swift +++ b/BeamWallet/ViewControllers/Main/DAO/DAOListViewModel.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 08.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import Foundation diff --git a/BeamWallet/ViewControllers/Main/DAO/DAOViewController.swift b/BeamWallet/ViewControllers/Main/DAO/DAOViewController.swift index 29f6feec..6b0d82fb 100644 --- a/BeamWallet/ViewControllers/Main/DAO/DAOViewController.swift +++ b/BeamWallet/ViewControllers/Main/DAO/DAOViewController.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 03.09.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -26,7 +26,7 @@ import SafariServices @objc var style = WBStyle() - var resultObject:XWVScriptObject? = nil + var resultObject:XWVScriptObject? @objc public func callWalletApi(_ json:NSString) { print("\(json)") @@ -44,11 +44,18 @@ class DAOViewController: BaseViewController, WKNavigationDelegate, WKScriptMessa private var webView:WKWebView? private var loadingImage = UIImageView() private var loadingLabel = UILabel() - - @objc private var channel:XWVChannel? = nil + private var dappURLHandler: DAppURLSchemeHandler? + + @objc private var channel:XWVChannel? @objc private var beam:WBBEAM? @objc public var app:BMApp! + /// When set, the controller treats `app.url` as the entry HTML and serves + /// the DApp through `DAppURLSchemeHandler` under `dapp://app/...` so that + /// XHR / fetch get HTTP-style status codes, CORS, and an `application/wasm` + /// content-type — none of which iOS WKWebView gives you for native file:// + /// loads. + @objc public var installedRoot:URL? @objc public var onCallWalletApi: ((NSString) -> Void)? @objc public var onRejected: ((NSString) -> Void)? @objc public var onApproved: ((NSString) -> Void)? @@ -70,7 +77,33 @@ class DAOViewController: BaseViewController, WKNavigationDelegate, WKScriptMessa self.onCallWalletApi?(json) } - webView?.load(URLRequest(url: URL(string: app.url)!)) + loadInitialURL() + } + + private func loadInitialURL() { + if let root = installedRoot, + let url = URL(string: app.url), + url.isFileURL { + let relative = relativeFilePath(of: url, under: root) + if let entry = DAppURLSchemeHandler.entryURL(for: relative) { + // Route the load through the dapp:// scheme handler — see + // DAppURLSchemeHandler for why file:// is unworkable here. + webView?.load(URLRequest(url: entry)) + return + } + } + guard let url = URL(string: app.url) else { return } + webView?.load(URLRequest(url: url)) + } + + private func relativeFilePath(of file: URL, under root: URL) -> String { + let rootPath = root.standardizedFileURL.path + let filePath = file.standardizedFileURL.path + if filePath.hasPrefix(rootPath) { + let suffix = filePath.dropFirst(rootPath.count) + return suffix.hasPrefix("/") ? String(suffix.dropFirst()) : String(suffix) + } + return "app/index.html" } override func viewWillDisappear(_ animated: Bool) { @@ -88,21 +121,32 @@ class DAOViewController: BaseViewController, WKNavigationDelegate, WKScriptMessa private func stupWebView() { let controller = WKUserContentController() controller.add(self, name: "BEAM") - + let configuration = WKWebViewConfiguration() configuration.userContentController = controller - + + // Register the per-DApp scheme handler before the WKWebView is + // instantiated — WebKit refuses to register schemes on a live + // configuration. The handler is retained by the controller so it + // outlives any in-flight requests. + if let root = installedRoot { + let handler = DAppURLSchemeHandler(rootURL: root) + dappURLHandler = handler + configuration.setURLSchemeHandler(handler, forURLScheme: DAppURLSchemeHandler.scheme) + } + let logSource = "function captureLog(msg) { window.webkit.messageHandlers.logHandler.postMessage(msg); } window.console.log = captureLog;" let logScript = WKUserScript(source: logSource, injectionTime: .atDocumentEnd, forMainFrameOnly: false) - - let jsLogScript2 = "console.log = (function(oriLogFunc){ return function(str) { window.webkit.messageHandlers.log.postMessage(str); oriLogFunc.call(console,str);} })(console.log);"; - + + let jsLogScript2 = "console.log = (function(oriLogFunc){ return function(str) { window.webkit.messageHandlers.log.postMessage(str); oriLogFunc.call(console,str);} })(console.log);" + webView = WKWebView(frame: self.view.bounds, configuration: configuration) webView?.navigationDelegate = self webView?.configuration.userContentController.addUserScript(logScript) webView?.configuration.userContentController.addUserScript(WKUserScript(source: jsLogScript2, injectionTime: .atDocumentStart, forMainFrameOnly: true)) webView?.configuration.userContentController.add(self, name: "logHandler") webView?.configuration.userContentController.add(self, name: "log") + webView?.configuration.userContentController.add(self, name: "dappLog") channel = webView?.loadPlugin(beam!, namespace: "BEAM") webView?.backgroundColor = self.view.backgroundColor webView?.isOpaque = false @@ -186,11 +230,17 @@ class DAOViewController: BaseViewController, WKNavigationDelegate, WKScriptMessa } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - if let msg = message.body as? String { - print("--------WEB LOG--------:\n" + msg) + let appName = app?.name ?? "DApp" + if message.name == "dappLog", let body = message.body as? [String: Any] { + let level = (body["level"] as? String) ?? "info" + let text = (body["message"] as? String) ?? "" + NSLog("DApp[%@] %@: %@", appName, level, text) + return } - else { - print(message.body) + if let msg = message.body as? String { + NSLog("DApp[%@] log: %@", appName, msg) + } else { + NSLog("DApp[%@] log: %@", appName, String(describing: message.body)) } } diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/DAppURLSchemeHandler.swift b/BeamWallet/ViewControllers/Main/DAO/JS/DAppURLSchemeHandler.swift new file mode 100644 index 00000000..cbb4a1dc --- /dev/null +++ b/BeamWallet/ViewControllers/Main/DAO/JS/DAppURLSchemeHandler.swift @@ -0,0 +1,220 @@ +// +// DAppURLSchemeHandler.swift +// BeamWallet +// +// Serves an installed DApp's local files to its WKWebView under a custom +// `dapp://app/...` scheme. Native file:// loading on iOS yields status 0 +// for XHR and rejects fetch(), which silently breaks the +// Utils.download("./*.wasm") path inside Beam DApps. Without those shader +// bytes, the wallet's `invoke_contract` call falls through to +// ShadersManager::nextRequest() with `m_BodyManager` empty and returns the +// "missing shader code" error. +// + +import Foundation +import WebKit + +@available(iOS 11.0, *) +final class DAppURLSchemeHandler: NSObject, WKURLSchemeHandler { + + static let scheme = "dapp" + static let host = "app" + + private let rootURL: URL + private let rootPath: String + private let lock = NSLock() + private var activeTasks = Set() + + init(rootURL: URL) { + self.rootURL = rootURL.standardizedFileURL + self.rootPath = self.rootURL.path + super.init() + } + + /// Builds the initial URL the WKWebView should load. `relativePath` is the + /// manifest entry (e.g. "app/index.html"). + static func entryURL(for relativePath: String) -> URL? { + let trimmed = relativePath.hasPrefix("/") ? String(relativePath.dropFirst()) : relativePath + return URL(string: "\(scheme)://\(host)/\(trimmed)") + } + + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + let id = ObjectIdentifier(urlSchemeTask) + lock.lock(); activeTasks.insert(id); lock.unlock() + + guard let url = urlSchemeTask.request.url, url.scheme == DAppURLSchemeHandler.scheme else { + finish(urlSchemeTask, withStatus: 400, body: nil) + return + } + + let relPath = url.path.hasPrefix("/") ? String(url.path.dropFirst()) : url.path + let target = rootURL.appendingPathComponent(relPath).standardizedFileURL + + // Path-traversal guard — refuse anything that resolves outside the + // DApp directory after symlink/`..` normalisation. + if target.path != rootPath && !target.path.hasPrefix(rootPath + "/") { + finish(urlSchemeTask, withStatus: 403, body: nil) + return + } + + var isDir: ObjCBool = false + let fm = FileManager.default + var fileURL = target + if fm.fileExists(atPath: target.path, isDirectory: &isDir), isDir.boolValue { + fileURL = target.appendingPathComponent("index.html") + } + guard let data = try? Data(contentsOf: fileURL) else { + finish(urlSchemeTask, withStatus: 404, body: nil) + return + } + + let ext = fileURL.pathExtension.lowercased() + let mime = DAppURLSchemeHandler.mimeType(for: ext) + let bodyData: Data + if ext == "html" || ext == "htm" { + bodyData = injectShim(into: data) + } else { + bodyData = data + } + + let headers: [String: String] = [ + "Content-Type": mime, + "Content-Length": "\(bodyData.count)", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "no-store", + ] + guard let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: headers) else { + finish(urlSchemeTask, withStatus: 500, body: nil) + return + } + + deliver(urlSchemeTask, response: response, body: bodyData) + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + let id = ObjectIdentifier(urlSchemeTask) + lock.lock(); activeTasks.remove(id); lock.unlock() + } + + // MARK: - Helpers + + private func deliver(_ task: WKURLSchemeTask, response: URLResponse, body: Data) { + let id = ObjectIdentifier(task) + lock.lock() + guard activeTasks.contains(id) else { lock.unlock(); return } + lock.unlock() + task.didReceive(response) + lock.lock() + guard activeTasks.contains(id) else { lock.unlock(); return } + lock.unlock() + task.didReceive(body) + lock.lock() + activeTasks.remove(id) + lock.unlock() + task.didFinish() + } + + private func finish(_ task: WKURLSchemeTask, withStatus status: Int, body: Data?) { + guard let url = task.request.url else { return } + let headers = ["Content-Length": "\(body?.count ?? 0)"] + if let response = HTTPURLResponse(url: url, statusCode: status, httpVersion: "HTTP/1.1", headerFields: headers) { + deliver(task, response: response, body: body ?? Data()) + } else { + let id = ObjectIdentifier(task) + lock.lock(); activeTasks.remove(id); lock.unlock() + task.didFailWithError(NSError(domain: "DApp", code: status, userInfo: nil)) + } + } + + private static func mimeType(for ext: String) -> String { + switch ext { + case "html", "htm": return "text/html; charset=utf-8" + case "js", "mjs": return "application/javascript; charset=utf-8" + case "css": return "text/css; charset=utf-8" + case "json": return "application/json; charset=utf-8" + case "wasm": return "application/wasm" + case "svg": return "image/svg+xml" + case "png": return "image/png" + case "jpg", "jpeg": return "image/jpeg" + case "gif": return "image/gif" + case "webp": return "image/webp" + case "ico": return "image/x-icon" + case "woff": return "font/woff" + case "woff2": return "font/woff2" + case "ttf": return "font/ttf" + case "otf": return "font/otf" + case "eot": return "application/vnd.ms-fontobject" + case "map": return "application/json; charset=utf-8" + case "txt": return "text/plain; charset=utf-8" + default: return "application/octet-stream" + } + } + + private func injectShim(into data: Data) -> Data { + guard let html = String(data: data, encoding: .utf8) else { return data } + let shim = DAppURLSchemeHandler.beamApiShim + let lower = html.lowercased() + let insertAt: String.Index + if let r = lower.range(of: "") { + insertAt = r.upperBound + } else if let r = lower.range(of: " — insert right after the closing '>' + if let close = html[r.upperBound...].firstIndex(of: ">") { + insertAt = html.index(after: close) + } else { + insertAt = html.startIndex + } + } else if let r = lower.range(of: "") { + insertAt = html.index(after: close) + } else { + insertAt = html.startIndex + } + } else { + insertAt = html.startIndex + } + var rewritten = html + rewritten.insert(contentsOf: shim, at: insertAt) + return rewritten.data(using: .utf8) ?? data + } + + // MARK: - JS shim + + /// Injected into every HTML response: + /// * Forwards uncaught errors / rejections to the `dappLog` message + /// handler so they reach the BEAM log alongside `Call Wallet Api`. + /// * Tees console.error to the same channel — silent JS crashes are the + /// failure mode users describe as "the UI doesn't load". + /// The XWVChannel plugin is still responsible for binding `window.BEAM`. + static let beamApiShim: String = """ + + """ +} diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVBindingObject.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVBindingObject.swift index 12c5d6ca..447f9161 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVBindingObject.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVBindingObject.swift @@ -97,7 +97,7 @@ final class XWVBindingObject : XWVScriptObject { var args = arguments.map(wrapScriptObject) if plugin is XWVScripting && name.isEmpty && selector == #selector(XWVScripting.invokeDefaultMethod(withArguments:)) { - args = [args]; + args = [args] } _ = performSelector(selector, with: args, waitUntilDone: false) } diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVChannel.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVChannel.swift index 1c9d6930..334f0bdc 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVChannel.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVChannel.swift @@ -98,9 +98,9 @@ public class XWVChannel : NSObject, WKScriptMessageHandler { public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { // A workaround for crash when postMessage(undefined) - //guard unsafeBitCast(message.body, to: OpaquePointer!.self) != nil else { return } + // guard unsafeBitCast(message.body, to: OpaquePointer!.self) != nil else { return } - //TODO: logs + // TODO: logs // print("msg:") // print(message.body) diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVHttpServer.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVHttpServer.swift index f2581799..8954b344 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVHttpServer.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVHttpServer.swift @@ -169,7 +169,7 @@ extension XWVHttpServer : XWVHttpConnectionDelegate { var headers: [String: String] = ["Date": dateFormatter.string(from: Date())] var statusCode = 500 - var fileURL: URL? = nil + var fileURL: URL? if request == nil { // Bad request statusCode = 400 diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVInvocation.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVInvocation.swift index 4587eb41..60dd50bb 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVInvocation.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVInvocation.swift @@ -292,9 +292,9 @@ public class XWVInvocation { } // Syntactic sugar for calling method - public subscript (selector: Selector) -> (Any?...)->Any? { + public subscript (selector: Selector) -> (Any?...) -> Any? { return { - (args: Any?...)->Any? in + (args: Any?...) -> Any? in self.call(selector, with: args) } } @@ -347,7 +347,7 @@ extension XWVInvocation { return nil } - var setter: Selector? = nil + var setter: Selector? var attr = property_copyAttributeValue(property, "R") if attr == nil { attr = property_copyAttributeValue(property, "S") diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVJson.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVJson.swift index 68990356..f5a05f2b 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVJson.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVJson.swift @@ -94,9 +94,9 @@ public func jsonify(_ value: Any?) -> String? { return n.boolValue.description } return n.stringValue - case let a as Array: + case let a as [Any?]: return jsonify(a) - case let d as Dictionary: + case let d as [String: Any?]: return jsonify(d) case let s as CustomJSONStringable: return s.jsonString @@ -119,6 +119,8 @@ public func jsonify(_ value: Any?) -> String? { return "{" + mirror.children.compactMap(jsonify).joined(separator: ",") + "}" case .enum: return jsonify(String(describing: value)) + @unknown default: + return nil } } } diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVLogging.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVLogging.swift index b2cb4423..e6a7fb39 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVLogging.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVLogging.swift @@ -112,7 +112,7 @@ public class XWVLogging : XWVScripting { @objc public func invokeDefaultMethod(withArguments args: [Any]!) -> Any! { guard args.count > 0 else { return nil } let message = args[0] as? String ?? "\(args[0])" - var level: Level? = nil + var level: Level? if args.count > 1, let num = args[1] as? Int { if 3 <= num && num <= 7 { level = Level(rawValue: Int32(num)) @@ -130,7 +130,7 @@ func log(_ message: String, level: XWVLogging.Level? = nil) { logger.log(message, level: level) } -func die(_ message: @autoclosure ()->String, file: StaticString = #file, line: UInt = #line) -> Never { +func die(_ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) -> Never { logger.log(message(), level: .Alert) fatalError(message(), file: file, line: line) } diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVMetaObject.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVMetaObject.swift index 0027b39f..dcd1fe36 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVMetaObject.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVMetaObject.swift @@ -111,7 +111,7 @@ class XWVMetaObject { return true } - case .Property(_, _): + case .Property: if let cls = plugin as? XWVScripting.Type { if let isExcluded = cls.isKeyExcluded(fromScript:), name.withCString(isExcluded) { return true @@ -140,7 +140,7 @@ class XWVMetaObject { } } - private func enumerate(excluding selectors: Set, callback: (String, Member)->Bool) -> Bool { + private func enumerate(excluding selectors: Set, callback: (String, Member) -> Bool) -> Bool { var known = selectors var count: UInt32 = 0 @@ -163,7 +163,7 @@ class XWVMetaObject { known.insert(getter) // get setter if readwrite - var setter: Selector? = nil + var setter: Selector? var attr = property_copyAttributeValue(propertyList[i], "R") if attr == nil { attr = property_copyAttributeValue(propertyList[i], "S") @@ -219,7 +219,7 @@ class XWVMetaObject { extension XWVMetaObject: Collection { typealias Element = (key: String, value: Member) typealias Index = DictionaryIndex - typealias SubSequence = Slice> + typealias SubSequence = Slice<[String: Member]> var startIndex: Index { return members.startIndex diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWVObject.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWVObject.swift index b3c95c65..d257cc75 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWVObject.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWVObject.swift @@ -85,7 +85,7 @@ public class XWVObject : NSObject { return } webView.asyncEvaluateJavaScript(scriptForRetaining(expression)) { - [weak self](result: Any?, error: Error?)->Void in + [weak self](result: Any?, error: Error?)in if let error = error { completionHandler(nil, error) } else if let result = result { diff --git a/BeamWallet/ViewControllers/Main/DAO/JS/XWebView.swift b/BeamWallet/ViewControllers/Main/DAO/JS/XWebView.swift index e852aa93..f460e70b 100644 --- a/BeamWallet/ViewControllers/Main/DAO/JS/XWebView.swift +++ b/BeamWallet/ViewControllers/Main/DAO/JS/XWebView.swift @@ -61,7 +61,7 @@ extension WKWebView { if Thread.isMainThread { evaluateJavaScript(script, completionHandler: handler) } else { - DispatchQueue.main.async() { + DispatchQueue.main.async { [weak self] in self?.evaluateJavaScript(script, completionHandler: handler) } @@ -76,7 +76,7 @@ extension WKWebView { let timeout = 3.0 if Thread.isMainThread { evaluateJavaScript(script) { - (obj: Any?, err: Error?)->Void in + (obj: Any?, err: Error?)in result = obj error = err done = true @@ -89,10 +89,10 @@ extension WKWebView { } } else { let condition: NSCondition = NSCondition() - DispatchQueue.main.async() { + DispatchQueue.main.async { [weak self] in self?.evaluateJavaScript(script) { - (obj: Any?, err: Error?)->Void in + (obj: Any?, err: Error?)in condition.lock() result = obj error = err diff --git a/BeamWallet/ViewControllers/Main/DAO/MyDAppsViewController.swift b/BeamWallet/ViewControllers/Main/DAO/MyDAppsViewController.swift new file mode 100644 index 00000000..abaca2f1 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/DAO/MyDAppsViewController.swift @@ -0,0 +1,334 @@ +// +// MyDAppsViewController.swift +// BeamWallet +// +// Single unified surface for: +// - the "Install from file…" sideload row, +// - locally installed DApps with an uninstall affordance, +// - on-chain + bundled DApps available for install, and +// - a top-right gear that opens the Publishers blocklist. +// + +import UIKit +import MobileCoreServices +import UniformTypeIdentifiers + +final class MyDAppsViewController: BaseTableViewController { + + private var installed: [BMInstalledDApp] = [] + private var available: [BMAvailableDApp] = [] + private var installedVersions: [String: String] = [:] + private var isLoadingAvailable = false + + private let sideloadCellID = "SideloadCell" + private let installedCellID = "MyDAppCell" + private let storeCellID = "DAppStoreCell" + + private enum Section: Int, CaseIterable { + case sideload = 0 + case installed + case available + } + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true, + menu: navigationController?.viewControllers.count == 1) + title = Localizable.shared.strings.myDApps.uppercased() + + tableView.register(SideloadCell.self, forCellReuseIdentifier: sideloadCellID) + tableView.register(MyDAppCell.self, forCellReuseIdentifier: installedCellID) + tableView.register(DAppStoreCell.self, forCellReuseIdentifier: storeCellID) + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.delegate = self + tableView.dataSource = self + + let refresher = UIRefreshControl() + refresher.tintColor = .white + refresher.addTarget(self, action: #selector(refreshAvailable), for: .valueChanged) + tableView.refreshControl = refresher + + addRightButton(title: Localizable.shared.strings.dapps_publishers, + target: self, + selector: #selector(openPublishers), + enabled: true) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + refreshInstalled() + if available.isEmpty { loadAvailable() } + } + + // MARK: - Data + + private func refreshInstalled() { + installed = DAppManager.shared.installed() + installedVersions = Dictionary(uniqueKeysWithValues: installed.map { ($0.guid, $0.version) }) + tableView.reloadData() + } + + @objc private func refreshAvailable() { + loadAvailable() + } + + private func loadAvailable() { + if isLoadingAvailable { return } + isLoadingAvailable = true + DAppStore.shared.queryAvailableDApps(filterUnwanted: true) { [weak self] list in + guard let self = self else { return } + self.available = list.sorted { lhs, rhs in + let lhsBundled = lhs.bundledAsset.isEmpty ? 1 : 0 + let rhsBundled = rhs.bundledAsset.isEmpty ? 1 : 0 + if lhsBundled != rhsBundled { return lhsBundled < rhsBundled } + let lhsHasPublisher = lhs.publisherName.isEmpty ? 1 : 0 + let rhsHasPublisher = rhs.publisherName.isEmpty ? 1 : 0 + if lhsHasPublisher != rhsHasPublisher { return lhsHasPublisher < rhsHasPublisher } + return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending + } + self.isLoadingAvailable = false + self.tableView.refreshControl?.endRefreshing() + self.tableView.reloadData() + } + } + + private func action(for dapp: BMAvailableDApp) -> DAppStoreCell.Action { + guard let local = installedVersions[dapp.guid] else { return .install } + return DAppStore.isVersionOlder(local, than: dapp.version) ? .update : .installed + } + + // MARK: - Actions + + @objc private func openPublishers() { + navigationController?.pushViewController(PublishersViewController(), animated: true) + } + + private func launch(_ dapp: BMInstalledDApp) { + SVProgressHUD.show() + DAppStore.shared.checkAndUpdate(dapp) { [weak self] _ in + guard let self = self else { return } + SVProgressHUD.dismiss() + let latest = DAppManager.shared.installed().first { $0.guid == dapp.guid } ?? dapp + guard let launchURL = DAppManager.shared.launchURL(for: latest) else { return } + let root = URL(fileURLWithPath: latest.localPath, isDirectory: true) + + let app = BMApp() + app.name = latest.name + app.desc = latest.desc + app.url = launchURL.absoluteString + app.icon = "" + app.api_version = "current" + app.min_api_version = "" + app.isSupported = true + + AppModel.sharedManager().startApp(self, app: app, installedRoot: root) + } + } + + private func confirmRemove(_ dapp: BMInstalledDApp) { + let message = String(format: Localizable.shared.strings.dapps_remove_confirm, dapp.name) + BMAlertViewController.present( + from: self, + title: nil, + message: message, + actions: [ + BMAlertViewController.Action(title: Localizable.shared.strings.dapps_uninstall, style: .destructive) { [weak self] in + DAppManager.shared.uninstall(guid: dapp.guid) + self?.refreshInstalled() + }, + BMAlertViewController.Action(title: Localizable.shared.strings.cancel, style: .cancel) + ] + ) + } + + private func install(_ dapp: BMAvailableDApp) { + SVProgressHUD.show() + DAppStore.shared.install(dapp) { [weak self] installed, error in + SVProgressHUD.dismiss() + guard let self = self else { return } + if let error = error { + self.showError(message: String(format: Localizable.shared.strings.dapps_install_failed_msg, error)) + return + } + if let installed = installed { + let toast = String(format: Localizable.shared.strings.dapps_installed_toast, installed.name) + SVProgressHUD.showSuccess(withStatus: toast) + } + self.refreshInstalled() + } + } + + private func showError(message: String) { + BMAlertViewController.present( + from: self, + title: nil, + message: message, + actions: [BMAlertViewController.Action(title: "OK", style: .default)] + ) + } + + // MARK: - Sideload + + @objc private func pickDAppFile() { + let picker: UIDocumentPickerViewController + if #available(iOS 14.0, *) { + let dappType = UTType("com.mw.beam.dapp") ?? UTType.zip + picker = UIDocumentPickerViewController(forOpeningContentTypes: [dappType, UTType.zip]) + } else { + picker = UIDocumentPickerViewController(documentTypes: [(kUTTypeZipArchive as String), "com.mw.beam.dapp"], in: .open) + } + picker.delegate = self + picker.allowsMultipleSelection = false + present(picker, animated: true) + } + + private func installFromURL(_ url: URL) { + let secured = url.startAccessingSecurityScopedResource() + defer { if secured { url.stopAccessingSecurityScopedResource() } } + guard let data = try? Data(contentsOf: url) else { + showError(message: Localizable.shared.strings.dapps_sideload_invalid) + return + } + do { + _ = try DAppManager.shared.installFromZip(data: data, + fallbackName: Localizable.shared.strings.dapps_sideloaded_name, + fallbackIcon: "") + refreshInstalled() + SVProgressHUD.showSuccess(withStatus: Localizable.shared.strings.dapps_install_complete) + } catch { + showError(message: String(format: Localizable.shared.strings.dapps_install_failed_msg, "\(error)")) + } + } +} + +extension MyDAppsViewController: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { return } + installFromURL(url) + } +} + +// MARK: - Table layout + +extension MyDAppsViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + guard let section = Section(rawValue: indexPath.section) else { return 0 } + switch section { + case .sideload: return 64 + case .installed: return 72 + case .available: + return available.isEmpty ? 56 : 72 + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let section = Section(rawValue: indexPath.section) else { return } + switch section { + case .sideload: + pickDAppFile() + case .installed: + guard indexPath.row < installed.count else { return } + launch(installed[indexPath.row]) + case .available: + guard !available.isEmpty, indexPath.row < available.count else { return } + let dapp = available[indexPath.row] + if action(for: dapp) != .installed { install(dapp) } + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let sec = Section(rawValue: section) else { return nil } + let title: String + switch sec { + case .sideload: + return nil + case .installed: + guard !installed.isEmpty else { return nil } + title = Localizable.shared.strings.myDApps + case .available: + title = Localizable.shared.strings.dapps_store_title + } + return makeHeader(title: title) + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard let sec = Section(rawValue: section) else { return 0 } + switch sec { + case .sideload: return 0 + case .installed: return installed.isEmpty ? 0 : 36 + case .available: return 36 + } + } + + private func makeHeader(title: String) -> UIView { + let container = UIView() + container.backgroundColor = .clear + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = title.uppercased() + label.font = SemiboldFont(size: 13) + label.textColor = UIColor.white.withAlphaComponent(0.7) + container.addSubview(label) + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16), + label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8), + ]) + return container + } +} + +extension MyDAppsViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { Section.allCases.count } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let sec = Section(rawValue: section) else { return 0 } + switch sec { + case .sideload: return 1 + case .installed: return installed.count + case .available: + return available.isEmpty ? 1 : available.count + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let sec = Section(rawValue: indexPath.section) else { + return UITableViewCell(style: .default, reuseIdentifier: nil) + } + switch sec { + case .sideload: + return tableView.dequeueReusableCell(withIdentifier: sideloadCellID, for: indexPath) + case .installed: + let cell = tableView.dequeueReusableCell(withIdentifier: installedCellID, for: indexPath) as! MyDAppCell + let dapp = installed[indexPath.row] + cell.configure(with: dapp) + cell.onRemoveTapped = { [weak self] in self?.confirmRemove(dapp) } + return cell + case .available: + if available.isEmpty { + let cell = UITableViewCell(style: .default, reuseIdentifier: "empty") + cell.backgroundColor = .clear + cell.selectionStyle = .none + cell.textLabel?.text = isLoadingAvailable + ? Localizable.shared.strings.dapps_store_loading + : Localizable.shared.strings.dapps_store_no_dapps + cell.textLabel?.textColor = UIColor.white.withAlphaComponent(0.6) + cell.textLabel?.font = ItalicFont(size: 14) + cell.textLabel?.textAlignment = .center + return cell + } + let cell = tableView.dequeueReusableCell(withIdentifier: storeCellID, for: indexPath) as! DAppStoreCell + let dapp = available[indexPath.row] + let actionKind = action(for: dapp) + cell.configure(with: dapp, action: actionKind) + cell.onActionTapped = { [weak self] in + if actionKind != .installed { self?.install(dapp) } + } + return cell + } + } +} diff --git a/BeamWallet/ViewControllers/Main/DAO/PublishersViewController.swift b/BeamWallet/ViewControllers/Main/DAO/PublishersViewController.swift new file mode 100644 index 00000000..4ff64653 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/DAO/PublishersViewController.swift @@ -0,0 +1,91 @@ +// +// PublishersViewController.swift +// BeamWallet +// +// Lets the user toggle each known DApp Store publisher on/off. Disabled +// publishers' DApps are filtered out of the browse listing. +// + +import UIKit + +final class PublishersViewController: BaseTableViewController { + + private var publishers: [BMPublisher] = [] + private var unwanted: Set = [] + private let cellID = "PublisherCell" + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true) + title = Localizable.shared.strings.dapps_publishers.uppercased() + + tableView.register(PublisherCell.self, forCellReuseIdentifier: cellID) + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.delegate = self + tableView.dataSource = self + + load() + } + + private func load() { + unwanted = DAppStore.shared.getUnwantedPublishers() + SVProgressHUD.show() + DAppStore.shared.queryPublishers { [weak self] list in + guard let self = self else { return } + self.publishers = list.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + SVProgressHUD.dismiss() + self.tableView.reloadData() + } + } +} + +extension PublishersViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 64 } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let header = UILabel() + header.text = " " + Localizable.shared.strings.dapps_publishers_subtitle + header.font = RegularFont(size: 13) + header.textColor = UIColor.white.withAlphaComponent(0.6) + header.numberOfLines = 0 + return header + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 48 } +} + +extension PublishersViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return publishers.isEmpty ? 1 : publishers.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if publishers.isEmpty { + let cell = UITableViewCell(style: .default, reuseIdentifier: "empty") + cell.backgroundColor = .clear + cell.textLabel?.text = Localizable.shared.strings.dapps_no_publishers + cell.textLabel?.textColor = UIColor.white.withAlphaComponent(0.6) + cell.textLabel?.textAlignment = .center + cell.textLabel?.font = ItalicFont(size: 14) + cell.selectionStyle = .none + return cell + } + let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! PublisherCell + let publisher = publishers[indexPath.row] + let allowed = !unwanted.contains(publisher.pubkey.lowercased()) + cell.configure(with: publisher, allowed: allowed) + cell.onToggleChanged = { [weak self] isOn in + guard let self = self else { return } + if isOn { + DAppStore.shared.removeUnwantedPublisher(publisher.pubkey) + self.unwanted.remove(publisher.pubkey.lowercased()) + } else { + DAppStore.shared.addUnwantedPublisher(publisher.pubkey) + self.unwanted.insert(publisher.pubkey.lowercased()) + } + } + return cell + } +} diff --git a/BeamWallet/ViewControllers/Main/Menu/LeftMenuViewController.swift b/BeamWallet/ViewControllers/Main/Menu/LeftMenuViewController.swift index b2ae7705..f727fbba 100644 --- a/BeamWallet/ViewControllers/Main/Menu/LeftMenuViewController.swift +++ b/BeamWallet/ViewControllers/Main/Menu/LeftMenuViewController.swift @@ -2,7 +2,7 @@ // LeftMenuViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -56,9 +56,10 @@ class LeftMenuViewController: BaseTableViewController { private var sections_0 = [ MenuItem(name: Localizable.shared.strings.wallet, icon: IconWallet(), selected: true, type: WalletViewController.self), + MenuItem(name: Localizable.shared.strings.messenger, icon: IconMessenger(), selected: false, type: MessengerChatListViewController.self), + MenuItem(name: Localizable.shared.strings.myDApps, icon: IconDappStore(), selected: false, type: MyDAppsViewController.self), MenuItem(name: Localizable.shared.strings.dAppStore, icon: IconDappStore(), selected: false, type: DAOViewController.self), - MenuItem(name: Localizable.shared.strings.beamx_dao, icon: IconBeamXDAO(), selected: false, type: DAOViewController.self), - MenuItem(name: Localizable.shared.strings.beamx_dao_dao_voting, icon: IconBeamXDAOVoting(), selected: false, type: DAOViewController.self)] + MenuItem(name: Localizable.shared.strings.asset_swaps, icon: IconAssetSwap(), selected: false, type: AssetSwapsViewController.self)] private var sections_1 = [ MenuItem(name: Localizable.shared.strings.addresses, icon: IconAddresses(), selected: false, type: AddressesViewController.self), @@ -69,12 +70,15 @@ class LeftMenuViewController: BaseTableViewController { override func viewDidLoad() { super.viewDidLoad() - + Settings.sharedManager().addDelegate(self) AppModel.sharedManager().addDelegate(self) tableView.delegate = self tableView.dataSource = self + tableView.isScrollEnabled = false + tableView.bounces = false + tableView.alwaysBounceVertical = false var height:CGFloat = 180 var offset:CGFloat = 20 @@ -177,7 +181,7 @@ extension LeftMenuViewController: UITableViewDelegate, UITableViewDataSource { cell?.textLabel?.highlightedTextColor = UIColor.main.brightTeal cell?.textLabel?.font = RegularFont(size: 17) - let countView = UIView(frame: CGRect(x: 39, y: 13, width: 12, height: 12)); + let countView = UIView(frame: CGRect(x: 39, y: 13, width: 12, height: 12)) countView.layer.cornerRadius = 6 countView.backgroundColor = UIColor.main.green countView.glow() @@ -205,11 +209,11 @@ extension LeftMenuViewController: UITableViewDelegate, UITableViewDataSource { let selectedBackgroundView = UIImageView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 60)) - selectedBackgroundView.image = MenuSelectedBackground(); + selectedBackgroundView.image = MenuSelectedBackground() selectedBackgroundView.tag = 12 let selectedBackgroundView1 = UIImageView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 60)) - selectedBackgroundView1.image = MenuSelectedBackground(); + selectedBackgroundView1.image = MenuSelectedBackground() cell?.contentView.viewWithTag(12)?.removeFromSuperview() @@ -290,14 +294,16 @@ extension LeftMenuViewController: UITableViewDelegate, UITableViewDataSource { navigationController.setViewControllers([SettingsViewController(type: .main)], animated: false) case Localizable.shared.strings.dAppStore : navigationController.setViewControllers([DAOAppsViewController()], animated: false) + case Localizable.shared.strings.myDApps : + navigationController.setViewControllers([MyDAppsViewController()], animated: false) + case Localizable.shared.strings.messenger : + navigationController.setViewControllers([MessengerChatListViewController()], animated: false) case Localizable.shared.strings.beam_faucet : AppModel.sharedManager().startBeamXDaoApp(navigationController, app: AppModel.sharedManager().daoFaucetApp()) case Localizable.shared.strings.beam_gallery : AppModel.sharedManager().startBeamXDaoApp(navigationController, app: AppModel.sharedManager().daoGalleryApp()) - case Localizable.shared.strings.beamx_dao: - AppModel.sharedManager().startBeamXDaoApp(navigationController, app: AppModel.sharedManager().daoBeamXApp()) - case Localizable.shared.strings.beamx_dao_dao_voting: - AppModel.sharedManager().startBeamXDaoApp(navigationController, app: AppModel.sharedManager().votingApp()) + case Localizable.shared.strings.asset_swaps : + navigationController.setViewControllers([AssetSwapsViewController()], animated: false) default : break } @@ -327,9 +333,10 @@ extension LeftMenuViewController : SettingsModelDelegate { addBackgroundView() sections_0 = [MenuItem(name: Localizable.shared.strings.wallet, icon: IconWallet(), selected: true, type: WalletViewController.self), + MenuItem(name: Localizable.shared.strings.messenger, icon: IconMessenger(), selected: false, type: MessengerChatListViewController.self), + MenuItem(name: Localizable.shared.strings.myDApps, icon: IconDappStore(), selected: false, type: MyDAppsViewController.self), MenuItem(name: Localizable.shared.strings.dAppStore, icon: IconDappStore(), selected: false, type: DAOViewController.self), - MenuItem(name: Localizable.shared.strings.beamx_dao, icon: IconBeamXDAO(), selected: false, type: DAOViewController.self), - MenuItem(name: Localizable.shared.strings.beamx_dao_dao_voting, icon: IconBeamXDAOVoting(), selected: false, type: DAOViewController.self)] + MenuItem(name: Localizable.shared.strings.asset_swaps, icon: IconAssetSwap(), selected: false, type: AssetSwapsViewController.self)] sections_1 = [ MenuItem(name: Localizable.shared.strings.addresses, icon: IconAddresses(), selected: false, type: AddressesViewController.self), diff --git a/BeamWallet/ViewControllers/Main/Messenger/MessengerBubbleCell.swift b/BeamWallet/ViewControllers/Main/Messenger/MessengerBubbleCell.swift new file mode 100644 index 00000000..b456f556 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Messenger/MessengerBubbleCell.swift @@ -0,0 +1,105 @@ +// +// MessengerBubbleCell.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class MessengerBubbleCell: RippleCell { + + private let bubble = UIView() + private let label = UILabel() + private var leadingConstraint: NSLayoutConstraint? + private var trailingConstraint: NSLayoutConstraint? + + private static let maxBubbleRatio: CGFloat = 0.75 + private static let messageFont: UIFont = RegularFont(size: 15) + private static let timeFont: UIFont = RegularFont(size: 11) + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + backgroundColor = .clear + contentView.backgroundColor = .clear + selectionStyle = .none + + bubble.translatesAutoresizingMaskIntoConstraints = false + bubble.layer.cornerRadius = 12 + contentView.addSubview(bubble) + + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + bubble.addSubview(label) + + let leading = bubble.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16) + let trailing = bubble.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16) + leadingConstraint = leading + trailingConstraint = trailing + + let maxLabelWidth = UIScreen.main.bounds.width * MessengerBubbleCell.maxBubbleRatio - 24 + + NSLayoutConstraint.activate([ + bubble.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), + bubble.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), + + label.leadingAnchor.constraint(equalTo: bubble.leadingAnchor, constant: 12), + label.trailingAnchor.constraint(equalTo: bubble.trailingAnchor, constant: -12), + label.topAnchor.constraint(equalTo: bubble.topAnchor, constant: 8), + label.bottomAnchor.constraint(equalTo: bubble.bottomAnchor, constant: -8), + label.widthAnchor.constraint(lessThanOrEqualToConstant: maxLabelWidth), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } + + func configure(with message: BMInstantMessage) { + let textColor: UIColor + if message.isIncome { + bubble.backgroundColor = UIColor.main.brightSkyBlue + textColor = UIColor.main.marineOriginal + leadingConstraint?.isActive = true + trailingConstraint?.isActive = false + } else { + bubble.backgroundColor = UIColor.main.heliotrope + textColor = UIColor.main.marineOriginal + leadingConstraint?.isActive = false + trailingConstraint?.isActive = true + } + + let attributed = NSMutableAttributedString() + attributed.append(NSAttributedString( + string: message.message, + attributes: [ + .font: MessengerBubbleCell.messageFont, + .foregroundColor: textColor, + ] + )) + attributed.append(NSAttributedString( + string: " \(message.formattedTime())", + attributes: [ + .font: MessengerBubbleCell.timeFont, + .foregroundColor: textColor.withAlphaComponent(0.55), + .baselineOffset: -1, + ] + )) + + label.attributedText = attributed + } +} diff --git a/BeamWallet/ViewControllers/Main/Messenger/MessengerChatCell.swift b/BeamWallet/ViewControllers/Main/Messenger/MessengerChatCell.swift new file mode 100644 index 00000000..4d0520b6 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Messenger/MessengerChatCell.swift @@ -0,0 +1,157 @@ +// +// MessengerChatCell.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class MessengerChatCell: RippleCell { + + private let mainView = UIView() + private let avatarView = UIView() + private let avatarLabel = UILabel() + private let nameLabel = UILabel() + private let previewLabel = UILabel() + private let dateLabel = UILabel() + private let unreadDot = UIView() + + private var topConstraint: NSLayoutConstraint! + private var bottomConstraint: NSLayoutConstraint! + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + backgroundColor = .clear + contentView.backgroundColor = .clear + selectionStyle = .none + + mainView.translatesAutoresizingMaskIntoConstraints = false + mainView.layer.cornerRadius = 0 + contentView.addSubview(mainView) + + avatarView.translatesAutoresizingMaskIntoConstraints = false + avatarView.backgroundColor = UIColor.main.brightTeal.withAlphaComponent(0.18) + avatarView.layer.cornerRadius = 20 + mainView.addSubview(avatarView) + + avatarLabel.translatesAutoresizingMaskIntoConstraints = false + avatarLabel.font = BoldFont(size: 16) + avatarLabel.textColor = UIColor.main.brightTeal + avatarLabel.textAlignment = .center + avatarView.addSubview(avatarLabel) + + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.font = BoldFont(size: 15) + nameLabel.textColor = UIColor.white + mainView.addSubview(nameLabel) + + previewLabel.translatesAutoresizingMaskIntoConstraints = false + previewLabel.font = RegularFont(size: 13) + previewLabel.textColor = UIColor.main.steelGrey + previewLabel.numberOfLines = 1 + previewLabel.lineBreakMode = .byTruncatingTail + mainView.addSubview(previewLabel) + + dateLabel.translatesAutoresizingMaskIntoConstraints = false + dateLabel.font = RegularFont(size: 12) + dateLabel.textColor = UIColor.main.steelGrey + dateLabel.textAlignment = .right + mainView.addSubview(dateLabel) + + unreadDot.translatesAutoresizingMaskIntoConstraints = false + unreadDot.backgroundColor = UIColor.main.brightTeal + unreadDot.layer.cornerRadius = 4 + unreadDot.isHidden = true + mainView.addSubview(unreadDot) + + topConstraint = mainView.topAnchor.constraint(equalTo: contentView.topAnchor) + bottomConstraint = mainView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + + NSLayoutConstraint.activate([ + mainView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + mainView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + topConstraint, + bottomConstraint, + + avatarView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: defaultX), + avatarView.centerYAnchor.constraint(equalTo: mainView.centerYAnchor), + avatarView.widthAnchor.constraint(equalToConstant: 40), + avatarView.heightAnchor.constraint(equalToConstant: 40), + + avatarLabel.centerXAnchor.constraint(equalTo: avatarView.centerXAnchor), + avatarLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor), + + nameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12), + nameLabel.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 12), + nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: dateLabel.leadingAnchor, constant: -8), + + dateLabel.trailingAnchor.constraint(equalTo: mainView.trailingAnchor, constant: -defaultX), + dateLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), + dateLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 80), + + previewLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), + previewLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4), + previewLabel.trailingAnchor.constraint(lessThanOrEqualTo: unreadDot.leadingAnchor, constant: -8), + previewLabel.bottomAnchor.constraint(lessThanOrEqualTo: mainView.bottomAnchor, constant: -12), + + unreadDot.trailingAnchor.constraint(equalTo: mainView.trailingAnchor, constant: -defaultX), + unreadDot.centerYAnchor.constraint(equalTo: previewLabel.centerYAnchor), + unreadDot.widthAnchor.constraint(equalToConstant: 8), + unreadDot.heightAnchor.constraint(equalToConstant: 8), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } + + func configure(with chat: BMChat, row: Int) { + nameLabel.text = chat.displayName() + previewLabel.text = chat.lastMessagePreview ?? "" + dateLabel.text = chat.lastMessageTimestamp > 0 + ? formatRelativeDate(timestamp: chat.lastMessageTimestamp) + : "" + + let initial = chat.displayName().trimmingCharacters(in: .whitespaces).first.map { String($0).uppercased() } ?? "?" + avatarLabel.text = initial + + unreadDot.isHidden = !chat.hasUnread + + if chat.hasUnread { + mainView.backgroundColor = UIColor.main.marineThree + topConstraint.constant = 5 + bottomConstraint.constant = -5 + } else { + mainView.backgroundColor = (row % 2 == 0) ? UIColor.main.cellBackgroundColor : UIColor.main.marine + topConstraint.constant = 0 + bottomConstraint.constant = 0 + } + } + + private func formatRelativeDate(timestamp: UInt64) -> String { + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + let formatter = DateFormatter() + if Calendar.current.isDateInToday(date) { + formatter.dateStyle = .none + formatter.timeStyle = .short + } else { + formatter.dateStyle = .short + formatter.timeStyle = .none + } + return formatter.string(from: date) + } +} diff --git a/BeamWallet/ViewControllers/Main/Messenger/MessengerChatListViewController.swift b/BeamWallet/ViewControllers/Main/Messenger/MessengerChatListViewController.swift new file mode 100644 index 00000000..3b4c7960 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Messenger/MessengerChatListViewController.swift @@ -0,0 +1,120 @@ +// +// MessengerChatListViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class MessengerChatListViewController: BaseTableViewController { + + private let viewModel = MessengerChatListViewModel() + private let emptyLabel = UILabel() + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true, menu: self.navigationController?.viewControllers.count == 1) + + title = Localizable.shared.strings.messenger + + addRightButton(image: IconAdd(), target: self, selector: #selector(onNewChat)) + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 10)) + tableView.register(MessengerChatCell.self, forCellReuseIdentifier: "MessengerChatCell") + tableView.addPullToRefresh(target: self, handler: #selector(refresh(_:))) + + emptyLabel.text = Localizable.shared.strings.messenger_no_chats + emptyLabel.textAlignment = .center + emptyLabel.numberOfLines = 0 + emptyLabel.font = RegularFont(size: 16) + emptyLabel.textColor = Settings.sharedManager().isDarkMode ? UIColor.main.steel : UIColor.main.blueyGrey + view.addSubview(emptyLabel) + + viewModel.onDataChanged = { [weak self] in + self?.tableView.stopRefreshing() + self?.tableView.reloadData() + self?.updateEmptyState() + } + + viewModel.reload() + updateEmptyState() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewModel.reload() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + emptyLabel.frame = CGRect(x: 20, y: navigationBarOffset + 80, width: view.bounds.width - 40, height: 60) + } + + private func updateEmptyState() { + emptyLabel.isHidden = !viewModel.chats.isEmpty + } + + @objc private func onNewChat() { + pushViewController(vc: MessengerNewChatViewController()) + } + + @objc private func refresh(_ sender: Any) { + viewModel.reload() + } +} + +extension MessengerChatListViewController: UITableViewDelegate, UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.chats.count + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return viewModel.chats[indexPath.row].hasUnread ? 80 : 70 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "MessengerChatCell", for: indexPath) as! MessengerChatCell + cell.configure(with: viewModel.chats[indexPath.row], row: indexPath.row) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let chat = viewModel.chats[indexPath.row] + let vc = MessengerChatViewController(peerWalletId: chat.peerWalletId, contactName: chat.contactName) + if let myId = chat.myWalletId, !myId.isEmpty { + vc.setMyAddress(myId) + } + pushViewController(vc: vc) + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let action = UIContextualAction(style: .destructive, title: Localizable.shared.strings.delete) { [weak self] _, _, completion in + self?.viewModel.remove(at: indexPath.row) + completion(true) + } + return UISwipeActionsConfiguration(actions: [action]) + } +} diff --git a/BeamWallet/ViewControllers/Main/Messenger/MessengerChatViewController.swift b/BeamWallet/ViewControllers/Main/Messenger/MessengerChatViewController.swift new file mode 100644 index 00000000..5ed13d6e --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Messenger/MessengerChatViewController.swift @@ -0,0 +1,343 @@ +// +// MessengerChatViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class MessengerChatViewController: BaseViewController { + + private let viewModel: MessengerConversationViewModel + private let contactName: String? + + private let tableView = UITableView(frame: .zero, style: .plain) + private let inputBar = UIView() + private let inputContainer = UIView() + private let textView = UITextView() + private let sendButton = UIButton(type: .custom) + private let placeholderLabel = UILabel() + + private var inputBarBottomConstraint: NSLayoutConstraint? + private let inputBarHeight: CGFloat = 64 + + init(peerWalletId: String, contactName: String? = nil) { + self.viewModel = MessengerConversationViewModel(peerWalletId: peerWalletId) + self.contactName = contactName + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } + + func setMyAddress(_ walletId: String) { + viewModel.myWalletId = walletId + } + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true) + + if let name = contactName, !name.isEmpty { + title = name + } else { + let peer = viewModel.peerWalletId + if peer.count > 12 { + title = "\(peer.prefix(6))…\(peer.suffix(6))" + } else { + title = peer + } + } + + addRightButton(image: IconCancel(), target: self, selector: #selector(onDeleteChat)) + + setupTable() + setupInputBar() + + viewModel.onDataChanged = { [weak self] in + guard let self = self else { return } + self.tableView.reloadData() + self.scrollToBottom(animated: true) + } + + viewModel.load() + + NotificationCenter.default.addObserver(self, selector: #selector(onKeyboard(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(onKeyboard(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.markAsRead() + scrollToBottom(animated: false) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + private func setupTable() { + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.dataSource = self + tableView.delegate = self + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.allowsSelection = false + tableView.estimatedRowHeight = 60 + tableView.rowHeight = UITableView.automaticDimension + tableView.keyboardDismissMode = .interactive + tableView.register(MessengerBubbleCell.self, forCellReuseIdentifier: "MessengerBubbleCell") + view.addSubview(tableView) + } + + private func setupInputBar() { + inputBar.translatesAutoresizingMaskIntoConstraints = false + inputBar.backgroundColor = UIColor.main.marine + view.addSubview(inputBar) + + let topDivider = UIView() + topDivider.translatesAutoresizingMaskIntoConstraints = false + topDivider.backgroundColor = UIColor.white.withAlphaComponent(0.05) + inputBar.addSubview(topDivider) + + inputContainer.translatesAutoresizingMaskIntoConstraints = false + inputContainer.backgroundColor = UIColor.white.withAlphaComponent(0.05) + inputContainer.layer.cornerRadius = 18 + inputBar.addSubview(inputContainer) + + textView.translatesAutoresizingMaskIntoConstraints = false + textView.font = RegularFont(size: 15) + textView.textColor = UIColor.white + textView.tintColor = UIColor.white + textView.backgroundColor = .clear + textView.isScrollEnabled = false + textView.delegate = self + textView.returnKeyType = .default + textView.textContainerInset = UIEdgeInsets(top: 11, left: 4, bottom: 11, right: 4) + textView.textContainer.lineFragmentPadding = 0 + inputContainer.addSubview(textView) + + placeholderLabel.translatesAutoresizingMaskIntoConstraints = false + placeholderLabel.text = Localizable.shared.strings.messenger_send_placeholder + placeholderLabel.font = ItalicFont(size: 15) + placeholderLabel.textColor = UIColor.white.withAlphaComponent(0.4) + textView.addSubview(placeholderLabel) + + sendButton.translatesAutoresizingMaskIntoConstraints = false + sendButton.backgroundColor = UIColor.main.brightTeal + sendButton.layer.cornerRadius = 22 + sendButton.setImage(IconNextBlue(), for: .normal) + sendButton.imageView?.contentMode = .scaleAspectFit + sendButton.tintColor = UIColor.main.marineOriginal + sendButton.isEnabled = false + sendButton.alpha = 0.4 + sendButton.addTarget(self, action: #selector(onSend), for: .touchUpInside) + inputBar.addSubview(sendButton) + + let bottom = inputBar.bottomAnchor.constraint(equalTo: view.bottomAnchor) + inputBarBottomConstraint = bottom + + NSLayoutConstraint.activate([ + inputBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + inputBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + bottom, + + topDivider.leadingAnchor.constraint(equalTo: inputBar.leadingAnchor), + topDivider.trailingAnchor.constraint(equalTo: inputBar.trailingAnchor), + topDivider.topAnchor.constraint(equalTo: inputBar.topAnchor), + topDivider.heightAnchor.constraint(equalToConstant: 1), + + inputContainer.leadingAnchor.constraint(equalTo: inputBar.leadingAnchor, constant: defaultX), + inputContainer.topAnchor.constraint(equalTo: inputBar.topAnchor, constant: 10), + inputContainer.bottomAnchor.constraint(equalTo: inputBar.safeAreaLayoutGuide.bottomAnchor, constant: -10), + inputContainer.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -10), + inputContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 44), + + textView.leadingAnchor.constraint(equalTo: inputContainer.leadingAnchor, constant: 12), + textView.trailingAnchor.constraint(equalTo: inputContainer.trailingAnchor, constant: -12), + textView.topAnchor.constraint(equalTo: inputContainer.topAnchor), + textView.bottomAnchor.constraint(equalTo: inputContainer.bottomAnchor), + + placeholderLabel.leadingAnchor.constraint(equalTo: textView.leadingAnchor), + placeholderLabel.centerYAnchor.constraint(equalTo: textView.centerYAnchor), + + sendButton.trailingAnchor.constraint(equalTo: inputBar.trailingAnchor, constant: -defaultX), + sendButton.bottomAnchor.constraint(equalTo: inputContainer.bottomAnchor), + sendButton.widthAnchor.constraint(equalToConstant: 44), + sendButton.heightAnchor.constraint(equalToConstant: 44), + ]) + + NSLayoutConstraint.activate([ + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: navigationBarOffset), + tableView.bottomAnchor.constraint(equalTo: inputBar.topAnchor), + ]) + } + + private func scrollToBottom(animated: Bool) { + let groups = sections + guard let last = groups.last, !last.messages.isEmpty else { return } + let indexPath = IndexPath(row: last.messages.count - 1, section: groups.count - 1) + tableView.scrollToRow(at: indexPath, at: .bottom, animated: animated) + } + + @objc private func onSend() { + let text = textView.text ?? "" + viewModel.send(text) + textView.text = "" + placeholderLabel.isHidden = false + sendButton.isEnabled = false + sendButton.alpha = 0.4 + } + + @objc private func onDeleteChat() { + confirmAlert( + title: Localizable.shared.strings.messenger_delete_chat, + message: "", + cancelTitle: Localizable.shared.strings.cancel, + confirmTitle: Localizable.shared.strings.delete, + cancelHandler: { _ in }, + confirmHandler: { [weak self] _ in + self?.viewModel.remove() + self?.navigationController?.popViewController(animated: true) + } + ) + } + + @objc private func onKeyboard(_ note: Notification) { + guard let frameEnd = (note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, + let duration = note.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return } + + let isHiding = note.name == UIResponder.keyboardWillHideNotification + let frameInView = view.convert(frameEnd, from: nil) + let bottomInset = isHiding ? 0 : max(0, view.bounds.height - frameInView.origin.y) + inputBarBottomConstraint?.constant = -bottomInset + + UIView.animate(withDuration: duration) { + self.view.layoutIfNeeded() + } completion: { _ in + self.scrollToBottom(animated: true) + } + } +} + +private struct MessengerSection { + let day: Date + let title: String + let messages: [BMInstantMessage] +} + +extension MessengerChatViewController { + + fileprivate var sections: [MessengerSection] { + let calendar = Calendar.current + let formatter = DateFormatter() + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .medium + formatter.timeStyle = .none + + var result: [MessengerSection] = [] + var currentDay: Date? + var bucket: [BMInstantMessage] = [] + + func flush() { + if let day = currentDay, !bucket.isEmpty { + result.append(MessengerSection(day: day, title: formatter.string(from: day), messages: bucket)) + } + } + + for msg in viewModel.messages { + let date = Date(timeIntervalSince1970: TimeInterval(msg.timestamp)) + let day = calendar.startOfDay(for: date) + if currentDay == day { + bucket.append(msg) + } else { + flush() + currentDay = day + bucket = [msg] + } + } + flush() + return result + } +} + +extension MessengerChatViewController: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].messages.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "MessengerBubbleCell", for: indexPath) as! MessengerBubbleCell + cell.configure(with: sections[indexPath.section].messages[indexPath.row]) + return cell + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let container = UIView() + container.backgroundColor = .clear + + let pill = UIView() + pill.translatesAutoresizingMaskIntoConstraints = false + pill.backgroundColor = Settings.sharedManager().isDarkMode + ? UIColor.white.withAlphaComponent(0.08) + : UIColor.black.withAlphaComponent(0.06) + pill.layer.cornerRadius = 11 + container.addSubview(pill) + + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = sections[section].title + label.font = BoldFont(size: 11) + label.textColor = Settings.sharedManager().isDarkMode ? UIColor.main.steel : UIColor.main.blueyGrey + label.textAlignment = .center + pill.addSubview(label) + + NSLayoutConstraint.activate([ + pill.centerXAnchor.constraint(equalTo: container.centerXAnchor), + pill.centerYAnchor.constraint(equalTo: container.centerYAnchor), + pill.heightAnchor.constraint(equalToConstant: 22), + + label.leadingAnchor.constraint(equalTo: pill.leadingAnchor, constant: 12), + label.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -12), + label.topAnchor.constraint(equalTo: pill.topAnchor), + label.bottomAnchor.constraint(equalTo: pill.bottomAnchor), + ]) + + return container + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 36 + } +} + +extension MessengerChatViewController: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + let trimmed = textView.text.trimmingCharacters(in: .whitespacesAndNewlines) + let canSend = !trimmed.isEmpty && (viewModel.myWalletId?.isEmpty == false) + sendButton.isEnabled = canSend + sendButton.alpha = canSend ? 1.0 : 0.4 + placeholderLabel.isHidden = !textView.text.isEmpty + } +} diff --git a/BeamWallet/ViewControllers/Main/Messenger/MessengerNewChatViewController.swift b/BeamWallet/ViewControllers/Main/Messenger/MessengerNewChatViewController.swift new file mode 100644 index 00000000..1e8dfd6a --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Messenger/MessengerNewChatViewController.swift @@ -0,0 +1,243 @@ +// +// MessengerNewChatViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class MessengerNewChatViewController: BaseViewController { + + private let scrollView = UIScrollView() + private let contentView = UIView() + + private let myAddressLabel = sectionLabel() + private let myAddressField = BMField() + + private let peerLabel = sectionLabel() + private let peerField = BMField() + + private let nameLabel = sectionLabel() + private let nameField = BMField() + + private lazy var startButton: BMButton = { + let button = BMButton.defaultButton(frame: CGRect(x: 0, y: 0, width: 220, height: 44), color: UIColor.main.brightTeal) + button.setTitle(Localizable.shared.strings.messenger_start.lowercased(), for: .normal) + button.setTitleColor(UIColor.main.marineOriginal, for: .normal) + button.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + button.titleLabel?.font = BoldFont(size: 14) + button.addTarget(self, action: #selector(onStart), for: .touchUpInside) + return button + }() + + private var ownAddresses: [BMAddress] = [] + private var selectedMyAddress: BMAddress? + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true) + title = Localizable.shared.strings.messenger_new_chat + + ownAddresses = (AppModel.sharedManager().walletAddresses as? [BMAddress] ?? []) + .filter { !$0.isExpired() } + selectedMyAddress = ownAddresses.first + + configureLabels() + configureFields() + layoutForm() + + refreshMyAddressField() + validate() + } + + private static func sectionLabel() -> UILabel { + let label = UILabel() + label.font = BoldFont(size: 14) + label.textColor = UIColor.main.blueyGrey + label.letterSpacing = 2 + label.adjustFontSize = true + return label + } + + private func configureLabels() { + myAddressLabel.text = Localizable.shared.strings.messenger_my_address.uppercased() + peerLabel.text = Localizable.shared.strings.messenger_peer_address.uppercased() + nameLabel.text = Localizable.shared.strings.messenger_contact_name.uppercased() + } + + private func configureFields() { + for field in [myAddressField, peerField, nameField] { + field.font = RegularFont(size: 16) + field.tintColor = UIColor.white + field.textColor = UIColor.white + field.adjustFontSize = true + field.spellCheckingType = .no + field.autocorrectionType = .no + } + + myAddressField.placeholder = Localizable.shared.strings.messenger_my_address + myAddressField.delegate = self + myAddressField.clearButtonMode = .never + myAddressField.awakeFromNib() + + peerField.placeholder = Localizable.shared.strings.messenger_peer_address + peerField.autocapitalizationType = .none + peerField.addTarget(self, action: #selector(onPeerChanged), for: .editingChanged) + peerField.awakeFromNib() + + nameField.placeholder = Localizable.shared.strings.messenger_contact_name + nameField.autocapitalizationType = .words + nameField.awakeFromNib() + } + + private func layoutForm() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.keyboardDismissMode = .interactive + view.addSubview(scrollView) + + contentView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) + + NSLayoutConstraint.activate([ + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: navigationBarOffset), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor), + ]) + + let topInset: CGFloat = 16 + let labelToFieldSpacing: CGFloat = 8 + let fieldHeight: CGFloat = 45 + let groupSpacing: CGFloat = 26 + let labelHeight: CGFloat = 20 + + var previousAnchor: NSLayoutYAxisAnchor = contentView.topAnchor + var topConstant: CGFloat = topInset + + for (label, field) in [(myAddressLabel, myAddressField), (peerLabel, peerField), (nameLabel, nameField)] { + label.translatesAutoresizingMaskIntoConstraints = false + field.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(label) + contentView.addSubview(field) + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: defaultX), + label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -defaultX), + label.topAnchor.constraint(equalTo: previousAnchor, constant: topConstant), + label.heightAnchor.constraint(equalToConstant: labelHeight), + + field.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: defaultX), + field.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -defaultX), + field.topAnchor.constraint(equalTo: label.bottomAnchor, constant: labelToFieldSpacing), + field.heightAnchor.constraint(equalToConstant: fieldHeight), + ]) + + previousAnchor = field.bottomAnchor + topConstant = groupSpacing + } + + startButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(startButton) + + NSLayoutConstraint.activate([ + startButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + startButton.topAnchor.constraint(equalTo: previousAnchor, constant: 30), + startButton.widthAnchor.constraint(equalToConstant: 220), + startButton.heightAnchor.constraint(equalToConstant: 44), + startButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30), + ]) + } + + private func refreshMyAddressField() { + myAddressField.text = selectedMyAddress.map(addressTitle(for:)) ?? "" + validate() + } + + private func addressTitle(for addr: BMAddress) -> String { + let label = addr.label.isEmpty ? "" : "\(addr.label) — " + let id = addr.walletId + let short = id.count > 12 ? "\(id.prefix(6))…\(id.suffix(6))" : id + return "\(label)\(short)" + } + + private func validate() { + let peer = (peerField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let valid = !peer.isEmpty && AppModel.sharedManager().isAddress(peer) && selectedMyAddress != nil + startButton.isEnabled = valid + startButton.alpha = valid ? 1.0 : 0.5 + } + + @objc private func onPickMyAddress() { + guard !ownAddresses.isEmpty else { return } + let alert = UIAlertController(title: Localizable.shared.strings.messenger_my_address, message: nil, preferredStyle: .actionSheet) + for addr in ownAddresses { + alert.addAction(UIAlertAction(title: addressTitle(for: addr), style: .default) { [weak self] _ in + self?.selectedMyAddress = addr + self?.refreshMyAddressField() + }) + } + alert.addAction(UIAlertAction(title: Localizable.shared.strings.cancel, style: .cancel)) + present(alert, animated: true) + } + + @objc private func onPeerChanged() { + validate() + } + + @objc private func onStart() { + let peer = (peerField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + guard !peer.isEmpty, + AppModel.sharedManager().isAddress(peer), + let myAddress = selectedMyAddress else { return } + + let name = (nameField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let resolvedPeer = AppModel.sharedManager().resolvedPeerWalletId(peer) + + if !name.isEmpty { + AppModel.sharedManager().addContact(resolvedPeer, address: resolvedPeer, name: name, identidy: nil) + } + + AppModel.sharedManager().addChatStub(resolvedPeer, contactName: name.isEmpty ? nil : name, myWalletId: myAddress.walletId) + + let chatVC = MessengerChatViewController(peerWalletId: resolvedPeer, contactName: name.isEmpty ? nil : name) + chatVC.setMyAddress(myAddress.walletId) + + if var stack = navigationController?.viewControllers { + stack.removeLast() + stack.append(chatVC) + navigationController?.setViewControllers(stack, animated: true) + } else { + pushViewController(vc: chatVC) + } + } +} + +extension MessengerNewChatViewController: UITextFieldDelegate { + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + if textField === myAddressField { + onPickMyAddress() + return false + } + return true + } +} diff --git a/BeamWallet/ViewControllers/Main/Notifications/Cells/NotificationTableViewCell.swift b/BeamWallet/ViewControllers/Main/Notifications/Cells/NotificationTableViewCell.swift index 3e1e36da..7363af26 100644 --- a/BeamWallet/ViewControllers/Main/Notifications/Cells/NotificationTableViewCell.swift +++ b/BeamWallet/ViewControllers/Main/Notifications/Cells/NotificationTableViewCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 29.04.2020. -// Copyright © 2020 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit diff --git a/BeamWallet/ViewControllers/Main/Notifications/NotificationItem.swift b/BeamWallet/ViewControllers/Main/Notifications/NotificationItem.swift index b465653f..010f81ee 100644 --- a/BeamWallet/ViewControllers/Main/Notifications/NotificationItem.swift +++ b/BeamWallet/ViewControllers/Main/Notifications/NotificationItem.swift @@ -2,7 +2,7 @@ // NotificationItem.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Notifications/NotificationVersionViewController.swift b/BeamWallet/ViewControllers/Main/Notifications/NotificationVersionViewController.swift index ad645df0..799ca017 100644 --- a/BeamWallet/ViewControllers/Main/Notifications/NotificationVersionViewController.swift +++ b/BeamWallet/ViewControllers/Main/Notifications/NotificationVersionViewController.swift @@ -2,7 +2,7 @@ // NotificationVersionViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Notifications/NotificationViewModel.swift b/BeamWallet/ViewControllers/Main/Notifications/NotificationViewModel.swift index 433838ab..b3e37776 100644 --- a/BeamWallet/ViewControllers/Main/Notifications/NotificationViewModel.swift +++ b/BeamWallet/ViewControllers/Main/Notifications/NotificationViewModel.swift @@ -2,7 +2,7 @@ // NotificationViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ class NotificationViewModel: NSObject { public func trailingSwipeActions(indexPath:IndexPath) -> UISwipeActionsConfiguration? { - let delete = UIContextualAction(style: .normal, title: nil) { (action, view, handler) in + let delete = UIContextualAction(style: .normal, title: nil) { (_, _, handler) in handler(true) self.deleteNotification(indexPath: indexPath) } @@ -86,7 +86,7 @@ class NotificationViewModel: NSObject { } } -//MARK: - Delegate +// MARK: - Delegate extension NotificationViewModel : WalletModelDelegate { diff --git a/BeamWallet/ViewControllers/Main/Notifications/NotificationsViewController.swift b/BeamWallet/ViewControllers/Main/Notifications/NotificationsViewController.swift index 7fe3017e..3e49da37 100644 --- a/BeamWallet/ViewControllers/Main/Notifications/NotificationsViewController.swift +++ b/BeamWallet/ViewControllers/Main/Notifications/NotificationsViewController.swift @@ -2,7 +2,7 @@ // NotificationsViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -106,9 +106,7 @@ class NotificationsViewController: BaseTableViewController { } } - viewModel.onDataDeleted = { [weak self] - indexPath, notification in - + viewModel.onDataDeleted = { [weak self] indexPath, notification in guard let strongSelf = self else { return } if strongSelf.viewModel.unreads.count == 0 && strongSelf.viewModel.reads.count == 0 { diff --git a/BeamWallet/ViewControllers/Main/Passwords/CreateWalletPasswordViewController.swift b/BeamWallet/ViewControllers/Main/Passwords/CreateWalletPasswordViewController.swift index 52a041a4..a2c986ca 100644 --- a/BeamWallet/ViewControllers/Main/Passwords/CreateWalletPasswordViewController.swift +++ b/BeamWallet/ViewControllers/Main/Passwords/CreateWalletPasswordViewController.swift @@ -2,7 +2,7 @@ // CreateWalletPasswordViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -84,10 +84,19 @@ class CreateWalletPasswordViewController: BaseWizardViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + let target: BMField? = isChangePassword ? oldPassField : passField + DispatchQueue.main.async { [weak target] in + _ = target?.becomeFirstResponder() + } + } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -118,13 +127,13 @@ class CreateWalletPasswordViewController: BaseWizardViewController { } else { confirmAlert(title: Localizable.shared.strings.return_to_seed_title, message: Localizable.shared.strings.return_to_seed_info, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.retur, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in let count = OnboardManager.shared.isSkipedSeed() ? 2 : 3 let viewControllers = self.navigationController?.viewControllers let vc = viewControllers![(viewControllers?.count)! - count] self.navigationController?.popToViewController(vc, animated: true) - } + }) } } @@ -158,14 +167,14 @@ class CreateWalletPasswordViewController: BaseWizardViewController { confirmAlert(title: title, message: message, cancelTitle: Localizable.shared.strings.dont_use, confirmTitle: Localizable.shared.strings.enable, cancelHandler: { _ in self.goNext(pass: pass) - + Settings.sharedManager().isEnableBiometric = false - - }) { _ in + + }, confirmHandler: { _ in self.goNext(pass: pass) - + Settings.sharedManager().isEnableBiometric = true - } + }) } else { goNext(pass: pass) @@ -197,31 +206,32 @@ class CreateWalletPasswordViewController: BaseWizardViewController { } private func openMainPage() { - if let phrase = phrase, let pass = passField.text { + guard let phrase = phrase, let pass = passField.text else { return } + + // Restore flow opens the wallet immediately because the chosen node is + // already settled. Create flow defers wallet creation to + // SelectNodeViewController so the chosen node is in place before + // wallet->start() runs — the whole DB-init/start sequence then runs + // inside a single io::Reactor::Scope. + if AppModel.sharedManager().isRestoreFlow { let created = AppModel.sharedManager().createWallet(phrase, pass: pass) - if(!created) - { - self.alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.wallet_not_created) { (_ ) in - - } - } - else { - OnboardManager.shared.saveSeed(seed: phrase) - _ = KeychainManager.addPassword(password: pass) - - if AppModel.sharedManager().isRestoreFlow { - let vc = OpenWalletProgressViewController(password: pass, phrase: phrase) - self.pushViewController(vc: vc) - } - else { - let vc = SelectNodeViewController() - vc.isCreateWallet = true - vc.isNeedDisconnect = false - vc.password = pass - vc.phrase = phrase - self.pushViewController(vc: vc) - } + if !created { + AppModel.sharedManager().abortCreateAndReset() + self.alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.wallet_not_created) { _ in } + return } + OnboardManager.shared.saveSeed(seed: phrase) + _ = KeychainManager.addPassword(password: pass) + let vc = OpenWalletProgressViewController(password: pass, phrase: phrase) + self.pushViewController(vc: vc) + } + else { + let vc = SelectNodeViewController() + vc.isCreateWallet = true + vc.isNeedDisconnect = false + vc.password = pass + vc.phrase = phrase + self.pushViewController(vc: vc) } } } diff --git a/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordPopover.swift b/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordPopover.swift index e3ef878b..acea56d1 100644 --- a/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordPopover.swift +++ b/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordPopover.swift @@ -2,7 +2,7 @@ // UnlockPasswordPopover.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -89,46 +89,46 @@ class UnlockPasswordPopover: BaseViewController { private func getTextForFaceID() -> String { switch event { - case .transaction: - return Localizable.shared.strings.confirm_transaction_2 - case .node: - return Localizable.shared.strings.change_node_text_1 - case .settings: - return Localizable.shared.strings.change_settings_text_1 - case .clear_wallet: - return String.empty() - case .none: - return String.empty() + case .transaction: + return Localizable.shared.strings.confirm_transaction_2 + case .node: + return Localizable.shared.strings.change_node_text_1 + case .settings: + return Localizable.shared.strings.change_settings_text_1 + case .clear_wallet: + return String.empty() + case .none: + return String.empty() } } - + private func getTextForTouchID() -> String { switch event { - case .transaction: - return Localizable.shared.strings.confirm_transaction_1 - case .node: - return Localizable.shared.strings.change_node_text_2 - case .settings: - return Localizable.shared.strings.change_settings_text_2 - case .clear_wallet: - return String.empty() - case .none: - return String.empty() + case .transaction: + return Localizable.shared.strings.confirm_transaction_1 + case .node: + return Localizable.shared.strings.change_node_text_2 + case .settings: + return Localizable.shared.strings.change_settings_text_2 + case .clear_wallet: + return String.empty() + case .none: + return String.empty() } } - + private func getTextForPassword() -> String { switch event { - case .transaction: - return Localizable.shared.strings.confirm_transaction_3 - case .node: - return Localizable.shared.strings.change_node_text_3 - case .settings: - return Localizable.shared.strings.change_settings_text_3 - case .clear_wallet: - return Localizable.shared.strings.clear_wallet_password - case .none: - return String.empty() + case .transaction: + return Localizable.shared.strings.confirm_transaction_3 + case .node: + return Localizable.shared.strings.change_node_text_3 + case .settings: + return Localizable.shared.strings.change_settings_text_3 + case .clear_wallet: + return Localizable.shared.strings.clear_wallet_password + case .none: + return String.empty() } } @@ -143,7 +143,8 @@ class UnlockPasswordPopover: BaseViewController { public func biometricAuthorization() { if BiometricAuthorization.shared.canAuthenticate(), Settings.sharedManager().isEnableBiometric { BiometricAuthorization.shared.authenticateWithBioMetrics(success: { - if let password = KeychainManager.getPassword() { + let context = BiometricAuthorization.shared.lastAuthenticatedContext + if let password = KeychainManager.getPassword(context: context) { self.passField.text = password self.dismiss(animated: true, completion: { self.completion?(true) diff --git a/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordViewController.swift b/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordViewController.swift index 48abb845..cab7e508 100644 --- a/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordViewController.swift +++ b/BeamWallet/ViewControllers/Main/Passwords/UnlockPasswordViewController.swift @@ -2,7 +2,7 @@ // UnlockPasswordViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/QRCode/PreviewQRViewController.swift b/BeamWallet/ViewControllers/Main/QRCode/PreviewQRViewController.swift index 6f3ad1d9..2edf0107 100644 --- a/BeamWallet/ViewControllers/Main/QRCode/PreviewQRViewController.swift +++ b/BeamWallet/ViewControllers/Main/QRCode/PreviewQRViewController.swift @@ -2,7 +2,7 @@ // PreviewQRViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -112,7 +112,7 @@ class PreviewQRViewController: BaseViewController { if let image = codeView.snapshot(), let topVC = UIApplication.getTopMostViewController() { let activityItem: [AnyObject] = [image] let vc = UIActivityViewController(activityItems: activityItem, applicationActivities: []) - vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in + vc.completionWithItemsHandler = {(_: UIActivity.ActivityType?, completed: Bool, _: [Any]?, _: Error?) in if completed && self.fromMore { self.back() @@ -138,13 +138,13 @@ class PreviewQRViewController: BaseViewController { let action1 = UIPreviewAction(title: Localizable.shared.strings.share_qr_code, style: .default, - handler: { previewAction, viewController in + handler: { _, _ in self.onShare() }) let action2 = UIPreviewAction(title: Localizable.shared.strings.copy_address, style: .default, - handler: { previewAction, viewController in + handler: { _, _ in self.onCopy() }) diff --git a/BeamWallet/ViewControllers/Main/QRCode/QRCodeSmallViewController.swift b/BeamWallet/ViewControllers/Main/QRCode/QRCodeSmallViewController.swift index c79d6e17..298ec8a9 100644 --- a/BeamWallet/ViewControllers/Main/QRCode/QRCodeSmallViewController.swift +++ b/BeamWallet/ViewControllers/Main/QRCode/QRCodeSmallViewController.swift @@ -2,7 +2,7 @@ // QRCodeSmallViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ class QRCodeSmallViewController: BaseViewController { weak var delegate: QRViewControllerDelegate? public var onShared : (() -> Void)? public var isMaxPrivacy = false + public var isSbbsOnly = false + public var isPublicOffline = false @IBOutlet weak private var infoLabel: UILabel! @IBOutlet weak private var codeConentView: UIView! @@ -70,8 +72,12 @@ class QRCodeSmallViewController: BaseViewController { infoLabel.text = text } + else if isPublicOffline { + infoLabel.text = Localizable.shared.strings.public_offline_address_info + } else { - if !AppModel.sharedManager().checkIsOwnNode() { + let isOwn = AppModel.sharedManager().checkIsOwnNode() + if isSbbsOnly || !isOwn { infoLabel.text = Localizable.shared.strings.receive_description_2 } else { @@ -84,15 +90,11 @@ class QRCodeSmallViewController: BaseViewController { addSwipeToDismiss() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - @IBAction func onShare(sender :UIButton) { if let image = codeConentView.snapshot() { let activityItem: [AnyObject] = [image] let vc = UIActivityViewController(activityItems: activityItem, applicationActivities: []) - vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in + vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, _: [Any]?, _: Error?) in if completed { self.dismiss(animated: true, completion: { if activityType == UIActivity.ActivityType.copyToPasteboard { diff --git a/BeamWallet/ViewControllers/Main/QRCode/QRScannerViewController.swift b/BeamWallet/ViewControllers/Main/QRCode/QRScannerViewController.swift index d62cbdc6..637ed68a 100644 --- a/BeamWallet/ViewControllers/Main/QRCode/QRScannerViewController.swift +++ b/BeamWallet/ViewControllers/Main/QRCode/QRScannerViewController.swift @@ -2,7 +2,7 @@ // QRScannerViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -142,7 +142,7 @@ class QRScannerViewController: BaseViewController { } -//MARK: - AVCaptureMetadataOutputObjectsDelegate +// MARK: - AVCaptureMetadataOutputObjectsDelegate extension QRScannerViewController : AVCaptureMetadataOutputObjectsDelegate { @@ -171,7 +171,7 @@ extension QRScannerViewController : AVCaptureMetadataOutputObjectsDelegate { { AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - scannedValue = code; + scannedValue = code print(scannedValue) @@ -283,7 +283,7 @@ extension QRScannerViewController : AVCaptureMetadataOutputObjectsDelegate { } -//MARK: - Permissions +// MARK: - Permissions extension QRScannerViewController { @@ -315,13 +315,13 @@ extension QRScannerViewController { } private func camDenied() { - self.confirmAlert(title: String.empty(), message: Localizable.shared.strings.camera_denied_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.open_settings, cancelHandler: { (_ ) in - - }) { (_ ) in + self.confirmAlert(title: String.empty(), message: Localizable.shared.strings.camera_denied_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.open_settings, cancelHandler: { _ in + + }, confirmHandler: { _ in UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: { (_ ) in self.back() }) - } + }) } } @@ -340,7 +340,7 @@ extension QRScannerViewController: UIImagePickerControllerDelegate { picker.dismiss(animated: true) { guard let image = info[.originalImage] as? UIImage else { - return; + return } if let features = self.detectQRCode(image) { diff --git a/BeamWallet/ViewControllers/Main/QRCode/QRViewController.swift b/BeamWallet/ViewControllers/Main/QRCode/QRViewController.swift index 720c9aac..a1d62da7 100644 --- a/BeamWallet/ViewControllers/Main/QRCode/QRViewController.swift +++ b/BeamWallet/ViewControllers/Main/QRCode/QRViewController.swift @@ -2,7 +2,7 @@ // QRViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -82,12 +82,12 @@ class QRViewController: BaseViewController { amountTitleLabel.text = Localizable.shared.strings.requested_amount.uppercased() infoLabel.text = Localizable.shared.strings.send_qr_secure - //+ "\n\n" + Localizable.shared.strings.receive_notice + // + "\n\n" + Localizable.shared.strings.receive_notice addressTitleLabel.text = Localizable.shared.strings.address.uppercased() if isToken { - addressLabel.text = "token"//address.token + addressLabel.text = "token"// address.token } else { if let display = address.displayAddress { @@ -152,7 +152,7 @@ class QRViewController: BaseViewController { if let image = codeConentView.snapshot() { let activityItem: [AnyObject] = [image] let vc = UIActivityViewController(activityItems: activityItem, applicationActivities: []) - vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in + vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, _: [Any]?, _: Error?) in if completed { self.dismiss(animated: true, completion: { if activityType == UIActivity.ActivityType.copyToPasteboard { diff --git a/BeamWallet/ViewControllers/Main/Settings/Cells/NodeCell.swift b/BeamWallet/ViewControllers/Main/Settings/Cells/NodeCell.swift index bff676cb..b2ea4c93 100644 --- a/BeamWallet/ViewControllers/Main/Settings/Cells/NodeCell.swift +++ b/BeamWallet/ViewControllers/Main/Settings/Cells/NodeCell.swift @@ -2,7 +2,7 @@ // NodeCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,8 +31,8 @@ class NodeCell: UITableViewCell { super.awakeFromNib() if Settings.sharedManager().isDarkMode { - nameLabel.textColor = UIColor.main.steel; - hintLabel.textColor = UIColor.main.steel; + nameLabel.textColor = UIColor.main.steel + hintLabel.textColor = UIColor.main.steel } selectionStyle = .none diff --git a/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsCell.swift b/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsCell.swift index 11d50661..d74d9f71 100644 --- a/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsCell.swift +++ b/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsCell.swift @@ -2,7 +2,7 @@ // SettingsCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsSubTitleCell.swift b/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsSubTitleCell.swift index 726ba123..d3fc637d 100644 --- a/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsSubTitleCell.swift +++ b/BeamWallet/ViewControllers/Main/Settings/Cells/SettingsSubTitleCell.swift @@ -2,7 +2,7 @@ // SettingsSubTitleCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Settings/NodePeersViewController.swift b/BeamWallet/ViewControllers/Main/Settings/NodePeersViewController.swift new file mode 100644 index 00000000..e1428abd --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Settings/NodePeersViewController.swift @@ -0,0 +1,182 @@ +// +// NodePeersViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +private class NodePeerCell: UITableViewCell { + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) + } + required init?(coder: NSCoder) { + fatalError(Localizable.shared.strings.fatalInitCoderError) + } +} + +class NodePeersViewController: BaseTableViewController { + + private static let cellId = "NodePeerCell" + + private var poolAddresses: [String] = [] + + private static let lastSeenFormatter: DateFormatter = { + let f = DateFormatter() + f.dateStyle = .none + f.timeStyle = .medium + return f + }() + + override var tableStyle: UITableView.Style { + get { return .grouped } + set { super.tableStyle = newValue } + } + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: AppModel.sharedManager().isLoggedin) + title = Localizable.shared.strings.node_peers + + tableView.register(NodePeerCell.self, forCellReuseIdentifier: NodePeersViewController.cellId) + tableView.dataSource = self + tableView.delegate = self + tableView.backgroundColor = UIColor.main.marine + tableView.separatorColor = UIColor.white.withAlphaComponent(0.13) + tableView.separatorStyle = .singleLine + tableView.contentInsetAdjustmentBehavior = .never + tableView.sectionHeaderHeight = UITableView.automaticDimension + tableView.estimatedSectionHeaderHeight = 30 + tableView.sectionFooterHeight = 15 + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20)) + + reloadPool() + AppModel.sharedManager().addDelegate(self) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if isMovingFromParent { + AppModel.sharedManager().removeDelegate(self) + } + } + + private func reloadPool() { + if Settings.sharedManager().connectToRandomNode { + let active = Settings.sharedManager().nodeAddress ?? "" + poolAddresses = AppModel.defaultPeerAddresses().filter { $0 != active } + } else { + poolAddresses = [] + } + } + + private func statusText() -> (text: String, color: UIColor) { + let app = AppModel.sharedManager() + if app.isConnected { + return (Localizable.shared.strings.node_peer_connected, UIColor.main.brightTeal) + } + if app.isConnecting { + return (Localizable.shared.strings.node_peer_connecting, UIColor.main.heliotrope) + } + return (Localizable.shared.strings.node_peer_disconnected, UIColor.main.coral) + } + + private func lastSeenText() -> String? { + guard let date = AppModel.sharedManager().lastConnectionChangedAt else { return nil } + let stamp = NodePeersViewController.lastSeenFormatter.string(from: date) + return String(format: Localizable.shared.strings.node_peer_last_seen, stamp) + } +} + +extension NodePeersViewController: UITableViewDataSource, UITableViewDelegate { + + func numberOfSections(in tableView: UITableView) -> Int { + return poolAddresses.isEmpty ? 1 : 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return section == 0 ? 1 : poolAddresses.count + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if section == 0 { + return Localizable.shared.strings.node_peer_active.uppercased() + } + return Localizable.shared.strings.node_peer_pool.uppercased() + } + + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + if let header = view as? UITableViewHeaderFooterView { + header.textLabel?.font = BoldFont(size: 13) + header.textLabel?.textColor = Settings.sharedManager().isDarkMode ? UIColor.main.steel : UIColor.main.steelGrey + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: NodePeersViewController.cellId, for: indexPath) + cell.backgroundColor = UIColor.main.cellBackgroundColor + cell.contentView.backgroundColor = UIColor.main.cellBackgroundColor + cell.selectionStyle = .none + cell.textLabel?.textColor = UIColor.white + cell.textLabel?.font = RegularFont(size: 16) + cell.textLabel?.numberOfLines = 0 + cell.detailTextLabel?.numberOfLines = 0 + cell.detailTextLabel?.font = RegularFont(size: 13) + + if indexPath.section == 0 { + cell.textLabel?.text = Settings.sharedManager().nodeAddress + let status = statusText() + let detail = NSMutableAttributedString( + string: status.text, + attributes: [ + .foregroundColor: status.color, + .font: BoldFont(size: 13) + ] + ) + if let last = lastSeenText() { + detail.append(NSAttributedString( + string: "\n" + last, + attributes: [ + .foregroundColor: Settings.sharedManager().isDarkMode ? UIColor.main.steel : UIColor.main.steelGrey, + .font: RegularFont(size: 13) + ] + )) + } + cell.detailTextLabel?.attributedText = detail + } else { + cell.textLabel?.text = poolAddresses[indexPath.row] + cell.detailTextLabel?.text = nil + } + return cell + } +} + +extension NodePeersViewController: WalletModelDelegate { + + private func refreshFromNetworkChange() { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.reloadPool() + self.tableView.reloadData() + } + } + + func onNetwotkStatusChange(_ connected: Bool) { refreshFromNetworkChange() } + func onNetwotkStartConnecting(_ connecting: Bool) { refreshFromNetworkChange() } + func onNetwotkStartReconnecting() { refreshFromNetworkChange() } +} diff --git a/BeamWallet/ViewControllers/Main/Settings/OwnerKeyViewController.swift b/BeamWallet/ViewControllers/Main/Settings/OwnerKeyViewController.swift index eca269f2..14449bcb 100644 --- a/BeamWallet/ViewControllers/Main/Settings/OwnerKeyViewController.swift +++ b/BeamWallet/ViewControllers/Main/Settings/OwnerKeyViewController.swift @@ -2,7 +2,7 @@ // OwnerKeyViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Settings/SelectNodeViewController.swift b/BeamWallet/ViewControllers/Main/Settings/SelectNodeViewController.swift index dc0d23f1..c171985f 100644 --- a/BeamWallet/ViewControllers/Main/Settings/SelectNodeViewController.swift +++ b/BeamWallet/ViewControllers/Main/Settings/SelectNodeViewController.swift @@ -2,7 +2,7 @@ // SelectNodeViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,51 +32,31 @@ class SelectNodeViewController: BaseTableViewController { private var items = [SelectNode]() private var inputField = BMField() private var oldSelected = 0 + private let actionButton = BMButton.defaultButton( + frame: CGRect(x: 0, y: 0, width: 220, height: 44), + color: UIColor.main.brightTeal) public var isNeedDisconnect = true public var isCreateWallet = false public var password:String? public var phrase:String? - + override var tableStyle: UITableView.Style { get { return .grouped } set { super.tableStyle = newValue } } - - private func footerView() -> UIView { - let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 155)) - - if !isCreateWallet && self.items[2].selected { - let nextButton = BMButton.defaultButton(frame: CGRect(x: (UIScreen.main.bounds.size.width-220)/2, y: 10, width: 220, height: 44), color: UIColor.main.brightTeal) - nextButton.setImage(IconNextBlue(), for: .normal) - nextButton.setTitle(Localizable.shared.strings.proceed.lowercased(), for: .normal) - nextButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) - nextButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) - nextButton.addTarget(self, action: #selector(onNext), for: .touchUpInside) - view.addSubview(nextButton) - } - else if isCreateWallet { - let connectButton = BMButton.defaultButton(frame: CGRect(x: (UIScreen.main.bounds.size.width-220)/2, y: 25, width: 220, height: 44), color: UIColor.main.brightTeal) - connectButton.setImage(IconNextBlue(), for: .normal) - connectButton.setTitle(Localizable.shared.strings.start_using_wallet.lowercased(), for: .normal) - connectButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) - connectButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) - connectButton.addTarget(self, action: #selector(onNext), for: .touchUpInside) - view.addSubview(connectButton) - } -// else if !isNeedDisconnect { -// let connectButton = BMButton.defaultButton(frame: CGRect(x: (UIScreen.main.bounds.size.width-220)/2, y: 80, width: 220, height: 44), color: UIColor.main.brightTeal) -// connectButton.setImage(IconDoneBlue(), for: .normal) -// connectButton.setTitle(Localizable.shared.strings.connect.lowercased(), for: .normal) -// connectButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) -// connectButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) -// connectButton.addTarget(self, action: #selector(onNext), for: .touchUpInside) -// view.addSubview(connectButton) -// } - - return view + + private func refreshActionButton() { + let show = isCreateWallet || items[2].selected + bottomAccessoryView?.isHidden = !show + let title = isCreateWallet + ? Localizable.shared.strings.start_using_wallet.lowercased() + : Localizable.shared.strings.proceed.lowercased() + actionButton.setTitle(title, for: .normal) + view.setNeedsLayout() } - + + override func viewDidLoad() { super.viewDidLoad() @@ -118,9 +98,18 @@ class SelectNodeViewController: BaseTableViewController { tableView.contentInsetAdjustmentBehavior = .never tableView.tableHeaderView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: 20)) tableView.tableHeaderView?.backgroundColor = UIColor.main.marine - tableView.tableFooterView = footerView() tableView.backgroundColor = UIColor.main.marine tableView.keyboardDismissMode = .interactive + + actionButton.setImage(IconNextBlue(), for: .normal) + actionButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) + actionButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + actionButton.addTarget(self, action: #selector(onNext), for: .touchUpInside) + let accessory = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 70)) + actionButton.frame = CGRect(x: (accessory.bounds.width - 220) / 2, y: 13, width: 220, height: 44) + accessory.addSubview(actionButton) + bottomAccessoryView = accessory + refreshActionButton() inputField.setNormalColor(color: .white) inputField.keyboardType = .numbersAndPunctuation @@ -144,7 +133,7 @@ class SelectNodeViewController: BaseTableViewController { @objc private func onDisconnect() { isNeedDisconnect = false - tableView.tableFooterView = footerView() + refreshActionButton() tableView.reloadData() } @@ -160,8 +149,8 @@ class SelectNodeViewController: BaseTableViewController { leftViewController: menuViewController, rightViewController: nil) - sideMenuController.leftViewWidth = UIScreen.main.bounds.size.width - 60; - sideMenuController.leftViewPresentationStyle = LGSideMenuPresentationStyle.slideAbove; + sideMenuController.leftViewWidth = UIScreen.main.bounds.size.width - 60 + sideMenuController.leftViewPresentationStyle = LGSideMenuPresentationStyle.slideAbove sideMenuController.rootViewLayerShadowRadius = 0 sideMenuController.rootViewLayerShadowColor = UIColor.clear sideMenuController.leftViewLayerShadowRadius = 0 @@ -178,38 +167,47 @@ class SelectNodeViewController: BaseTableViewController { @objc private func onNext() { isNeedDisconnect = true - tableView.tableFooterView = footerView() + refreshActionButton() tableView.reloadData() - + if items[0].selected { if isCreateWallet { Settings.sharedManager().removeCustomNode() } - + if Settings.sharedManager().isNodeProtocolEnabled { Settings.sharedManager().isNodeProtocolEnabled = false AppModel.sharedManager().enableBodyRequests(false) } Settings.sharedManager().connectToRandomNode = true - Settings.sharedManager().nodeAddress = AppModel.chooseRandomNode(); + Settings.sharedManager().nodeAddress = AppModel.chooseRandomNode() AppModel.sharedManager().changeNodeAddress() - + if isCreateWallet { - openMain() + // OpenWalletProgressViewController owns creation when phrase + // is non-nil (see startCreateWallet). Calling createWallet here + // too would run it twice and any future failure path would + // land in abortCreateAndReset(), wiping the DB we just made. + let vc = OpenWalletProgressViewController(password: self.password ?? "", phrase: self.phrase) + self.pushViewController(vc: vc) } } else if items[1].selected { if isCreateWallet { Settings.sharedManager().removeCustomNode() } - + Settings.sharedManager().connectToRandomNode = true Settings.sharedManager().isNodeProtocolEnabled = true - Settings.sharedManager().nodeAddress = AppModel.chooseRandomNode(); + Settings.sharedManager().nodeAddress = AppModel.chooseRandomNode() AppModel.sharedManager().changeNodeAddress() AppModel.sharedManager().enableBodyRequests(true) - + if isCreateWallet { + // OpenWalletProgressViewController owns creation when phrase + // is non-nil (see startCreateWallet). Calling createWallet here + // too would run it twice and any future failure path would + // land in abortCreateAndReset(), wiping the DB we just made. let vc = OpenWalletProgressViewController(password: self.password ?? "", phrase: self.phrase) self.pushViewController(vc: vc) } @@ -227,7 +225,7 @@ class SelectNodeViewController: BaseTableViewController { if let fullAddress = inputField.text { if fullAddress.isEmpty { isNeedDisconnect = false - tableView.tableFooterView = footerView() + refreshActionButton() tableView.reloadData() alert(title: Localizable.shared.strings.invalid_address_title, message: Localizable.shared.strings.enter_node_address, handler: nil) @@ -242,7 +240,11 @@ class SelectNodeViewController: BaseTableViewController { Settings.sharedManager().connectToRandomNode = false Settings.sharedManager().nodeAddress = fullAddress AppModel.sharedManager().changeNodeAddress() - + + // OpenWalletProgressViewController owns creation when phrase + // is non-nil (see startCreateWallet). Calling createWallet here + // too would run it twice and any future failure path would + // land in abortCreateAndReset(), wiping the DB we just made. let vc = OpenWalletProgressViewController(password: self.password ?? "", phrase: self.phrase) self.pushViewController(vc: vc) } @@ -286,7 +288,7 @@ class SelectNodeViewController: BaseTableViewController { } else { isNeedDisconnect = false - tableView.tableFooterView = footerView() + refreshActionButton() tableView.reloadData() alert(title: Localizable.shared.strings.invalid_address_title, message: Localizable.shared.strings.invalid_address_text, handler: nil) @@ -322,7 +324,7 @@ extension SelectNodeViewController : UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - + if isCreateWallet { self.onNextSelected(indexPath: indexPath) } @@ -397,13 +399,13 @@ extension SelectNodeViewController : UITableViewDelegate { } private func onNextSelected(indexPath: IndexPath) { - for (index, _) in items.enumerated() { + for index in items.indices { items[index].selected = false } - + items[indexPath.section].selected = true - - tableView.tableFooterView = footerView() + + refreshActionButton() tableView.reloadData() } } @@ -429,16 +431,16 @@ extension SelectNodeViewController : UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { return items.count } - + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 2 { return items[section].selected ? 2 : 1 } return 1 } - + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - + if indexPath.row == 1 { var cell = tableView.dequeueReusableCell(withIdentifier: "Cell") if cell == nil { diff --git a/BeamWallet/ViewControllers/Main/Settings/SettingsViewController.swift b/BeamWallet/ViewControllers/Main/Settings/SettingsViewController.swift index 597c8ccc..5c6ff2a2 100644 --- a/BeamWallet/ViewControllers/Main/Settings/SettingsViewController.swift +++ b/BeamWallet/ViewControllers/Main/Settings/SettingsViewController.swift @@ -2,7 +2,7 @@ // WalletViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import UIKit class SettingsViewController: BaseTableViewController { - + private var viewModel:SettingsViewModel! private var type:SettingsViewModel.SettingsType! - + private var searchView:BMSearchView! init(type:SettingsViewModel.SettingsType) { @@ -68,6 +68,8 @@ class SettingsViewController: BaseTableViewController { tableView.dataSource = self tableView.delegate = self tableView.contentInsetAdjustmentBehavior = .never + tableView.sectionHeaderHeight = 15 + tableView.sectionFooterHeight = 15 tableView.tableHeaderView?.backgroundColor = UIColor.main.marine tableView.backgroundColor = UIColor.main.marine @@ -77,35 +79,35 @@ class SettingsViewController: BaseTableViewController { searchView.y = 12 searchView.searchField.placeholder = Localizable.shared.strings.search_settings - searchView.onSearchTextChanged = { - [weak self] text in + searchView.onSearchTextChanged = { [weak self] text in guard let strongSelf = self else { return } if !text.isEmpty { strongSelf.tableView.sectionHeaderHeight = 5 strongSelf.tableView.sectionFooterHeight = 5 - strongSelf.tableView.tableFooterView = nil + strongSelf.bottomAccessoryView?.isHidden = true } else { strongSelf.tableView.sectionHeaderHeight = 15 strongSelf.tableView.sectionFooterHeight = 15 - strongSelf.tableView.tableFooterView = strongSelf.versionView() + strongSelf.bottomAccessoryView?.isHidden = false } + strongSelf.view.setNeedsLayout() strongSelf.viewModel.searchString = text } - searchView.onCancelSearch = { - [weak self] in + searchView.onCancelSearch = { [weak self] in guard let strongSelf = self else { return } strongSelf.tableView.sectionHeaderHeight = 15 strongSelf.tableView.sectionFooterHeight = 15 strongSelf.viewModel.searchString = "" - strongSelf.tableView.tableFooterView = strongSelf.versionView() + strongSelf.bottomAccessoryView?.isHidden = false + strongSelf.view.setNeedsLayout() } - + let tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 70)) tableHeaderView.addSubview(searchView) - + tableView.tableHeaderView = tableHeaderView - tableView.tableFooterView = versionView() + bottomAccessoryView = makeVersionAccessory() } else { tableView.tableHeaderView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: 20)) @@ -117,22 +119,13 @@ class SettingsViewController: BaseTableViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + if type == .main { - tableView.tableFooterView = versionView() + bottomAccessoryView = makeVersionAccessory() } } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - tableView.frame = CGRect(x: 0, y: tableView.y - 5, width: self.view.bounds.width, height: tableView.h + 10) - } - + + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) @@ -142,35 +135,37 @@ class SettingsViewController: BaseTableViewController { } } - private func versionView() -> UIView { + private func makeVersionAccessory() -> UIView { let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 70)) view.backgroundColor = UIColor.clear - - let v = UIApplication.appVersion() - let string = "v " + v - - let label = UILabel(frame: CGRect(x: 0, y: 35, width: UIScreen.main.bounds.size.width, height: 20)) - label.textAlignment = .center - label.font = BoldFont(size: 14) - label.textColor = UIColor.main.blueyGrey - label.text = string - view.addSubview(label) - + let bHeight = AppModel.sharedManager().walletStatus?.currentHeight ?? "" - let height = UILabel(frame: CGRect(x: 0, y: 5, width: UIScreen.main.bounds.size.width, height: 20)) - height.textAlignment = .center - height.font = RegularFont(size: 14) - height.textColor = UIColor.main.blueyGrey - height.text = Localizable.shared.strings.blockchain_height + ": " + bHeight - view.addSubview(height) - - if Device.isXDevice { - view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 280) - label.frame = CGRect(x: 0, y: view.h - 60, width: UIScreen.main.bounds.size.width, height: 20) - height.frame = CGRect(x: 0, y: view.h - 90, width: UIScreen.main.bounds.size.width, height: 20) + let heightLabel = UILabel() + heightLabel.translatesAutoresizingMaskIntoConstraints = false + heightLabel.textAlignment = .center + heightLabel.font = RegularFont(size: 14) + heightLabel.textColor = UIColor.main.blueyGrey + heightLabel.text = Localizable.shared.strings.blockchain_height + ": " + bHeight + view.addSubview(heightLabel) + + let versionLabel = UILabel() + versionLabel.translatesAutoresizingMaskIntoConstraints = false + versionLabel.textAlignment = .center + versionLabel.font = BoldFont(size: 14) + versionLabel.textColor = UIColor.main.blueyGrey + versionLabel.text = "v" + UIApplication.appVersion() + view.addSubview(versionLabel) + + NSLayoutConstraint.activate([ + versionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor), + versionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor), + versionLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -15), + + heightLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor), + heightLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor), + heightLabel.bottomAnchor.constraint(equalTo: versionLabel.topAnchor, constant: -5), + ]) - } - return view } } @@ -267,9 +262,7 @@ extension SettingsViewController : SettingsCellDelegate { if value == false && item.type == .ask_password { let vc = UnlockPasswordPopover(event: .settings) - vc.completion = { [weak self] - obj in - + vc.completion = { [weak self] obj in if obj == false { item.isSwitch = true @@ -286,9 +279,7 @@ extension SettingsViewController : SettingsCellDelegate { } else if value == true && item.type == .ask_password { let vc = UnlockPasswordPopover(event: .settings) - vc.completion = { [weak self] - obj in - + vc.completion = { [weak self] obj in if obj == false { item.isSwitch = false @@ -305,9 +296,7 @@ extension SettingsViewController : SettingsCellDelegate { } else if value == false && item.type == .enable_bio { let vc = UnlockPasswordPopover(event: .settings, allowBiometric: false) - vc.completion = { - obj in - + vc.completion = { obj in if obj == false { item.isSwitch = true @@ -316,6 +305,7 @@ extension SettingsViewController : SettingsCellDelegate { } else{ Settings.sharedManager().isEnableBiometric = false + KeychainManager.deletePassword() } } vc.modalPresentationStyle = .overFullScreen @@ -336,13 +326,13 @@ extension SettingsViewController : SettingsCellDelegate { if(value) { - Settings.sharedManager().nodeAddress = AppModel.chooseRandomNode(); + Settings.sharedManager().nodeAddress = AppModel.chooseRandomNode() AppModel.sharedManager().changeNodeAddress() viewModel.items[0][1].detail = Settings.sharedManager().nodeAddress } else if Settings.sharedManager().customNode().isEmpty == false { - Settings.sharedManager().nodeAddress = Settings.sharedManager().customNode(); + Settings.sharedManager().nodeAddress = Settings.sharedManager().customNode() AppModel.sharedManager().changeNodeAddress() viewModel.items[0][1].detail = Settings.sharedManager().nodeAddress @@ -353,6 +343,19 @@ extension SettingsViewController : SettingsCellDelegate { else if item.type == .mobile_node { AppModel.sharedManager().enableBodyRequests(value) } + else if item.type == .price_oracle { + Settings.sharedManager().isOracleEnabled = value + if value { + Settings.sharedManager().currency = BMCurrencyType(BMCurrencyUSD) + OraclePriceManager.shared.start() + } + else { + OraclePriceManager.shared.stop() + AppModel.sharedManager().refreshAddresses() + } + viewModel.reload() + tableView.reloadData() + } } } } @@ -362,7 +365,7 @@ extension SettingsViewController : WalletModelDelegate { func onNetwotkStartReconnecting() { DispatchQueue.main.async { if self.type == SettingsViewModel.SettingsType.node { - self.viewModel.items[0][1].detail = Settings.sharedManager().nodeAddress + self.viewModel.reload() self.tableView.reloadData() } } @@ -371,7 +374,7 @@ extension SettingsViewController : WalletModelDelegate { func onWalletStatusChange(_ status: BMWalletStatus) { DispatchQueue.main.async { if self.type == .main { - self.tableView.tableFooterView = self.versionView() + self.bottomAccessoryView = self.makeVersionAccessory() } } } @@ -383,7 +386,7 @@ extension SettingsViewController : SettingsModelDelegate { setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true, menu: self.navigationController?.viewControllers.first == self) title = viewModel.title() if type == .main { - tableView.tableFooterView = versionView() + bottomAccessoryView = makeVersionAccessory() } viewModel.reload() tableView.reloadData() diff --git a/BeamWallet/ViewControllers/Main/Settings/TrustedNodeViewController.swift b/BeamWallet/ViewControllers/Main/Settings/TrustedNodeViewController.swift index 0e4a8f1b..e565413a 100644 --- a/BeamWallet/ViewControllers/Main/Settings/TrustedNodeViewController.swift +++ b/BeamWallet/ViewControllers/Main/Settings/TrustedNodeViewController.swift @@ -2,7 +2,7 @@ // TrustedNodeViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/UTXO/AssetUTXOListViewController.swift b/BeamWallet/ViewControllers/Main/UTXO/AssetUTXOListViewController.swift new file mode 100644 index 00000000..97be48a4 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/UTXO/AssetUTXOListViewController.swift @@ -0,0 +1,187 @@ +// +// AssetUTXOListViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +final class AssetUTXOListViewController: BaseTableViewController { + + private let assetId: Int32 + private var group: AssetUTXOGroup? + + private let emptyView: BMEmptyView = UIView.fromNib() + private let hideUTXOView: BMEmptyView = UIView.fromNib() + + init(assetId: Int32) { + self.assetId = assetId + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError(Localizable.shared.strings.fatalInitCoderError) + } + + override func viewDidLoad() { + tableStyle = .grouped + super.viewDidLoad() + + Settings.sharedManager().addDelegate(self) + + hideUTXOView.text = Localizable.shared.strings.secutiry_utxo + hideUTXOView.image = IconUTXOSecurity() + + emptyView.text = Localizable.shared.strings.utxo_empty + emptyView.image = IconUtxoEmpty() + + emptyView.isHidden = true + emptyView.backgroundColor = view.backgroundColor + view.addSubview(emptyView) + + hideUTXOView.isHidden = !Settings.sharedManager().isHideAmounts + hideUTXOView.backgroundColor = view.backgroundColor + view.addSubview(hideUTXOView) + + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true) + + tableView.register([UTXOCell.self]) + tableView.dataSource = self + tableView.delegate = self + tableView.sectionFooterHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } + } + + deinit { + Settings.sharedManager().removeDelegate(self) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + AppModel.sharedManager().addDelegate(self) + AppModel.sharedManager().getUTXO() + reload() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + AppModel.sharedManager().removeDelegate(self) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + emptyView.frame = tableView.frame + hideUTXOView.frame = tableView.frame + } + + private func reload() { + let match = UTXOViewModel().groupedByAsset().first { $0.assetId == assetId } + group = match + + if let asset = match?.asset ?? AssetsManager.shared().getAsset(assetId) { + title = asset.unitName.uppercased() + } + + emptyView.isHidden = match != nil + tableView.reloadData() + } + + private func presentSheet(mode: SplitCoinsViewModel.Mode) { + guard let group = group else { return } + let vm = SplitCoinsViewModel(group: group, mode: mode) + let vc = SplitCoinsViewController(viewModel: vm) + present(vc, animated: false, completion: nil) + } +} + +extension AssetUTXOListViewController: UITableViewDataSource, UITableViewDelegate { + + func numberOfSections(in tableView: UITableView) -> Int { + return group == nil ? 0 : 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return group?.utxos.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let utxo = group?.utxos[indexPath.row] else { return UITableViewCell() } + let cell = tableView + .dequeueReusableCell(withType: UTXOCell.self, for: indexPath) + .configured(with: (row: indexPath.row, utxo: utxo)) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let utxo = group?.utxos[indexPath.row] else { return } + let vc = UTXODetailViewController(utxo: utxo) + pushViewController(vc: vc) + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let group = group else { return nil } + let header = AssetUTXOSectionHeaderView(group: group) + header.onSplitTapped = { [weak self] in + self?.presentSheet(mode: .split) + } + header.onConsolidateTapped = { [weak self] in + self?.presentSheet(mode: .consolidate) + } + return header + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return group == nil ? CGFloat.leastNormalMagnitude : AssetUTXOSectionHeaderView.preferredHeight + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return nil + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + return CGFloat.leastNormalMagnitude + } +} + +extension AssetUTXOListViewController: SettingsModelDelegate { + func onChangeHideAmounts() { + hideUTXOView.isHidden = !Settings.sharedManager().isHideAmounts + tableView.isUserInteractionEnabled = !Settings.sharedManager().isHideAmounts + tableView.reloadData() + } +} + +extension AssetUTXOListViewController: WalletModelDelegate { + + func onWalletStatusChange(_ status: BMWalletStatus) { + DispatchQueue.main.async { [weak self] in + self?.reload() + } + } + + func onReceivedUTXOs(_ utxos: [BMUTXO]) { + DispatchQueue.main.async { [weak self] in + self?.reload() + } + } +} diff --git a/BeamWallet/ViewControllers/Main/UTXO/Cells/AssetUTXOSectionHeaderView.swift b/BeamWallet/ViewControllers/Main/UTXO/Cells/AssetUTXOSectionHeaderView.swift new file mode 100644 index 00000000..ab543f6c --- /dev/null +++ b/BeamWallet/ViewControllers/Main/UTXO/Cells/AssetUTXOSectionHeaderView.swift @@ -0,0 +1,141 @@ +// +// AssetUTXOSectionHeaderView.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +final class AssetUTXOSectionHeaderView: UIView { + + static let preferredHeight: CGFloat = 116 + private static let iconSize: CGFloat = 36 + + private let card = UIView() + private let iconHost = UIView() + private let iconView: AssetIconView + private let nameLabel = UILabel() + private let subtitleLabel = UILabel() + private let labelStack = UIStackView() + private let splitButton = makePillButton(title: Localizable.shared.strings.split) + private let consolidateButton = makePillButton(title: Localizable.shared.strings.consolidate) + private let buttonRow = UIStackView() + + var onSplitTapped: (() -> Void)? + var onConsolidateTapped: (() -> Void)? + + init(group: AssetUTXOGroup) { + self.iconView = AssetIconView(frame: CGRect(x: 0, y: 0, width: Self.iconSize, height: Self.iconSize)) + super.init(frame: .zero) + setupViews() + configure(with: group) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private static func makePillButton(title: String) -> UIButton { + let btn = UIButton(type: .system) + btn.setTitle(title.uppercased(), for: .normal) + btn.titleLabel?.font = BoldFont(size: 12) + btn.setTitleColor(UIColor.main.brightTeal, for: .normal) + btn.setTitleColor(UIColor.main.brightTeal.withAlphaComponent(0.4), for: .disabled) + btn.layer.borderColor = UIColor.main.brightTeal.cgColor + btn.layer.borderWidth = 1 + btn.layer.cornerRadius = 16 + btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14) + btn.translatesAutoresizingMaskIntoConstraints = false + return btn + } + + private func setupViews() { + backgroundColor = .clear + + card.backgroundColor = UIColor.main.cellBackgroundColor + card.layer.cornerRadius = 12 + card.translatesAutoresizingMaskIntoConstraints = false + addSubview(card) + + iconView.cornerRadius = Self.iconSize / 2 + iconHost.translatesAutoresizingMaskIntoConstraints = false + iconHost.addSubview(iconView) + card.addSubview(iconHost) + + nameLabel.font = BoldFont(size: 20) + nameLabel.textColor = UIColor.white + + subtitleLabel.font = RegularFont(size: 13) + subtitleLabel.textColor = UIColor.main.blueyGrey + + labelStack.axis = .vertical + labelStack.alignment = .leading + labelStack.spacing = 2 + labelStack.translatesAutoresizingMaskIntoConstraints = false + labelStack.addArrangedSubview(nameLabel) + labelStack.addArrangedSubview(subtitleLabel) + card.addSubview(labelStack) + + splitButton.addTarget(self, action: #selector(onSplit), for: .touchUpInside) + consolidateButton.addTarget(self, action: #selector(onConsolidate), for: .touchUpInside) + + buttonRow.axis = .horizontal + buttonRow.distribution = .fillEqually + buttonRow.spacing = 10 + buttonRow.translatesAutoresizingMaskIntoConstraints = false + buttonRow.addArrangedSubview(splitButton) + buttonRow.addArrangedSubview(consolidateButton) + card.addSubview(buttonRow) + + NSLayoutConstraint.activate([ + card.topAnchor.constraint(equalTo: topAnchor, constant: 6), + card.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -6), + card.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + card.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + + iconHost.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14), + iconHost.topAnchor.constraint(equalTo: card.topAnchor, constant: 14), + iconHost.widthAnchor.constraint(equalToConstant: Self.iconSize), + iconHost.heightAnchor.constraint(equalToConstant: Self.iconSize), + + labelStack.leadingAnchor.constraint(equalTo: iconHost.trailingAnchor, constant: 12), + labelStack.centerYAnchor.constraint(equalTo: iconHost.centerYAnchor), + labelStack.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -14), + + buttonRow.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14), + buttonRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14), + buttonRow.topAnchor.constraint(equalTo: iconHost.bottomAnchor, constant: 12), + buttonRow.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14), + buttonRow.heightAnchor.constraint(equalToConstant: 32), + ]) + } + + private func configure(with group: AssetUTXOGroup) { + nameLabel.text = group.asset.unitName + subtitleLabel.text = group.asset.name + iconView.setAsset(group.asset) + splitButton.isEnabled = group.canSplit + consolidateButton.isEnabled = group.utxos.count >= 2 + } + + @objc private func onSplit() { + onSplitTapped?() + } + + @objc private func onConsolidate() { + onConsolidateTapped?() + } +} diff --git a/BeamWallet/ViewControllers/Main/UTXO/Cells/SplitCoinPreviewCard.swift b/BeamWallet/ViewControllers/Main/UTXO/Cells/SplitCoinPreviewCard.swift new file mode 100644 index 00000000..6a166aa8 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/UTXO/Cells/SplitCoinPreviewCard.swift @@ -0,0 +1,100 @@ +// +// SplitCoinPreviewCard.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +final class SplitCoinPreviewCard: UIView { + + static let preferredHeight: CGFloat = 86 + private static let swatchSize: CGFloat = 20 + + private let swatch = UIView() + private let amountLabel = UILabel() + private let unitLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + backgroundColor = UIColor.main.cellBackgroundColor + layer.cornerRadius = 10 + translatesAutoresizingMaskIntoConstraints = false + + swatch.layer.cornerRadius = 5 + swatch.translatesAutoresizingMaskIntoConstraints = false + addSubview(swatch) + + amountLabel.font = BoldFont(size: 13) + amountLabel.textColor = UIColor.white + amountLabel.adjustsFontSizeToFitWidth = true + amountLabel.minimumScaleFactor = 0.7 + amountLabel.textAlignment = .center + amountLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(amountLabel) + + unitLabel.font = RegularFont(size: 11) + unitLabel.textColor = UIColor.main.blueyGrey + unitLabel.textAlignment = .center + unitLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(unitLabel) + + NSLayoutConstraint.activate([ + heightAnchor.constraint(equalToConstant: SplitCoinPreviewCard.preferredHeight), + + swatch.topAnchor.constraint(equalTo: topAnchor, constant: 12), + swatch.centerXAnchor.constraint(equalTo: centerXAnchor), + swatch.widthAnchor.constraint(equalToConstant: SplitCoinPreviewCard.swatchSize), + swatch.heightAnchor.constraint(equalToConstant: SplitCoinPreviewCard.swatchSize), + + amountLabel.topAnchor.constraint(equalTo: swatch.bottomAnchor, constant: 8), + amountLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6), + amountLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6), + + unitLabel.topAnchor.constraint(equalTo: amountLabel.bottomAnchor, constant: 4), + unitLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6), + unitLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6), + ]) + } + + func configure(amountText: String, unitText: String, color: UIColor) { + swatch.isHidden = false + swatch.backgroundColor = color + amountLabel.text = amountText + amountLabel.textColor = UIColor.white + amountLabel.font = BoldFont(size: 13) + unitLabel.text = unitText.uppercased() + unitLabel.isHidden = false + } + + func configureMore(text: String) { + swatch.isHidden = true + swatch.backgroundColor = .clear + amountLabel.text = text + amountLabel.textColor = UIColor.main.blueyGrey + amountLabel.font = BoldFont(size: 13) + unitLabel.text = nil + unitLabel.isHidden = true + } +} diff --git a/BeamWallet/ViewControllers/Main/UTXO/Cells/SplitCoinsStripeView.swift b/BeamWallet/ViewControllers/Main/UTXO/Cells/SplitCoinsStripeView.swift new file mode 100644 index 00000000..21e52b3e --- /dev/null +++ b/BeamWallet/ViewControllers/Main/UTXO/Cells/SplitCoinsStripeView.swift @@ -0,0 +1,73 @@ +// +// SplitCoinsStripeView.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +final class SplitCoinsStripeView: UIView { + + static let palette: [UIColor] = [ + UIColor.main.brightTeal, + UIColor.main.heliotrope, + UIColor.main.green, + UIColor.main.brightSkyBlue, + UIColor.main.blueyGrey, + ] + + private var amounts: [UInt64] = [] + + override init(frame: CGRect) { + super.init(frame: frame) + clipsToBounds = true + layer.cornerRadius = 4 + backgroundColor = UIColor.white.withAlphaComponent(0.06) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setAmounts(_ amounts: [UInt64]) { + self.amounts = amounts + setNeedsLayout() + } + + override func layoutSubviews() { + super.layoutSubviews() + subviews.forEach { $0.removeFromSuperview() } + + let total = amounts.reduce(UInt64(0), +) + guard total > 0 else { return } + + var x: CGFloat = 0 + let h = bounds.height + let w = bounds.width + + for (idx, value) in amounts.enumerated() { + let segmentWidth = w * CGFloat(Double(value) / Double(total)) + let seg = UIView(frame: CGRect(x: x, y: 0, width: segmentWidth, height: h)) + seg.backgroundColor = SplitCoinsStripeView.palette[idx % SplitCoinsStripeView.palette.count] + addSubview(seg) + x += segmentWidth + } + } + + static func color(forIndex index: Int) -> UIColor { + return palette[index % palette.count] + } +} diff --git a/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOCell.swift b/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOCell.swift index 9e68c09d..04f240f0 100644 --- a/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOCell.swift +++ b/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOCell.swift @@ -2,7 +2,7 @@ // UTXOCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,10 +28,6 @@ class UTXOCell: RippleCell { @IBOutlet weak private var typeLabel: UILabel! @IBOutlet weak private var dateLabel: UILabel! @IBOutlet weak private var assetIconView: AssetIconView! - - override func awakeFromNib() { - super.awakeFromNib() - } } extension UTXOCell: Configurable { diff --git a/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXODetailCell.swift b/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXODetailCell.swift index 3a97bc64..a66c2b2a 100644 --- a/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXODetailCell.swift +++ b/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXODetailCell.swift @@ -2,7 +2,7 @@ // UTXODetailCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOTransactionCell.swift b/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOTransactionCell.swift index 79b6171a..f37abe22 100644 --- a/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOTransactionCell.swift +++ b/BeamWallet/ViewControllers/Main/UTXO/Cells/UTXOTransactionCell.swift @@ -2,7 +2,7 @@ // UTXOTransactionCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/UTXO/SplitCoinsViewController.swift b/BeamWallet/ViewControllers/Main/UTXO/SplitCoinsViewController.swift new file mode 100644 index 00000000..0bc62e47 --- /dev/null +++ b/BeamWallet/ViewControllers/Main/UTXO/SplitCoinsViewController.swift @@ -0,0 +1,555 @@ +// +// SplitCoinsViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +final class SplitCoinsViewController: UIViewController { + + private let viewModel: SplitCoinsViewModel + + private let backdrop = UIView() + private let card = UIView() + private let grabHandle = UIView() + private let scrollView = UIScrollView() + private let contentStack = UIStackView() + + private let titleLabel = UILabel() + private let subtitleLabel = UILabel() + private let balanceTitleLabel = UILabel() + private let balanceValueLabel = UILabel() + private let currentCoinsLabel = UILabel() + private let stripeView = SplitCoinsStripeView() + private let summaryLabel = UILabel() + private let tileStack = UIStackView() + private let warningContainer = UIView() + private let warningLabel = UILabel() + private let splitIntoLabel = UILabel() + private let splitIntoStack = UIStackView() + private let previewLabel = UILabel() + private let previewValueLabel = UILabel() + private let feeTitleLabel = UILabel() + private let feeValueLabel = UILabel() + private let previewLeadingLabel = UILabel() + private let offlineContainer = UIView() + private let offlineTitleLabel = UILabel() + private let offlineHintLabel = UILabel() + private let offlineSwitch = UISwitch() + private let ctaButton: BMButton + + private var splitButtons: [UIButton] = [] + + init(viewModel: SplitCoinsViewModel) { + self.viewModel = viewModel + self.ctaButton = BMButton.defaultButton( + frame: CGRect(x: 0, y: 0, width: 100, height: 56), + color: UIColor.main.brightTeal + ) + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .overFullScreen + modalTransitionStyle = .crossDissolve + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear + + setupBackdrop() + setupCard() + setupContent() + + viewModel.onDataChanged = { [weak self] in self?.refresh() } + viewModel.onError = { [weak self] msg in self?.alertWithMessage(msg) } + viewModel.onSubmitted = { [weak self] in + guard let self = self else { return } + let message = self.viewModel.mode == .consolidate + ? Localizable.shared.strings.consolidate_started_message + : Localizable.shared.strings.split_started_message + let presenter = self.presentingViewController + self.dismissAnimated { + presenter?.alert(message: message) + } + } + + viewModel.recalculateFee() + refresh() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + view.layoutIfNeeded() + card.transform = CGAffineTransform(translationX: 0, y: card.bounds.height) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIView.animate(withDuration: 0.28, delay: 0, options: [.curveEaseOut], animations: { + self.backdrop.alpha = 1 + self.card.transform = .identity + }) + } + + private func setupBackdrop() { + backdrop.backgroundColor = UIColor.black.withAlphaComponent(0.55) + backdrop.alpha = 0 + backdrop.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(backdrop) + let tap = UITapGestureRecognizer(target: self, action: #selector(onCancel)) + backdrop.addGestureRecognizer(tap) + + NSLayoutConstraint.activate([ + backdrop.topAnchor.constraint(equalTo: view.topAnchor), + backdrop.bottomAnchor.constraint(equalTo: view.bottomAnchor), + backdrop.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backdrop.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + } + + private func setupCard() { + card.backgroundColor = UIColor.main.peacockBlue + card.layer.cornerRadius = 16 + card.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + card.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(card) + + NSLayoutConstraint.activate([ + card.leadingAnchor.constraint(equalTo: view.leadingAnchor), + card.trailingAnchor.constraint(equalTo: view.trailingAnchor), + card.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12), + card.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + grabHandle.backgroundColor = UIColor.white.withAlphaComponent(0.25) + grabHandle.layer.cornerRadius = 2 + grabHandle.translatesAutoresizingMaskIntoConstraints = false + card.addSubview(grabHandle) + + let pan = UIPanGestureRecognizer(target: self, action: #selector(handleDragDown(_:))) + pan.delegate = self + card.addGestureRecognizer(pan) + + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.alwaysBounceVertical = true + scrollView.showsVerticalScrollIndicator = false + card.addSubview(scrollView) + + contentStack.axis = .vertical + contentStack.spacing = 0 + contentStack.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentStack) + + let ctaTitle = viewModel.mode == .consolidate + ? Localizable.shared.strings.consolidate_coins_cta + : Localizable.shared.strings.split_coins_cta + ctaButton.translatesAutoresizingMaskIntoConstraints = false + ctaButton.setTitle(ctaTitle.uppercased(), for: .normal) + ctaButton.setTitleColor(UIColor.main.marine, for: .normal) + ctaButton.addTarget(self, action: #selector(onSplitTapped), for: .touchUpInside) + card.addSubview(ctaButton) + + NSLayoutConstraint.activate([ + grabHandle.topAnchor.constraint(equalTo: card.topAnchor, constant: 8), + grabHandle.centerXAnchor.constraint(equalTo: card.centerXAnchor), + grabHandle.widthAnchor.constraint(equalToConstant: 36), + grabHandle.heightAnchor.constraint(equalToConstant: 4), + + scrollView.topAnchor.constraint(equalTo: grabHandle.bottomAnchor, constant: 12), + scrollView.leadingAnchor.constraint(equalTo: card.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: card.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: ctaButton.topAnchor, constant: -12), + + contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 20), + contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -20), + contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -40), + + ctaButton.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 20), + ctaButton.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -20), + ctaButton.heightAnchor.constraint(equalToConstant: 56), + ctaButton.bottomAnchor.constraint(equalTo: card.safeAreaLayoutGuide.bottomAnchor, constant: -16), + ]) + } + + private func setupContent() { + titleLabel.font = BoldFont(size: 22) + titleLabel.textColor = UIColor.white + contentStack.addArrangedSubview(titleLabel) + + contentStack.setCustomSpacing(8, after: titleLabel) + subtitleLabel.font = RegularFont(size: 14) + subtitleLabel.textColor = UIColor.main.blueyGrey + subtitleLabel.numberOfLines = 0 + contentStack.addArrangedSubview(subtitleLabel) + contentStack.setCustomSpacing(20, after: subtitleLabel) + + let balanceRow = UIStackView() + balanceRow.axis = .horizontal + balanceRow.alignment = .center + balanceRow.distribution = .equalSpacing + balanceTitleLabel.font = RegularFont(size: 14) + balanceTitleLabel.textColor = UIColor.main.blueyGrey + balanceTitleLabel.text = Localizable.shared.strings.regular_balance + balanceValueLabel.font = BoldFont(size: 16) + balanceValueLabel.textColor = UIColor.white + balanceRow.addArrangedSubview(balanceTitleLabel) + balanceRow.addArrangedSubview(balanceValueLabel) + contentStack.addArrangedSubview(balanceRow) + contentStack.setCustomSpacing(20, after: balanceRow) + + currentCoinsLabel.font = BoldFont(size: 11) + currentCoinsLabel.textColor = UIColor.main.blueyGrey + currentCoinsLabel.letterSpacing = 2 + currentCoinsLabel.text = Localizable.shared.strings.current_coins.uppercased() + contentStack.addArrangedSubview(currentCoinsLabel) + contentStack.setCustomSpacing(12, after: currentCoinsLabel) + + stripeView.translatesAutoresizingMaskIntoConstraints = false + contentStack.addArrangedSubview(stripeView) + stripeView.heightAnchor.constraint(equalToConstant: 12).isActive = true + contentStack.setCustomSpacing(8, after: stripeView) + + summaryLabel.font = RegularFont(size: 13) + summaryLabel.textColor = UIColor.main.blueyGrey + summaryLabel.numberOfLines = 0 + contentStack.addArrangedSubview(summaryLabel) + contentStack.setCustomSpacing(14, after: summaryLabel) + + tileStack.axis = .horizontal + tileStack.alignment = .fill + tileStack.distribution = .fillEqually + tileStack.spacing = 8 + contentStack.addArrangedSubview(tileStack) + contentStack.setCustomSpacing(20, after: tileStack) + + warningContainer.backgroundColor = UIColor.main.heliotrope.withAlphaComponent(0.12) + warningContainer.layer.cornerRadius = 8 + warningContainer.translatesAutoresizingMaskIntoConstraints = false + warningLabel.font = RegularFont(size: 12) + warningLabel.textColor = UIColor.main.heliotrope + warningLabel.numberOfLines = 0 + warningLabel.translatesAutoresizingMaskIntoConstraints = false + warningContainer.addSubview(warningLabel) + NSLayoutConstraint.activate([ + warningLabel.topAnchor.constraint(equalTo: warningContainer.topAnchor, constant: 10), + warningLabel.bottomAnchor.constraint(equalTo: warningContainer.bottomAnchor, constant: -10), + warningLabel.leadingAnchor.constraint(equalTo: warningContainer.leadingAnchor, constant: 12), + warningLabel.trailingAnchor.constraint(equalTo: warningContainer.trailingAnchor, constant: -12), + ]) + contentStack.addArrangedSubview(warningContainer) + contentStack.setCustomSpacing(20, after: warningContainer) + + setupOfflineToggle() + + splitIntoLabel.font = BoldFont(size: 11) + splitIntoLabel.textColor = UIColor.main.blueyGrey + splitIntoLabel.letterSpacing = 2 + splitIntoLabel.text = Localizable.shared.strings.split_into.uppercased() + contentStack.addArrangedSubview(splitIntoLabel) + contentStack.setCustomSpacing(12, after: splitIntoLabel) + + splitIntoStack.axis = .horizontal + splitIntoStack.distribution = .equalSpacing + splitIntoStack.alignment = .center + for count in viewModel.allowedSplitCounts { + let btn = makeSplitCountButton(count: count) + splitIntoStack.addArrangedSubview(btn) + splitButtons.append(btn) + } + contentStack.addArrangedSubview(splitIntoStack) + contentStack.setCustomSpacing(24, after: splitIntoStack) + + previewLabel.font = BoldFont(size: 11) + previewLabel.textColor = UIColor.main.blueyGrey + previewLabel.letterSpacing = 2 + previewLabel.text = Localizable.shared.strings.preview.uppercased() + contentStack.addArrangedSubview(previewLabel) + contentStack.setCustomSpacing(10, after: previewLabel) + + let previewRow = UIStackView() + previewRow.axis = .horizontal + previewRow.alignment = .center + previewRow.distribution = .fill + previewRow.spacing = 8 + previewLeadingLabel.font = RegularFont(size: 14) + previewLeadingLabel.textColor = UIColor.main.blueyGrey + previewLeadingLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + previewLeadingLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + previewValueLabel.font = BoldFont(size: 16) + previewValueLabel.textColor = UIColor.white + previewValueLabel.numberOfLines = 0 + previewValueLabel.textAlignment = .right + previewValueLabel.adjustsFontSizeToFitWidth = true + previewValueLabel.minimumScaleFactor = 0.7 + previewRow.addArrangedSubview(previewLeadingLabel) + previewRow.addArrangedSubview(previewValueLabel) + contentStack.addArrangedSubview(previewRow) + contentStack.setCustomSpacing(10, after: previewRow) + + let feeRow = UIStackView() + feeRow.axis = .horizontal + feeRow.alignment = .center + feeRow.distribution = .fill + feeRow.spacing = 8 + feeTitleLabel.font = RegularFont(size: 13) + feeTitleLabel.textColor = UIColor.main.blueyGrey + feeTitleLabel.text = Localizable.shared.strings.fee.uppercased() + feeTitleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + feeValueLabel.font = RegularFont(size: 13) + feeValueLabel.textColor = UIColor.white + feeValueLabel.textAlignment = .right + feeRow.addArrangedSubview(feeTitleLabel) + feeRow.addArrangedSubview(feeValueLabel) + contentStack.addArrangedSubview(feeRow) + contentStack.setCustomSpacing(20, after: feeRow) + } + + private func setupOfflineToggle() { + offlineContainer.translatesAutoresizingMaskIntoConstraints = false + offlineContainer.backgroundColor = UIColor.white.withAlphaComponent(0.04) + offlineContainer.layer.cornerRadius = 10 + + offlineTitleLabel.font = SemiboldFont(size: 14) + offlineTitleLabel.textColor = UIColor.white + offlineTitleLabel.text = Localizable.shared.strings.consolidate_offline_toggle + offlineTitleLabel.translatesAutoresizingMaskIntoConstraints = false + + offlineHintLabel.font = RegularFont(size: 12) + offlineHintLabel.textColor = UIColor.main.blueyGrey + offlineHintLabel.numberOfLines = 0 + offlineHintLabel.text = Localizable.shared.strings.consolidate_offline_hint + offlineHintLabel.translatesAutoresizingMaskIntoConstraints = false + + offlineSwitch.onTintColor = UIColor.main.brightTeal + offlineSwitch.translatesAutoresizingMaskIntoConstraints = false + offlineSwitch.addTarget(self, action: #selector(onOfflineToggleChanged(_:)), for: .valueChanged) + + offlineContainer.addSubview(offlineTitleLabel) + offlineContainer.addSubview(offlineHintLabel) + offlineContainer.addSubview(offlineSwitch) + + NSLayoutConstraint.activate([ + offlineTitleLabel.topAnchor.constraint(equalTo: offlineContainer.topAnchor, constant: 12), + offlineTitleLabel.leadingAnchor.constraint(equalTo: offlineContainer.leadingAnchor, constant: 14), + offlineTitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: offlineSwitch.leadingAnchor, constant: -8), + + offlineHintLabel.topAnchor.constraint(equalTo: offlineTitleLabel.bottomAnchor, constant: 2), + offlineHintLabel.leadingAnchor.constraint(equalTo: offlineTitleLabel.leadingAnchor), + offlineHintLabel.trailingAnchor.constraint(lessThanOrEqualTo: offlineSwitch.leadingAnchor, constant: -8), + offlineHintLabel.bottomAnchor.constraint(equalTo: offlineContainer.bottomAnchor, constant: -12), + + offlineSwitch.trailingAnchor.constraint(equalTo: offlineContainer.trailingAnchor, constant: -14), + offlineSwitch.centerYAnchor.constraint(equalTo: offlineContainer.centerYAnchor), + ]) + + contentStack.addArrangedSubview(offlineContainer) + contentStack.setCustomSpacing(20, after: offlineContainer) + } + + private func makeSplitCountButton(count: Int) -> UIButton { + let btn = UIButton(type: .system) + btn.tag = count + btn.setTitle("\(count)", for: .normal) + btn.titleLabel?.font = BoldFont(size: 16) + btn.layer.cornerRadius = 22 + btn.layer.borderWidth = 1 + btn.translatesAutoresizingMaskIntoConstraints = false + btn.widthAnchor.constraint(equalToConstant: 56).isActive = true + btn.heightAnchor.constraint(equalToConstant: 44).isActive = true + btn.addTarget(self, action: #selector(onSplitCountTapped(_:)), for: .touchUpInside) + return btn + } + + private func refresh() { + let group = viewModel.group + let asset = group.asset + let isConsolidate = viewModel.mode == .consolidate + + let actionWord = isConsolidate + ? Localizable.shared.strings.consolidate + : Localizable.shared.strings.split + titleLabel.text = "\(actionWord.capitalized) \(asset.unitName.uppercased())" + subtitleLabel.text = isConsolidate + ? Localizable.shared.strings.consolidate_coins_subtitle + : Localizable.shared.strings.split_coins_subtitle + + balanceValueLabel.text = formattedAmount(real: asset.realAmount, unit: asset.unitName) + + stripeView.setAmounts(group.utxos.map { $0.amount }) + + let countText = Localizable.shared.strings.coin_count_format + .replacingOccurrences(of: "(count)", with: "\(group.utxos.count)") + if isConsolidate { + summaryLabel.text = countText + } else { + let largestText = formattedAmount(real: group.largestUtxo?.realAmount ?? 0, unit: asset.unitName) + let largestRow = Localizable.shared.strings.largest_coin_format + .replacingOccurrences(of: "(amount)", with: largestText) + summaryLabel.text = "\(countText) · \(largestRow)" + } + + rebuildTiles(group: group) + + if let warn = viewModel.concentrationWarning { + warningLabel.text = warn + warningContainer.isHidden = false + } else { + warningContainer.isHidden = true + } + + // Hide the split-into selector in consolidate mode or whenever the + // shielded toggle collapses the output to a single push. + let hideSplitInto = isConsolidate || viewModel.isShielded + splitIntoLabel.isHidden = hideSplitInto + splitIntoStack.isHidden = hideSplitInto + offlineContainer.isHidden = false + offlineSwitch.isOn = viewModel.sendOffline + + for btn in splitButtons { + let isSelected = btn.tag == viewModel.splitInto + btn.backgroundColor = isSelected ? UIColor.main.brightTeal : UIColor.clear + btn.layer.borderColor = (isSelected + ? UIColor.main.brightTeal + : UIColor.white.withAlphaComponent(0.18)).cgColor + btn.setTitleColor(isSelected ? UIColor.main.marine : UIColor.white, for: .normal) + } + + let perOutputText = formattedAmount(real: grothToBeam(viewModel.perOutputGroth), unit: asset.unitName) + previewLeadingLabel.text = (isConsolidate || viewModel.isShielded) + ? "" + : "\(viewModel.splitInto)x equal" + previewValueLabel.text = perOutputText + + feeValueLabel.text = formattedAmount(real: grothToBeam(viewModel.feeGroth), unit: "BEAM") + + ctaButton.isEnabled = viewModel.canSubmit + ctaButton.alpha = viewModel.canSubmit ? 1 : 0.5 + } + + private func rebuildTiles(group: AssetUTXOGroup) { + tileStack.arrangedSubviews.forEach { + tileStack.removeArrangedSubview($0) + $0.removeFromSuperview() + } + let visibleCount = min(4, group.utxos.count) + for i in 0.. 4 { + let remainder = group.utxos.count - 4 + let card = SplitCoinPreviewCard() + let label = Localizable.shared.strings.more_coins_format + .replacingOccurrences(of: "(count)", with: "\(remainder)") + card.configureMore(text: label) + tileStack.addArrangedSubview(card) + } + } + + private func grothToBeam(_ groth: UInt64) -> Double { + return Double(groth) / 100_000_000 + } + + private func formattedAmount(real: Double, unit: String) -> String { + return String.currency(value: real, name: unit) + } + + @objc private func onSplitCountTapped(_ sender: UIButton) { + viewModel.splitInto = sender.tag + } + + @objc private func onOfflineToggleChanged(_ sender: UISwitch) { + viewModel.sendOffline = sender.isOn + } + + @objc private func onSplitTapped() { + if Settings.sharedManager().isNeedaskPasswordForSend { + let popover = UnlockPasswordPopover(event: .transaction) + popover.completion = { [weak self] ok in + if ok { self?.viewModel.submit() } + } + popover.modalPresentationStyle = .overFullScreen + popover.modalTransitionStyle = .crossDissolve + present(popover, animated: true, completion: nil) + } else { + viewModel.submit() + } + } + + @objc private func onCancel() { + dismissAnimated(completion: nil) + } + + @objc private func handleDragDown(_ pan: UIPanGestureRecognizer) { + let translation = pan.translation(in: view).y + let velocity = pan.velocity(in: view).y + switch pan.state { + case .changed: + let offset = max(0, translation) + card.transform = CGAffineTransform(translationX: 0, y: offset) + backdrop.alpha = 1 - min(1, offset / max(1, card.bounds.height)) * 0.5 + case .ended, .cancelled: + if translation > card.bounds.height * 0.25 || velocity > 800 { + dismissAnimated(completion: nil) + } else { + UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseOut], animations: { + self.card.transform = .identity + self.backdrop.alpha = 1 + }) + } + default: + break + } + } + + private func dismissAnimated(completion: (() -> Void)?) { + UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseIn], animations: { + self.backdrop.alpha = 0 + self.card.transform = CGAffineTransform(translationX: 0, y: self.card.bounds.height) + }, completion: { _ in + self.dismiss(animated: false, completion: completion) + }) + } + + private func alertWithMessage(_ message: String) { + alert(message: message) + } +} + +extension SplitCoinsViewController: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let pan = gestureRecognizer as? UIPanGestureRecognizer else { return true } + let velocity = pan.velocity(in: view) + return scrollView.contentOffset.y <= 0 && velocity.y > abs(velocity.x) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} diff --git a/BeamWallet/ViewControllers/Main/UTXO/UTXODetailViewController.swift b/BeamWallet/ViewControllers/Main/UTXO/UTXODetailViewController.swift index 9f5d9a70..b932d856 100644 --- a/BeamWallet/ViewControllers/Main/UTXO/UTXODetailViewController.swift +++ b/BeamWallet/ViewControllers/Main/UTXO/UTXODetailViewController.swift @@ -2,7 +2,7 @@ // UTXODetailViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -75,12 +75,12 @@ class UTXODetailViewController: BaseTableViewController { @objc private func onMoreDetails() { viewModel.detailsExpand = !viewModel.detailsExpand - tableView.reloadSections(IndexSet(arrayLiteral: 1), with: .fade) + tableView.reloadSections([1], with: .fade) } - + @objc private func onMoreHistory() { viewModel.historyExpand = !viewModel.historyExpand - tableView.reloadSections(IndexSet(arrayLiteral: 2), with: .fade) + tableView.reloadSections([2], with: .fade) } } diff --git a/BeamWallet/ViewControllers/Main/UTXO/UTXOTableView.swift b/BeamWallet/ViewControllers/Main/UTXO/UTXOTableView.swift index 1db30ff3..30861ce0 100644 --- a/BeamWallet/ViewControllers/Main/UTXO/UTXOTableView.swift +++ b/BeamWallet/ViewControllers/Main/UTXO/UTXOTableView.swift @@ -2,7 +2,7 @@ // UTXOTableView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/UTXO/UTXOViewController.swift b/BeamWallet/ViewControllers/Main/UTXO/UTXOViewController.swift index cc5f13c7..e66abfde 100644 --- a/BeamWallet/ViewControllers/Main/UTXO/UTXOViewController.swift +++ b/BeamWallet/ViewControllers/Main/UTXO/UTXOViewController.swift @@ -2,7 +2,7 @@ // UTXOViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,186 +19,129 @@ import UIKit - class UTXOViewController: BaseTableViewController { - - private let pagingViewController = BMPagingViewController() - private var titles = [Localizable.shared.strings.available, Localizable.shared.strings.in_progress, Localizable.shared.strings.spent, Localizable.shared.strings.unavailable] - + private let viewModel = UTXOViewModel() + private var groups: [AssetUTXOGroup] = [] + private let emptyView: BMEmptyView = UIView.fromNib() private let hideUTXOView: BMEmptyView = UIView.fromNib() - private var _selectedIndex = 0 - public var selectedIndex:Int { - get{ - return _selectedIndex - } - set{ - _selectedIndex = newValue - } - } - - override func viewDidLoad() { + tableStyle = .plain super.viewDidLoad() - + Settings.sharedManager().addDelegate(self) - + hideUTXOView.text = Localizable.shared.strings.secutiry_utxo hideUTXOView.image = IconUTXOSecurity() - + emptyView.text = Localizable.shared.strings.utxo_empty emptyView.image = IconUtxoEmpty() - - let pagingView = pagingViewController.view as! PagingView - pagingView.options.menuItemSpacing = 20 - - addChild(pagingViewController) - view.addSubview(pagingViewController.view) - pagingViewController.didMove(toParent: self) - - pagingViewController.dataSource = self - pagingViewController.delegate = self emptyView.isHidden = true emptyView.backgroundColor = view.backgroundColor view.addSubview(emptyView) - + hideUTXOView.isHidden = !Settings.sharedManager().isHideAmounts hideUTXOView.backgroundColor = view.backgroundColor view.addSubview(hideUTXOView) - + setGradientTopBar(mainColor: UIColor.main.peacockBlue, addedStatusView: true) - title = Localizable.shared.strings.utxo + tableView.register([AssetAvailableCell.self]) + tableView.dataSource = self + tableView.delegate = self + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 120 + rightButton() } - + deinit { Settings.sharedManager().removeDelegate(self) } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - checkIsEmpty() - + AppModel.sharedManager().addDelegate(self) AppModel.sharedManager().getUTXO() + reloadGroups() } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - + AppModel.sharedManager().removeDelegate(self) } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - - let frame = tableView.frame - // frame.origin.y = frame.origin.y + 130 - // frame.size.height = frame.size.height - 130 - - pagingViewController.view.frame = frame - emptyView.frame = frame - hideUTXOView.frame = frame + + emptyView.frame = tableView.frame + hideUTXOView.frame = tableView.frame } - - private func checkIsEmpty() { - var allUtxos = [BMUTXO]() - - if let utxos = AppModel.sharedManager().utxos { - allUtxos.append(contentsOf: utxos as! [BMUTXO]) - } - - if let utxos = AppModel.sharedManager().shildedUtxos { - allUtxos.append(contentsOf: utxos as! [BMUTXO]) - } - - let count = allUtxos.count - if count == 0 { - pagingViewController.view.alpha = 0 - emptyView.isHidden = false - } - else{ - pagingViewController.view.alpha = 1 - emptyView.isHidden = true - } + private func reloadGroups() { + groups = viewModel.groupedByAsset() + emptyView.isHidden = !groups.isEmpty + tableView.reloadData() } - + private func rightButton() { - addRightButton(image: Settings.sharedManager().isHideAmounts ? IconShowBalance() : IconHideBalance(), target: self, selector: #selector(onHideAmounts)) - } - - private func menuOffset(for scrollView: UIScrollView) -> CGFloat { - return min(pagingViewController.options.menuHeight, max(0, scrollView.contentOffset.y)) + addRightButton( + image: Settings.sharedManager().isHideAmounts ? IconShowBalance() : IconHideBalance(), + target: self, + selector: #selector(onHideAmounts) + ) } } +extension UTXOViewController: UITableViewDataSource, UITableViewDelegate { -extension UTXOViewController: PagingViewControllerDataSource { - - func pagingViewController(_ pagingViewController: PagingViewController, viewControllerForIndex index: Int) -> UIViewController { - - let viewController = UTXOTableView() - viewController.view.backgroundColor = UIColor.clear - viewController.selectedIndex = index - - return viewController - } - - func pagingViewController(_ pagingViewController: PagingViewController, pagingItemForIndex index: Int) -> T { - return PagingIndexItem(index: index, title: titles[index].uppercased()) as! T - } - - func numberOfViewControllers(in: PagingViewController) -> Int{ - return titles.count + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return groups.count } -} - -extension UTXOViewController : PagingViewControllerDelegate { - func pagingViewController(_ pagingViewController: PagingViewController, didScrollToItem pagingItem: T, startingViewController: UIViewController?, destinationViewController: UIViewController, transitionSuccessful: Bool) where T : PagingItem, T : Comparable, T : Hashable { - let index = pagingItem as! PagingIndexItem - selectedIndex = index.index + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let group = groups[indexPath.row] + let cell = tableView.dequeueReusableCell(withType: AssetAvailableCell.self, for: indexPath) + cell.setAsset(group.asset) + cell.setUTXOAccessory(count: group.utxos.count) + return cell } - - func pagingViewController( - _ pagingViewController: PagingViewController, - widthForPagingItem pagingItem: T, - isSelected: Bool) -> CGFloat? { - - let index = pagingItem as! PagingIndexItem - let title = index.title - let size = title.boundingWidth(with: pagingViewController.options.font, kern: 1.5) - return size + 20 + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let group = groups[indexPath.row] + let vc = AssetUTXOListViewController(assetId: group.assetId) + pushViewController(vc: vc) } } -extension UTXOViewController : SettingsModelDelegate { +extension UTXOViewController: SettingsModelDelegate { func onChangeHideAmounts() { rightButton() - + hideUTXOView.isHidden = !Settings.sharedManager().isHideAmounts tableView.isUserInteractionEnabled = !Settings.sharedManager().isHideAmounts tableView.reloadData() } } -extension UTXOViewController : WalletModelDelegate { - - func onWalletStatusChange(_ status: BMWalletStatus) { +extension UTXOViewController: WalletModelDelegate { + func onWalletStatusChange(_ status: BMWalletStatus) { + DispatchQueue.main.async { [weak self] in + self?.reloadGroups() + } } - + func onReceivedUTXOs(_ utxos: [BMUTXO]) { - DispatchQueue.main.async { - self.checkIsEmpty() + DispatchQueue.main.async { [weak self] in + self?.reloadGroups() } } } - diff --git a/BeamWallet/ViewControllers/Main/Wallet/Cells/AssetAvailableCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Cells/AssetAvailableCell.swift index 57dbb9c5..16444677 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Cells/AssetAvailableCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Cells/AssetAvailableCell.swift @@ -2,7 +2,7 @@ // AssetAvailableCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -40,13 +40,56 @@ class AssetAvailableCell: RippleCell { UIApplication.getTopMostViewController()?.openUrl(url: url) } } - + + private var utxoAccessory: UIStackView? + private var utxoCountLabel: UILabel? + override func awakeFromNib() { super.awakeFromNib() - + assedIdStackView.isUserInteractionEnabled = true assedIdStackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onAssetIdClicked))) } + + override func prepareForReuse() { + super.prepareForReuse() + utxoAccessory?.isHidden = true + } + + func setUTXOAccessory(count: Int?) { + guard let count = count else { + utxoAccessory?.isHidden = true + return + } + if utxoAccessory == nil { + let label = UILabel() + label.font = RegularFont(size: 12) + label.textColor = UIColor.white.withAlphaComponent(0.7) + + let arrow = UIImageView(image: IconNextArrow()) + arrow.contentMode = .scaleAspectFit + arrow.translatesAutoresizingMaskIntoConstraints = false + arrow.widthAnchor.constraint(equalToConstant: 6).isActive = true + arrow.heightAnchor.constraint(equalToConstant: 10).isActive = true + + let stack = UIStackView(arrangedSubviews: [label, arrow]) + stack.axis = .horizontal + stack.alignment = .center + stack.spacing = 6 + stack.translatesAutoresizingMaskIntoConstraints = false + mainView.addSubview(stack) + NSLayoutConstraint.activate([ + stack.trailingAnchor.constraint(equalTo: mainView.trailingAnchor, constant: -15), + stack.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: -15), + ]) + + utxoAccessory = stack + utxoCountLabel = label + } + utxoAccessory?.isHidden = false + utxoCountLabel?.text = Localizable.shared.strings.coin_count_format + .replacingOccurrences(of: "(count)", with: "\(count)") + } public func setAsset(_ asset:BMAsset) { assetId = Int(asset.assetId) diff --git a/BeamWallet/ViewControllers/Main/Wallet/Cells/OnboardCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Cells/OnboardCell.swift index cf630421..bec60e2d 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Cells/OnboardCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Cells/OnboardCell.swift @@ -2,7 +2,7 @@ // OnboardCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -67,15 +67,15 @@ class OnboardCell: UITableViewCell { } } - @IBAction private func onClose (sender :UIButton) { + @IBAction private func onClose(sender :UIButton) { self.delegate?.onClickCloseFaucet(cell:self) } - @IBAction private func onVerefication (sender :UIButton) { + @IBAction private func onVerefication(sender :UIButton) { self.delegate?.onClickMakeSecure(cell:self) } - @IBAction private func onReceive (sender :UIButton) { + @IBAction private func onReceive(sender :UIButton) { self.delegate?.onClickReceiveFaucet(cell: self) } } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletStatusCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletStatusCell.swift index f6fbf241..ed120055 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletStatusCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletStatusCell.swift @@ -2,7 +2,7 @@ // WalletStatusCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,10 +35,6 @@ class WalletStatusCell: BaseCell { selectionStyle = .none } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - } @IBAction func onReceived(sender :UIButton) { self.delegate?.onClickReceived() diff --git a/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionCell.swift index 8c9f5fd9..9193c70c 100755 --- a/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionCell.swift @@ -2,7 +2,7 @@ // WalletTransactionCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -42,13 +42,6 @@ class WalletTransactionCell: UITableViewCell { selectionStyle = .none } - - override func layoutSubviews() { - super.layoutSubviews() - -// assetIcon2.x = 20 -// assetIcon3.x = 40 - } } extension WalletTransactionCell: Configurable { diff --git a/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionSearchCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionSearchCell.swift index 7085fd61..3399300f 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionSearchCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Cells/WalletTransactionSearchCell.swift @@ -2,7 +2,7 @@ // WalletTransactionCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailCell.swift b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailCell.swift index d8d89467..bfff367b 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailCell.swift @@ -2,7 +2,7 @@ // MaxPrivacyDetailCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailHeader.swift b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailHeader.swift index 2f4fd17f..cee9d1fe 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailHeader.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/Cell/MaxPrivacyDetailHeader.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 18.12.2020. -// Copyright © 2020 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit diff --git a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewController.swift index a60e0bc0..2f620718 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewController.swift @@ -2,7 +2,7 @@ // MaxPrivacyDetailViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -38,8 +38,7 @@ class MaxPrivacyDetailViewController: BaseTableViewController { tableView.delegate = self tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 10)) - viewModel.onDataChanged = { - [weak self] in + viewModel.onDataChanged = { [weak self] in guard let strongSelf = self else { return } diff --git a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewModel.swift b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewModel.swift index 4b79586d..a715cdf1 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewModel.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/MaxPrivacyDetail/MaxPrivacyDetailViewModel.swift @@ -2,7 +2,7 @@ // MaxPrivacyDetailViewModel.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -56,8 +56,7 @@ class MaxPrivacyDetailViewModel: NSObject { func filterUTXOS() { - DispatchQueue.main.async { - [weak self] in + DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/AssetSearchViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/AssetSearchViewController.swift new file mode 100644 index 00000000..3ed8fe6c --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/AssetSearchViewController.swift @@ -0,0 +1,207 @@ +// +// AssetSearchViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +import UIKit + +final class AssetSearchViewController: BaseViewController { + + public var completion: ((BMAsset) -> Void)? + + private let searchView = BMSearchView() + private let tableView = UITableView() + + private var allAssets: [BMAsset] = [] + private var filtered: [BMAsset] = [] + private let initialSelectedId: Int + + init(selectedAssetId: Int) { + self.initialSelectedId = selectedAssetId + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError(Localizable.shared.strings.fatalInitCoderError) + } + + deinit { + AppModel.sharedManager().removeDelegate(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.brightSkyBlue) + title = Localizable.shared.strings.select_asset.uppercased() + addRightButton(title: Localizable.shared.strings.cancel.lowercased(), target: self, selector: #selector(onCancel), enabled: true) + + searchView.searchField.placeholder = Localizable.shared.strings.search_assets + searchView.onSearchTextChanged = { [weak self] text in + self?.applyFilter(text) + } + searchView.onCancelSearch = { [weak self] in + self?.applyFilter("") + } + view.addSubview(searchView) + + tableView.delegate = self + tableView.dataSource = self + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.rowHeight = 56 + tableView.keyboardDismissMode = .onDrag + view.addSubview(tableView) + + AppModel.sharedManager().addDelegate(self) + + AppModel.sharedManager().loadFullAssetsList() + + reloadAssets() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + let top = navigationBarOffset + searchView.frame = CGRect(x: 0, y: top, width: view.bounds.width, height: 46) + let tableY = searchView.frame.maxY + 6 + tableView.frame = CGRect(x: 0, y: tableY, width: view.bounds.width, height: view.bounds.height - tableY) + } + + @objc private func onCancel() { + dismiss(animated: true) + } + + private func reloadAssets() { + let assets = (AssetsManager.shared().assets as? [BMAsset]) ?? [] + let beam = assets.first(where: { $0.isBeam() }) + let others = assets.filter { !$0.isBeam() } + allAssets = (beam.map { [$0] } ?? []) + others + applyFilter(searchView.searchField.text ?? "") + } + + private func applyFilter(_ text: String) { + let q = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + if q.isEmpty { + filtered = allAssets + } else { + filtered = allAssets.filter { asset in + if asset.unitName.lowercased().contains(q) { return true } + if asset.name.lowercased().contains(q) { return true } + if asset.shortName.lowercased().contains(q) { return true } + if String(asset.assetId).contains(q) { return true } + return false + } + } + tableView.reloadData() + } +} + +extension AssetSearchViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return filtered.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let identifier = "AssetSearchCell" + let cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? AssetSearchCell + ?? AssetSearchCell(style: .default, reuseIdentifier: identifier) + let asset = filtered[indexPath.row] + cell.configure(with: asset, isSelected: Int(asset.assetId) == initialSelectedId) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let asset = filtered[indexPath.row] + let completion = self.completion + dismiss(animated: true) { completion?(asset) } + } +} + +extension AssetSearchViewController: WalletModelDelegate { + func onAssetInfoChange() { + DispatchQueue.main.async { [weak self] in self?.reloadAssets() } + } +} + +private final class AssetSearchCell: UITableViewCell { + + private let iconView = AssetIconView() + private let unitLabel = UILabel() + private let idLabel = UILabel() + private let checkmark = UIImageView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + backgroundColor = .clear + contentView.backgroundColor = .clear + selectionStyle = .none + + iconView.frame = CGRect(x: 0, y: 0, width: 26, height: 26) + iconView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(iconView) + + unitLabel.font = BoldFont(size: 16) + unitLabel.textColor = .white + unitLabel.translatesAutoresizingMaskIntoConstraints = false + unitLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + contentView.addSubview(unitLabel) + + idLabel.font = RegularFont(size: 14) + idLabel.textColor = UIColor.white.withAlphaComponent(0.5) + idLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(idLabel) + + checkmark.image = UIImage(named: "iconDoneBlue")?.withRenderingMode(.alwaysTemplate) + checkmark.tintColor = UIColor.main.brightSkyBlue + checkmark.contentMode = .scaleAspectFit + checkmark.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(checkmark) + + NSLayoutConstraint.activate([ + iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconView.widthAnchor.constraint(equalToConstant: 26), + iconView.heightAnchor.constraint(equalToConstant: 26), + + unitLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12), + unitLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + idLabel.leadingAnchor.constraint(equalTo: unitLabel.trailingAnchor, constant: 8), + idLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + idLabel.trailingAnchor.constraint(lessThanOrEqualTo: checkmark.leadingAnchor, constant: -8), + + checkmark.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + checkmark.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + checkmark.widthAnchor.constraint(equalToConstant: 18), + checkmark.heightAnchor.constraint(equalToConstant: 18), + ]) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + contentView.backgroundColor = highlighted ? UIColor.white.withAlphaComponent(0.05) : .clear + } + + func configure(with asset: BMAsset, isSelected: Bool) { + iconView.setAsset(asset) + unitLabel.text = asset.unitName + idLabel.text = "(\(asset.assetId))" + checkmark.isHidden = !isSelected + } +} diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.swift index 7b59b064..71e920f8 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.swift @@ -2,7 +2,7 @@ // ReceiveAddressButtonsCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,19 +24,39 @@ class ReceiveAddressButtonsCell: BaseCell { weak var delegate: BMCellProtocol? + private lazy var copyButton: BMButton = { + let button = BMButton.defaultButton(frame: CGRect(x: 0, y: 0, width: 240, height: 44), color: UIColor.main.brightSkyBlue) + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(IconCopyBlue(), for: .normal) + button.setTitle(Localizable.shared.strings.copy_address_close.lowercased(), for: .normal) + button.setTitleColor(UIColor.main.marineOriginal, for: .normal) + button.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + button.addTarget(self, action: #selector(onCopyAndClose), for: .touchUpInside) + return button + }() + override func awakeFromNib() { super.awakeFromNib() - + allowHighlighted = false selectionStyle = .none - + if Settings.sharedManager().isDarkMode { - infoLabel.textColor = UIColor.main.steel; + infoLabel.textColor = UIColor.main.steel } else { infoLabel.textColor = UIColor.main.blueyGrey } + + contentView.addSubview(copyButton) + NSLayoutConstraint.activate([ + copyButton.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 24), + copyButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + copyButton.widthAnchor.constraint(equalToConstant: 240), + copyButton.heightAnchor.constraint(equalToConstant: 44), + copyButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24), + ]) } @@ -44,9 +64,7 @@ class ReceiveAddressButtonsCell: BaseCell { infoLabel.text = text } - @IBAction func onShare(sender: UIButton) { - delegate?.onClickShare?() + @objc private func onCopyAndClose() { + delegate?.onClickCopyAndClose?() } } - - diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.xib b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.xib index 65c811e2..99d95461 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.xib +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAddressButtonsCell.xib @@ -41,47 +41,14 @@ For the transaction to complete, you should get online during the 12 hours after - - - - - - - - - - - - @@ -93,12 +60,4 @@ For the transaction to complete, you should get online during the 12 hours after - - - - - - - - diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAmountCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAmountCell.swift index 05975c23..c64030f4 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAmountCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveAmountCell.swift @@ -2,7 +2,7 @@ // BMAmountCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTokenCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTokenCell.swift index a9b8427c..880bba22 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTokenCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTokenCell.swift @@ -3,15 +3,15 @@ // BeamWallet // // Created by Denis on 04.11.2020. -// Copyright © 2020 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @objc protocol ReceiveAddressTokensCellDelegate: AnyObject { - @objc optional func onShowToken(token:String) - @objc optional func onShowQR(token:String) - @objc optional func onShareToken(token:String) + @objc optional func onShowToken(token: String) + @objc optional func onShowQR(token: String) + @objc optional func onShareToken(token: String) @objc optional func onCopyToken(token: String) } @@ -19,61 +19,185 @@ class ReceiveTokenCell: BaseCell { weak var delegate: ReceiveAddressTokensCellDelegate? - @IBOutlet private var nameLabel: UILabel! - @IBOutlet private var detailLabel: BMCopyLabel! - @IBOutlet private var detailButton: UIButton! - @IBOutlet private var hintLabel: UILabel! - @IBOutlet private var hintLabelTopOffset: NSLayoutConstraint! - - override func awakeFromNib() { - super.awakeFromNib() - - if Settings.sharedManager().isDarkMode { - hintLabel.textColor = UIColor.main.steel; - } - else { - hintLabel.textColor = UIColor.main.blueyGrey - } - + private let nameLabel = UILabel() + private let detailButton = UIButton(type: .system) + private let detailLabel = BMCopyLabel() + private let copyButton = UIButton(type: .system) + private let qrButton = UIButton(type: .system) + private let shareButton = UIButton(type: .system) + private let sbbsContainer = UIStackView() + private let sbbsSeparator = UIView() + private let sbbsTitleLabel = UILabel() + private let sbbsValueLabel = BMCopyLabel() + private let sbbsCopyButton = UIButton(type: .system) + private let sbbsHintLabel = UILabel() + private let hintLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + backgroundColor = .clear + contentView.backgroundColor = UIColor.main.marineThree selectionStyle = .none allowHighlighted = false - detailButton.setTitle(Localizable.shared.strings.address_details.lowercased(), for: .normal) - + let accent = UIColor.main.brightSkyBlue + let hintColor = Settings.sharedManager().isDarkMode ? UIColor.main.steel : UIColor.main.blueyGrey + nameLabel.text = Localizable.shared.strings.address.uppercased() + nameLabel.textColor = .white + nameLabel.font = BoldFont(size: 14) + nameLabel.numberOfLines = 1 nameLabel.letterSpacing = 2 - + nameLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + detailButton.setTitle(Localizable.shared.strings.address_details.lowercased(), for: .normal) + detailButton.setTitleColor(accent, for: .normal) + detailButton.contentHorizontalAlignment = .right + detailButton.addTarget(self, action: #selector(onShowTokenTap), for: .touchUpInside) + detailButton.setContentHuggingPriority(.required, for: .horizontal) + detailButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + let titleRow = UIStackView(arrangedSubviews: [nameLabel, detailButton]) + titleRow.axis = .horizontal + titleRow.alignment = .center + titleRow.spacing = 8 + + detailLabel.font = RegularFont(size: 16) + detailLabel.textColor = .white + detailLabel.lineBreakMode = .byTruncatingMiddle + detailLabel.adjustsFontSizeToFitWidth = false detailLabel.copiedText = Localizable.shared.strings.address_copied + + configureIconButton(copyButton, image: UIImage(named: "iconCopyWhite24") ?? UIImage(named: "iconCopyWhite"), tint: accent, action: #selector(onCopyTap)) + configureIconButton(qrButton, image: UIImage(named: "iconScanQr"), tint: accent, action: #selector(onQRTap)) + configureIconButton(shareButton, image: UIImage(named: "iconShareNew"), tint: accent, action: #selector(onShareTap)) + + let actionRow = UIStackView(arrangedSubviews: [copyButton, qrButton, shareButton]) + actionRow.axis = .horizontal + actionRow.alignment = .center + actionRow.distribution = .fillEqually + actionRow.spacing = 16 + + sbbsSeparator.backgroundColor = UIColor.white.withAlphaComponent(0.1) + sbbsSeparator.translatesAutoresizingMaskIntoConstraints = false + sbbsSeparator.heightAnchor.constraint(equalToConstant: 1).isActive = true + + sbbsTitleLabel.text = Localizable.shared.strings.sbbs_address.uppercased() + sbbsTitleLabel.textColor = .white + sbbsTitleLabel.font = BoldFont(size: 14) + sbbsTitleLabel.letterSpacing = 2 + + sbbsValueLabel.font = RegularFont(size: 16) + sbbsValueLabel.textColor = .white + sbbsValueLabel.lineBreakMode = .byTruncatingMiddle + sbbsValueLabel.adjustsFontSizeToFitWidth = false + sbbsValueLabel.copiedText = Localizable.shared.strings.address_copied + sbbsValueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + sbbsValueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + configureIconButton(sbbsCopyButton, image: UIImage(named: "iconCopyWhite24") ?? UIImage(named: "iconCopyWhite"), tint: accent, action: #selector(onCopySBBSTap)) + sbbsCopyButton.setContentHuggingPriority(.required, for: .horizontal) + sbbsCopyButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + let sbbsRow = UIStackView(arrangedSubviews: [sbbsValueLabel, sbbsCopyButton]) + sbbsRow.axis = .horizontal + sbbsRow.alignment = .center + sbbsRow.spacing = 8 + + sbbsHintLabel.font = ItalicFont(size: 12) + sbbsHintLabel.textColor = hintColor + sbbsHintLabel.numberOfLines = 0 + sbbsHintLabel.text = Localizable.shared.strings.sbbs_address_hint + + sbbsContainer.axis = .vertical + sbbsContainer.spacing = 8 + sbbsContainer.addArrangedSubview(sbbsSeparator) + sbbsContainer.addArrangedSubview(sbbsTitleLabel) + sbbsContainer.addArrangedSubview(sbbsRow) + sbbsContainer.addArrangedSubview(sbbsHintLabel) + sbbsContainer.isHidden = true + sbbsContainer.setCustomSpacing(14, after: sbbsSeparator) + sbbsContainer.setCustomSpacing(4, after: sbbsRow) + + hintLabel.font = ItalicFont(size: 12) + hintLabel.textColor = hintColor + hintLabel.numberOfLines = 0 + hintLabel.text = Localizable.shared.strings.receive_address_hint + hintLabel.isHidden = true + + let mainStack = UIStackView(arrangedSubviews: [titleRow, detailLabel, actionRow, sbbsContainer, hintLabel]) + mainStack.axis = .vertical + mainStack.spacing = 14 + mainStack.translatesAutoresizingMaskIntoConstraints = false + mainStack.setCustomSpacing(12, after: detailLabel) + + contentView.addSubview(mainStack) + NSLayoutConstraint.activate([ + mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), + mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), + mainStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20), + ]) } - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) + private func configureIconButton(_ button: UIButton, image: UIImage?, tint: UIColor, action: Selector) { + button.setImage(image?.withRenderingMode(.alwaysTemplate), for: .normal) + button.tintColor = tint + button.imageView?.contentMode = .scaleAspectFit + button.addTarget(self, action: action, for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + button.heightAnchor.constraint(equalToConstant: 32).isActive = true } - - @IBAction func onShowToken(sender: UIButton) { - if let token = detailLabel.text { - self.delegate?.onShowToken?(token: token) - } + + @objc private func onShowTokenTap() { + if let token = detailLabel.text { delegate?.onShowToken?(token: token) } } - - @IBAction func onCopyToken(sender: UIButton) { - if let token = detailLabel.text { - self.delegate?.onCopyToken?(token: token) - } + + @objc private func onCopyTap() { + if let token = detailLabel.text { delegate?.onCopyToken?(token: token) } } - - @IBAction func onQRToken(sender: UIButton) { - if let token = detailLabel.text { - self.delegate?.onShowQR?(token: token) - } + + @objc private func onQRTap() { + if let token = detailLabel.text { delegate?.onShowQR?(token: token) } + } + + @objc private func onShareTap() { + if let token = detailLabel.text { delegate?.onShareToken?(token: token) } + } + + @objc private func onCopySBBSTap() { + guard let token = sbbsValueLabel.text, !token.isEmpty else { return } + delegate?.onCopyToken?(token: token) } - - func configure(with value: String, title:String, showHint:Bool) { + + func configure(with value: String, title: String, sbbsAddress: String?, showHint: Bool) { detailLabel.text = value + detailLabel.copyText = value nameLabel.text = title nameLabel.letterSpacing = 2 + + let trimmedSBBS = sbbsAddress?.trimmingCharacters(in: .whitespacesAndNewlines) + if let sbbs = trimmedSBBS, !sbbs.isEmpty, sbbs != value { + sbbsValueLabel.text = sbbs + sbbsValueLabel.copyText = sbbs + sbbsContainer.isHidden = false + } else { + sbbsValueLabel.text = nil + sbbsValueLabel.copyText = nil + sbbsContainer.isHidden = true + } + hintLabel.isHidden = !showHint hintLabel.text = showHint ? Localizable.shared.strings.receive_address_hint : nil - hintLabelTopOffset.constant = showHint ? 10 : 0 } } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTokenCell.xib b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTokenCell.xib deleted file mode 100644 index f4bb2464..00000000 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTokenCell.xib +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - SFProDisplay-Bold - - - SFProDisplay-Regular - - - SFProDisplay-Italic - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTransactionTypeCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTransactionTypeCell.swift index 250d8e6a..a436012e 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTransactionTypeCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/Cells/ReceiveTransactionTypeCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 08.04.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -14,7 +14,7 @@ protocol ReceiveTransactionTypeCellDelegate: AnyObject { class ReceiveTransactionTypeCell: UITableViewCell { - public weak var delegate:ReceiveTransactionTypeCellDelegate? = nil + public weak var delegate:ReceiveTransactionTypeCellDelegate? @IBOutlet private var transactionTypeLabel: UILabel! @IBOutlet private var errorLabel: UILabel! diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/OfflineAddressViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/OfflineAddressViewController.swift index c0b358df..215dca50 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/OfflineAddressViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/OfflineAddressViewController.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 02.11.2020. -// Copyright © 2020 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -28,7 +28,7 @@ class OfflineAddressViewController: BaseViewController { topOffset?.constant = topOffset!.constant - 20 - shqreButton.backgroundColor = UIColor.main.marineThree; + shqreButton.backgroundColor = UIColor.main.marineThree shqreButton.awakeFromNib() addressTitleLabel.text = Localizable.shared.strings.address.uppercased() @@ -37,8 +37,8 @@ class OfflineAddressViewController: BaseViewController { if Settings.sharedManager().isDarkMode { shqreButton.setTitleColor(UIColor.white, for: .normal) - infoLabel.textColor = UIColor.main.steel; - addressTitleLabel.textColor = UIColor.main.steel; + infoLabel.textColor = UIColor.main.steel + addressTitleLabel.textColor = UIColor.main.steel } @@ -56,15 +56,11 @@ class OfflineAddressViewController: BaseViewController { ShowCopied(text: Localizable.shared.strings.address_copied) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - @IBAction private func onShare() { if let image = codeConentView.snapshot() { let activityItem: [AnyObject] = [image] let vc = UIActivityViewController(activityItems: activityItem, applicationActivities: []) - vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in + vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, _: [Any]?, _: Error?) in if completed { self.dismiss(animated: true, completion: { if activityType == UIActivity.ActivityType.copyToPasteboard { diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveListViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveListViewController.swift index 80dd7f8d..66db058f 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveListViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveListViewController.swift @@ -2,7 +2,7 @@ // ReceiveListViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -63,10 +63,8 @@ class ReceiveListViewController: BaseTableViewController { var mainColor = UIColor.main.brightSkyBlue if let viewControllers = self.navigationController?.viewControllers{ - for vc in viewControllers { - if vc is SendViewController { - mainColor = UIColor.main.heliotrope - } + for vc in viewControllers where vc is SendViewController { + mainColor = UIColor.main.heliotrope } } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveViewController.swift index c58d09e2..394abb0f 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Receive/ReceiveViewController.swift @@ -2,7 +2,7 @@ // ReceiveViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -53,13 +53,17 @@ class ReceiveViewController: BaseTableViewController { override func viewDidLoad() { super.viewDidLoad() - + setGradientTopBar(mainColor: UIColor.main.brightSkyBlue) - + title = Localizable.shared.strings.receive.uppercased() + + AppModel.sharedManager().loadFullAssetsList() - tableView.register([BMFieldCell.self, ReceiveTransactionTypeCell.self, ReceiveTokenCell.self, BMExpandCell.self, BMAmountCell.self, ReceiveAddressButtonsCell.self]) + tableView.register([BMFieldCell.self, ReceiveTransactionTypeCell.self, BMExpandCell.self, BMAmountCell.self, ReceiveAddressButtonsCell.self]) + tableView.register(ReceiveTokenCell.self, forCellReuseIdentifier: ReceiveTokenCell.reuseIdentifier) tableView.register(UINib(nibName: "BMPickerCell3", bundle: nil), forCellReuseIdentifier: "BMPickerCell3") + tableView.register(UINib(nibName: "BMPickerCell", bundle: nil), forCellReuseIdentifier: "BMPickerCell") tableView.keyboardDismissMode = .interactive tableView.contentInsetAdjustmentBehavior = .never @@ -84,8 +88,7 @@ class ReceiveViewController: BaseTableViewController { self?.back() } - viewModel.onAddressCreated = {[weak self] - error in + viewModel.onAddressCreated = {[weak self] error in DispatchQueue.main.async { if let reason = error?.localizedDescription { self?.alert(title: Localizable.shared.strings.error, message: reason, handler: { (_ ) in @@ -105,7 +108,7 @@ class ReceiveViewController: BaseTableViewController { if token { let params = AppModel.sharedManager().getTransactionParameters(self?.viewModel.address.address ?? "") if params.newAddressType == BMAddressTypeMaxPrivacy { - self?.viewModel.transaction = .privacy + self?.viewModel.selectedTokenType = .maxPrivacy } } } @@ -118,8 +121,7 @@ class ReceiveViewController: BaseTableViewController { } } - viewModel.onAddressUpdate = {[weak self] - error in + viewModel.onAddressUpdate = {[weak self] _ in UIView.performWithoutAnimation { guard let strongSelf = self else { return } @@ -129,17 +131,8 @@ class ReceiveViewController: BaseTableViewController { amoutnCell.setSecondAmount(amount: strongSelf.viewModel.secondAmount ?? "") } else if let tokenCell = cell as? ReceiveTokenCell { - let isOwn = AppModel.sharedManager().checkIsOwnNode() - - if !isOwn { - tokenCell.configure(with: strongSelf.viewModel.address.walletId, title: Localizable.shared.strings.address.uppercased(), showHint: false) - } - else if strongSelf.viewModel.transaction == .regular { - tokenCell.configure(with: strongSelf.viewModel.address.offlineToken ?? "", title: Localizable.shared.strings.address.uppercased(), showHint: false) - } - else { - tokenCell.configure(with: strongSelf.viewModel.address.maxPrivacyToken ?? "", title: "\(Localizable.shared.strings.address.uppercased()) (\(Localizable.shared.strings.maximum_anonymity.lowercased()))", showHint: false) - } + let token = strongSelf.viewModel.currentToken + tokenCell.configure(with: token, title: strongSelf.tokenCellTitle(), sbbsAddress: strongSelf.tokenCellSbbsAddress(for: token), showHint: false) } } strongSelf.tableView.endUpdates() @@ -203,15 +196,35 @@ class ReceiveViewController: BaseTableViewController { searchTableView.view.frame = CGRect(x: 0, y: y, width: view.bounds.width, height: view.bounds.size.height - y) } + private func sbbsAddress(for token: String) -> String? { + guard !token.isEmpty, AppModel.sharedManager().isToken(token) else { return nil } + let params = AppModel.sharedManager().getTransactionParameters(token) + let sbbs = params.address ?? "" + return sbbs.isEmpty ? nil : sbbs + } + + fileprivate func tokenCellTitle() -> String { + let base = Localizable.shared.strings.address.uppercased() + if viewModel.selectedTokenType == .maxPrivacy { + return "\(base) (\(Localizable.shared.strings.maximum_anonymity.lowercased()))" + } + return base + } + + fileprivate func tokenCellSbbsAddress(for token: String) -> String? { + if !AppModel.sharedManager().checkIsOwnNode() { return nil } + return sbbsAddress(for: token) + } + @objc private func onBack() { let state = viewModel.isNeedAskToSave() if state != .none { - self.confirmAndSkipAlert(title: state == .new ? Localizable.shared.strings.save_address_title : Localizable.shared.strings.save_changes, message: state == .new ? Localizable.shared.strings.save_address_text : Localizable.shared.strings.save_edit_address_text, cancelTitle: Localizable.shared.strings.not_save, confirmTitle: Localizable.shared.strings.save, cancelHandler: { [weak self] (_ ) in + self.confirmAndSkipAlert(title: state == .new ? Localizable.shared.strings.save_address_title : Localizable.shared.strings.save_changes, message: state == .new ? Localizable.shared.strings.save_address_text : Localizable.shared.strings.save_edit_address_text, cancelTitle: Localizable.shared.strings.not_save, confirmTitle: Localizable.shared.strings.save, cancelHandler: { [weak self] _ in self?.back() - }) { [weak self] (_ ) in + }, confirmHandler: { [weak self] _ in self?.viewModel.isShared = true self?.back() - } + }) } else{ back() @@ -237,6 +250,20 @@ extension ReceiveViewController : UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) + + if indexPath.section == 3 && indexPath.row == 1 && AppModel.sharedManager().checkIsOwnNode() { + let picker = BMDataPickerViewController(type: .address_type, selectedValue: viewModel.selectedTokenType.rawValue) + picker.completion = { [weak self] selected in + guard let self = self, + let raw = selected as? Int, + let type = ReceiveAddressViewModel.ReceiveTokenType(rawValue: raw) else { return } + self.view.endEditing(true) + self.viewModel.selectedTokenType = type + self.viewModel.generateTokens() + self.tableView.reloadSections(IndexSet([3, 4]), with: .fade) + } + pushViewController(vc: picker) + } } } @@ -261,7 +288,8 @@ extension ReceiveViewController : UITableViewDataSource { } } else if section == 3 { - return showAdvanced ? 2 : 1 + if !showAdvanced { return 1 } + return viewModel.selectedTokenType.supportsVouchers ? 3 : 2 } return 1 } @@ -273,17 +301,8 @@ extension ReceiveViewController : UITableViewDataSource { .dequeueReusableCell(withType: ReceiveTokenCell.self, for: indexPath) cell.contentView.backgroundColor = UIColor.main.marineThree cell.delegate = self - if viewModel.transaction == .regular { - if !AppModel.sharedManager().checkIsOwnNode() { - cell.configure(with: viewModel.address.walletId, title: Localizable.shared.strings.address.uppercased(), showHint: false) - } - else { - cell.configure(with: viewModel.address.offlineToken ?? "", title: Localizable.shared.strings.address.uppercased(), showHint: false) - } - } - else { - cell.configure(with: viewModel.address.maxPrivacyToken ?? "", title: "\(Localizable.shared.strings.address.uppercased()) (\(Localizable.shared.strings.maximum_anonymity.lowercased()))", showHint: false) - } + let token = viewModel.currentToken + cell.configure(with: token, title: tokenCellTitle(), sbbsAddress: tokenCellSbbsAddress(for: token), showHint: false) return cell } else if indexPath.section == 1 { @@ -302,6 +321,7 @@ extension ReceiveViewController : UITableViewDataSource { .dequeueReusableCell(withType: BMAmountCell.self, for: indexPath).configured(with: (name: String.empty(), value: viewModel.amount)) cell.delegate = self cell.hideNameLabel = true + cell.allowAllAssets = true cell.selectedAssetId = viewModel.selectedAssetId cell.contentView.backgroundColor = UIColor.main.marineThree cell.setSecondAmount(amount: viewModel.secondAmount ?? "") @@ -363,23 +383,46 @@ extension ReceiveViewController : UITableViewDataSource { return cell } else if indexPath.row == 1 { - let detail = !AppModel.sharedManager().checkIsOwnNode() ? Localizable.shared.strings.connect_node_offline : nil - + let isOwn = AppModel.sharedManager().checkIsOwnNode() + let detail: String? = isOwn ? viewModel.selectedTokenType.localizedName : Localizable.shared.strings.connect_node_offline + let cell = tableView - .dequeueReusableCell(withIdentifier: "BMPickerCell3", for: indexPath) as! BMPickerCell - cell.configure(data: BMPickerData(title: Localizable.shared.strings.maximum_anonymity_set, detail: nil, titleColor: UIColor.white, arrowType: viewModel.transaction == .privacy ? BMPickerData.ArrowType.selected : BMPickerData.ArrowType.unselected, unique: 0, multiplie: false, isSwitch: true)) - - if detail != nil { + .dequeueReusableCell(withIdentifier: "BMPickerCell", for: indexPath) as! BMPickerCell + cell.configure(data: BMPickerData(title: Localizable.shared.strings.address_type, detail: detail, titleColor: UIColor.white, arrowType: BMPickerData.ArrowType.selected, unique: 0, multiplie: false, isSwitch: false)) + + if !isOwn { cell.titleLabel.alpha = 0.5 - cell.switchView.alpha = 0.5 cell.isUserInteractionEnabled = false + } else { + cell.titleLabel.alpha = 1 + cell.isUserInteractionEnabled = true } cell.botOffset?.constant = 20 - cell.delegate = self cell.backgroundColor = UIColor.clear cell.mainView.backgroundColor = UIColor.main.marineThree cell.contentView.backgroundColor = UIColor.main.marine + return cell + } + else if indexPath.row == 2 { + let isOwn = AppModel.sharedManager().checkIsOwnNode() + + let cell = tableView + .dequeueReusableCell(withType: BMFieldCell.self, for: indexPath) + .configured(with: (name: Localizable.shared.strings.vouchers_count.uppercased(), value: "\(viewModel.vouchersCount)")) + cell.delegate = self + cell.keyboardType = .numberPad + cell.contentView.backgroundColor = UIColor.main.marineThree + cell.info = String(format: Localizable.shared.strings.vouchers_count_hint, ReceiveAddressViewModel.maxVouchersCount) + + if !isOwn { + cell.alpha = 0.5 + cell.isUserInteractionEnabled = false + } else { + cell.alpha = 1 + cell.isUserInteractionEnabled = true + } + return cell } } @@ -387,27 +430,34 @@ extension ReceiveViewController : UITableViewDataSource { let cell = tableView .dequeueReusableCell(withType: ReceiveAddressButtonsCell.self, for: indexPath) cell.delegate = self - if viewModel.transaction == .privacy { + if viewModel.selectedTokenType == .maxPrivacy { var text = "\n\n" + Localizable.shared.strings.max_privacy_fee let locValue = Settings.sharedManager().currentMaxPrivacyLockValue() - + if locValue.hours == 0 { text = Localizable.shared.strings.transaction_indefinitely + text } else { text = String(format: Localizable.shared.strings.transaction_time, locValue.title) + text } - + cell.setText(text: text) } - else { - if !AppModel.sharedManager().checkIsOwnNode() { + else if viewModel.selectedTokenType.supportsOnlineReceive { + let isOwn = AppModel.sharedManager().checkIsOwnNode() + if viewModel.selectedTokenType == .sbbs || !isOwn { cell.setText(text: Localizable.shared.strings.receive_description_2) } else { cell.setText(text: Localizable.shared.strings.receive_description) } } + else if viewModel.selectedTokenType == .publicOffline { + cell.setText(text: Localizable.shared.strings.public_offline_address_info) + } + else { + cell.setText(text: "") + } return cell } @@ -445,11 +495,11 @@ extension ReceiveViewController : BMCellProtocol { if let path = tableView.indexPath(for: sender) { if path.section == 2 { viewModel.transactionComment = text - + } else if path.section == 1 { viewModel.amount = text - + UIView.performWithoutAnimation { tableView.beginUpdates() for cell in tableView.visibleCells { @@ -457,27 +507,36 @@ extension ReceiveViewController : BMCellProtocol { amoutnCell.setSecondAmount(amount: viewModel.secondAmount ?? "") } else if let tokenCell = cell as? ReceiveTokenCell { - if viewModel.transaction == .regular { - if !AppModel.sharedManager().checkIsOwnNode() { - tokenCell.configure(with: viewModel.address.walletId, title: Localizable.shared.strings.address.uppercased(), showHint: true) - } - else { - tokenCell.configure(with: viewModel.address.offlineToken ?? "", title: Localizable.shared.strings.address.uppercased(), showHint: true) - } - } - else { - tokenCell.configure(with: viewModel.address.maxPrivacyToken ?? "", title: "\(Localizable.shared.strings.address.uppercased()) (\(Localizable.shared.strings.maximum_anonymity.lowercased()))", showHint: false) - } + let token = viewModel.currentToken + tokenCell.configure(with: token, title: tokenCellTitle(), sbbsAddress: tokenCellSbbsAddress(for: token), showHint: viewModel.selectedTokenType != .maxPrivacy) } } tableView.endUpdates() } } + else if path.section == 3 && path.row == 2 { + guard let fieldCell = sender as? BMFieldCell else { return } + let trimmed = text.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty { + fieldCell.error = nil + return + } + if let value = Int(trimmed), (1...ReceiveAddressViewModel.maxVouchersCount).contains(value) { + fieldCell.error = nil + viewModel.vouchersCount = value + } else { + fieldCell.error = String(format: Localizable.shared.strings.vouchers_count_error, ReceiveAddressViewModel.maxVouchersCount) + } + } } } - - func textValueDidReturn(_ sender: UITableViewCell) { + func textValueDidReturn(_ sender: UITableViewCell) { + if let path = tableView.indexPath(for: sender), path.section == 3 && path.row == 2, + let fieldCell = sender as? BMFieldCell { + fieldCell.error = nil + fieldCell.setText("\(viewModel.vouchersCount)") + } } func onExpandCell(_ sender: UITableViewCell) { @@ -504,13 +563,7 @@ extension ReceiveViewController : BMCellProtocol { } else if path.section == 3 { showAdvanced = !showAdvanced - - if showAdvanced { - self.tableView.insertRows(at: [IndexPath(row: 1, section: 3)], with: .fade) - } - else{ - self.tableView.deleteRows(at: [IndexPath(row: 1, section: 3)], with: .fade) - } + self.tableView.reloadSections(IndexSet(integer: 3), with: .fade) } } } @@ -525,33 +578,26 @@ extension ReceiveViewController : BMCellProtocol { func onRightButton(_ sender: UITableViewCell) { - if tableView.indexPath(for: sender) != nil, let cell = sender as? BMAmountCell { - var menu = [BMPopoverMenu.BMPopoverMenuItem]() - - for asset in AssetsManager.shared().getAssetsWithBalanceWithBeam() as! [BMAsset] { - let m = BMPopoverMenu.BMPopoverMenuItem(name: asset.unitName, icon: nil, action: .asset, selected: self.viewModel.selectedAssetId == Int(asset.assetId)) - m.id = Int(asset.assetId) - menu.append(m) - } - - BMPopoverMenu.showForSenderAssets(sender: cell.currencyView, with: menu) { item in - let asset = AssetsManager.shared().getAsset(Int32(item?.id ?? 0)) - self.viewModel.selectedAssetId = Int(asset?.assetId ?? 0) - self.viewModel.amount = nil - self.tableView.reloadData() - - } cancel: { - self.tableView.reloadData() - } + guard tableView.indexPath(for: sender) != nil, sender is BMAmountCell else { return } + view.endEditing(true) + + let picker = AssetSearchViewController(selectedAssetId: viewModel.selectedAssetId ?? 0) + picker.completion = { [weak self] asset in + guard let self = self else { return } + self.viewModel.selectedAssetId = Int(asset.assetId) + self.viewModel.amount = nil + self.tableView.reloadData() } + let nav = BaseNavigationController.navigationController(rootViewController: picker) + nav.modalPresentationStyle = .fullScreen + present(nav, animated: true, completion: nil) } } extension ReceiveViewController : ReceiveAddressTokensCellDelegate { @objc func onSwitchToPool() { - viewModel.transaction = .regular - // viewModel.expire = .parmanent + viewModel.selectedTokenType = .regular viewModel.needReloadButtons = true self.tableView.reloadData() } @@ -576,7 +622,9 @@ extension ReceiveViewController : ReceiveAddressTokensCellDelegate { modalViewController.onShared = { [weak self] in self?.onBack() } - modalViewController.isMaxPrivacy = viewModel.transaction == ReceiveAddressViewModel.TransactionOptions.privacy + modalViewController.isMaxPrivacy = viewModel.selectedTokenType == .maxPrivacy + modalViewController.isSbbsOnly = viewModel.selectedTokenType == .sbbs + modalViewController.isPublicOffline = viewModel.selectedTokenType == .publicOffline modalViewController.modalPresentationStyle = .overFullScreen modalViewController.modalTransitionStyle = .crossDissolve @@ -597,47 +645,34 @@ extension ReceiveViewController : ReceiveAddressTokensCellDelegate { } func onClickShare() { - if viewModel.transaction == .privacy { - onShareToken(token: viewModel.address.maxPrivacyToken ?? "") - } - else { - if !AppModel.sharedManager().checkIsOwnNode() { - onShareToken(token: viewModel.address.walletId) - } - else { - onShareToken(token: viewModel.address.offlineToken ?? "") - } - } + onShareToken(token: viewModel.currentToken) + } + + func onClickCopyAndClose() { + let token = viewModel.currentToken + guard !token.isEmpty else { return } + UIPasteboard.general.string = token + viewModel.isShared = true + ShowCopied(text: Localizable.shared.strings.address_copied) + back() } } -extension ReceiveViewController : ReceiveTransactionTypeCellDelegate, BMPickerCellDelegate { - +extension ReceiveViewController : ReceiveTransactionTypeCellDelegate { + func onDidSelectTrasactionType(type: ReceiveAddressViewModel.TransactionOptions) { - viewModel.transaction = type - + viewModel.selectedTokenType = (type == .privacy) ? .maxPrivacy : .regular + viewModel.generateTokens() + UIView.performWithoutAnimation { self.tableView.reloadRow(ReceiveTokenCell.self, animated: false) self.tableView.reloadRow(ReceiveAddressButtonsCell.self, animated: false) } } - + func onShareToken() { viewModel.isShared = true } - - func onClickSwitch(value: Bool, cell: BMPickerCell) { - if value { - viewModel.transaction = .privacy - } - else { - viewModel.transaction = .regular - } - - UIView.performWithoutAnimation { - self.tableView.reloadData() - } - } } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Receive/b.swift b/BeamWallet/ViewControllers/Main/Wallet/Receive/b.swift deleted file mode 100644 index b7b5f266..00000000 --- a/BeamWallet/ViewControllers/Main/Wallet/Receive/b.swift +++ /dev/null @@ -1,356 +0,0 @@ -// -// ReceiveViewController.swift -// BeamWallet -// -// Copyright 2018 Beam Development -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// 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 - -class ReceiveViewController: BaseTableViewController { - - private var address:BMAddress! - private var amount:String? - private var showAdvanced = false - private var showEdit = false - - init(address:BMAddress) { - super.init(nibName: nil, bundle: nil) - - self.address = address - } - - required init?(coder aDecoder: NSCoder) { - fatalError(LocalizableStrings.fatalInitCoderError) - } - - override func viewDidLoad() { - super.viewDidLoad() - - setGradientTopBar(image: GradientBlue()) - attributedTitle = LocalizableStrings.receive.uppercased() - - tableView.register([ReceiveFieldCell.self, ReceiveAddressButtonsCell.self, ReceiveAmountCell.self, ReceiveExpandCell.self, ReceiveAddressCell.self, ReceiveDetailCell.self]) - - tableView.delegate = self - tableView.dataSource = self - tableView.keyboardDismissMode = .interactive - - self.sideMenuController?.isLeftViewSwipeGestureEnabled = false - } - - override func viewDidLayoutSubviews() { - - tableView.frame = CGRect(x: 0, y: 150, width: self.view.bounds.width, height: self.view.bounds.size.height - 150) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - self.navigationController?.isNavigationBarHidden = true - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if isMovingFromParent { - self.navigationController?.isNavigationBarHidden = false - } - - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification , object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification , object: nil) - } - - private func onExpire() { - let vc = AddressExpiresPickerViewController(duration: -1) - vc.completion = { - obj in - - self.address.duration = obj == 24 ? 86400 : 0 - - AppModel.sharedManager().setExpires(Int32(obj), toAddress: self.address.walletId) - - self.tableView.reloadRows(at: [IndexPath(row: 0, section: 2)], with: .fade) - } - vc.isGradient = true - pushViewController(vc: vc) - } - - private func onCategory() { - if AppModel.sharedManager().categories.count == 0 { - self.alert(title: LocalizableStrings.categories_empty_title, message: LocalizableStrings.categories_empty_text, handler: nil) - } - else{ - let vc = CategoryPickerViewController(category: AppModel.sharedManager().findCategory(byId: self.address.category)) - vc.completion = { - obj in - if let category = obj { - self.didSelectCategory(category: category) - } - } - vc.isGradient = true - pushViewController(vc: vc) - } - } - - private func didSelectCategory(category:BMCategory) { - address.category = String(category.id) - - AppModel.sharedManager().setWalletCategory(self.address.category, toAddress: address.walletId) - - tableView.reloadRows(at: [IndexPath(row: 0, section: 3)], with: .fade) - } -} - -extension ReceiveViewController : UITableViewDelegate { - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - switch section { - case 0: - return 0 - case 2: - return showEdit ? 20 : 0 - case 3: - return showEdit ? 20 : 0 - case 4: - return showEdit ? 20 : 0 - case 6: - return showAdvanced ? 20 : 0 - default: - return 30 - } - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - switch indexPath.section { - case 2: - self.onExpire() - case 3: - self.onCategory() - default: - return - } - } -} - -extension ReceiveViewController : UITableViewDataSource { - - func numberOfSections(in tableView: UITableView) -> Int { - return 8 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 2 || section == 3 || section == 4 { - return showEdit ? 1 : 0 - } - else if section == 6 { - return showAdvanced ? 1 : 0 - } - return 1 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - switch indexPath.section { - case 0: - let cell = tableView - .dequeueReusableCell(withType: ReceiveAddressCell.self, for: indexPath) - .configured(with: (hideLine: true, address: address, title: nil)) - cell.delegate = self - return cell - case 1: - let cell = tableView - .dequeueReusableCell(withType: ReceiveExpandCell.self, for: indexPath) - .configured(with: (expand: showEdit, title: LocalizableStrings.edit_address.uppercased())) - cell.delegate = self - return cell - case 2: - let cell = tableView - .dequeueReusableCell(withType: ReceiveDetailCell.self, for: indexPath) - .configured(with: (title: LocalizableStrings.expires.uppercased(), value: (address.duration > 0 ? LocalizableStrings.hours_24 : LocalizableStrings.never), valueColor: UIColor.white)) - return cell - case 3: - var name = LocalizableStrings.none - var color = UIColor.main.steelGrey - - if let category = AppModel.sharedManager().findCategory(byId: address.category) { - name = category.name - color = UIColor.init(hexString: category.color) - } - - let cell = tableView - .dequeueReusableCell(withType: ReceiveDetailCell.self, for: indexPath) - .configured(with: (title: LocalizableStrings.category.uppercased(), value: name, valueColor: color)) - return cell - case 4: - let cell = tableView - .dequeueReusableCell(withType: ReceiveFieldCell.self, for: indexPath) - .configured(with: (name: LocalizableStrings.local_annotation_not_shared, value: address.label)) - cell.delegate = self - return cell - case 5: - let cell = tableView - .dequeueReusableCell(withType: ReceiveExpandCell.self, for: indexPath) - .configured(with: (expand: showAdvanced, title: LocalizableStrings.advanced.uppercased())) - cell.delegate = self - return cell - case 6: - let cell = tableView - .dequeueReusableCell(withType: ReceiveAmountCell.self, for: indexPath).configured(with: amount) - cell.delegate = self - return cell - case 7: - let cell = tableView - .dequeueReusableCell(withType: ReceiveAddressButtonsCell.self, for: indexPath) - cell.delegate = self - return cell - default: - return BaseCell() - } - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let view = UIView() - view.backgroundColor = section > 0 ? UIColor.clear : UIColor.main.marineTwo.withAlphaComponent(0.2) - return view - } -} - -extension ReceiveViewController : ReceiveCellProtocol { - - func textValueDidBegin(_ sender: UITableViewCell) { - if let path = tableView.indexPath(for: sender) { - tableView.scrollToRow(at: path, at: .middle, animated: true) - } - } - - func textValueDidChange(_ sender: UITableViewCell, _ text: String) { - if sender is ReceiveFieldCell { - address.label = text - } - else if sender is ReceiveAmountCell { - amount = text - } - } - - func textValueDidReturn(_ sender: UITableViewCell) { - if sender is ReceiveFieldCell { - AppModel.sharedManager().setWalletComment(address.label, toAddress: address.walletId) - } - } - - func onExpandRecieveAddress(_ sender: UITableViewCell) { - if let path = tableView.indexPath(for: sender) - { - if path.section == 1 { - showEdit = !showEdit - - if showEdit { - self.tableView.insertRows(at: [IndexPath(row: 0, section: 2), IndexPath(row: 0, section: 3), IndexPath(row: 0, section: 4)], with: .fade) - } - else{ - self.tableView.deleteRows(at: [IndexPath(row: 0, section: 2), IndexPath(row: 0, section: 3), IndexPath(row: 0, section: 4)], with: .fade) - } - } - else{ - showAdvanced = !showAdvanced - - if showAdvanced { - self.tableView.insertRows(at: [IndexPath(row: 0, section: 6)], with: .fade) - } - else{ - self.tableView.deleteRows(at: [IndexPath(row: 0, section: 6)], with: .fade) - } - } - } - } - - func onChangeAddress() { - let vc = ReceiveListViewController() - vc.completion = { - obj in - - if ((self.address.category.isEmpty || self.address.category == LocalizableStrings.zero) && self.address.label.isEmpty) - { - AppModel.sharedManager().deleteAddress(self.address.walletId) - } - - self.address = obj - self.tableView.reloadData() - } - pushViewController(vc: vc) - } - - func onClickQRCode() { - self.view.endEditing(true) - - let modalViewController = ReceiveQRViewController(address: address, amount: amount) - modalViewController.delegate = self - modalViewController.modalPresentationStyle = .overFullScreen - modalViewController.modalTransitionStyle = .crossDissolve - present(modalViewController, animated: true, completion: nil) - } - - func onClickShare() { - self.view.endEditing(true) - - let vc = UIActivityViewController(activityItems: [address.walletId ?? String.empty()], applicationActivities: []) - vc.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in - if completed { - - if activityType == UIActivity.ActivityType.copyToPasteboard { - ShowCopied() - } - - self.navigationController?.popViewController(animated: true) - } - } - vc.excludedActivityTypes = [UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.print,UIActivity.ActivityType.openInIBooks] - present(vc, animated: true) - } -} - -extension ReceiveViewController : ReceiveQRViewControllerDelegate { - func onCopyDone() { - self.navigationController?.popViewController(animated: true) - } -} - -// MARK: Keyboard Handling - -extension ReceiveViewController { - - @objc func keyboardWillShow(_ notification: Notification) { - if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { - let keyboardRectangle = keyboardFrame.cgRectValue - let keyboardHeight = keyboardRectangle.height - - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) - } - } - - @objc func keyboardWillHide(notification: NSNotification) { - tableView.contentInset = UIEdgeInsets.zero - } -} diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/BMSearchAddressCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/BMSearchAddressCell.swift index fc9014de..6067bdda 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/BMSearchAddressCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/BMSearchAddressCell.swift @@ -2,7 +2,7 @@ // BMSearchAddressCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -71,12 +71,12 @@ class BMSearchAddressCell: BaseCell { nameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap(_:)))) if Settings.sharedManager().isDarkMode { - nameLabel.textColor = UIColor.main.steel; - addressTypeLabel.textColor = UIColor.main.steel; - additionalErrorLabel.textColor = UIColor.main.steel; + nameLabel.textColor = UIColor.main.steel + addressTypeLabel.textColor = UIColor.main.steel + additionalErrorLabel.textColor = UIColor.main.steel } - NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: textField, queue: nil) { [weak self] notification in + NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: textField, queue: nil) { [weak self] _ in guard let strongSelf = self else { return } strongSelf.showTokenButton.isHidden = true @@ -227,8 +227,7 @@ extension BMSearchAddressCell: UITextFieldDelegate { if let copy = copyText { let inputBar = BMInputCopyBar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44), copy: copy) - inputBar.completion = { - (obj: String?) -> Void in + inputBar.completion = { (obj: String?) in if let text = obj { self.delegate?.textValueDidChange?(self, text, false) _ = self.textField.resignFirstResponder() diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/FeeCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/FeeCell.swift index 6e5aaf36..c62265f4 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/FeeCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/FeeCell.swift @@ -2,7 +2,7 @@ // FeeCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -81,9 +81,7 @@ class FeeCell: BaseCell { modalViewController.modalPresentationStyle = .overFullScreen modalViewController.modalTransitionStyle = .crossDissolve modalViewController.type = self.type - modalViewController.completion = { - (obj : String) -> Void in - + modalViewController.completion = { (obj : String) in let nFee = Double(obj) ?? 0 if nFee > Double(self.feeSlider.maximumValue) { diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendAllCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendAllCell.swift index 9c56f8ea..07121b90 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendAllCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendAllCell.swift @@ -2,7 +2,7 @@ // SendAllCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ class SendAllCell: BaseCell { allButton.setTitle(Localizable.shared.strings.add_max.lowercased(), for: .normal) if Settings.sharedManager().isDarkMode { - titleLabel.textColor = UIColor.main.steel; + titleLabel.textColor = UIColor.main.steel } } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendContactAddressCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendContactAddressCell.swift index 53e69579..d0a6a502 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendContactAddressCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendContactAddressCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 13.04.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -14,7 +14,7 @@ class SendContactAddressCell: BaseCell { @IBOutlet private weak var addressLabel: UILabel! @IBOutlet private weak var typeLabel: UILabel! - private var contact: BMContact? = nil + private var contact: BMContact? weak var delegate: BMCellProtocol? @@ -25,8 +25,8 @@ class SendContactAddressCell: BaseCell { nameLabel.letterSpacing = 1.5 if Settings.sharedManager().isDarkMode { - addressLabel.textColor = UIColor.main.steel; - typeLabel.textColor = UIColor.main.steel; + addressLabel.textColor = UIColor.main.steel + typeLabel.textColor = UIColor.main.steel } contentView.backgroundColor = UIColor.main.marineThree diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendSaveAddressCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendSaveAddressCell.swift index 94b9b0fc..4bae24d5 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendSaveAddressCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendSaveAddressCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 13.04.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -14,7 +14,7 @@ class SendSaveAddressCell: BaseCell, UITextFieldDelegate { @IBOutlet private weak var typeLabel: UILabel! @IBOutlet private weak var textField: BMField! - private var token: String? = nil + private var token: String? weak var delegate: BMCellProtocol? @@ -30,7 +30,7 @@ class SendSaveAddressCell: BaseCell, UITextFieldDelegate { textField.placeHolderFont = ItalicFont(size: 16) if Settings.sharedManager().isDarkMode { - typeLabel.textColor = UIColor.main.steel; + typeLabel.textColor = UIColor.main.steel } contentView.backgroundColor = UIColor.main.marineThree diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendTransactionTypeCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendTransactionTypeCell.swift index b9e0ef00..3a475a0b 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendTransactionTypeCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/Cells/SendTransactionTypeCell.swift @@ -3,7 +3,7 @@ // BeamWallet // // Created by Denis on 20.04.2021. -// Copyright © 2021 Denis. All rights reserved. +// Copyright © 2026 Denis. All rights reserved. // import UIKit @@ -14,7 +14,7 @@ protocol SendTransactionTypeCellDelegate: AnyObject { class SendTransactionTypeCell: UITableViewCell { - public weak var delegate:SendTransactionTypeCellDelegate? = nil + public weak var delegate:SendTransactionTypeCellDelegate? @IBOutlet private var transactionTypeLabel: UILabel! @@ -36,12 +36,6 @@ class SendTransactionTypeCell: UITableViewCell { } } - override func layoutSubviews() { - super.layoutSubviews() - -// transactionTypeSegment.selectedSegmentIndex = selectedIndex - } - override func awakeFromNib() { super.awakeFromNib() diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/InputFeePopover.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/InputFeePopover.swift index 00477d88..b668135f 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/InputFeePopover.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/InputFeePopover.swift @@ -2,7 +2,7 @@ // InputFeePopover.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/SaveContactViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/SaveContactViewController.swift index 614c9929..5c185e9d 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/SaveContactViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/SaveContactViewController.swift @@ -2,7 +2,7 @@ // SaveContactViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ class SaveContactViewController: BaseTableViewController { if(AppModel.sharedManager().isToken(address!)) { let params = AppModel.sharedManager().getTransactionParameters(address!) - self.address.walletId = address!//params.address + self.address.walletId = address!// params.address self.address.identity = params.identity self.address.address = address } @@ -117,10 +117,6 @@ class SaveContactViewController: BaseTableViewController { } } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - @objc private func onSave() { var walletId = address.walletId var shouldSaveToken = false @@ -147,12 +143,12 @@ class SaveContactViewController: BaseTableViewController { if isContactFound { alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.address_already_exist_1, handler: nil) - return; + return } if isMyAddress { alert(title: Localizable.shared.strings.error, message: Localizable.shared.strings.address_already_exist_2, handler: nil) - return; + return } } @@ -226,7 +222,7 @@ extension SaveContactViewController : UITableViewDataSource { switch indexPath.section { case 0: if isAddContact { - let trim = self.address.walletId.count > 0 ? "\(self.address.walletId.prefix(6))...\(self.address.walletId.suffix(6))" : ""; + let trim = self.address.walletId.count > 0 ? "\(self.address.walletId.prefix(6))...\(self.address.walletId.suffix(6))" : "" let cell = tableView .dequeueReusableCell(withType: BMSearchAddressCell.self, for: indexPath) @@ -241,7 +237,7 @@ extension SaveContactViewController : UITableViewDataSource { return cell } else{ - let trim = "\(self.address.walletId.prefix(6))...\(self.address.walletId.suffix(6))"; + let trim = "\(self.address.walletId.prefix(6))...\(self.address.walletId.suffix(6))" let item = BMMultiLineItem(title: Localizable.shared.strings.address.uppercased(), detail: trim, detailFont: RegularFont(size: 16), detailColor: UIColor.white) let cell = tableView diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/SearchTableView.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/SearchTableView.swift index 05ddf95a..7fd443ad 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/SearchTableView.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/SearchTableView.swift @@ -2,7 +2,7 @@ // UTXOTableView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -38,10 +38,6 @@ class SearchTableView: UITableViewController { tableView.keyboardDismissMode = .interactive } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - public func reload() { tableView.reloadData() diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/SendConfirmViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/SendConfirmViewController.swift index 284fbc06..b3b14563 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/SendConfirmViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/SendConfirmViewController.swift @@ -2,7 +2,7 @@ // SendConfirmViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/SendViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/SendViewController.swift index 7073a805..f8897f95 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/SendViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/SendViewController.swift @@ -2,7 +2,7 @@ // SendViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ class SendViewController: BaseTableViewController { private let nextButton = BMButton.defaultButton(frame: CGRect(x: (UIScreen.main.bounds.size.width - 180) / 2, y: 40, width: 180, height: 44), color: UIColor.main.heliotrope.withAlphaComponent(0.1)) - private func footerView()-> UIView { + private func footerView() -> UIView { let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 0)) infoLabel.frame = CGRect(x: 20, y: 25, width: UIScreen.main.bounds.width-40, height: 0) @@ -55,7 +55,7 @@ class SendViewController: BaseTableViewController { infoLabel.font = ItalicFont(size: 16) infoLabel.textAlignment = .center if Settings.sharedManager().isDarkMode { - infoLabel.textColor = UIColor.main.steel; + infoLabel.textColor = UIColor.main.steel } else { infoLabel.textColor = UIColor.main.blueyGrey @@ -104,7 +104,7 @@ class SendViewController: BaseTableViewController { override func viewDidLoad() { super.viewDidLoad() - viewModel.onTokensCountChanged = {[weak self] obj in + viewModel.onTokensCountChanged = {[weak self] _ in guard let strongSelf = self else { return } if let cell = strongSelf.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? BMSearchAddressCell { cell.setAddressType(BMAddressType(strongSelf.viewModel.addressType), strongSelf.viewModel.isSendOffline, strongSelf.viewModel.tokensLeft) @@ -245,17 +245,6 @@ class SendViewController: BaseTableViewController { super.viewDidAppear(animated) self.isAppear = true - - if viewModel.isNeedFocus { - if let cell = tableView.findCell(BMSearchAddressCell.self) as? BMSearchAddressCell { - cell.beginEditing(text: viewModel.copyAddress) - } - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } override func viewWillDisappear(_ animated: Bool) { @@ -287,7 +276,7 @@ class SendViewController: BaseTableViewController { viewModel.toAddress = value tableView.reloadData() - tableView.reloadSections(IndexSet(arrayLiteral: 0), with: .none) + tableView.reloadSections([0], with: .none) if viewModel.amount.isEmpty { if let cell = tableView.findCell(BMAmountCell.self) as? BMAmountCell { @@ -371,7 +360,7 @@ extension SendViewController: UITableViewDataSource { cell.delegate = self cell.additionalError = viewModel.newVersionError cell.copyText = viewModel.copyAddress - cell.configure(with: (name: Localizable.shared.strings.send_to.uppercased(), value: viewModel.toAddress, rightIcons: [IconScanQr()])) //IconAddressBookSmall(), + cell.configure(with: (name: Localizable.shared.strings.send_to.uppercased(), value: viewModel.toAddress, rightIcons: [IconScanQr()])) // IconAddressBookSmall(), cell.contact = viewModel.selectedContact cell.setAddressType(BMAddressType(viewModel.addressType), viewModel.isSendOffline, viewModel.tokensLeft) cell.nameLabelTopOffset.constant = 20 diff --git a/BeamWallet/ViewControllers/Main/Wallet/Send/WalletSendViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Send/WalletSendViewController.swift index 3acc18c2..e4e44ec8 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Send/WalletSendViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Send/WalletSendViewController.swift @@ -2,7 +2,7 @@ // LegacyWalletSendViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -66,11 +66,11 @@ class LegacyWalletSendViewController: BaseViewController { if (Device.screenType == .iPhone_XR || Device.screenType == .iPhones_X_XS) { - mainStack.spacing = 45; + mainStack.spacing = 45 } else if (Device.screenType == .iPhones_Plus || Device.screenType == .iPhone_XSMax) { - mainStack.spacing = 55; + mainStack.spacing = 55 } mainViewWidth.constant = UIScreen.main.bounds.width @@ -125,10 +125,6 @@ class LegacyWalletSendViewController: BaseViewController { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification , object: nil) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - private func updateLayout() { mainViewHeight.constant = mainStack.frame.height + mainStack.frame.origin.y + 20 @@ -146,28 +142,26 @@ class LegacyWalletSendViewController: BaseViewController { self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: icon, style: .plain, target: self, action: #selector(onHideAmounts)) } -//MARK: - IBAction +// MARK: - IBAction @objc private func onHideAmounts() { if !Settings.sharedManager().isHideAmounts { if Settings.sharedManager().isAskForHideAmounts { - let alert = UIAlertController(title: "Activate security mode", message: "All the balances will be hidden until the eye icon is tapped again", preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler:{ (UIAlertAction)in - })) - - alert.addAction(UIAlertAction(title: "Activate", style: .default, handler:{ (UIAlertAction)in - - Settings.sharedManager().isHideAmounts = !Settings.sharedManager().isHideAmounts - Settings.sharedManager().isAskForHideAmounts = false - - self.balanceTotalView.isHidden = Settings.sharedManager().isHideAmounts - - self.rightButton() - })) - - self.present(alert, animated: true) + confirmAlert( + title: "Activate security mode", + message: "All the balances will be hidden until the eye icon is tapped again", + cancelTitle: "Cancel", + confirmTitle: "Activate", + cancelHandler: { _ in }, + confirmHandler: { [weak self] _ in + guard let self = self else { return } + Settings.sharedManager().isHideAmounts = !Settings.sharedManager().isHideAmounts + Settings.sharedManager().isAskForHideAmounts = false + self.balanceTotalView.isHidden = Settings.sharedManager().isHideAmounts + self.rightButton() + } + ) } else{ Settings.sharedManager().isHideAmounts = !Settings.sharedManager().isHideAmounts @@ -374,8 +368,7 @@ extension LegacyWalletSendViewController : UITextFieldDelegate { if AppModel.sharedManager().isValidAddress(text) { let inputBar = BMInputCopyBar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 44), copy:text) - inputBar.completion = { - (obj : String?) -> Void in + inputBar.completion = { (obj : String?) in if let text = obj { self.toAddressField.text = text self.amountField.becomeFirstResponder() @@ -439,7 +432,7 @@ extension LegacyWalletSendViewController { } } -//MARK: - WalletQRCodeScannerViewControllerDelegate +// MARK: - WalletQRCodeScannerViewControllerDelegate extension LegacyWalletSendViewController : WalletQRCodeScannerViewControllerDelegate { diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionPaymentProofCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionPaymentProofCell.swift index d47714d4..8a43bd5a 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionPaymentProofCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionPaymentProofCell.swift @@ -2,7 +2,7 @@ // GeneralInfoCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -60,10 +60,6 @@ class TransactionPaymentProofCell: BaseCell { titleLabel.textColor = UIColor.main.steel } } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - } @IBAction func onDetails(sender :UIButton) { diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionUTXOCell.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionUTXOCell.swift index 2bc557b2..176b5d6d 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionUTXOCell.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/Cell/TransactionUTXOCell.swift @@ -2,7 +2,7 @@ // TransactionUTXOCell.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/PaymentProofDetailViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/PaymentProofDetailViewController.swift index b0fdd7f6..e907aee0 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/PaymentProofDetailViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/PaymentProofDetailViewController.swift @@ -2,7 +2,7 @@ // PaymentProofDetailViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,19 +35,18 @@ class PaymentProofDetailViewController: BaseTableViewController { @IBOutlet private weak var headerdHeight: NSLayoutConstraint! @IBOutlet private weak var keyKodeTitle: UILabel! - private lazy var footerView: UIView = { - var view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 115)) - - var sendButton = BMButton.defaultButton(frame: CGRect(x: (UIScreen.main.bounds.size.width-143)/2, y: 30, width: 143, height: 44), color: UIColor.main.brightTeal) - sendButton.setImage(IconCopyBlue(), for: .normal) - sendButton.setTitle(Localizable.shared.strings.copy_details.lowercased(), for: .normal) - sendButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) - sendButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) - sendButton.addTarget(self, action: #selector(onCopyCodeDetails), for: .touchUpInside) - view.addSubview(sendButton) - - - return view + private lazy var bottomAccessory: UIView = { + let container = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 70)) + + let copyButton = BMButton.defaultButton(frame: CGRect(x: (UIScreen.main.bounds.size.width - 143) / 2, y: 13, width: 143, height: 44), color: UIColor.main.brightTeal) + copyButton.setImage(IconCopyBlue(), for: .normal) + copyButton.setTitle(Localizable.shared.strings.copy_details.lowercased(), for: .normal) + copyButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) + copyButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + copyButton.addTarget(self, action: #selector(onCopyCodeDetails), for: .touchUpInside) + container.addSubview(copyButton) + + return container }() override var tableStyle: UITableView.Style { @@ -98,10 +97,6 @@ class PaymentProofDetailViewController: BaseTableViewController { } } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - @objc private func onCopyCodeDetails() { if let transactionDetail = transaction?.details() { UIPasteboard.general.string = transactionDetail @@ -123,7 +118,7 @@ class PaymentProofDetailViewController: BaseTableViewController { @objc private func onMoreDetails() { detailsExpand = !detailsExpand - tableView.reloadSections(IndexSet(arrayLiteral: tableView.tableHeaderView == codeInputView ? 0 : 1), with: .fade) + tableView.reloadSections([tableView.tableHeaderView == codeInputView ? 0 : 1], with: .fade) } private func fillTransactionInfo() { @@ -132,19 +127,19 @@ class PaymentProofDetailViewController: BaseTableViewController { if let paymentProof = self.paymentProof { var section_1 = [BMMultiLineItem]() section_1.append(BMMultiLineItem(title: Localizable.shared.strings.key_code.uppercased(), detail: paymentProof.code, detailFont: RegularFont(size: 16), detailColor: UIColor.white, copy: true)) - + details.append(section_1) - - tableView.tableFooterView = footerView + + bottomAccessoryView = bottomAccessory } - + if let info = AppModel.sharedManager().getPaymentProofInfo(self.paymentProof?.code ?? String.empty()){ self.fillInfoFromPaymentInfo(info: info) - tableView.tableFooterView = footerView + bottomAccessoryView = bottomAccessory } else { self.paymentInfo = nil - tableView.tableFooterView = nil + bottomAccessoryView = nil } } @@ -160,8 +155,8 @@ class PaymentProofDetailViewController: BaseTableViewController { section_2.append(BMMultiLineItem(title: Localizable.shared.strings.kernel_id.uppercased(), detail: info.kernelId, detailFont: RegularFont(size: 16), detailColor: UIColor.white, copy: true)) details.append(section_2) - - tableView.tableFooterView = footerView + + bottomAccessoryView = bottomAccessory } } @@ -268,8 +263,7 @@ extension PaymentProofDetailViewController: UITextViewDelegate { if let text = UIPasteboard.general.string { if text.lengthOfBytes(using: .utf8) >= 330 { let inputBar = BMInputCopyBar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 44), copy: text) - inputBar.completion = { - (obj: String?) -> Void in + inputBar.completion = { (obj: String?) in if let text = obj { self.codeInputField.text = text self.textViewDidChange(self.codeInputField) diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionPageViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionPageViewController.swift index 09516e4a..fbe09270 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionPageViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionPageViewController.swift @@ -2,7 +2,7 @@ // TransactionPageViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionShareView.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionShareView.swift index f6098be0..272831ce 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionShareView.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionShareView.swift @@ -2,7 +2,7 @@ // TransactionShareView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -166,7 +166,7 @@ class TransactionShareView: UIView { amountLabel.textColor = UIColor.main.heliotrope } - addressTypeValueLabel.text = transaction.getAddressType(); + addressTypeValueLabel.text = transaction.getAddressType() transactionKernelValueLabel.text = transaction.kernelId if transaction.kernelId.contains("000000") || transaction.isExpired() || transaction.isFailed() { diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionViewController.swift index 516dbc34..b271d827 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionViewController.swift @@ -2,7 +2,7 @@ // TransactionViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -127,10 +127,6 @@ class TransactionViewController: UITableViewController { subscribeToUpdates() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - public func didShow() { } @@ -142,14 +138,12 @@ class TransactionViewController: UITableViewController { } } - viewModel.onDataUpdated = { [weak self] - _, transaction in + viewModel.onDataUpdated = { [weak self] _, transaction in AppModel.sharedManager().cancelTransaction(transaction) self?.back() } - viewModel.onDataDeleted = { [weak self] - _, transaction in + viewModel.onDataDeleted = { [weak self] _, transaction in AppModel.sharedManager().prepareDeleteTransaction(transaction) self?.back() } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsTableView.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsTableView.swift index 7b3dcb72..10e73cc7 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsTableView.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsTableView.swift @@ -2,7 +2,7 @@ // TransactionsTableView.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ class TransactionsTableView: UITableViewController { } -//MARK: - Updates +// MARK: - Updates @objc private func refreshData(_ sender: Any) { AppModel.sharedManager().getWalletStatus() @@ -99,9 +99,7 @@ class TransactionsTableView: UITableViewController { } } - viewModel.onDataDeleted = { [weak self] - indexPath, transaction in - + viewModel.onDataDeleted = { [weak self] indexPath, transaction in guard let strongSelf = self else { return } if strongSelf.viewModel.transactions.count == 0 { @@ -119,9 +117,7 @@ class TransactionsTableView: UITableViewController { } } - viewModel.onDataUpdated = { [weak self] - indexPath, transaction in - + viewModel.onDataUpdated = { [weak self] indexPath, transaction in if let path = indexPath { self?.tableView.performUpdate({ self?.tableView.reloadRows(at: [path], with: .fade) @@ -132,7 +128,7 @@ class TransactionsTableView: UITableViewController { } } -//MARK: - TableView +// MARK: - TableView override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { if viewModel.transactions.count == 0 { @@ -216,7 +212,7 @@ class TransactionsTableView: UITableViewController { } } -//MARK: - Keyboard +// MARK: - Keyboard extension TransactionsTableView { @@ -328,7 +324,7 @@ extension TransactionsTableView: UIContextMenuInteractionDelegate { return UIContextMenuConfiguration(identifier: indexPath as NSCopying, previewProvider: { return detailVC - }, actionProvider: { suggestedActions in + }, actionProvider: { _ in return self.makeContextMenu(transaction: self.viewModel.transactions[indexPath.row]) }) } @@ -342,45 +338,45 @@ extension TransactionsTableView: UIContextMenuInteractionDelegate { let viewModel = DetailTransactionViewModel(transaction: transaction) if transaction.canSaveContact() { - let action1 = UIAction(title: Localizable.shared.strings.save_contact_title, image: nil) { action in + let action1 = UIAction(title: Localizable.shared.strings.save_contact_title, image: nil) { _ in viewModel.saveContact() } array.append(action1) } - let action1 = UIAction(title: Localizable.shared.strings.share_details, image: nil) { action in + let action1 = UIAction(title: Localizable.shared.strings.share_details, image: nil) { _ in viewModel.share() } array.append(action1) - let action2 = UIAction(title: Localizable.shared.strings.copy_details, image: nil) { action in + let action2 = UIAction(title: Localizable.shared.strings.copy_details, image: nil) { _ in viewModel.copyDetails() } array.append(action2) if !transaction.isIncome && !transaction.isDapps { - let action3 = UIAction(title: Localizable.shared.strings.repeat_transaction, image: nil) { action in + let action3 = UIAction(title: Localizable.shared.strings.repeat_transaction, image: nil) { _ in viewModel.repeatTransation(transaction: viewModel.transaction!) } array.append(action3) } if transaction.canCancel && !transaction.isDapps { - let action4 = UIAction(title: Localizable.shared.strings.cancel_transaction, image: nil) { action in + let action4 = UIAction(title: Localizable.shared.strings.cancel_transaction, image: nil) { _ in viewModel.cancelTransation(indexPath: nil) } array.append(action4) } if transaction.canDelete { - let action5 = UIAction(title: Localizable.shared.strings.delete_transaction, image: nil) { action in + let action5 = UIAction(title: Localizable.shared.strings.delete_transaction, image: nil) { _ in viewModel.deleteTransationNew(indexPath: nil) } array.append(action5) } if transaction.isDapps { - let action6 = UIAction(title: Localizable.shared.strings.open_dapp, image: nil) { action in + let action6 = UIAction(title: Localizable.shared.strings.open_dapp, image: nil) { _ in viewModel.openDapp() } array.append(action6) diff --git a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsViewController.swift index c229b2e0..cf171ca2 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/Transaction/TransactionsViewController.swift @@ -2,7 +2,7 @@ // TransactionsViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -69,9 +69,8 @@ class TransactionsViewController: BaseTableViewController { addRightButton(image: MoreIcon(), target: self, selector: #selector(onMore)) searchView = BMSearchView() - searchView.onSearchTextChanged = { - [weak self] text in - + searchView.onSearchTextChanged = { [weak self] text in + guard let strongSelf = self else { return } for model in strongSelf.viewModels { @@ -80,8 +79,7 @@ class TransactionsViewController: BaseTableViewController { model.search() } } - searchView.onCancelSearch = { - [weak self] in + searchView.onCancelSearch = { [weak self] in guard let strongSelf = self else { return } for model in strongSelf.viewModels { @@ -129,7 +127,7 @@ class TransactionsViewController: BaseTableViewController { } @objc private func onExporToCSV() { - AppModel.sharedManager().exportTransactions { (data, url) in + AppModel.sharedManager().exportTransactions { (_, url) in DispatchQueue.main.async { let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil) vc.excludedActivityTypes = [UIActivity.ActivityType.postToFacebook, UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.copyToPasteboard, UIActivity.ActivityType.print,UIActivity.ActivityType.openInIBooks] diff --git a/BeamWallet/ViewControllers/Main/Wallet/WalletViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/WalletViewController.swift index ce259921..5c61c162 100644 --- a/BeamWallet/ViewControllers/Main/Wallet/WalletViewController.swift +++ b/BeamWallet/ViewControllers/Main/Wallet/WalletViewController.swift @@ -2,7 +2,7 @@ // WalletViewController.swift // BeamWallet // -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -103,9 +103,7 @@ class WalletViewController: BaseTableViewController { } } - transactionViewModel.onDataDeleted = { [weak self] - indexPath, transaction in - + transactionViewModel.onDataDeleted = { [weak self] indexPath, transaction in guard let strongSelf = self else { return } if strongSelf.transactionViewModel.transactions.count == 0 || strongSelf.transactionViewModel.transactions.count >= strongSelf.maximumTransactionsCount { @@ -123,9 +121,7 @@ class WalletViewController: BaseTableViewController { } } - transactionViewModel.onDataUpdated = { [weak self] - indexPath, transaction in - + transactionViewModel.onDataUpdated = { [weak self] indexPath, transaction in if let path = indexPath { self?.tableView.performUpdate({ self?.tableView.reloadRows(at: [path], with: .fade) @@ -258,7 +254,7 @@ extension WalletViewController: UITableViewDelegate { return UIContextMenuConfiguration(identifier: indexPath as NSCopying, previewProvider: { return detailVC - }, actionProvider: { suggestedActions in + }, actionProvider: { _ in return self.makeContextMenu(transaction: self.transactionViewModel.transactions[indexPath.row]) }) } @@ -272,45 +268,45 @@ extension WalletViewController: UITableViewDelegate { let viewModel = DetailTransactionViewModel(transaction: transaction) if transaction.canSaveContact() { - let action1 = UIAction(title: Localizable.shared.strings.save_contact_title, image: nil) { action in + let action1 = UIAction(title: Localizable.shared.strings.save_contact_title, image: nil) { _ in viewModel.saveContact() } array.append(action1) } - let action1 = UIAction(title: Localizable.shared.strings.share_details, image: nil) { action in + let action1 = UIAction(title: Localizable.shared.strings.share_details, image: nil) { _ in viewModel.share() } array.append(action1) - let action2 = UIAction(title: Localizable.shared.strings.copy_details, image: nil) { action in + let action2 = UIAction(title: Localizable.shared.strings.copy_details, image: nil) { _ in viewModel.copyDetails() } array.append(action2) if !transaction.isIncome && !transaction.isDapps { - let action3 = UIAction(title: Localizable.shared.strings.repeat_transaction, image: nil) { action in + let action3 = UIAction(title: Localizable.shared.strings.repeat_transaction, image: nil) { _ in viewModel.repeatTransation(transaction: viewModel.transaction!) } array.append(action3) } if transaction.canCancel && !transaction.isDapps { - let action4 = UIAction(title: Localizable.shared.strings.cancel_transaction, image: nil) { action in + let action4 = UIAction(title: Localizable.shared.strings.cancel_transaction, image: nil) { _ in viewModel.cancelTransation(indexPath: nil) } array.append(action4) } if transaction.canDelete { - let action5 = UIAction(title: Localizable.shared.strings.delete_transaction, image: nil) { action in + let action5 = UIAction(title: Localizable.shared.strings.delete_transaction, image: nil) { _ in viewModel.deleteTransationNew(indexPath: nil) } array.append(action5) } if transaction.isDapps { - let action6 = UIAction(title: Localizable.shared.strings.open_dapp, image: nil) { action in + let action6 = UIAction(title: Localizable.shared.strings.open_dapp, image: nil) { _ in viewModel.openDapp() } array.append(action6) @@ -494,10 +490,10 @@ extension WalletViewController: OnboardCellDelegate { } else { strongSelf.confirmAlert(title: Localizable.shared.strings.external_link_title, message: Localizable.shared.strings.external_link_text, cancelTitle: Localizable.shared.strings.cancel, confirmTitle: Localizable.shared.strings.open, cancelHandler: { _ in - - }) { _ in + + }, confirmHandler: { _ in BMOverlayTimerView.show(text: Localizable.shared.strings.faucet_redirect_text, link: result) - } + }) } } } diff --git a/BeamWallet/ViewControllers/Main/Wallet/Withdraw/WithdrawViewController.swift b/BeamWallet/ViewControllers/Main/Wallet/Withdraw/WithdrawViewController.swift new file mode 100644 index 00000000..156f320e --- /dev/null +++ b/BeamWallet/ViewControllers/Main/Wallet/Withdraw/WithdrawViewController.swift @@ -0,0 +1,112 @@ +// +// WithdrawViewController.swift +// BeamWallet +// +// Copyright 2026 Beam Development +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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 + +class WithdrawViewController: BaseViewController { + + private var viewModel: WithdrawViewModel + + private let amountLabel = UILabel() + private let infoLabel = UILabel() + private let confirmButton = BMButton.defaultButton(frame: .zero, color: UIColor.main.heliotrope) + private let cancelButton = BMButton.defaultButton(frame: .zero, color: UIColor.clear) + + init(amount: String, userId: String) { + self.viewModel = WithdrawViewModel(amount: amount, userId: userId) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError(Localizable.shared.strings.fatalInitCoderError) + } + + override func viewDidLoad() { + super.viewDidLoad() + + setGradientTopBar(mainColor: UIColor.main.heliotrope) + title = Localizable.shared.strings.withdraw.uppercased() + + setupLayout() + } + + private func setupLayout() { + amountLabel.font = UIFont.boldSystemFont(ofSize: 36) + amountLabel.textColor = UIColor.white + amountLabel.textAlignment = .center + amountLabel.text = String.currency(value: viewModel.amountDouble) + " BEAM" + amountLabel.translatesAutoresizingMaskIntoConstraints = false + + infoLabel.font = UIFont.systemFont(ofSize: 14) + infoLabel.textColor = UIColor.main.blueyGrey + infoLabel.textAlignment = .center + infoLabel.numberOfLines = 0 + infoLabel.text = Localizable.shared.strings.withdraw_cofirm + infoLabel.translatesAutoresizingMaskIntoConstraints = false + + confirmButton.setTitle(Localizable.shared.strings.confirm.lowercased(), for: .normal) + confirmButton.setTitleColor(UIColor.main.marineOriginal, for: .normal) + confirmButton.setTitleColor(UIColor.main.marineOriginal.withAlphaComponent(0.5), for: .highlighted) + confirmButton.translatesAutoresizingMaskIntoConstraints = false + confirmButton.addTarget(self, action: #selector(onConfirm), for: .touchUpInside) + + cancelButton.setTitle(Localizable.shared.strings.cancel.lowercased(), for: .normal) + cancelButton.setTitleColor(UIColor.white, for: .normal) + cancelButton.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted) + cancelButton.layer.borderColor = UIColor.white.withAlphaComponent(0.3).cgColor + cancelButton.layer.borderWidth = 1 + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.addTarget(self, action: #selector(onCancel), for: .touchUpInside) + + view.addSubview(amountLabel) + view.addSubview(infoLabel) + view.addSubview(confirmButton) + view.addSubview(cancelButton) + + NSLayoutConstraint.activate([ + amountLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + amountLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100), + amountLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + amountLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + + infoLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + infoLabel.topAnchor.constraint(equalTo: amountLabel.bottomAnchor, constant: 30), + infoLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30), + infoLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30), + + confirmButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + confirmButton.bottomAnchor.constraint(equalTo: cancelButton.topAnchor, constant: -15), + confirmButton.widthAnchor.constraint(equalToConstant: 180), + confirmButton.heightAnchor.constraint(equalToConstant: 44), + + cancelButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + cancelButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40), + cancelButton.widthAnchor.constraint(equalToConstant: 180), + cancelButton.heightAnchor.constraint(equalToConstant: 44), + ]) + } + + @objc private func onConfirm() { + navigationController?.popViewController(animated: true) + } + + @objc private func onCancel() { + navigationController?.popViewController(animated: true) + } +} diff --git a/BeamWalletNotificationView/NotificationViewController.swift b/BeamWalletNotificationView/NotificationViewController.swift index 6b1d5ca6..a3d3424c 100644 --- a/BeamWalletNotificationView/NotificationViewController.swift +++ b/BeamWalletNotificationView/NotificationViewController.swift @@ -1,7 +1,7 @@ // // NotificationViewController.swift // BeamWalletNotificationView -// Copyright 2018 Beam Development +// Copyright 2026 Beam Development // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Podfile.lock b/Podfile.lock index 16c0ff78..b82da3cf 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -123,4 +123,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 8d3d860214cabe1cbfdea48a4ebe5f4a1584a521 -COCOAPODS: 1.11.2 +COCOAPODS: 1.16.2 diff --git a/Resources/Assets.xcassets/iconDaoVote.imageset/Contents.json b/Resources/Assets.xcassets/iconAssetSwap.imageset/Contents.json similarity index 57% rename from Resources/Assets.xcassets/iconDaoVote.imageset/Contents.json rename to Resources/Assets.xcassets/iconAssetSwap.imageset/Contents.json index 3f95f556..edc00515 100644 --- a/Resources/Assets.xcassets/iconDaoVote.imageset/Contents.json +++ b/Resources/Assets.xcassets/iconAssetSwap.imageset/Contents.json @@ -2,21 +2,22 @@ "images" : [ { "idiom" : "universal", + "filename" : "iconAssetSwap.png", "scale" : "1x" }, { - "filename" : "iconDaoVote@2x.png", "idiom" : "universal", + "filename" : "iconAssetSwap@2x.png", "scale" : "2x" }, { - "filename" : "iconDaoVote@3x.png", "idiom" : "universal", + "filename" : "iconAssetSwap@3x.png", "scale" : "3x" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } } diff --git a/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap.png b/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap.png new file mode 100644 index 00000000..e8766205 Binary files /dev/null and b/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap.png differ diff --git a/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap@2x.png b/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap@2x.png new file mode 100644 index 00000000..34cce943 Binary files /dev/null and b/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap@2x.png differ diff --git a/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap@3x.png b/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap@3x.png new file mode 100644 index 00000000..29262d2b Binary files /dev/null and b/Resources/Assets.xcassets/iconAssetSwap.imageset/iconAssetSwap@3x.png differ diff --git a/Resources/Assets.xcassets/iconDaoVote.imageset/iconDaoVote@2x.png b/Resources/Assets.xcassets/iconDaoVote.imageset/iconDaoVote@2x.png deleted file mode 100644 index e2205cad..00000000 Binary files a/Resources/Assets.xcassets/iconDaoVote.imageset/iconDaoVote@2x.png and /dev/null differ diff --git a/Resources/Assets.xcassets/iconDaoVote.imageset/iconDaoVote@3x.png b/Resources/Assets.xcassets/iconDaoVote.imageset/iconDaoVote@3x.png deleted file mode 100644 index 2896b9bb..00000000 Binary files a/Resources/Assets.xcassets/iconDaoVote.imageset/iconDaoVote@3x.png and /dev/null differ diff --git a/Resources/Assets.xcassets/iconMessenger.imageset/Contents.json b/Resources/Assets.xcassets/iconMessenger.imageset/Contents.json new file mode 100644 index 00000000..845c0141 --- /dev/null +++ b/Resources/Assets.xcassets/iconMessenger.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "iconMessenger.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "iconMessenger@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "iconMessenger@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger.png b/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger.png new file mode 100644 index 00000000..9f51fd66 Binary files /dev/null and b/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger.png differ diff --git a/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger@2x.png b/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger@2x.png new file mode 100644 index 00000000..5671cf43 Binary files /dev/null and b/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger@2x.png differ diff --git a/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger@3x.png b/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger@3x.png new file mode 100644 index 00000000..d8bbf5ae Binary files /dev/null and b/Resources/Assets.xcassets/iconMessenger.imageset/iconMessenger@3x.png differ diff --git a/Resources/cs.lproj/Localizable.strings b/Resources/cs.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/cs.lproj/Localizable.strings +++ b/Resources/cs.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/dao-core-app.dapp b/Resources/dao-core-app.dapp new file mode 100644 index 00000000..08975109 Binary files /dev/null and b/Resources/dao-core-app.dapp differ diff --git a/Resources/dao-voting-app.dapp b/Resources/dao-voting-app.dapp new file mode 100644 index 00000000..6f563ed8 Binary files /dev/null and b/Resources/dao-voting-app.dapp differ diff --git a/Resources/dapps_store_app.wasm b/Resources/dapps_store_app.wasm new file mode 100644 index 00000000..3899342b Binary files /dev/null and b/Resources/dapps_store_app.wasm differ diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index 52e230be..37be8d69 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -335,6 +338,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -408,6 +419,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -461,12 +473,10 @@ and never kept on the blockchain."; "shielded_pool" = "Shielded pool"; "shielded" = "Shielded"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "connect_node_offline" = "Connect to own node or enable mobile node in the settings to enable receiving max privacy transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; "online_address" = "Online address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Maximum anonymity"; "public_offline" = "Public offline"; @@ -563,7 +573,11 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with the blockchain: "; +"syncing_with_blockchain" = "Syncing with the blockchain"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -572,7 +586,6 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -606,18 +619,50 @@ and never kept on the blockchain."; "transaction_indefinitely" = "Transaction can last indefinitely."; "transaction_time" = "Transaction can last at most %@."; "advanced" = "Advanced"; -"receive_description" = "Sender will be given a choice between regular and offline payment.\n\nFor online payment to complete, you should get online during 12 hours after coins are sent."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "max_privacy_fee" = "Min transaction fee is 0.01 BEAM."; "offline_left_address" = "Offline address: %d transactions left."; "offline_left_address_warning" = "Offline address: %d transactions left. Ask receiver to come online to support more offline transactions."; "online_new_status" = "online: random node supports online transactions only"; "online_own_not_conifg" = "online: configure your own node with owner key"; "remote_node_hint" = "To support maximum anonymity set and offline\ntransactions please connect to mobile node\nor own node configured with your owner key."; -"receive_description_2" = "For online payment to complete, you should get online during 12 hours after coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "only_online_support" = "You are currently connected to the node\nthat supports only online transactions."; "max_funds_error" = "Insufficient funds to complete the transaction. Maximum amount is %@."; "max_funds_hint" = "Maximum amount is %@"; "dAppStore" = "My DApp Browser"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; + "fee" = "Fee"; "confirm_accept" = "Confirm"; "will_take_funds" = "%@ will take the funds from your wallet"; @@ -625,7 +670,6 @@ and never kept on the blockchain."; "maximum_anonymity" = "Maximum anonymity"; "dapp_anme" = "DAPP name"; "app_shader_id" = "Application shader id"; -"beamx_dao" = "BeamX DAO"; "new_password" = "New password"; "very_weak_password" = "Very weak password"; "weak_password" = "Weak password"; @@ -697,5 +741,79 @@ and never kept on the blockchain."; "switch_to_mobile_from_own" = "Are you sure to switch from own node %@ to mobile node?"; "sw" = "Switch"; "search_settings" = "Search by section name or parameter"; -"beamx_dao_dao_voting" = "BeamX DAO Voting"; -"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals)"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; diff --git a/Resources/es.lproj/Localizable.strings b/Resources/es.lproj/Localizable.strings index 8fb2f4b7..19be1ad9 100644 --- a/Resources/es.lproj/Localizable.strings +++ b/Resources/es.lproj/Localizable.strings @@ -47,6 +47,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -330,6 +333,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -403,6 +414,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -454,10 +466,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -553,7 +563,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -562,7 +572,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "desc_paper" = "Description paper"; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/fi-FI.lproj/Localizable.strings b/Resources/fi-FI.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/fi-FI.lproj/Localizable.strings +++ b/Resources/fi-FI.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/fr.lproj/Localizable.strings b/Resources/fr.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/fr.lproj/Localizable.strings +++ b/Resources/fr.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/ko.lproj/Localizable.strings b/Resources/ko.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/ko.lproj/Localizable.strings +++ b/Resources/ko.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/nl.lproj/Localizable.strings b/Resources/nl.lproj/Localizable.strings index 8fb2f4b7..19be1ad9 100644 --- a/Resources/nl.lproj/Localizable.strings +++ b/Resources/nl.lproj/Localizable.strings @@ -47,6 +47,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -330,6 +333,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -403,6 +414,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -454,10 +466,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -553,7 +563,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -562,7 +572,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "desc_paper" = "Description paper"; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/oracle2-app.wasm b/Resources/oracle2-app.wasm new file mode 100644 index 00000000..5aca2951 Binary files /dev/null and b/Resources/oracle2-app.wasm differ diff --git a/Resources/ru.lproj/Localizable.strings b/Resources/ru.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/ru.lproj/Localizable.strings +++ b/Resources/ru.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/sv-SE.lproj/Localizable.strings b/Resources/sv-SE.lproj/Localizable.strings index efe111b4..d4d41d75 100644 --- a/Resources/sv-SE.lproj/Localizable.strings +++ b/Resources/sv-SE.lproj/Localizable.strings @@ -51,6 +51,9 @@ "no_internet" = "Internetåtkomst saknas"; "wallet_not_created" = "Plånbok ej skapad"; "wallet_not_opened" = "Plånbok kan ej öppnas"; +"wallet_recovery_title" = "Återställning av plånbok behövs"; +"wallet_recovery_message" = "En tidigare installation slutfördes inte. Återställ för att börja om."; +"reset_and_retry" = "Återställ och försök igen"; "restor_wallet_warning" = "Att återställa plånboken kan ta ett tag.\n\nVänta tills noden är helt synkroniserad med blockkedjan, stäng inte eller minimera plånboksappen."; "restor_wallet_info" = "Endast plånbokens balans (UTXO) kan återställas, transaktionsinformation är alltid privata och lagras aldrig i block-kedjan."; "receive" = "Ta emot"; @@ -422,6 +425,14 @@ "NSCameraUsageDescription" = "Kameran behövs för att scanna QR-koder"; "search_transactions" = "Sök med transaktions eller kernel-ID, kommentar, adress eller kontakt"; "random_node" = "Anslut till slumpmässig nod"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Återställ plånbok eller skapa en ny"; "restore_create_text" = "Om du återställer en plånbok så kommer all transaktionshistorik och adresser att förloras."; "proceed" = "Fortsätt"; @@ -508,6 +519,7 @@ "faucet_redirect_text" = "Omdirigering till faucet-hemsidan.\nDin adress kommer att fyllas i automatiskt."; "clear_wallet_transactions_text" = "Det går inte att ta bort plånboken med aktiva transaktioner"; "show_amounts_in" = "Visa belopp i"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Sekundär valuta"; "notifications" = "Notifieringar"; "news" = "Nyheter"; @@ -593,11 +605,9 @@ "shielded_pool" = "Skyddad pool"; "maturity" = "Mognad"; "connect_node_offline" = "Anslut till integrerad eller egen nod för att aktivera mottagande av maximal sekretess och offline-transaktioner"; -"connect_node_offline_public" = "Anslut till integrerad eller egen nod för att aktivera mottagande av offentliga offlinetransaktioner"; "shielded" = "Skärmade"; "public_offline_address_info" = "Genom att publicera denna adress kan du identifieras"; "offline_address" = "Offline adress"; -"show_public_offline" = "Visa publik offline-adress"; "public" = "offentlig"; "max_privacy" = "Max sekretess"; "public_offline" = "Publik offline"; @@ -625,3 +635,115 @@ "h60" = "60 timmar"; "h72" = "72 timmar"; "last_unlock_time" = "Latest unlocked time"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/sv.lproj/Localizable.strings b/Resources/sv.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/sv.lproj/Localizable.strings +++ b/Resources/sv.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/th.lproj/Localizable.strings b/Resources/th.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/th.lproj/Localizable.strings +++ b/Resources/th.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/tr.lproj/Localizable.strings b/Resources/tr.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/tr.lproj/Localizable.strings +++ b/Resources/tr.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/vi.lproj/Localizable.strings b/Resources/vi.lproj/Localizable.strings index 0b24faed..37034d0a 100644 --- a/Resources/vi.lproj/Localizable.strings +++ b/Resources/vi.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -582,3 +593,113 @@ and never kept on the blockchain."; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp"; diff --git a/Resources/zh-Hans.lproj/Localizable.strings b/Resources/zh-Hans.lproj/Localizable.strings index cfcb5daf..bfdd23e9 100644 --- a/Resources/zh-Hans.lproj/Localizable.strings +++ b/Resources/zh-Hans.lproj/Localizable.strings @@ -46,6 +46,9 @@ "no_internet" = "No internet connection"; "wallet_not_created" = "Wallet not created"; "wallet_not_opened" = "Wallet can not be opened"; +"wallet_recovery_title" = "Wallet recovery needed"; +"wallet_recovery_message" = "A previous setup did not complete. Reset to start fresh."; +"reset_and_retry" = "Reset and retry"; "restor_wallet_warning" = "Restoring the wallet may take a while.\n\nPlease wait till the node is fully synchronized with the blockchain, do not close or minimize the wallet app."; "restor_wallet_info" = "Only the wallet balance (UTXO) can be restored, transaction info is always private and never kept on the blockchain."; @@ -329,6 +332,14 @@ and never kept on the blockchain."; "NSCameraUsageDescription" = "The camera is needed to scan QR codes"; "search_transactions" = "Search by transaction or kernel ID, comment, address or contact"; "random_node" = "Connect to random node"; +"node_peers" = "Node peers"; +"node_type" = "Node type"; +"node_peer_active" = "Active node"; +"node_peer_pool" = "Random node pool"; +"node_peer_connected" = "Connected"; +"node_peer_connecting" = "Connecting…"; +"node_peer_disconnected" = "Disconnected"; +"node_peer_last_seen" = "Last seen %@"; "restore_create_title" = "Restore wallet or create new one"; "restore_create_text" = "If you'll restore a wallet all transaction history and addresses will be lost."; "proceed" = "Proceed"; @@ -402,6 +413,7 @@ and never kept on the blockchain."; "faucet_redirect_text" = "Redirecting to the faucet website.\nYour address will be filled-in automatically."; "clear_wallet_transactions_text" = "Unable to delete wallet with active transactions"; "show_amounts_in" = "Show amounts in"; +"use_on_chain_price_oracle" = "Use on-chain price oracle"; "second_currency" = "Secondary currency"; "notifications" = "Notifications"; "news" = "News"; @@ -453,10 +465,8 @@ and never kept on the blockchain."; "sbbs_address" = "SBBS Address"; "shielded_pool" = "Shielded pool"; "maturity" = "Maturity"; -"connect_node_offline_public" = "Connect to own node or enable mobile node in the settings to enable receiving public offline transactions"; "public_offline_address_info" = "Publishing this address will allow you to be identified"; "offline_address" = "Offline address"; -"show_public_offline" = "Show public offline address"; "public" = "public"; "max_privacy" = "Max Privacy"; "public_offline" = "Public offline"; @@ -552,7 +562,7 @@ and never kept on the blockchain."; "fast_secure_advance" = "Fast and secure, advanced"; "own_node_text" = "For those who run a node configured with the wallet’s Owner Key."; "connect_to_mobilenode" = "Connecting to Mobile node"; -"syncing_with_blockchain" = "Syncing with blockchain"; +"syncing_with_blockchain" = "Syncing with the blockchain"; "please_no_lock" = "Please do not lock your phone and do not close the wallet."; "wallet_connected_to_mobile_node" = "Wallet connected to Mobile node"; "disconnect" = "Disconnect"; @@ -561,7 +571,8 @@ and never kept on the blockchain."; "senf_offline_notice" = "Min transaction fee to send offline transaction\nis 0.01 BEAM."; "regular_online_only" = "Regular online-only address"; "regular_offline_only" = "Regular offline-only address"; -"sender_choice" = "Sender will be given a choice between\nregular and offline payment."; +"receive_description" = "Sender will be given a choice between online and offline payment.\n\nFor online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; +"receive_description_2" = "For online payment to complete, you will need to open and connect your wallet sometime within 12 hours after the coins are sent."; "assets" = "Assets"; "usage_recent_old" = "Usage: recent to oldest"; "usage_old_recent" = "Usage: oldest to recent"; @@ -581,3 +592,113 @@ and never kept on the blockchain."; "desc_paper" = "Description paper"; "website" = "Website"; "beam_conf_asset" = "Beam Confidential Asset"; +"sbbs_address_new" = "SBBS ADDRESS\n(use for CEX withdrawals and mining)"; +"sbbs_address_hint" = "use for CEX withdrawals and mining"; +"select_asset" = "Select asset"; +"search_assets" = "Search assets"; + +"messenger" = "Messenger"; +"messenger_new_chat" = "New chat"; +"messenger_no_chats" = "No chats yet. Tap + to start a new conversation."; +"messenger_send_placeholder" = "Type your message…"; +"messenger_delete_chat" = "Delete this chat?"; +"messenger_my_address" = "My address"; +"messenger_peer_address" = "Recipient address"; +"messenger_contact_name" = "Contact name (optional)"; +"messenger_start" = "Start chat"; +"sign_message" = "Sign message"; +"verify_message" = "Verify message"; + +"asset_swaps" = "Asset Swaps"; +"asset_swap_new" = "New swap"; +"asset_swap_open_orders" = "Open orders"; +"asset_swap_my_orders" = "My orders"; +"asset_swap_history" = "History"; +"asset_swap_send" = "You send"; +"asset_swap_receive" = "You receive"; +"asset_swap_send_amount" = "Send amount"; +"asset_swap_receive_amount" = "Receive amount"; +"asset_swap_expiration" = "Offer expiration"; +"asset_swap_publish" = "Publish offer"; +"asset_swap_accept" = "Accept offer"; +"asset_swap_cancel_order" = "Cancel offer"; +"asset_swap_no_orders" = "No swap offers yet"; +"asset_swap_expired" = "Expired"; +"asset_swap_completed" = "Completed"; +"asset_swap_canceled" = "Canceled"; +"asset_swap_details" = "Swap details"; +"asset_swap_rate" = "Rate"; +"asset_swap_status" = "Status"; +"asset_swap_pick_send_asset" = "Pick the asset you want to send"; +"asset_swap_pick_receive_asset" = "Pick the asset you want to receive"; +"asset_swap_insufficient_funds" = "Insufficient funds for this swap"; +"asset_swap_same_asset_error" = "Send and receive assets must differ"; +"asset_swap_confirm_cancel" = "Cancel this offer?"; +"asset_swap_created" = "Created"; +"asset_swap_expires_in" = "Expires in"; +"asset_swap_peer_id" = "Peer ID"; +"asset_swap_order_id" = "Order ID"; +"asset_swap_show_details" = "Show technical details"; +"asset_swap_hide_details" = "Hide technical details"; + + +// MARK: Split Coins +"split" = "split"; +"split_coins_subtitle" = "Create smaller coins to prevent transaction failures"; +"split_coins_cta" = "split"; +"regular_balance" = "Regular Balance"; +"current_coins" = "Current Coins"; +"coin_count_format" = "(count) coins"; +"largest_coin_format" = "Largest (amount)"; +"more_coins_format" = "+(count) more"; +"split_into" = "Split Into"; +"preview" = "Preview"; +"nx_equal_format" = "(count)x equal · (amount)"; +"split_concentration_warning_format" = "(percent)% in one coin — split into (count) for safety"; +"split_too_small" = "Coin too small to split"; +"split_started_message" = "Your split transaction has been started"; +"consolidate" = "Consolidate"; +"consolidate_coins_subtitle" = "Combine all UTXOs of this asset into one"; +"consolidate_coins_cta" = "Consolidate now"; +"consolidate_started_message" = "Your consolidate transaction has been started"; +"consolidate_offline_toggle" = "Send to shielded pool"; +"consolidate_offline_hint" = "Increases Lelantus pool size and breaks linkability"; + +"vouchers_count" = "Vouchers count"; +"vouchers_count_hint" = "Up to %d vouchers per address"; +"vouchers_count_error" = "Enter a value between 1 and %d"; +"sync_phase_connecting" = "Connecting to node…"; +"sync_phase_reconnecting" = "Reconnecting to node…"; +"sync_phase_almost_done" = "Almost done…"; +"sync_phase_finalizing" = "Finalizing…"; + +"myDApps" = "My DApps"; +"dapps_empty_title" = "No DApps installed"; +"dapps_empty_subtitle" = "Browse the DApp Store to install BEAM-powered apps."; +"dapps_empty_button" = "Browse DApp Store"; +"dapps_get_more" = "Get more DApps"; +"dapps_uninstall" = "Remove"; +"dapps_remove_confirm" = "Remove %@?"; +"dapps_install_from_file" = "Install from file…"; +"dapps_sideload_desc" = "Pick a .dapp file from Files or iCloud Drive"; +"dapps_sideload_invalid" = "That file is not a valid DApp package."; +"dapps_install_label" = "Install"; +"dapps_installed_label" = "Installed"; +"dapps_update" = "Update"; +"dapps_installed_toast" = "%@ installed"; +"dapps_updated_toast" = "%@ updated to v%@"; +"dapps_install_complete" = "Installation complete"; +"dapps_install_failed_msg" = "Installation failed: %@"; +"dapps_no_download_source" = "No download source available for this DApp."; +"dapps_source_bundled" = "Bundled"; +"dapps_source_ipfs" = "IPFS"; +"dapps_store_title" = "DApp Store"; +"dapps_store_subtitle" = "Discover DApps published on the BEAM chain."; +"dapps_store_loading" = "Loading DApps…"; +"dapps_store_no_dapps" = "No DApps to show right now."; +"dapps_store_empty_hint" = "Once publishers register, their DApps will appear here."; +"dapps_publishers" = "Publishers"; +"dapps_publishers_subtitle" = "Turn off a publisher to hide their DApps."; +"dapps_no_publishers" = "No publishers found."; +"dapps_back_button" = "Back"; +"dapps_sideloaded_name" = "Sideloaded DApp";