diff --git a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj index f63f9411b..6c18b73dd 100644 --- a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj +++ b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj @@ -81,7 +81,6 @@ DC2F431427B6972C0006FCC4 /* SwapInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2F431327B6972C0006FCC4 /* SwapInView.swift */; }; DC2F431627B6983B0006FCC4 /* CopyShareOptionsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2F431527B6983B0006FCC4 /* CopyShareOptionsSheet.swift */; }; DC2F431A27B699800006FCC4 /* ModifyInvoiceSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2F431927B699800006FCC4 /* ModifyInvoiceSheet.swift */; }; - DC32FB3529A3D3FE009912AC /* XpcManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC32FB3429A3D3FE009912AC /* XpcManager.swift */; }; DC33369826BAF721000E3F49 /* ShortSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC33369726BAF721000E3F49 /* ShortSheet.swift */; }; DC3345D02C2B4C1200EDD2D4 /* ManageContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3345CF2C2B4C1200EDD2D4 /* ManageContact.swift */; }; DC3345D22C2C761800EDD2D4 /* CameraPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3345D12C2C761800EDD2D4 /* CameraPicker.swift */; }; @@ -233,8 +232,8 @@ DCA4DA372C66A0960010363C /* CurrencySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA4DA362C66A0960010363C /* CurrencySelector.swift */; }; DCA5391A29F1DDE7001BD3D5 /* SegmentedPicker in Frameworks */ = {isa = PBXBuildFile; productRef = DCA5391929F1DDE7001BD3D5 /* SegmentedPicker */; }; DCA5391C29F7202F001BD3D5 /* ChannelInfoPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA5391B29F7202F001BD3D5 /* ChannelInfoPopup.swift */; }; - DCA6DEC62829BDEB0073C658 /* CrossProcessCommunication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA6DEC52829BDEB0073C658 /* CrossProcessCommunication.swift */; }; - DCA6DEC72829BFD70073C658 /* CrossProcessCommunication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA6DEC52829BDEB0073C658 /* CrossProcessCommunication.swift */; }; + DCA6DEC62829BDEB0073C658 /* XPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA6DEC52829BDEB0073C658 /* XPC.swift */; }; + DCA6DEC72829BFD70073C658 /* XPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA6DEC52829BDEB0073C658 /* XPC.swift */; }; DCA6DEC82829C3150073C658 /* GenericPasswordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCACF6F62566D0BA0009B01E /* GenericPasswordStore.swift */; }; DCA6DEC92829C3180073C658 /* GenericPasswordConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCACF6F92566D0BA0009B01E /* GenericPasswordConvertible.swift */; }; DCA6DECA2829C31B0073C658 /* KeyStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCACF6F52566D0BA0009B01E /* KeyStoreError.swift */; }; @@ -281,6 +280,8 @@ DCBA60CD2C909C7600878895 /* SendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBA60CC2C909C7600878895 /* SendView.swift */; }; DCBA60CF2C90E41000878895 /* ScanQrCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBA60CE2C90E41000878895 /* ScanQrCodeView.swift */; }; DCBA60D42C93544C00878895 /* InsetGroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBA60D32C93544C00878895 /* InsetGroupBoxStyle.swift */; }; + DCC3E57F2D08A63900CCDA40 /* XPC+Foreground.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC3E57E2D08A63500CCDA40 /* XPC+Foreground.swift */; }; + DCC3E5822D08A65400CCDA40 /* XPC+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC3E5812D08A65000CCDA40 /* XPC+Background.swift */; }; DCC46F1625C3521C005D32D9 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = DC72C31825A3CF87008A927A /* FirebaseMessaging */; }; DCC9D99A267BD28600EA36DD /* SyncBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC9D999267BD28600EA36DD /* SyncBackupManager.swift */; }; DCC9D99C267BEB3D00EA36DD /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC9D99B267BEB3D00EA36DD /* CloudKit.framework */; }; @@ -502,7 +503,6 @@ DC2F431327B6972C0006FCC4 /* SwapInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapInView.swift; sourceTree = ""; }; DC2F431527B6983B0006FCC4 /* CopyShareOptionsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyShareOptionsSheet.swift; sourceTree = ""; }; DC2F431927B699800006FCC4 /* ModifyInvoiceSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifyInvoiceSheet.swift; sourceTree = ""; }; - DC32FB3429A3D3FE009912AC /* XpcManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XpcManager.swift; sourceTree = ""; }; DC33369726BAF721000E3F49 /* ShortSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortSheet.swift; sourceTree = ""; }; DC3345CF2C2B4C1200EDD2D4 /* ManageContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageContact.swift; sourceTree = ""; }; DC3345D12C2C761800EDD2D4 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; @@ -635,7 +635,7 @@ DCA4DA342C669FDF0010363C /* CurrencyConverterRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyConverterRow.swift; sourceTree = ""; }; DCA4DA362C66A0960010363C /* CurrencySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySelector.swift; sourceTree = ""; }; DCA5391B29F7202F001BD3D5 /* ChannelInfoPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelInfoPopup.swift; sourceTree = ""; }; - DCA6DEC52829BDEB0073C658 /* CrossProcessCommunication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossProcessCommunication.swift; sourceTree = ""; }; + DCA6DEC52829BDEB0073C658 /* XPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPC.swift; sourceTree = ""; }; DCA6DECB282AAA740073C658 /* SharedSecurity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedSecurity.swift; sourceTree = ""; }; DCA6DECF282AB7E20073C658 /* KeychainConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainConstants.swift; sourceTree = ""; }; DCA7263A2C80BA0E00600716 /* SyncBackupManager+Payments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SyncBackupManager+Payments.swift"; sourceTree = ""; }; @@ -679,6 +679,8 @@ DCBA60D32C93544C00878895 /* InsetGroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetGroupBoxStyle.swift; sourceTree = ""; }; DCBDB8812BE154840097F940 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = de; path = de.lproj/about.html; sourceTree = ""; }; DCBDB8822BE154840097F940 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = de; path = de.lproj/liquidity.html; sourceTree = ""; }; + DCC3E57E2D08A63500CCDA40 /* XPC+Foreground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XPC+Foreground.swift"; sourceTree = ""; }; + DCC3E5812D08A65000CCDA40 /* XPC+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XPC+Background.swift"; sourceTree = ""; }; DCC9D999267BD28600EA36DD /* SyncBackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBackupManager.swift; sourceTree = ""; }; DCC9D99B267BEB3D00EA36DD /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; DCCC7FD426B0A006008ACD9B /* SquareSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareSize.swift; sourceTree = ""; }; @@ -1276,7 +1278,9 @@ DCA6DEC42829BD060073C658 /* xpc */ = { isa = PBXGroup; children = ( - DCA6DEC52829BDEB0073C658 /* CrossProcessCommunication.swift */, + DCA6DEC52829BDEB0073C658 /* XPC.swift */, + DCC3E5812D08A65000CCDA40 /* XPC+Background.swift */, + DCC3E57E2D08A63500CCDA40 /* XPC+Foreground.swift */, ); path = xpc; sourceTree = ""; @@ -1392,7 +1396,6 @@ DC641C6E282085F500862DCD /* phoenix-notifySrvExt.entitlements */, DCB511C9281AED58001BC525 /* NotificationService.swift */, DC641C6A2820803100862DCD /* PhoenixManager.swift */, - DC32FB3429A3D3FE009912AC /* XpcManager.swift */, DCB511CB281AED58001BC525 /* Info.plist */, ); path = "phoenix-notifySrvExt"; @@ -1869,7 +1872,7 @@ DCCFE6B92B69A4FA002FFF11 /* LogFileInfo.swift in Sources */, DC63BDF929AEB8180067A361 /* BackgroundPaymentsSelector.swift in Sources */, DCB30E542A0AABAF00E7D7A2 /* InfoPopoverWindow.swift in Sources */, - DCA6DEC62829BDEB0073C658 /* CrossProcessCommunication.swift in Sources */, + DCA6DEC62829BDEB0073C658 /* XPC.swift in Sources */, DC682FE8258175CE00CA1114 /* Popover.swift in Sources */, DC1844032A2690BB004D9578 /* MinerFeeSheet.swift in Sources */, DC39A2662A12C04D00F59E39 /* LiquidityPolicyHelp.swift in Sources */, @@ -2004,6 +2007,7 @@ DCDD9ED2286377C5001800A3 /* MainView_Small.swift in Sources */, C8D7AFF5BC5754DBBEEB2688 /* ElectrumConfigurationView.swift in Sources */, 53BEFBECABE13063AB28A4D6 /* publishers.swift in Sources */, + DCC3E57F2D08A63900CCDA40 /* XPC+Foreground.swift in Sources */, DC99E94025BA141000FB20F7 /* LocalWebView.swift in Sources */, 53BEFA633D95514CA5C0422A /* ChannelsConfigurationView.swift in Sources */, DCED09D42625DBC4005D5EE2 /* AnimationCompletion.swift in Sources */, @@ -2129,6 +2133,7 @@ DCA6DEC82829C3150073C658 /* GenericPasswordStore.swift in Sources */, DCA6DECD282AB10C0073C658 /* SharedSecurity.swift in Sources */, DCEB2799282D7B260096B87E /* KotlinExtensions+Conversion.swift in Sources */, + DCC3E5822D08A65400CCDA40 /* XPC+Background.swift in Sources */, DCEB2795282D7A9F0096B87E /* KotlinPublishers+Phoenix.swift in Sources */, DC641C6B2820803100862DCD /* PhoenixManager.swift in Sources */, DC641C7B2821726F00862DCD /* FormattedAmount.swift in Sources */, @@ -2138,11 +2143,10 @@ DCF9CFD52862656E001AD33F /* Asserts.swift in Sources */, DC641C7C282172BB00862DCD /* DelayedSave.swift in Sources */, DCA6DECE282AB12B0073C658 /* SecurityFile.swift in Sources */, - DC32FB3529A3D3FE009912AC /* XpcManager.swift in Sources */, DCCFE6BE2B713FB8002FFF11 /* LogFileInfo.swift in Sources */, DC49FE9C2AC49E0400D8D2E2 /* KotlinExtensions+Lightning.swift in Sources */, DC6ACC592B10F4FE0079179B /* RecoveryPhrase.swift in Sources */, - DCA6DEC72829BFD70073C658 /* CrossProcessCommunication.swift in Sources */, + DCA6DEC72829BFD70073C658 /* XPC.swift in Sources */, DCE81C172BC883BE0094B950 /* KotlinFlow.swift in Sources */, DC641C7428208BD600862DCD /* String+VersionComparison.swift in Sources */, DCA6DEC92829C3180073C658 /* GenericPasswordConvertible.swift in Sources */, diff --git a/phoenix-ios/phoenix-ios/AppDelegate.swift b/phoenix-ios/phoenix-ios/AppDelegate.swift index 59f0fe772..328c50a05 100644 --- a/phoenix-ios/phoenix-ios/AppDelegate.swift +++ b/phoenix-ios/phoenix-ios/AppDelegate.swift @@ -30,7 +30,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { private var groupPrefsCancellables = Set() private var isInBackground = false - private var xpc: CrossProcessCommunication? = nil public var externalLightningUrlPublisher = PassthroughSubject() @@ -89,10 +88,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { self._applicationDidEnterBackground() }.store(in: &appCancellables) - xpc = CrossProcessCommunication(actor: .mainApp, receivedMessage: {(_: XpcMessage) in + XPC.shared.receivedMessagePublisher.sink { (msg: XpcMessage) in self.didReceivePaymentViaAppExtension() - }) + }.store(in: &appCancellables) + + XPC.shared.resume() NotificationsManager.shared.requestPermissionForProvisionalNotifications() return true @@ -146,7 +147,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { if isInBackground { isInBackground = false - xpc?.resume() + XPC.shared.resume() } } @@ -155,7 +156,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { if !isInBackground { isInBackground = true - xpc?.suspend() + XPC.shared.suspend() } } @@ -243,7 +244,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { } // -------------------------------------------------- - // MARK: CrossProcessCommunication + // MARK: XPC // -------------------------------------------------- private func didReceivePaymentViaAppExtension() { diff --git a/phoenix-ios/phoenix-ios/extensions/Data+Hexadecimal.swift b/phoenix-ios/phoenix-ios/extensions/Data+Hexadecimal.swift index 7b568e82c..85b6ac497 100644 --- a/phoenix-ios/phoenix-ios/extensions/Data+Hexadecimal.swift +++ b/phoenix-ios/phoenix-ios/extensions/Data+Hexadecimal.swift @@ -1,14 +1,38 @@ import Foundation +import CoreTransferable +import CryptoKit -extension Data { - enum HexOptions { - case lowerCase - case upperCase +enum HexOptions { + case lowerCase + case upperCase + + var formatString: String { + switch self { + case .lowerCase: return "%02hhx" // <- lowercase 'x' + case .upperCase: return "%02hhX" // <- UPPERCASE 'X' + } } +} + +extension SHA256.Digest { + + func toHex(options: HexOptions = .lowerCase) -> String { + return self.map { String(format: options.formatString, $0) }.joined() + } +} + +extension Array where Element == UInt8 { + + func toHex(options: HexOptions = .lowerCase) -> String { + return self.map { String(format: options.formatString, $0) }.joined() + } +} + + +extension Data { func toHex(options: HexOptions = .lowerCase) -> String { - let format = options == .upperCase ? "%02hhX" : "%02hhx" - return map { String(format: format, $0) }.joined() + return self.map { String(format: options.formatString, $0) }.joined() } init?(fromHex string: String) { diff --git a/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Contacts.swift b/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Contacts.swift index 134cda54f..2c06e0667 100644 --- a/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Contacts.swift +++ b/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Contacts.swift @@ -635,7 +635,7 @@ extension SyncBackupManager { let hashMe = prefix + suffix let digest = SHA256.hash(data: hashMe) - let hash = digest.map { String(format: "%02hhx", $0) }.joined() + let hash = digest.toHex(options: .lowerCase) return CKRecord.ID(recordName: hash, zoneID: recordZoneID()) } diff --git a/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Payments.swift b/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Payments.swift index 4f168eee7..c4f29dca3 100644 --- a/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Payments.swift +++ b/phoenix-ios/phoenix-ios/sync/SyncBackupManager+Payments.swift @@ -655,7 +655,7 @@ extension SyncBackupManager { let hashMe = prefix + suffix let digest = SHA256.hash(data: hashMe) - let hash = digest.map { String(format: "%02hhx", $0) }.joined() + let hash = digest.toHex(options: .lowerCase) return CKRecord.ID(recordName: hash, zoneID: recordZoneID()) } diff --git a/phoenix-ios/phoenix-ios/utils/Utils+CurrencyPrefs.swift b/phoenix-ios/phoenix-ios/utils/Utils+CurrencyPrefs.swift index 1f33d2f0e..22a15cad5 100644 --- a/phoenix-ios/phoenix-ios/utils/Utils+CurrencyPrefs.swift +++ b/phoenix-ios/phoenix-ios/utils/Utils+CurrencyPrefs.swift @@ -145,7 +145,7 @@ extension Utils { } // -------------------------------------------------- - // MARK: Alt Formatting + // MARK: Hidden Amounts // -------------------------------------------------- static let hiddenCharacter = "\u{2217}" // asterisk operator diff --git a/phoenix-ios/phoenix-ios/utils/Utils.swift b/phoenix-ios/phoenix-ios/utils/Utils.swift index 73605ab07..60874ff30 100644 --- a/phoenix-ios/phoenix-ios/utils/Utils.swift +++ b/phoenix-ios/phoenix-ios/utils/Utils.swift @@ -475,7 +475,35 @@ class Utils { } // -------------------------------------------------- - // MARK: Alt Formatting + // MARK: Switched Formatting + // -------------------------------------------------- + + static func format( + currencyAmount : CurrencyAmount, + policy : MsatsPolicy = .hideMsats, + locale : Locale? = nil + ) -> FormattedAmount { + + switch currencyAmount.currency { + case .bitcoin(let bitcoinUnit): + return formatBitcoin( + amount : currencyAmount.amount, + bitcoinUnit : bitcoinUnit, + policy : policy, + locale : locale + ) + + case .fiat(let fiatCurrency): + return formatFiat( + amount : currencyAmount.amount, + fiatCurrency : fiatCurrency, + locale : locale + ) + } + } + + // -------------------------------------------------- + // MARK: Unknown Amount // -------------------------------------------------- static func unknownBitcoinAmount( diff --git a/phoenix-ios/phoenix-ios/views/currency_converter/CurrencyConverterView.swift b/phoenix-ios/phoenix-ios/views/currency_converter/CurrencyConverterView.swift index ecb1a8278..2864d95d5 100644 --- a/phoenix-ios/phoenix-ios/views/currency_converter/CurrencyConverterView.swift +++ b/phoenix-ios/phoenix-ios/views/currency_converter/CurrencyConverterView.swift @@ -74,16 +74,10 @@ struct CurrencyConverterView: View { // MARK: Init // -------------------------------------------------- - init() { - self.initialAmount = nil - self.didChange = nil - self.didClose = nil - } - init( - initialAmount: CurrencyAmount?, - didChange: @escaping (CurrencyAmount?) -> Void, - didClose: @escaping () -> Void + initialAmount: CurrencyAmount? = nil, + didChange: ((CurrencyAmount?) -> Void)? = nil, + didClose: (() -> Void)? = nil ) { self.initialAmount = initialAmount self.didChange = didChange diff --git a/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift b/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift index 3010e98e3..0034a47ba 100644 --- a/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift +++ b/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift @@ -9,16 +9,14 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .warning) #endif struct LightningDualView: View { - - enum NavLinkTag: String, Codable { - case CurrencyConverter - } @ObservedObject var mvi: MVIState @ObservedObject var inboundFeeState: InboundFeeState @ObservedObject var toast: Toast @Binding var didAppear: Bool + + let navigateTo: (ReceiveView.NavLinkTag) -> Void @StateObject var qrCode = QRCode() @@ -53,10 +51,6 @@ struct LightningDualView: View { ) @State var maxButtonWidth: CGFloat? = nil - // - @State var navLinkTag: NavLinkTag? = nil - // - // To workaround a bug in SwiftUI, we're using multiple namespaces for our animation. // In particular, animating the border around the qrcode doesn't work well. @Namespace private var qrCodeAnimation_inner @@ -67,7 +61,6 @@ struct LightningDualView: View { @Environment(\.presentationMode) var presentationMode: Binding @Environment(\.colorScheme) var colorScheme: ColorScheme - @EnvironmentObject var navCoordinator: NavigationCoordinator @EnvironmentObject var currencyPrefs: CurrencyPrefs @EnvironmentObject var deepLinkManager: DeepLinkManager @EnvironmentObject var popoverState: PopoverState @@ -83,12 +76,6 @@ struct LightningDualView: View { content() .navigationTitle(NSLocalizedString("Receive", comment: "Navigation bar title")) .navigationBarTitleDisplayMode(.inline) - .navigationStackDestination(isPresented: navLinkTagBinding()) { // iOS 16 - navLinkView() - } - .navigationStackDestination(for: NavLinkTag.self) { tag in // iOS 17+ - navLinkView(tag) - } } @ViewBuilder @@ -620,41 +607,10 @@ struct LightningDualView: View { } } - @ViewBuilder - func navLinkView() -> some View { - - if let tag = self.navLinkTag { - navLinkView(tag) - } else { - EmptyView() - } - } - - @ViewBuilder - func navLinkView(_ tag: NavLinkTag) -> some View { - - switch tag { - case .CurrencyConverter: - CurrencyConverterView( - initialAmount: modificationAmount, - didChange: currencyConverterDidChange, - didClose: currencyConvertDidClose - ) - } - } - // -------------------------------------------------- // MARK: View Helpers // -------------------------------------------------- - func navLinkTagBinding() -> Binding { - - return Binding( - get: { navLinkTag != nil }, - set: { if !$0 { navLinkTag = nil }} - ) - } - func title() -> String { switch activeType { @@ -785,7 +741,23 @@ struct LightningDualView: View { func openCurrencyConverter() { log.trace("openCurrencyConverter()") - navigateTo(.CurrencyConverter) + navigateTo( + .CurrencyConverter( + initialAmount: modificationAmount, + didChange: currencyConverterDidChange, + didClose: currencyConvertDidClose + ) + ) + } + + func modifyInvoiceSheetDidSave(_ msat: Lightning_kmpMilliSatoshi?, _ desc: String) { + log.trace("modifyInvoiceSheetDidSave()") + + mvi.intent(Receive.IntentAsk( + amount: msat, + desc: desc, + expirySeconds: Prefs.shared.invoiceExpirationSeconds + )) } func currencyConverterDidChange(_ amount: CurrencyAmount?) { @@ -807,11 +779,11 @@ struct LightningDualView: View { smartModalState.display(dismissable: true) { ModifyInvoiceSheet( - mvi: mvi, savedAmount: $modificationAmount, amount: amount, desc: desc ?? "", - openCurrencyConverter: openCurrencyConverter + openCurrencyConverter: openCurrencyConverter, + didSave: modifyInvoiceSheetDidSave ) } } @@ -820,16 +792,6 @@ struct LightningDualView: View { // MARK: Actions // -------------------------------------------------- - func navigateTo(_ tag: NavLinkTag) { - log.trace("navigateTo(\(tag.rawValue))") - - if #available(iOS 17, *) { - navCoordinator.path.append(tag) - } else { - navLinkTag = tag - } - } - func showFullScreenQRCode() { log.trace("showFullScreenQRCode()") @@ -950,11 +912,11 @@ struct LightningDualView: View { smartModalState.display(dismissable: true) { ModifyInvoiceSheet( - mvi: mvi, savedAmount: $modificationAmount, amount: model.amount, desc: model.desc ?? "", - openCurrencyConverter: openCurrencyConverter + openCurrencyConverter: openCurrencyConverter, + didSave: modifyInvoiceSheetDidSave ) } } diff --git a/phoenix-ios/phoenix-ios/views/receive/ModifyInvoiceSheet.swift b/phoenix-ios/phoenix-ios/views/receive/ModifyInvoiceSheet.swift index 1f3d6cee3..6b79c9bfe 100644 --- a/phoenix-ios/phoenix-ios/views/receive/ModifyInvoiceSheet.swift +++ b/phoenix-ios/phoenix-ios/views/receive/ModifyInvoiceSheet.swift @@ -10,12 +10,12 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .warning) struct ModifyInvoiceSheet: View { - @ObservedObject var mvi: MVIState - @Binding var savedAmount: CurrencyAmount? + let initialAmount: Lightning_kmpMilliSatoshi? + let openCurrencyConverter: () -> Void + let didSave: (Lightning_kmpMilliSatoshi?, String) -> Void - let initialAmount: Lightning_kmpMilliSatoshi? @State var desc: String @State var currency: Currency = Currency.bitcoin(.sat) @@ -26,8 +26,6 @@ struct ModifyInvoiceSheet: View { @State var parsedAmount: Result = Result.failure(.emptyInput) @State var altAmount: String = "" - @State var isInvalidAmount: Bool = false - @State var isEmptyAmount: Bool = false @EnvironmentObject var currencyPrefs: CurrencyPrefs @EnvironmentObject var smartModalState: SmartModalState @@ -41,21 +39,21 @@ struct ModifyInvoiceSheet: View { @State var textHeight: CGFloat? = nil init( - mvi: MVIState, savedAmount: Binding, amount: Lightning_kmpMilliSatoshi?, desc: String, - openCurrencyConverter: @escaping () -> Void + openCurrencyConverter: @escaping () -> Void, + didSave: @escaping (Lightning_kmpMilliSatoshi?, String) -> Void ) { - self.mvi = mvi self._savedAmount = savedAmount self.initialAmount = amount self._desc = State(initialValue: desc) self.openCurrencyConverter = openCurrencyConverter + self.didSave = didSave } // -------------------------------------------------- - // MARK: ViewBuilders + // MARK: View Builders // -------------------------------------------------- @ViewBuilder @@ -74,7 +72,7 @@ struct ModifyInvoiceSheet: View { ) .keyboardType(.decimalPad) .disableAutocorrection(true) - .foregroundColor(isInvalidAmount ? Color.appNegative : Color.primaryForeground) + .foregroundColor(parsedAmount.isError ? Color.appNegative : Color.primaryForeground) .read(textHeightReader) .padding([.top, .bottom], 8) .padding(.leading, 16) @@ -98,7 +96,7 @@ struct ModifyInvoiceSheet: View { Text(altAmount) .font(.caption) - .foregroundColor(isInvalidAmount && !isEmptyAmount ? Color.appNegative : .secondary) + .foregroundColor(parsedAmount.isError && !isEmptyAmount ? Color.appNegative : .secondary) .padding(.top, 0) .padding(.leading, 16) .padding(.bottom, 4) @@ -132,7 +130,7 @@ struct ModifyInvoiceSheet: View { didTapSaveButton() } .font(.title2) - .disabled(isInvalidAmount && !isEmptyAmount) + .disabled(parsedAmount.isError && !isEmptyAmount) } .padding(.bottom) @@ -178,9 +176,25 @@ struct ModifyInvoiceSheet: View { } // -------------------------------------------------- - // MARK: UI Content Helpers + // MARK: View Helpers // -------------------------------------------------- + var isEmptyAmount: Bool { + + switch parsedAmount { + case .success(let amt): + return false + + case .failure(let reason): + switch reason { + case .emptyInput: + return true + case .invalidInput: + return false + } + } + } + func currencyStyler() -> TextFieldCurrencyStyler { return TextFieldCurrencyStyler( currency: currency, @@ -218,33 +232,19 @@ struct ModifyInvoiceSheet: View { func onAppear() -> Void { log.trace("onAppear()") - if let savedAmount = savedAmount { + if let savedAmount { // We have a saved amount from a previous modification. // That is, from using the ModifyInvoiceSheet earlier, or from using the CurrencyConverter. // So we display this amount as-is. - let formattedAmt: FormattedAmount - switch savedAmount.currency { - case .bitcoin(let bitcoinUnit): - formattedAmt = Utils.formatBitcoin( - amount: savedAmount.amount, - bitcoinUnit: bitcoinUnit, - policy: .showMsatsIfNonZero - ) - - case .fiat(let fiatCurrency): - formattedAmt = Utils.formatFiat( - amount: savedAmount.amount, - fiatCurrency: fiatCurrency - ) - } + let formattedAmt = Utils.format(currencyAmount: savedAmount, policy: .showMsatsIfNonZero) parsedAmount = Result.success(formattedAmt.amount) amount = formattedAmt.digits currency = savedAmount.currency - } else if let initialAmount = initialAmount { + } else if let initialAmount { // Since there's an amount in bitcoin, we use the user's preferred bitcoin unit. // We try to use the user's preferred currency. @@ -311,8 +311,6 @@ struct ModifyInvoiceSheet: View { switch parsedAmount { case .failure(let error): - isInvalidAmount = true - isEmptyAmount = error == .emptyInput switch error { case .emptyInput: @@ -322,8 +320,6 @@ struct ModifyInvoiceSheet: View { } case .success(let amt): - isInvalidAmount = false - isEmptyAmount = false var msat: Int64? = nil switch currency { @@ -406,15 +402,10 @@ struct ModifyInvoiceSheet: View { savedAmount = nil } - smartModalState.close { - - mvi.intent(Receive.IntentAsk( - amount: msat, - desc: trimmedDesc, - expirySeconds: Prefs.shared.invoiceExpirationSeconds - )) - } - } + smartModalState.close(animationCompletion: { + didSave(msat, trimmedDesc) + }) + } } enum CurrencyPickerOption: Hashable, Identifiable, CustomStringConvertible { diff --git a/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift b/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift index 4f6074057..e52e161d0 100644 --- a/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift +++ b/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift @@ -10,25 +10,89 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .warning) struct ReceiveView: MVIView { - @StateObject var mvi = MVIState({ $0.receive() }) - - @Environment(\.controllerFactory) var factoryEnv - var factory: ControllerFactory { return factoryEnv } - + enum NavLinkTag: Hashable, CustomStringConvertible { + + case CurrencyConverter( + initialAmount: CurrencyAmount?, + didChange: ((CurrencyAmount?) -> Void)?, + didClose: (() -> Void)? + ) + + private var internalValue: Int { + switch self { + case .CurrencyConverter(_, _, _): return 1 + } + } + + static func == (lhs: NavLinkTag, rhs: NavLinkTag) -> Bool { + return lhs.internalValue == rhs.internalValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.internalValue) + } + + var description: String { + switch self { + case .CurrencyConverter: return "CurrencyConverter" + } + } + } + // The order of items within this enum controls the order in the UI. // If you change the order, you might also consider changing the initial value for `selectedTab`. - enum Tab: CaseIterable, Identifiable { + enum Tab: CaseIterable, Identifiable, CustomStringConvertible { case lightning case blockchain - + var id: Self { self } + + var description: String { + switch self { + case .lightning : return "lightning" + case .blockchain : return "blockchain" + } + } + + func previous() -> Tab? { + var previous: Tab? = nil + for t in Tab.allCases { + if t == self { + return previous + } else { + previous = t + } + } + return nil + } + + func next() -> Tab? { + var found = false + for t in Tab.allCases { + if t == self { + found = true + } else if found { + return t + } + } + return nil + } } + @StateObject var mvi = MVIState({ $0.receive() }) + + @Environment(\.controllerFactory) var factoryEnv + var factory: ControllerFactory { return factoryEnv } + @State var selectedTab: Tab = .lightning @State var lightningInvoiceView_didAppear = false @State var showSendView = false + // + @State var navLinkTag: NavLinkTag? = nil + // + @StateObject var inboundFeeState = InboundFeeState() @StateObject var toast = Toast() @@ -36,6 +100,7 @@ struct ReceiveView: MVIView { @EnvironmentObject var deviceInfo: DeviceInfo @EnvironmentObject var popoverState: PopoverState + @EnvironmentObject var navCoordinator: NavigationCoordinator // -------------------------------------------------- // MARK: ViewBuilders @@ -44,6 +109,18 @@ struct ReceiveView: MVIView { @ViewBuilder var view: some View { + layers() + .navigationStackDestination(isPresented: navLinkTagBinding()) { // iOS 16 + navLinkView() + } + .navigationStackDestination(for: NavLinkTag.self) { tag in // iOS 17+ + navLinkView(tag) + } + } + + @ViewBuilder + func layers() -> some View { + ZStack { Color.primaryBackground @@ -102,7 +179,8 @@ struct ReceiveView: MVIView { mvi: mvi, inboundFeeState: inboundFeeState, toast: toast, - didAppear: $lightningInvoiceView_didAppear + didAppear: $lightningInvoiceView_didAppear, + navigateTo: navigateTo ) .tag(Tab.lightning) @@ -216,25 +294,68 @@ struct ReceiveView: MVIView { } } + @ViewBuilder + func navLinkView() -> some View { + + if let tag = self.navLinkTag { + navLinkView(tag) + } else { + EmptyView() + } + } + + @ViewBuilder + func navLinkView(_ tag: NavLinkTag) -> some View { + + switch tag { + case .CurrencyConverter(let initialAmount, let didChange, let didClose): + CurrencyConverterView( + initialAmount: initialAmount, + didChange: didChange, + didClose: didClose + ) + } + } + + // -------------------------------------------------- + // MARK: View Helpers + // -------------------------------------------------- + + func navLinkTagBinding() -> Binding { + + return Binding( + get: { navLinkTag != nil }, + set: { if !$0 { navLinkTag = nil }} + ) + } + // -------------------------------------------------- // MARK: Actions // -------------------------------------------------- + func navigateTo(_ tag: NavLinkTag) { + log.trace("navigateTo(\(tag.description))") + + if #available(iOS 17, *) { + navCoordinator.path.append(tag) + } else { + navLinkTag = tag + } + } + func moveToPreviousTab() { log.trace("moveToPreviousTab()") - switch selectedTab { - case Tab.lightning : break - case Tab.blockchain : selectTabWithAnimation(.lightning) + if let previousTag = selectedTab.previous() { + selectTabWithAnimation(previousTag) } } func moveToNextTab() { log.trace("moveToNextTab()") - switch selectedTab { - case Tab.lightning : selectTabWithAnimation(.blockchain) - case Tab.blockchain : break + if let nextTab = selectedTab.next() { + selectTabWithAnimation(nextTab) } } diff --git a/phoenix-ios/phoenix-ios/views/send/PaymentSummary.swift b/phoenix-ios/phoenix-ios/views/send/PaymentSummary.swift index a1f6d8a86..ad3474d7a 100644 --- a/phoenix-ios/phoenix-ios/views/send/PaymentSummary.swift +++ b/phoenix-ios/phoenix-ios/views/send/PaymentSummary.swift @@ -35,7 +35,7 @@ struct PaymentSummaryStrings { static func create( from source: PaymentSummary?, currencyPrefs: CurrencyPrefs, - problem: Problem? + problem: ValidateView.Problem? ) -> PaymentSummaryStrings { let bitcoinUnit = currencyPrefs.bitcoinUnit diff --git a/phoenix-ios/phoenix-ios/views/send/ValidateView.swift b/phoenix-ios/phoenix-ios/views/send/ValidateView.swift index 0abf850f7..47325b113 100644 --- a/phoenix-ios/phoenix-ios/views/send/ValidateView.swift +++ b/phoenix-ios/phoenix-ios/views/send/ValidateView.swift @@ -8,14 +8,6 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .trace) fileprivate var log = LoggerFactory.shared.logger(filename, .warning) #endif -enum Problem: Error { - case emptyInput - case invalidInput - case amountExceedsBalance - case finalAmountExceedsBalance // including minerFee - case amountOutOfRange -} - struct ValidateView: View { enum NavLinkTag: Hashable, CustomStringConvertible { @@ -45,6 +37,14 @@ struct ValidateView: View { @State var parsedAmount: Result = Result.failure(.emptyInput) @State var altAmount: String = "" + + enum Problem: Error { + case emptyInput + case invalidInput + case amountExceedsBalance + case finalAmountExceedsBalance // including minerFee + case amountOutOfRange + } @State var problem: Problem? = nil @State var paymentInProgress: Bool = false @@ -526,7 +526,7 @@ struct ValidateView: View { CurrencyConverterView( initialAmount: currentAmount(), didChange: currencyConverterAmountChanged, - didClose: {} + didClose: nil ) case .PaymentRequestedView(let invoice): @@ -2111,21 +2111,7 @@ struct ValidateView: View { currency = newAmt.currency currencyPickerChoice = newAmt.currency.shortName - let formattedAmt: FormattedAmount - switch newAmt.currency { - case .bitcoin(let bitcoinUnit): - formattedAmt = Utils.formatBitcoin( - amount: newAmt.amount, - bitcoinUnit: bitcoinUnit, - policy: .showMsatsIfNonZero - ) - case .fiat(let fiatCurrency): - formattedAmt = Utils.formatFiat( - amount: newAmt.amount, - fiatCurrency: fiatCurrency - ) - } - + let formattedAmt = Utils.format(currencyAmount: newAmt, policy: .showMsatsIfNonZero) parsedAmount = Result.success(newAmt.amount) amount = formattedAmt.digits diff --git a/phoenix-ios/phoenix-ios/xpc/XPC+Background.swift b/phoenix-ios/phoenix-ios/xpc/XPC+Background.swift new file mode 100644 index 000000000..0c461532b --- /dev/null +++ b/phoenix-ios/phoenix-ios/xpc/XPC+Background.swift @@ -0,0 +1,7 @@ +import Foundation + +/// This file is **ONLY** for the Notify-Service-Extension (background process) +/// +extension XPC { + public static let shared = XPC(actor: .notifySrvExt) +} diff --git a/phoenix-ios/phoenix-ios/xpc/XPC+Foreground.swift b/phoenix-ios/phoenix-ios/xpc/XPC+Foreground.swift new file mode 100644 index 000000000..5c01420a3 --- /dev/null +++ b/phoenix-ios/phoenix-ios/xpc/XPC+Foreground.swift @@ -0,0 +1,7 @@ +import Foundation + +/// This file is **ONLY** for the main Phoenix app +/// +extension XPC { + public static let shared = XPC(actor: .mainApp) +} diff --git a/phoenix-ios/phoenix-ios/xpc/CrossProcessCommunication.swift b/phoenix-ios/phoenix-ios/xpc/XPC.swift similarity index 76% rename from phoenix-ios/phoenix-ios/xpc/CrossProcessCommunication.swift rename to phoenix-ios/phoenix-ios/xpc/XPC.swift index 742e02061..fce905cb9 100644 --- a/phoenix-ios/phoenix-ios/xpc/CrossProcessCommunication.swift +++ b/phoenix-ios/phoenix-ios/xpc/XPC.swift @@ -1,4 +1,5 @@ import Foundation +import Combine import notify fileprivate let filename = "XPC" @@ -8,14 +9,28 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .trace) fileprivate var log = LoggerFactory.shared.logger(filename, .warning) #endif -enum XpcActor { +enum XpcActor: CustomStringConvertible { case mainApp case notifySrvExt + + var description: String { + switch self { + case .mainApp : return "mainApp" + case .notifySrvExt : return "notifySrvExt" + } + } } -enum XpcMessage { +enum XpcMessage: CustomStringConvertible { case available case unavailable + + var description: String { + switch self { + case .available : return "available" + case .unavailable : return "unavailable" + } + } } fileprivate let msgPing_mainApp: UInt64 = 0b0001 @@ -53,27 +68,24 @@ fileprivate let msgUnavailable_notifySrvExt: UInt64 = 0b1100 * * Using these simple primitives, we're able to determine if the other process is active. */ -class CrossProcessCommunication { +class XPC { private let actor: XpcActor - private let receivedMessage: ((XpcMessage) -> Void) - private let queue = DispatchQueue(label: "CrossProcessCommunication") + private let queue = DispatchQueue(label: "XPC") private let channelPrefix = "co.acinq.phoenix" private let groupIdentifier = "group.co.acinq.phoenix" private var channel: String? = nil private var notifyToken: Int32 = NOTIFY_TOKEN_INVALID - private var pendingSuspendCount: UInt32 = 0 + private var suspendCount: UInt32 = 1 // You have to call resume() to start XPC - init( - actor: XpcActor, - receivedMessage: @escaping (XpcMessage) -> Void - ) { - log.trace("init()") + public let receivedMessagePublisher = PassthroughSubject() + + init(actor: XpcActor) { + log.trace("init(\(actor))") self.actor = actor - self.receivedMessage = receivedMessage DispatchQueue.global(qos: .utility).async { self.readChannelID() @@ -95,36 +107,44 @@ class CrossProcessCommunication { public func suspend() { - queue.async { + queue.async { [self] in + log.trace("suspend()") - if notify_is_valid_token(self.notifyToken) { - switch self.actor { - case .mainApp : self.sendMessage(msgUnavailable_mainApp) - case .notifySrvExt : self.sendMessage(msgUnavailable_notifySrvExt) - } + guard suspendCount < UInt32.max else { + log.warning("suspend(): suspendCount is already at UInt32.max") + return + } + + suspendCount += 1 + log.debug("suspendCount = \(suspendCount)") + + if suspendCount == 1, notify_is_valid_token(notifyToken) { + sendUnavailableMessage() + log.debug("notify_suspend()") - notify_suspend(self.notifyToken) - } else { - if (self.pendingSuspendCount < UInt32.max) { - log.debug("pendingSuspendCount += 1") - self.pendingSuspendCount += 1 - } + notify_suspend(notifyToken) } } } public func resume() { - queue.async { + queue.async { [self] in + log.trace("resume()") - if notify_is_valid_token(self.notifyToken) { + guard suspendCount > 0 else { + log.warning("resume(): suspendCount is already at 0") + return + } + + suspendCount -= 1 + log.debug("suspendCount = \(suspendCount)") + + if suspendCount == 0, notify_is_valid_token(notifyToken) { log.debug("notify_resume()") - notify_resume(self.notifyToken) - } else { - if (self.pendingSuspendCount > 0) { - log.debug("pendingSuspendCount -= 1") - self.pendingSuspendCount -= 1 - } + notify_resume(notifyToken) + + sendPingMessage() } } } @@ -216,6 +236,10 @@ class CrossProcessCommunication { guard let self = self else { return } + guard self.suspendCount == 0 else { + log.info("ignoring received message: suspended") + return + } var msg: UInt64 = 0 notify_get_state(token, &msg) @@ -225,21 +249,12 @@ class CrossProcessCommunication { if notify_is_valid_token(notifyToken) { - if pendingSuspendCount > 0 { - - for _ in 0 ..< pendingSuspendCount { - log.debug("notify_suspend()") - notify_suspend(notifyToken) - } - pendingSuspendCount = 0 + if suspendCount > 0 { + log.debug("notify_suspend()") + notify_suspend(notifyToken) } else { - - if actor == .mainApp { - sendMessage(msgPing_mainApp) - } else { - sendMessage(msgPing_notifySrvExt) - } + sendPingMessage() } } } @@ -259,7 +274,7 @@ class CrossProcessCommunication { case msgPing_notifySrvExt: log.debug("received message: \(msgStr)") notifyReceivedMessage(.available) - sendMessage(msgPong_mainApp) + sendPongMessage() case msgPong_notifySrvExt: log.debug("received message: \(msgStr)") @@ -285,7 +300,7 @@ class CrossProcessCommunication { case msgPing_mainApp: log.debug("received message: \(msgStr)") notifyReceivedMessage(.available) - sendMessage(msgPong_notifySrvExt) + sendPongMessage() case msgPong_mainApp: log.debug("received message: \(msgStr)") @@ -302,10 +317,32 @@ class CrossProcessCommunication { } // } + private func sendPingMessage() { + switch actor { + case .mainApp : sendMessage(msgPing_mainApp) + case .notifySrvExt : sendMessage(msgPing_notifySrvExt) + } + } + + private func sendPongMessage() { + switch actor { + case .mainApp : sendMessage(msgPong_mainApp) + case .notifySrvExt : sendMessage(msgPong_notifySrvExt) + } + } + + private func sendUnavailableMessage() { + switch actor { + case .mainApp : sendMessage(msgUnavailable_mainApp) + case .notifySrvExt : sendMessage(msgUnavailable_notifySrvExt) + } + } + private func sendMessage(_ msg: UInt64) { - log.trace("sendMessage(\(self.messageToString(msg)))") + log.trace("sendMessage(\(messageToString(msg)))") guard notify_is_valid_token(notifyToken), let channel = channel else { + log.debug("sendMessage(\(messageToString(msg))): ignoring: channel not setup yet") return } @@ -334,7 +371,7 @@ class CrossProcessCommunication { log.trace("notifyReceivedMessage()") DispatchQueue.main.async { - self.receivedMessage(msg) + self.receivedMessagePublisher.send(msg) } } } diff --git a/phoenix-ios/phoenix-notifySrvExt/NotificationService.swift b/phoenix-ios/phoenix-notifySrvExt/NotificationService.swift index 2ac1bdff9..ec8c3e429 100644 --- a/phoenix-ios/phoenix-notifySrvExt/NotificationService.swift +++ b/phoenix-ios/phoenix-notifySrvExt/NotificationService.swift @@ -27,7 +27,7 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .warning) * * This means that the following instances are recycled (continue existing in memory): * - PhoenixManager.shared - * - XpcManager.shared + * - XPC.shared */ class NotificationService: UNNotificationServiceExtension { @@ -175,9 +175,11 @@ class NotificationService: UNNotificationServiceExtension { if !xpcStarted && !srvExtDone { xpcStarted = true - XpcManager.shared.register {[weak self] in - self?.didReceiveXpcMessage() - } + XPC.shared.receivedMessagePublisher.sink {[weak self](msg: XpcMessage) in + self?.didReceiveXpcMessage(msg) + }.store(in: &cancellables) + + XPC.shared.resume() } } @@ -188,33 +190,36 @@ class NotificationService: UNNotificationServiceExtension { if xpcStarted { xpcStarted = false - XpcManager.shared.unregister() + XPC.shared.suspend() } } - private func didReceiveXpcMessage() { + private func didReceiveXpcMessage(_ msg: XpcMessage) { log.trace("didReceiveXpcMessage()") assertMainThread() - // This means the main phoenix app is running. - - if isConnectedToPeer { - - // But we're already connected to the peer, and processing the payment. - // So we're going to continue working on the payment, - // and the main app will have to wait for us to finish before connecting to the peer itself. - - log.debug("isConnectedToPeer is true => continue processing incoming payment") + if msg == .available { - } else { + // The main phoenix app is running. - // Since we're not connected yet, we'll just go ahead and allow the main app to handle the payment. - // - // And we don't have to wait for the main app to finish handling the payment. - // Because whatever we emit from this app extension won't be displayed to the user. - // That is, the modified push content we emit isn't actually shown to the user. - - displayPushNotification() + if isConnectedToPeer { + + // But we're already connected to the peer, and processing the payment. + // So we're going to continue working on the payment, + // and the main app will have to wait for us to finish before connecting to the peer itself. + + log.debug("isConnectedToPeer is true => continue processing incoming payment") + + } else { + + // Since we're not connected yet, we'll just go ahead and allow the main app to handle the payment. + // + // And we don't have to wait for the main app to finish handling the payment. + // Because whatever we emit from this app extension won't be displayed to the user. + // That is, the modified push content we emit isn't actually shown to the user. + + displayPushNotification() + } } } @@ -229,14 +234,30 @@ class NotificationService: UNNotificationServiceExtension { if !phoenixStarted && !srvExtDone { phoenixStarted = true - PhoenixManager.shared.register( - connectionsListener: {[weak self](connections: Connections) in - self?.connectionsChanged(connections) - }, - paymentListener: {[weak self](payment: Lightning_kmpIncomingPayment) in - self?.didReceivePayment(payment) + let newBusiness = PhoenixManager.shared.setupBusiness() + + newBusiness.connectionsManager.connectionsPublisher().sink { + [weak self](connections: Connections) in + + self?.connectionsChanged(connections) + } + .store(in: &cancellables) + + let pushReceivedAt = Date() + newBusiness.paymentsManager.lastIncomingPaymentPublisher().sink { + [weak self](payment: Lightning_kmpIncomingPayment) in + + guard + let paymentReceivedAt = payment.received?.receivedAtDate, + paymentReceivedAt > pushReceivedAt + else { + // Ignoring - this is the most recently received incomingPayment, but not a new one + return } - ) + + self?.didReceivePayment(payment) + } + .store(in: &cancellables) } } @@ -247,7 +268,7 @@ class NotificationService: UNNotificationServiceExtension { if phoenixStarted { phoenixStarted = false - PhoenixManager.shared.unregister() + PhoenixManager.shared.teardownBusiness() } } @@ -320,10 +341,7 @@ class NotificationService: UNNotificationServiceExtension { } srvExtDone = true - guard - let contentHandler = contentHandler, - let bestAttemptContent = bestAttemptContent - else { + guard let contentHandler, let bestAttemptContent else { return } @@ -336,6 +354,18 @@ class NotificationService: UNNotificationServiceExtension { stopXpc() stopPhoenix() + updateBestAttemptContent() + contentHandler(bestAttemptContent) + } + + private func updateBestAttemptContent() { + log.trace("updateBestAttemptContent()") + assertMainThread() + + guard let bestAttemptContent else { + return + } + if receivedPayments.isEmpty { if pushNotificationReason() == .pendingSettlement { @@ -347,30 +377,18 @@ class NotificationService: UNNotificationServiceExtension { } else { // received 1 or more payments - let bitcoinUnit = GroupPrefs.shared.bitcoinUnit - let fiatCurrency = GroupPrefs.shared.fiatCurrency - let exchangeRate = PhoenixManager.shared.exchangeRate(fiatCurrency: fiatCurrency) - var msat: Int64 = 0 for payment in receivedPayments { msat += payment.amount.msat } - let bitcoinAmt = Utils.formatBitcoin(msat: msat, bitcoinUnit: bitcoinUnit) - - var fiatAmt: FormattedAmount? = nil - if let exchangeRate { - fiatAmt = Utils.formatFiat(msat: msat, exchangeRate: exchangeRate) - } - - var amountString = bitcoinAmt.string - if let fiatAmt { - amountString += " (≈\(fiatAmt.string))" - } + let amountString = formatAmount(msat: msat) if receivedPayments.count == 1 { - bestAttemptContent.title = - NSLocalizedString("Received payment", comment: "Push notification title") + bestAttemptContent.title = String( + localized: "Received payment", + comment: "Push notification title" + ) if !GroupPrefs.shared.discreetNotifications { let paymentInfo = WalletPaymentInfo( @@ -387,8 +405,10 @@ class NotificationService: UNNotificationServiceExtension { } } else { - bestAttemptContent.title = - NSLocalizedString("Received multiple payments", comment: "Push notification title") + bestAttemptContent.title = String( + localized: "Received multiple payments", + comment: "Push notification title" + ) if !GroupPrefs.shared.discreetNotifications { bestAttemptContent.body = amountString @@ -403,7 +423,22 @@ class NotificationService: UNNotificationServiceExtension { GroupPrefs.shared.badgeCount += receivedPayments.count bestAttemptContent.badge = NSNumber(value: GroupPrefs.shared.badgeCount) } + } + + private func formatAmount(msat: Int64) -> String { - contentHandler(bestAttemptContent) + let bitcoinUnit = GroupPrefs.shared.bitcoinUnit + let fiatCurrency = GroupPrefs.shared.fiatCurrency + let exchangeRate = PhoenixManager.shared.exchangeRate(fiatCurrency: fiatCurrency) + + let bitcoinAmt = Utils.formatBitcoin(msat: msat, bitcoinUnit: bitcoinUnit) + var amountString = bitcoinAmt.string + + if let exchangeRate { + let fiatAmt = Utils.formatFiat(msat: msat, exchangeRate: exchangeRate) + amountString += " (≈\(fiatAmt.string))" + } + + return amountString } } diff --git a/phoenix-ios/phoenix-notifySrvExt/PhoenixManager.swift b/phoenix-ios/phoenix-notifySrvExt/PhoenixManager.swift index 3e8ad0ae7..b44064f96 100644 --- a/phoenix-ios/phoenix-notifySrvExt/PhoenixManager.swift +++ b/phoenix-ios/phoenix-notifySrvExt/PhoenixManager.swift @@ -29,7 +29,7 @@ typealias PaymentListener = (Lightning_kmpIncomingPayment) -> Void * * This means that the following instances are recycled (continue existing in memory): * - PhoenixManager.shared - * - XpcManager.shared + * - XPC.shared * * --------------------- * # Architecture notes: @@ -48,9 +48,6 @@ typealias PaymentListener = (Lightning_kmpIncomingPayment) -> Void class PhoenixManager { public static let shared = PhoenixManager() - - private var connectionsListener: ConnectionsListener? = nil - private var paymentListener: PaymentListener? = nil private var business: PhoenixBusiness? = nil private var oldBusiness: PhoenixBusiness? = nil @@ -66,46 +63,13 @@ class PhoenixManager { // MARK: Public Functions // -------------------------------------------------- - public func register( - connectionsListener: @escaping ConnectionsListener, - paymentListener: @escaping PaymentListener - ) { - log.trace("register(::)") - assertMainThread() - - self.connectionsListener = connectionsListener - self.paymentListener = paymentListener - - setupBusiness() - unlock() - } - - public func unregister() { - log.trace("unregister()") - assertMainThread() - - self.connectionsListener = nil - self.paymentListener = nil - - teardownBusiness() - } - - public func exchangeRate(fiatCurrency: FiatCurrency) -> ExchangeRate.BitcoinPriceRate? { - - return Utils.exchangeRate(for: fiatCurrency, fromRates: fiatExchangeRates) - } - - // -------------------------------------------------- - // MARK: Business management - // -------------------------------------------------- - - private func setupBusiness() { + public func setupBusiness() -> PhoenixBusiness { log.trace("setupBusiness()") assertMainThread() - - guard business == nil else { - log.warning("ignoring: business != nil") - return + + if let currentBusiness = business { + log.warning("setupBusiness(): business already setup") + return currentBusiness } let newBusiness = PhoenixBusiness(ctx: PlatformContext.default) @@ -135,29 +99,6 @@ class PhoenixManager { newBusiness.currencyManager.refreshAll(targets: [primaryFiatCurrency], force: false) - newBusiness.connectionsManager.connectionsPublisher().sink { - [weak self](connections: Connections) in - - self?.connectionsChanged(connections) - } - .store(in: &cancellables) - - let pushReceivedAt = Date() - newBusiness.paymentsManager.lastIncomingPaymentPublisher().sink { - [weak self](payment: Lightning_kmpIncomingPayment) in - - guard - let paymentReceivedAt = payment.received?.receivedAtDate, - paymentReceivedAt > pushReceivedAt - else { - // Ignoring - this is the most recently received incomingPayment, but not a new one - return - } - - self?.didReceivePayment(payment) - } - .store(in: &cancellables) - newBusiness.currencyManager.ratesPubliser().sink { [weak self](rates: [ExchangeRate]) in @@ -168,14 +109,17 @@ class PhoenixManager { // Setup complete business = newBusiness + + startAsyncUnlock() + return newBusiness } - private func teardownBusiness() { + public func teardownBusiness() { log.trace("teardownBusiness()") assertMainThread() guard let currentBusiness = business else { - log.warning("ignoring: business == nil") + log.warning("teardownBusiness(): business already nil") return } @@ -204,17 +148,22 @@ class PhoenixManager { // and the oldBusiness doesn't get properly cleaned up. oldConnectionsChanged(currentBusiness.connectionsManager.currentValue) } + + public func exchangeRate(fiatCurrency: FiatCurrency) -> ExchangeRate.BitcoinPriceRate? { + + return Utils.exchangeRate(for: fiatCurrency, fromRates: fiatExchangeRates) + } // -------------------------------------------------- // MARK: Flow // -------------------------------------------------- - private func unlock() { - log.trace("unlock()") + private func startAsyncUnlock() { + log.trace("startAsyncUnlock()") let connectWithRecoveryPhrase = {(recoveryPhrase: RecoveryPhrase?) in DispatchQueue.main.async { - self.connect(recoveryPhrase: recoveryPhrase) + self.connect(recoveryPhrase) } } @@ -251,20 +200,20 @@ class PhoenixManager { } } - private func connect(recoveryPhrase: RecoveryPhrase?) { - log.trace("connect(recoveryPhrase:)") + private func connect(_ recoveryPhrase: RecoveryPhrase?) { + log.trace("connect()") assertMainThread() guard let recoveryPhrase = recoveryPhrase else { - log.warning("ignoring: recoveryPhrase == nil") + log.warning("connect(): ignoring: recoveryPhrase == nil") return } guard let language = recoveryPhrase.language else { - log.warning("ignoring: recoveryPhrase.language == nil") + log.warning("connect(): ignoring: recoveryPhrase.language == nil") return } guard let business = business else { - log.warning("ignoring: business == nil") + log.warning("connect(): ignoring: business == nil") return } @@ -276,28 +225,6 @@ class PhoenixManager { business.walletManager.loadWallet(seed: seed) } - // -------------------------------------------------- - // MARK: Notifications - // -------------------------------------------------- - - private func connectionsChanged(_ connections: Connections) { - log.trace("connectionsChanged(_)") - assertMainThread() - - if let listener = self.connectionsListener { - listener(connections) - } - } - - private func didReceivePayment(_ payment: Lightning_kmpIncomingPayment) { - log.trace("didReceivePayment(_)") - assertMainThread() - - if let listener = self.paymentListener { - listener(payment) - } - } - // -------------------------------------------------- // MARK: Cleanup // -------------------------------------------------- diff --git a/phoenix-ios/phoenix-notifySrvExt/XpcManager.swift b/phoenix-ios/phoenix-notifySrvExt/XpcManager.swift deleted file mode 100644 index 39c555c04..000000000 --- a/phoenix-ios/phoenix-notifySrvExt/XpcManager.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation - -fileprivate let filename = "XpcManager" -#if DEBUG && true -fileprivate var log = LoggerFactory.shared.logger(filename, .trace) -#else -fileprivate var log = LoggerFactory.shared.logger(filename, .warning) -#endif - -typealias XpcListener = () -> Void - -/** - * What happens if multiple push notifications arrive ? - * - * iOS will launch the notification-service-extension upon receiving the first push notification. - * Subsequent push notifications are queued by the OS. After the app extension finishes processing - * the first notification (by invoking the `contentHandler`), then iOS will: - * - * - display the first push notification - * - dealloc the `UNNotificationServiceExtension` - * - Initialize a new `UNNotificationServiceExtension` instance - * - And invoke it's `didReceive(_:)` function with the next item in the queue - * - * Note that it does **NOT** create a new app extension process. - * It re-uses the existing process, and launches a new `UNNotificationServiceExtension` within it. - * - * This means that the following instances are recycled (continue existing in memory): - * - PhoenixManager.shared - * - XpcManager.shared - */ -class XpcManager { - - public static let shared = XpcManager() - - private var listener: XpcListener? = nil - private var xpc: CrossProcessCommunication? = nil - - private init() {} // Must use shared instance - - // -------------------------------------------------- - // MARK: Public Functions - // -------------------------------------------------- - - public func register(mainAppIsRunning newListener: @escaping XpcListener) { - log.trace("register(mainAppIsRunning:)") - assertMainThread() - - guard listener == nil else { - return - } - - listener = newListener - setupXpc() - } - - public func unregister() { - log.trace("unregister()") - assertMainThread() - - guard listener != nil else { - return - } - - listener = nil - teardownXpc() - } - - // -------------------------------------------------- - // MARK: Business management - // -------------------------------------------------- - - private func setupXpc() { - log.trace("setupXpc()") - assertMainThread() - - xpc = CrossProcessCommunication( - actor: .notifySrvExt, - receivedMessage: {[weak self](msg: XpcMessage) in - self?.didReceiveXpcMessage(msg) - } - ) - } - - private func teardownXpc() { - log.trace("teardownXpc()") - assertMainThread() - - xpc = nil - } - - // -------------------------------------------------- - // MARK: Notifications - // -------------------------------------------------- - - private func didReceiveXpcMessage(_ msg: XpcMessage) { - log.trace("didReceiveXpcMessage()") - assertMainThread() - - // Receiving a message means the main phoenix app is running. - let mainAppIsRunning = (msg == .available) - if mainAppIsRunning, let serviceListener = listener { - DispatchQueue.main.async { - serviceListener() - } - } - } -}