diff --git a/phoenix-android/build.gradle.kts b/phoenix-android/build.gradle.kts index 8bc6d89f0..eaa26f3f6 100644 --- a/phoenix-android/build.gradle.kts +++ b/phoenix-android/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "fr.acinq.phoenix.mainnet" minSdk = 26 targetSdk = 34 - versionCode = 83 - versionName = "2.3.2" + versionCode = 84 + versionName = gitCommitHash() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resourceConfigurations.addAll(listOf("en", "fr", "de", "es", "b+es+419", "cs", "pt-rBR", "sk", "vi")) } diff --git a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml index cce096df6..bf0444cb7 100644 --- a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml @@ -449,6 +449,6 @@ Esta es una dirección humanamente legible para su solicitud de pago Bolt12. - ¿Quieres una dirección más bonita? Utilice servicios de terceros como twelve.cash o aloje usted mismo la dirección. + ¿Quieres una dirección más bonita? Utilice servicios de terceros o aloje usted mismo la dirección. diff --git a/phoenix-android/src/main/res/values-cs/important_strings.xml b/phoenix-android/src/main/res/values-cs/important_strings.xml index 29a04df17..5eb05b892 100644 --- a/phoenix-android/src/main/res/values-cs/important_strings.xml +++ b/phoenix-android/src/main/res/values-cs/important_strings.xml @@ -460,6 +460,6 @@ Toto je lidsky čitelná adresa pro vaši žádost o platbu Bolt12. - Chcete hezčí adresu? Použijte služby třetích stran, jako je twelve.cash, nebo si adresu hostujte sami! + Chcete hezčí adresu? Použijte služby třetích stran, nebo si adresu hostujte sami! diff --git a/phoenix-android/src/main/res/values-de/important_strings.xml b/phoenix-android/src/main/res/values-de/important_strings.xml index 284862400..962006dcb 100644 --- a/phoenix-android/src/main/res/values-de/important_strings.xml +++ b/phoenix-android/src/main/res/values-de/important_strings.xml @@ -458,6 +458,6 @@ Dies ist eine von Menschen lesbare Adresse für Ihre Bolt12-Zahlungsanfrage. - Wollen Sie eine hübschere Adresse? Verwenden Sie Dienste von Drittanbietern wie twelve.cash, oder hosten Sie die Adresse selbst! + Wollen Sie eine hübschere Adresse? Verwenden Sie Dienste von Drittanbietern, oder hosten Sie die Adresse selbst! \ No newline at end of file diff --git a/phoenix-android/src/main/res/values-es/important_strings.xml b/phoenix-android/src/main/res/values-es/important_strings.xml index 120e0cf22..dc1a9d667 100644 --- a/phoenix-android/src/main/res/values-es/important_strings.xml +++ b/phoenix-android/src/main/res/values-es/important_strings.xml @@ -461,6 +461,6 @@ Esta es una dirección humanamente legible para su solicitud de pago Bolt12. - ¿Quieres una dirección más bonita? Utilice servicios de terceros como twelve.cash o aloje usted mismo la dirección. + ¿Quieres una dirección más bonita? Utilice servicios de terceros o aloje usted mismo la dirección. \ No newline at end of file diff --git a/phoenix-android/src/main/res/values-fr/important_strings.xml b/phoenix-android/src/main/res/values-fr/important_strings.xml index 29fa33b2c..80d223975 100644 --- a/phoenix-android/src/main/res/values-fr/important_strings.xml +++ b/phoenix-android/src/main/res/values-fr/important_strings.xml @@ -461,6 +461,6 @@ Il s\'agit d\'une adresse humainement compréhensible pour votre requête de paiement Bolt12. - Vous voulez une adresse plus jolie ? Utilisez des services tiers comme twelve.cash, ou hébergez vous-même l\'adresse! + Vous voulez une adresse plus jolie ? Utilisez des services tiers, ou hébergez vous-même l\'adresse! \ No newline at end of file diff --git a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml index e5ab251ae..977b784f4 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml @@ -456,6 +456,6 @@ Este é um endereço legível por humanos para sua solicitação de pagamento do Bolt12. - Quer um endereço mais bonito? Use serviços de terceiros, como twelve.cash, ou hospede o endereço por conta própria! + Quer um endereço mais bonito? Use serviços de terceiros, ou hospede o endereço por conta própria! \ No newline at end of file diff --git a/phoenix-android/src/main/res/values-sk/important_strings.xml b/phoenix-android/src/main/res/values-sk/important_strings.xml index 247b8adfc..d30e4e85c 100644 --- a/phoenix-android/src/main/res/values-sk/important_strings.xml +++ b/phoenix-android/src/main/res/values-sk/important_strings.xml @@ -461,6 +461,6 @@ Toto je ľudsky čitateľná adresa pre vašu žiadosť o platbu Bolt12. - Chcete krajšiu adresu? Použite služby tretích strán, ako je twelve.cash, alebo si adresu vytvorte sami! + Chcete krajšiu adresu? Použite služby tretích strán, alebo si adresu vytvorte sami! diff --git a/phoenix-android/src/main/res/values-vi/important_strings.xml b/phoenix-android/src/main/res/values-vi/important_strings.xml index 7de7418d4..9e19665b5 100644 --- a/phoenix-android/src/main/res/values-vi/important_strings.xml +++ b/phoenix-android/src/main/res/values-vi/important_strings.xml @@ -468,6 +468,6 @@ Đây là địa chỉ mà con người có thể đọc được cho yêu cầu thanh toán Bolt12 của bạn. - Bạn muốn một địa chỉ đẹp hơn? Sử dụng các dịch vụ của bên thứ ba như Twelve.cash hoặc tự lưu trữ địa chỉ! + Bạn muốn một địa chỉ đẹp hơn? Sử dụng dịch vụ của bên thứ ba hoặc tự lưu trữ địa chỉ! diff --git a/phoenix-android/src/main/res/values/important_strings.xml b/phoenix-android/src/main/res/values/important_strings.xml index 0c7bada4f..ff84c087f 100644 --- a/phoenix-android/src/main/res/values/important_strings.xml +++ b/phoenix-android/src/main/res/values/important_strings.xml @@ -463,6 +463,6 @@ This is a human-readable address for your Bolt12 payment request. - Want a prettier address? Use third-party services like twelve.cash, or self-host the address! + Want a prettier address? Use third-party services, or self-host the address! \ No newline at end of file diff --git a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj index 46f068072..9b72c1382 100644 --- a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj +++ b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj @@ -185,6 +185,7 @@ DC6D68252AC1DD5C0099929F /* Currencies.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DC6D68242AC1DD5C0099929F /* Currencies.xcstrings */; }; DC6D68272AC1DD5C0099929F /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DC6D68262AC1DD5C0099929F /* Localizable.xcstrings */; }; DC6D68292AC1DD5C0099929F /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DC6D68282AC1DD5C0099929F /* InfoPlist.xcstrings */; }; + DC6F04232C35EB9900627B4F /* SummaryInfoGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6F04222C35EB9900627B4F /* SummaryInfoGrid.swift */; }; DC70A99C2BBB6093002DBFF8 /* InboundFeeWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC70A99B2BBB6093002DBFF8 /* InboundFeeWarning.swift */; }; DC71E7302723240E0063613D /* KotlinObservables.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC71E72F2723240E0063613D /* KotlinObservables.swift */; }; DC71E7332728645B0063613D /* CurrencyConverterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC71E7322728645B0063613D /* CurrencyConverterView.swift */; }; @@ -574,6 +575,7 @@ DC6D68242AC1DD5C0099929F /* Currencies.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Currencies.xcstrings; sourceTree = ""; }; DC6D68262AC1DD5C0099929F /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; DC6D68282AC1DD5C0099929F /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; + DC6F04222C35EB9900627B4F /* SummaryInfoGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryInfoGrid.swift; sourceTree = ""; }; DC70A99B2BBB6093002DBFF8 /* InboundFeeWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboundFeeWarning.swift; sourceTree = ""; }; DC71E72F2723240E0063613D /* KotlinObservables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KotlinObservables.swift; sourceTree = ""; }; DC71E7322728645B0063613D /* CurrencyConverterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyConverterView.swift; sourceTree = ""; }; @@ -1375,6 +1377,7 @@ children = ( 53BEF4DAE061532668494988 /* PaymentView.swift */, DCCD045E27EE0301007D57A5 /* SummaryView.swift */, + DC6F04222C35EB9900627B4F /* SummaryInfoGrid.swift */, DCCD046027EE045C007D57A5 /* DetailsView.swift */, DCCD045C27EE0173007D57A5 /* EditInfoView.swift */, DCCD046227EE04E1007D57A5 /* WalletPaymentExtensions.swift */, @@ -1603,8 +1606,9 @@ 7555FF73242A565900829871 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1330; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1540; ORGANIZATIONNAME = Acinq; TargetAttributes = { 7555FF7A242A565900829871 = { @@ -1984,6 +1988,7 @@ DC65D86428E2F7D700686355 /* ResetWalletView_Action.swift in Sources */, DC71E7332728645B0063613D /* CurrencyConverterView.swift in Sources */, DCA3B41F2A5471C900E6B231 /* MinerFeeInfo.swift in Sources */, + DC6F04232C35EB9900627B4F /* SummaryInfoGrid.swift in Sources */, DCACF6F02566D0A60009B01E /* Data+Hexadecimal.swift in Sources */, DCE3C7AB2A6AD3CC00F4D385 /* MempoolMonitor.swift in Sources */, DC3780392C04D60400937C8E /* KotlinEnums.swift in Sources */, @@ -2277,7 +2282,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Green"; CODE_SIGN_ENTITLEMENTS = "phoenix-ios/Phoenix.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_ASSET_PATHS = "\"phoenix-ios/Preview Content\""; DEVELOPMENT_TEAM = XD77LN4376; ENABLE_PREVIEWS = YES; @@ -2306,7 +2311,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Green"; CODE_SIGN_ENTITLEMENTS = "phoenix-ios/Phoenix.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_ASSET_PATHS = "\"phoenix-ios/Preview Content\""; DEVELOPMENT_TEAM = XD77LN4376; ENABLE_PREVIEWS = YES; @@ -2410,8 +2415,9 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_TEAM = XD77LN4376; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -2439,8 +2445,9 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_TEAM = XD77LN4376; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -2470,7 +2477,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "phoenix-notifySrvExt/phoenix-notifySrvExt.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_TEAM = XD77LN4376; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "phoenix-notifySrvExt/Info.plist"; @@ -2497,7 +2504,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "phoenix-notifySrvExt/phoenix-notifySrvExt.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_TEAM = XD77LN4376; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "phoenix-notifySrvExt/Info.plist"; diff --git a/phoenix-ios/phoenix-ios.xcodeproj/xcshareddata/xcschemes/phoenix-ios-framework.xcscheme b/phoenix-ios/phoenix-ios.xcodeproj/xcshareddata/xcschemes/phoenix-ios-framework.xcscheme index 9c2812d62..3ce63cfbd 100644 --- a/phoenix-ios/phoenix-ios.xcodeproj/xcshareddata/xcschemes/phoenix-ios-framework.xcscheme +++ b/phoenix-ios/phoenix-ios.xcodeproj/xcshareddata/xcschemes/phoenix-ios-framework.xcscheme @@ -1,6 +1,6 @@ + let minKeyColumnWidth: CGFloat = 50 + let maxKeyColumnWidth: CGFloat = 200 + + @State var keyColumnSizes: [InfoGridRow_KeyColumn_Size] = [] + func setKeyColumnSizes(_ value: [InfoGridRow_KeyColumn_Size]) { + keyColumnSizes = value + } + func getKeyColumnSizes() -> [InfoGridRow_KeyColumn_Size] { + return keyColumnSizes + } + + @State var rowSizes: [InfoGridRow_Size] = [] + func setRowSizes(_ sizes: [InfoGridRow_Size]) { + rowSizes = sizes + } + func getRowSizes() -> [InfoGridRow_Size] { + return rowSizes + } + // + + private let verticalSpacingBetweenRows: CGFloat = 12 + private let horizontalSpacingBetweenColumns: CGFloat = 8 + + @State var popoverPresent_standardFees = false + @State var popoverPresent_minerFees = false + @State var popoverPresent_serviceFees = false + + @Environment(\.openURL) var openURL + @EnvironmentObject var currencyPrefs: CurrencyPrefs + + @ViewBuilder + var infoGridRows: some View { + + VStack( + alignment : HorizontalAlignment.leading, + spacing : verticalSpacingBetweenRows + ) { + + // Splitting this up into separate ViewBuilders, + // because the compiler will sometimes choke while processing this method. + + paymentServiceRow() + paymentDescriptionRow() + paymentMessageRow() + customNotesRow() + attachedMessageRow() + paymentTypeRow() + channelClosingRow() + + paymentFeesRow_StandardFees() + paymentFeesRow_MinerFees() + paymentFeesRow_ServiceFees() + paymentDurationRow() + + paymentErrorRow() + } + .padding([.leading, .trailing]) + } + + @ViewBuilder + func keyColumn(_ title: LocalizedStringKey) -> some View { + + Text(title).foregroundColor(.secondary) + } + + @ViewBuilder + func keyColumn(verbatim title: String) -> some View { + + Text(title).foregroundColor(.secondary) + } + + @ViewBuilder + func paymentServiceRow() -> some View { + let identifier: String = #function + + if let lnurlPay = paymentInfo.metadata.lnurl?.pay { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Service") + + } valueColumn: { + + Text(lnurlPay.initialUrl.host) + + } // + } + } + + @ViewBuilder + func paymentDescriptionRow() -> some View { + let identifier: String = #function + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Desc") + .accessibilityLabel("Description") + + } valueColumn: { + + let description = paymentInfo.paymentDescription() ?? paymentInfo.defaultPaymentDescription() + Text(description) + .contextMenu { + Button(action: { + UIPasteboard.general.string = description + }) { + Text("Copy") + } + } + + } // + } + + @ViewBuilder + func paymentMessageRow() -> some View { + let identifier: String = #function + let successAction = paymentInfo.metadata.lnurl?.successAction + + if let sa_message = successAction as? LnurlPay.Invoice_SuccessAction_Message { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Message") + + } valueColumn: { + + Text(sa_message.message) + .contextMenu { + Button(action: { + UIPasteboard.general.string = sa_message.message + }) { + Text("Copy") + } + } + + } // + + } else if let sa_url = successAction as? LnurlPay.Invoice_SuccessAction_Url { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Message") + + } valueColumn: { + + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + + Text(sa_url.description_) + + if let url = URL(string: sa_url.url.description()) { + Button { + openURL(url) + } label: { + Text("open link") + } + .contextMenu { + Button(action: { + UIPasteboard.general.string = url.absoluteString + }) { + Text("Copy link") + } + } + } + } // + + } // + + } else if let sa_aes = successAction as? LnurlPay.Invoice_SuccessAction_Aes { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Message") + + } valueColumn: { + + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + + Text(sa_aes.description_) + + if let sa_aes_decrypted = decrypt(aes: sa_aes) { + + if let url = URL(string: sa_aes_decrypted.plaintext) { + Button { + openURL(url) + } label: { + Text("open link") + } + .contextMenu { + Button(action: { + UIPasteboard.general.string = url.absoluteString + }) { + Text("Copy link") + } + } + } else { + Text(sa_aes_decrypted.plaintext) + .contextMenu { + Button(action: { + UIPasteboard.general.string = sa_aes_decrypted.plaintext + }) { + Text("Copy") + } + } + } + + } else { + Text("") + } + } + } // + + } // + } + + @ViewBuilder + func customNotesRow() -> some View { + let identifier: String = #function + + if let notes = paymentInfo.metadata.userNotes, notes.count > 0 { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Notes") + + } valueColumn: { + + Text(notes) + + } // + } + } + + @ViewBuilder + func attachedMessageRow() -> some View { + + let identifier: String = #function + + if let msg = paymentInfo.attachedMessage() { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Message") + + } valueColumn: { + + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + Text(msg) + if paymentInfo.payment.isIncoming() { + Text("Be careful with messages from unknown sources") + .foregroundColor(.secondary) + .font(.subheadline) + } + } + + + + } // + } + } + + @ViewBuilder + func paymentTypeRow() -> some View { + let identifier: String = #function + + if let paymentTypeTuple = paymentInfo.payment.paymentType() { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Type") + + } valueColumn: { + + VStack(alignment: HorizontalAlignment.leading, spacing: 0) { + + let (type, explanation) = paymentTypeTuple + Text(type) + + Text(verbatim: " (\(explanation))") + .font(.footnote) + .foregroundColor(.secondary) + + if let link = paymentInfo.payment.paymentLink() { + Button { + openURL(link) + } label: { + Text("view on blockchain") + } + } + } + + } // + } + } + + @ViewBuilder + func channelClosingRow() -> some View { + let identifier: String = #function + + if let pClosingInfo = paymentInfo.payment.channelClosing() { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Output") + + } valueColumn: { + + VStack(alignment: HorizontalAlignment.leading, spacing: 0) { + + // Bitcoin address (copyable) + Text(pClosingInfo.address) + .contextMenu { + Button(action: { + UIPasteboard.general.string = pClosingInfo.address + }) { + Text("Copy") + } + } + + if pClosingInfo.isSentToDefaultAddress { + Text("(This is your address - derived from your seed. You alone possess your seed.)") + .font(.footnote) + .foregroundColor(.secondary) + .padding(.top, 4) + } + } + + } // + } + } + + @ViewBuilder + func paymentFeesRow_StandardFees() -> some View { + + if let standardFees = paymentInfo.payment.standardFees() { + paymentFeesRow( + msat: standardFees.0, + title: standardFees.1, + explanation: standardFees.2, + binding: $popoverPresent_standardFees + ) + } + } + + @ViewBuilder + func paymentFeesRow_MinerFees() -> some View { + + if let minerFees = paymentInfo.payment.minerFees() { + paymentFeesRow( + msat: minerFees.0, + title: minerFees.1, + explanation: minerFees.2, + binding: $popoverPresent_minerFees + ) + } + } + + @ViewBuilder + func paymentFeesRow_ServiceFees() -> some View { + + if let serviceFees = paymentInfo.payment.serviceFees() { + paymentFeesRow( + msat: serviceFees.0, + title: serviceFees.1, + explanation: serviceFees.2, + binding: $popoverPresent_serviceFees + ) + } + } + + @ViewBuilder + func paymentFeesRow( + msat: Int64, + title: String, + explanation: String, + binding: Binding + ) -> some View { + let identifier: String = "paymentFeesRow:\(title)" + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn(verbatim: title) + + } valueColumn: { + + HStack(alignment: VerticalAlignment.center, spacing: 6) { + + let amount = formattedAmount(msat: msat) + HStack(alignment: VerticalAlignment.firstTextBaseline, spacing: 0) { + + if amount.hasSubFractionDigits { + + // We're showing sub-fractional values. + // For example, we're showing millisatoshis. + // + // It's helpful to downplay the sub-fractional part visually. + + let hasStdFractionDigits = amount.hasStdFractionDigits + + Text(verbatim: amount.integerDigits) + + Text(verbatim: amount.decimalSeparator) + .foregroundColor(hasStdFractionDigits ? .primary : .secondary) + + Text(verbatim: amount.stdFractionDigits) + + Text(verbatim: amount.subFractionDigits) + .foregroundColor(.secondary) + .font(.callout) + .fontWeight(.light) + + } else { + Text(amount.digits) + } + + Text(verbatim: " ") + Text_CurrencyName(currency: amount.currency, fontTextStyle: .body) + + } // + .onTapGesture { toggleCurrencyType() } + .accessibilityLabel("\(amount.string)") + + if !explanation.isEmpty { + + Button { + binding.wrappedValue.toggle() + } label: { + Image(systemName: "questionmark.circle") + .renderingMode(.template) + .foregroundColor(.secondary) + .font(.body) + } + .popover(present: binding) { + InfoPopoverWindow { + Text(verbatim: explanation) + } + } + } + } // + + } // + } + + @ViewBuilder + func paymentDurationRow() -> some View { + let identifier: String = #function + + if let _ = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Duration") + + } valueColumn: { + + Text("1 year") + + } // + } + } + + @ViewBuilder + func paymentErrorRow() -> some View { + let identifier: String = #function + + if let pError = paymentInfo.payment.paymentFinalError() { + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Error") + + } valueColumn: { + + Text(pError) + + } // + } + } + + func formattedAmount(msat: Int64) -> FormattedAmount { + + if showOriginalFiatValue && currencyPrefs.currencyType == .fiat { + + if let originalExchangeRate = paymentInfo.metadata.originalFiat { + return Utils.formatFiat(msat: msat, exchangeRate: originalExchangeRate) + } else { + return Utils.unknownFiatAmount(fiatCurrency: currencyPrefs.fiatCurrency) + } + + } else { + + return Utils.format(currencyPrefs, msat: msat, policy: .showMsatsIfNonZero) + } + } + + func decrypt(aes sa_aes: LnurlPay.Invoice_SuccessAction_Aes) -> LnurlPay.Invoice_SuccessAction_Aes_Decrypted? { + + guard + let outgoingPayment = paymentInfo.payment as? Lightning_kmpLightningOutgoingPayment, + let offchainSuccess = outgoingPayment.status.asOffChain() + else { + return nil + } + + do { + let aes = try AES256( + key: offchainSuccess.preimage.toSwiftData(), + iv: sa_aes.iv.toSwiftData() + ) + + let plaintext_data = try aes.decrypt(sa_aes.ciphertext.toSwiftData(), padding: .PKCS7) + if let plaintext_str = String(bytes: plaintext_data, encoding: .utf8) { + + return LnurlPay.Invoice_SuccessAction_Aes_Decrypted( + description: sa_aes.description_, + plaintext: plaintext_str + ) + } + + } catch { + log.error("Error decrypting LnurlPay.Invoice_SuccessAction_Aes: \(String(describing: error))") + } + + return nil + } + + func toggleCurrencyType() -> Void { + currencyPrefs.toggleCurrencyType() + } +} diff --git a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift index 3c45e15f5..daa7bd239 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift @@ -29,6 +29,10 @@ struct SummaryView: View { @State var didAppear = false @State var popToDestination: PopToDestination? = nil + @State var buttonListTruncationDetected_standard: Bool = false + @State var buttonListTruncationDetected_squeezed: Bool = false + @State var buttonListTruncationDetected_compact: Bool = false + @Environment(\.presentationMode) var presentationMode: Binding @EnvironmentObject var currencyPrefs: CurrencyPrefs @@ -472,67 +476,295 @@ struct SummaryView: View { @ViewBuilder func buttonList() -> some View { - // Details | Edit | Delete + Group { + if buttonListTruncationDetected_compact { + buttonList_accessibility() + } else if buttonListTruncationDetected_squeezed { + buttonList_compact() + } else if buttonListTruncationDetected_standard { + buttonList_squeezed() + } else { + buttonList_standard() + } + } // + .confirmationDialog("Delete payment?", + isPresented: $showDeletePaymentConfirmationDialog, + titleVisibility: Visibility.hidden + ) { + Button("Delete payment", role: ButtonRole.destructive) { + deletePayment() + } + } + } + + @ViewBuilder + func buttonList_standard() -> some View { + + // We're making all the buttons the same size. + // + // --------------------------- + // Details | Edit | Delete + // --------------------------- + // ^ ^ ^ < same size HStack(alignment: VerticalAlignment.center, spacing: 16) { - NavigationLink(destination: detailsView()) { - Label { - Text("Details") - } icon: { - Image(systemName: "magnifyingglass").imageScale(.small) + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + NavigationLink(destination: detailsView()) { + buttonLabel_details() + .lineLimit(1) } .frame(minWidth: buttonWidth, alignment: Alignment.trailing) .read(buttonWidthReader) .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_standard = true (details)") + buttonListTruncationDetected_standard = true } if let buttonHeight = buttonHeight { Divider().frame(height: buttonHeight) } - NavigationLink(destination: editInfoView()) { - Label { - Text("Edit") - } icon: { - Image(systemName: "pencil.line").imageScale(.small) + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + NavigationLink(destination: editInfoView()) { + buttonLabel_edit() } - .frame(minWidth: buttonWidth, alignment: Alignment.center) + .frame(minWidth: buttonWidth, alignment: Alignment.trailing) .read(buttonWidthReader) .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_standard = true (edit)") + buttonListTruncationDetected_standard = true } if let buttonHeight = buttonHeight { Divider().frame(height: buttonHeight) } - Button { - showDeletePaymentConfirmationDialog = true - } label: { - Label { - Text("Delete") - } icon: { - Image(systemName: "eraser.line.dashed").imageScale(.small) + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + Button { + showDeletePaymentConfirmationDialog = true + } label: { + buttonLabel_delete() } - .foregroundColor(.appNegative) .frame(minWidth: buttonWidth, alignment: Alignment.leading) .read(buttonWidthReader) .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_standard = true (delete)") + buttonListTruncationDetected_standard = true } - .confirmationDialog("Delete payment?", - isPresented: $showDeletePaymentConfirmationDialog, - titleVisibility: Visibility.hidden - ) { - Button("Delete payment", role: ButtonRole.destructive) { - deletePayment() + } + .padding(.all) + .assignMaxPreference(for: buttonWidthReader.key, to: $buttonWidth) + .assignMaxPreference(for: buttonHeightReader.key, to: $buttonHeight) + } + + @ViewBuilder + func buttonList_squeezed() -> some View { + + // There's not enough space to make all the buttons the same size. + // So we're just making the left & right buttons the same size. + // This still ensures that the center button is perfectly centered on screen. + // + // --------------------------- + // Details | Edit | Delete + // --------------------------- + // ^ ^ < same size + + HStack(alignment: VerticalAlignment.center, spacing: 16) { + + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + NavigationLink(destination: detailsView()) { + buttonLabel_details() + .lineLimit(1) + } + .frame(minWidth: buttonWidth, alignment: Alignment.trailing) + .read(buttonWidthReader) + .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_squeezed = true (edit)") + buttonListTruncationDetected_squeezed = true + } + + if let buttonHeight = buttonHeight { + Divider().frame(height: buttonHeight) + } + + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + NavigationLink(destination: editInfoView()) { + buttonLabel_edit() + .lineLimit(1) + } + .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_squeezed = true (edit)") + buttonListTruncationDetected_squeezed = true + } + + if let buttonHeight = buttonHeight { + Divider().frame(height: buttonHeight) + } + + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + Button { + showDeletePaymentConfirmationDialog = true + } label: { + buttonLabel_delete() + .lineLimit(1) } + .frame(minWidth: buttonWidth, alignment: Alignment.leading) + .read(buttonWidthReader) + .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_squeezed = true (delete)") + buttonListTruncationDetected_squeezed = true } } - .padding([.top, .bottom]) + .padding(.horizontal, 10) // allow content to be closer to edges + .padding(.vertical) .assignMaxPreference(for: buttonWidthReader.key, to: $buttonWidth) .assignMaxPreference(for: buttonHeightReader.key, to: $buttonHeight) } + @ViewBuilder + func buttonList_compact() -> some View { + + // There's a large font being used, and possibly a small screen too. + // Thus horizontal space is tight. + // + // So we're going to just try to squeeze all the buttons into a single line. + // + // ----------------------- + // Details | Edit | Delete + // ----------------------- + // ^ might not be centered, but at least the buttons fit on 1 line + + HStack(alignment: VerticalAlignment.center, spacing: 8) { + + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + NavigationLink(destination: detailsView()) { + buttonLabel_details() + .lineLimit(1) + } + .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_compact = true (details)") + buttonListTruncationDetected_compact = true + } + + if let buttonHeight = buttonHeight { + Divider().frame(height: buttonHeight) + } + + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + NavigationLink(destination: editInfoView()) { + buttonLabel_edit() + .lineLimit(1) + } + .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_compact = true (edit)") + buttonListTruncationDetected_compact = true + } + + if let buttonHeight = buttonHeight { + Divider().frame(height: buttonHeight) + } + + TruncatableView(fixedHorizontal: true, fixedVertical: true) { + Button { + showDeletePaymentConfirmationDialog = true + } label: { + buttonLabel_delete() + .lineLimit(1) + } + .read(buttonHeightReader) + } wasTruncated: { + log.debug("buttonListTruncationDetected_compact = true (delete)") + buttonListTruncationDetected_compact = true + } + } + .padding(.horizontal, 4) // allow content to be closer to edges + .padding(.vertical) + .assignMaxPreference(for: buttonHeightReader.key, to: $buttonHeight) + } + + @ViewBuilder + func buttonList_accessibility() -> some View { + + // There's a large font being used, and possibly a small screen too. + // Horizontal space is so tight that we can't get the 3 buttons on a single line. + // + // So we're going to put them on multiple lines. + // + // -------------- + // Details | Edit + // Delete + // -------------- + + VStack(alignment: HorizontalAlignment.center, spacing: 16) { + + HStack(alignment: VerticalAlignment.center, spacing: 8) { + + NavigationLink(destination: detailsView()) { + buttonLabel_details() + .read(buttonHeightReader) + } + + if let buttonHeight = buttonHeight { + Divider().frame(height: buttonHeight) + } + + NavigationLink(destination: editInfoView()) { + buttonLabel_edit() + .read(buttonHeightReader) + } + } + + Button { + showDeletePaymentConfirmationDialog = true + } label: { + buttonLabel_delete() + } + } + .padding(.horizontal, 4) // allow content to be closer to edges + .padding(.vertical) + .assignMaxPreference(for: buttonHeightReader.key, to: $buttonHeight) + } + + @ViewBuilder + func buttonLabel_details() -> some View { + + Label { + Text("Details") + } icon: { + Image(systemName: "magnifyingglass").imageScale(.small) + } + } + + @ViewBuilder + func buttonLabel_edit() -> some View { + + Label { + Text("Edit") + } icon: { + Image(systemName: "pencil.line").imageScale(.small) + } + } + + @ViewBuilder + func buttonLabel_delete() -> some View { + + Label { + Text("Delete") + } icon: { + Image(systemName: "eraser.line.dashed").imageScale(.small) + } + .foregroundColor(.appNegative) + } + @ViewBuilder func detailsView() -> some View { DetailsView( @@ -808,609 +1040,3 @@ struct SummaryView: View { } } } - -// See InfoGridView for architecture discussion. -// -fileprivate struct SummaryInfoGrid: InfoGridView { - - @Binding var paymentInfo: WalletPaymentInfo - @Binding var showOriginalFiatValue: Bool - - // - let minKeyColumnWidth: CGFloat = 50 - let maxKeyColumnWidth: CGFloat = 200 - - @State var keyColumnSizes: [InfoGridRow_KeyColumn_Size] = [] - func setKeyColumnSizes(_ value: [InfoGridRow_KeyColumn_Size]) { - keyColumnSizes = value - } - func getKeyColumnSizes() -> [InfoGridRow_KeyColumn_Size] { - return keyColumnSizes - } - - @State var rowSizes: [InfoGridRow_Size] = [] - func setRowSizes(_ sizes: [InfoGridRow_Size]) { - rowSizes = sizes - } - func getRowSizes() -> [InfoGridRow_Size] { - return rowSizes - } - // - - private let verticalSpacingBetweenRows: CGFloat = 12 - private let horizontalSpacingBetweenColumns: CGFloat = 8 - - @State var popoverPresent_standardFees = false - @State var popoverPresent_minerFees = false - @State var popoverPresent_serviceFees = false - - @Environment(\.openURL) var openURL - @EnvironmentObject var currencyPrefs: CurrencyPrefs - - @ViewBuilder - var infoGridRows: some View { - - VStack( - alignment : HorizontalAlignment.leading, - spacing : verticalSpacingBetweenRows - ) { - - // Splitting this up into separate ViewBuilders, - // because the compiler will sometimes choke while processing this method. - - paymentServiceRow() - paymentDescriptionRow() - paymentMessageRow() - customNotesRow() - attachedMessageRow() - paymentTypeRow() - channelClosingRow() - - paymentFeesRow_StandardFees() - paymentFeesRow_MinerFees() - paymentFeesRow_ServiceFees() - paymentDurationRow() - - paymentErrorRow() - } - .padding([.leading, .trailing]) - } - - @ViewBuilder - func keyColumn(_ title: LocalizedStringKey) -> some View { - - Text(title).foregroundColor(.secondary) - } - - @ViewBuilder - func keyColumn(verbatim title: String) -> some View { - - Text(title).foregroundColor(.secondary) - } - - @ViewBuilder - func paymentServiceRow() -> some View { - let identifier: String = #function - - if let lnurlPay = paymentInfo.metadata.lnurl?.pay { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Service") - - } valueColumn: { - - Text(lnurlPay.initialUrl.host) - - } // - } - } - - @ViewBuilder - func paymentDescriptionRow() -> some View { - let identifier: String = #function - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Desc") - .accessibilityLabel("Description") - - } valueColumn: { - - let description = paymentInfo.paymentDescription() ?? paymentInfo.defaultPaymentDescription() - Text(description) - .contextMenu { - Button(action: { - UIPasteboard.general.string = description - }) { - Text("Copy") - } - } - - } // - } - - @ViewBuilder - func paymentMessageRow() -> some View { - let identifier: String = #function - let successAction = paymentInfo.metadata.lnurl?.successAction - - if let sa_message = successAction as? LnurlPay.Invoice_SuccessAction_Message { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Message") - - } valueColumn: { - - Text(sa_message.message) - .contextMenu { - Button(action: { - UIPasteboard.general.string = sa_message.message - }) { - Text("Copy") - } - } - - } // - - } else if let sa_url = successAction as? LnurlPay.Invoice_SuccessAction_Url { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Message") - - } valueColumn: { - - VStack(alignment: HorizontalAlignment.leading, spacing: 4) { - - Text(sa_url.description_) - - if let url = URL(string: sa_url.url.description()) { - Button { - openURL(url) - } label: { - Text("open link") - } - .contextMenu { - Button(action: { - UIPasteboard.general.string = url.absoluteString - }) { - Text("Copy link") - } - } - } - } // - - } // - - } else if let sa_aes = successAction as? LnurlPay.Invoice_SuccessAction_Aes { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Message") - - } valueColumn: { - - VStack(alignment: HorizontalAlignment.leading, spacing: 4) { - - Text(sa_aes.description_) - - if let sa_aes_decrypted = decrypt(aes: sa_aes) { - - if let url = URL(string: sa_aes_decrypted.plaintext) { - Button { - openURL(url) - } label: { - Text("open link") - } - .contextMenu { - Button(action: { - UIPasteboard.general.string = url.absoluteString - }) { - Text("Copy link") - } - } - } else { - Text(sa_aes_decrypted.plaintext) - .contextMenu { - Button(action: { - UIPasteboard.general.string = sa_aes_decrypted.plaintext - }) { - Text("Copy") - } - } - } - - } else { - Text("") - } - } - } // - - } // - } - - @ViewBuilder - func customNotesRow() -> some View { - let identifier: String = #function - - if let notes = paymentInfo.metadata.userNotes, notes.count > 0 { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Notes") - - } valueColumn: { - - Text(notes) - - } // - } - } - - @ViewBuilder - func attachedMessageRow() -> some View { - - let identifier: String = #function - - if let msg = paymentInfo.attachedMessage() { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Message") - - } valueColumn: { - - VStack(alignment: HorizontalAlignment.leading, spacing: 4) { - Text(msg) - if paymentInfo.payment.isIncoming() { - Text("Be careful with messages from unknown sources") - .foregroundColor(.secondary) - .font(.subheadline) - } - } - - - - } // - } - } - - @ViewBuilder - func paymentTypeRow() -> some View { - let identifier: String = #function - - if let paymentTypeTuple = paymentInfo.payment.paymentType() { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Type") - - } valueColumn: { - - VStack(alignment: HorizontalAlignment.leading, spacing: 0) { - - let (type, explanation) = paymentTypeTuple - Text(type) - + Text(verbatim: " (\(explanation))") - .font(.footnote) - .foregroundColor(.secondary) - - if let link = paymentInfo.payment.paymentLink() { - Button { - openURL(link) - } label: { - Text("view on blockchain") - } - } - } - - } // - } - } - - @ViewBuilder - func channelClosingRow() -> some View { - let identifier: String = #function - - if let pClosingInfo = paymentInfo.payment.channelClosing() { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Output") - - } valueColumn: { - - VStack(alignment: HorizontalAlignment.leading, spacing: 0) { - - // Bitcoin address (copyable) - Text(pClosingInfo.address) - .contextMenu { - Button(action: { - UIPasteboard.general.string = pClosingInfo.address - }) { - Text("Copy") - } - } - - if pClosingInfo.isSentToDefaultAddress { - Text("(This is your address - derived from your seed. You alone possess your seed.)") - .font(.footnote) - .foregroundColor(.secondary) - .padding(.top, 4) - } - } - - } // - } - } - - @ViewBuilder - func paymentFeesRow_StandardFees() -> some View { - - if let standardFees = paymentInfo.payment.standardFees() { - paymentFeesRow( - msat: standardFees.0, - title: standardFees.1, - explanation: standardFees.2, - binding: $popoverPresent_standardFees - ) - } - } - - @ViewBuilder - func paymentFeesRow_MinerFees() -> some View { - - if let minerFees = paymentInfo.payment.minerFees() { - paymentFeesRow( - msat: minerFees.0, - title: minerFees.1, - explanation: minerFees.2, - binding: $popoverPresent_minerFees - ) - } - } - - @ViewBuilder - func paymentFeesRow_ServiceFees() -> some View { - - if let serviceFees = paymentInfo.payment.serviceFees() { - paymentFeesRow( - msat: serviceFees.0, - title: serviceFees.1, - explanation: serviceFees.2, - binding: $popoverPresent_serviceFees - ) - } - } - - @ViewBuilder - func paymentFeesRow( - msat: Int64, - title: String, - explanation: String, - binding: Binding - ) -> some View { - let identifier: String = "paymentFeesRow:\(title)" - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn(verbatim: title) - - } valueColumn: { - - HStack(alignment: VerticalAlignment.center, spacing: 6) { - - let amount = formattedAmount(msat: msat) - HStack(alignment: VerticalAlignment.firstTextBaseline, spacing: 0) { - - if amount.hasSubFractionDigits { - - // We're showing sub-fractional values. - // For example, we're showing millisatoshis. - // - // It's helpful to downplay the sub-fractional part visually. - - let hasStdFractionDigits = amount.hasStdFractionDigits - - Text(verbatim: amount.integerDigits) - + Text(verbatim: amount.decimalSeparator) - .foregroundColor(hasStdFractionDigits ? .primary : .secondary) - + Text(verbatim: amount.stdFractionDigits) - + Text(verbatim: amount.subFractionDigits) - .foregroundColor(.secondary) - .font(.callout) - .fontWeight(.light) - - } else { - Text(amount.digits) - } - - Text(verbatim: " ") - Text_CurrencyName(currency: amount.currency, fontTextStyle: .body) - - } // - .onTapGesture { toggleCurrencyType() } - .accessibilityLabel("\(amount.string)") - - if !explanation.isEmpty { - - Button { - binding.wrappedValue.toggle() - } label: { - Image(systemName: "questionmark.circle") - .renderingMode(.template) - .foregroundColor(.secondary) - .font(.body) - } - .popover(present: binding) { - InfoPopoverWindow { - Text(verbatim: explanation) - } - } - } - } // - - } // - } - - @ViewBuilder - func paymentDurationRow() -> some View { - let identifier: String = #function - - if let _ = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Duration") - - } valueColumn: { - - Text("1 year") - - } // - } - } - - @ViewBuilder - func paymentErrorRow() -> some View { - let identifier: String = #function - - if let pError = paymentInfo.payment.paymentFinalError() { - - InfoGridRow( - identifier: identifier, - vAlignment: .firstTextBaseline, - hSpacing: horizontalSpacingBetweenColumns, - keyColumnWidth: keyColumnWidth(identifier: identifier), - keyColumnAlignment: .trailing - ) { - - keyColumn("Error") - - } valueColumn: { - - Text(pError) - - } // - } - } - - func formattedAmount(msat: Int64) -> FormattedAmount { - - if showOriginalFiatValue && currencyPrefs.currencyType == .fiat { - - if let originalExchangeRate = paymentInfo.metadata.originalFiat { - return Utils.formatFiat(msat: msat, exchangeRate: originalExchangeRate) - } else { - return Utils.unknownFiatAmount(fiatCurrency: currencyPrefs.fiatCurrency) - } - - } else { - - return Utils.format(currencyPrefs, msat: msat, policy: .showMsatsIfNonZero) - } - } - - func decrypt(aes sa_aes: LnurlPay.Invoice_SuccessAction_Aes) -> LnurlPay.Invoice_SuccessAction_Aes_Decrypted? { - - guard - let outgoingPayment = paymentInfo.payment as? Lightning_kmpLightningOutgoingPayment, - let offchainSuccess = outgoingPayment.status.asOffChain() - else { - return nil - } - - do { - let aes = try AES256( - key: offchainSuccess.preimage.toSwiftData(), - iv: sa_aes.iv.toSwiftData() - ) - - let plaintext_data = try aes.decrypt(sa_aes.ciphertext.toSwiftData(), padding: .PKCS7) - if let plaintext_str = String(bytes: plaintext_data, encoding: .utf8) { - - return LnurlPay.Invoice_SuccessAction_Aes_Decrypted( - description: sa_aes.description_, - plaintext: plaintext_str - ) - } - - } catch { - log.error("Error decrypting LnurlPay.Invoice_SuccessAction_Aes: \(String(describing: error))") - } - - return nil - } - - func toggleCurrencyType() -> Void { - currencyPrefs.toggleCurrencyType() - } -} diff --git a/phoenix-ios/phoenix-ios/views/send/PaymentDetails.swift b/phoenix-ios/phoenix-ios/views/send/PaymentDetails.swift index 9dd02099b..5661afedb 100644 --- a/phoenix-ios/phoenix-ios/views/send/PaymentDetails.swift +++ b/phoenix-ios/phoenix-ios/views/send/PaymentDetails.swift @@ -8,11 +8,19 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .trace) fileprivate var log = LoggerFactory.shared.logger(filename, .warning) #endif +fileprivate let grid_horizontalSpacing: CGFloat = 8 +fileprivate let grid_verticalSpacing: CGFloat = 12 + struct PaymentDetails: View { let parent: ValidateView - private let valueFont: Font = .subheadline + enum GridWidth: Preference {} + let gridWidthReader = GeometryPreferenceReader( + key: AppendValue.self, + value: { [$0.size.width] } + ) + @State var gridWidth: CGFloat? = nil @ViewBuilder var body: some View { @@ -34,11 +42,24 @@ struct PaymentDetails: View { @available(iOS 16.0, *) func details_ios16() -> some View { - Grid(horizontalSpacing: 8, verticalSpacing: 12) { + HStack(alignment: VerticalAlignment.center, spacing: 0) { + Spacer(minLength: 0) + grid() + Spacer(minLength: 0) + } + .read(gridWidthReader) + .assignMaxPreference(for: gridWidthReader.key, to: $gridWidth) + } + + @ViewBuilder + @available(iOS 16.0, *) + func grid() -> some View { + + Grid(horizontalSpacing: grid_horizontalSpacing, verticalSpacing: grid_verticalSpacing) { if let model = parent.mvi.model as? Scan.Model_OnChainFlow { gridRows_onChain(model) - } else { + } else if requestDescription() != nil { gridRows_description() } if let model = parent.mvi.model as? Scan.Model_OfferFlow { @@ -52,23 +73,22 @@ struct PaymentDetails: View { @ViewBuilder @available(iOS 16.0, *) - func gridRows_onChain(_ model: Scan.Model_OnChainFlow) -> some View { + func gridRows_onChain( + _ model: Scan.Model_OnChainFlow + ) -> some View { let message = bitcoinUriMessage() if message != nil { - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("Type") - + } valueColumn: { Text("On-Chain Payment") - .font(valueFont) - .gridCellColumns(2) - .gridCellAnchor(.leading) - - } // + } // } - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + + GridRowWrapper(gridWidth: gridWidth) { titleColumn("Desc") - + } valueColumn: { Group { if let message { Text(message) @@ -76,21 +96,15 @@ struct PaymentDetails: View { Text("On-Chain Payment") } } - .font(valueFont) - .gridCellColumns(2) - .gridCellAnchor(.leading) - - } // - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + } // + + GridRowWrapper(gridWidth: gridWidth) { titleColumn("Send To") - + } valueColumn: { let btcAddr = model.uri.address Text(btcAddr) .lineLimit(2) .truncationMode(.middle) - .font(valueFont) - .gridCellColumns(2) - .gridCellAnchor(.leading) .contextMenu { Button { UIPasteboard.general.string = btcAddr @@ -98,127 +112,46 @@ struct PaymentDetails: View { Text("Copy") } } - } // + } // } @ViewBuilder @available(iOS 16.0, *) func gridRows_description() -> some View { - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("Desc") - - Text(requestDescription()) + } valueColumn: { + Text(requestDescription() ?? "") .lineLimit(3) .truncationMode(.tail) - .font(valueFont) - .gridCellColumns(2) - .gridCellAnchor(.leading) - - } // + } // } @ViewBuilder @available(iOS 16.0, *) - func gridRows_offer(_ model: Scan.Model_OfferFlow) -> some View { + func gridRows_offer( + _ model: Scan.Model_OfferFlow + ) -> some View { - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("Send To") - - if CONTACTS_ENABLED, let contact = parent.contact { - - HStack(alignment: VerticalAlignment.center, spacing: 4) { - Group { - if let photoUri = contact.photoUri, - let uiImage = UIImage(contentsOfFile: photoUri) - { - Image(uiImage: uiImage) - .resizable() - .aspectRatio(contentMode: .fill) // FILL ! - } else { - Image(systemName: "person.circle") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.gray) - } - } - .frame(width: 32, height: 32) - .clipShape(Circle()) - - Text(contact.name) - } - .font(valueFont) - .onTapGesture { - parent.showManageContactSheet() - } - .gridCellColumns(2) - .gridCellAnchor(.leading) - - } else { - - let offer: String = model.offer.encode() - VStack(alignment: HorizontalAlignment.leading, spacing: 4) { - Text(offer) - .lineLimit(2) - .truncationMode(.middle) - .contextMenu { - Button { - UIPasteboard.general.string = offer - } label: { - Text("Copy") - } - } - if CONTACTS_ENABLED { - Button { - parent.showManageContactSheet() - } label: { - HStack(alignment: VerticalAlignment.firstTextBaseline, spacing: 2) { - Image(systemName: "person") - Text("Add contact") - } - } - } - } // - .font(valueFont) - .gridCellColumns(2) - .gridCellAnchor(.leading) - } - } // + } valueColumn: { + valueColumn_offer_sendTo(model) + } // - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("Message") - - Group { - let comment = parent.comment - if comment.isEmpty { - Button { - parent.showCommentSheet() - } label: { - Text("Attach a message") - } - } else { - HStack(alignment: VerticalAlignment.firstTextBaseline, spacing: 4) { - Text(comment) - .lineLimit(3) - .truncationMode(.tail) - Button { - parent.showCommentSheet() - } label: { - Image(systemName: "square.and.pencil").font(.body) - } - } - } - } // - .font(valueFont) - .gridCellColumns(2) - .gridCellAnchor(.leading) - - } // + } valueColumn: { + valueColumn_offer_message(model) + } // } @ViewBuilder @available(iOS 16.0, *) - func gridRows_paymentSummary(_ info: PaymentSummaryStrings) -> some View { + func gridRows_paymentSummary( + _ info: PaymentSummaryStrings + ) -> some View { let titleColor = info.isEmpty ? Color.clear : Color.secondary let bitcoinColor = info.isEmpty ? Color.clear : Color.primary @@ -226,96 +159,77 @@ struct PaymentDetails: View { let percentColor = info.isEmpty ? Color.clear : Color.secondary if info.hasTip { - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("tip", titleColor) - + } valueColumn: { VStack(alignment: HorizontalAlignment.leading, spacing: 4) { Text(verbatim: info.bitcoin_tip.string) .foregroundColor(bitcoinColor) Text(verbatim: " ≈\(info.fiat_tip.string)") .foregroundColor(fiatColor) + Text(verbatim: info.percent_tip) + .foregroundColor(percentColor) } - .font(valueFont) - .gridCellAnchor(.leading) - - Text(verbatim: info.percent_tip) - .font(valueFont) - .foregroundColor(percentColor) - .gridCellAnchor(.leading) - - } // + } // .accessibilityLabel(accessibilityLabel_tipAmount(info)) } // if info.hasLightningFee || info.isEmpty { - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("lightning fee", titleColor) - + } valueColumn: { VStack(alignment: HorizontalAlignment.leading, spacing: 4) { Text(verbatim: info.bitcoin_lightningFee.string) .foregroundColor(bitcoinColor) Text(verbatim: " ≈\(info.fiat_lightningFee.string)") .foregroundColor(fiatColor) + Text(verbatim: info.percent_lightningFee) + .foregroundColor(percentColor) } - .font(valueFont) - .gridCellAnchor(.leading) - - Text(verbatim: info.percent_lightningFee) - .font(valueFont) - .foregroundColor(percentColor) - .gridCellAnchor(.leading) - - } // + } // .accessibilityLabel(accessibilityLabel_lightningFeeAmount(info)) } // if info.hasMinerFee { - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("miner fee", titleColor) - - VStack(alignment: HorizontalAlignment.leading, spacing: 4) { - Text(verbatim: info.bitcoin_minerFee.string) - .foregroundColor(bitcoinColor) - Text(verbatim: " ≈\(info.fiat_minerFee.string)") - .foregroundColor(fiatColor) - } - .font(valueFont) - .gridCellAnchor(.leading) - - VStack(alignment: HorizontalAlignment.leading, spacing: 4) { - Text(verbatim: info.percent_minerFee) - .font(valueFont) - .foregroundColor(percentColor) + } valueColumn: { + HStack(alignment: VerticalAlignment.center, spacing: 4) { + + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + Text(verbatim: info.bitcoin_minerFee.string) + .foregroundColor(bitcoinColor) + Text(verbatim: " ≈\(info.fiat_minerFee.string)") + .foregroundColor(fiatColor) + Text(verbatim: info.percent_minerFee) + .foregroundColor(percentColor) + } // Button { parent.showMinerFeeSheet() } label: { Image(systemName: "square.and.pencil").font(.body) } - } - .gridCellAnchor(.leading) - - } // + + } // + } // .accessibilityLabel(accessibilityLabel_minerFeeAmount(info)) } // if info.hasTip || info.hasLightningFee || info.hasMinerFee || info.isEmpty { - GridRow(alignment: VerticalAlignment.firstTextBaseline) { + GridRowWrapper(gridWidth: gridWidth) { titleColumn("total", titleColor) - + } valueColumn: { VStack(alignment: HorizontalAlignment.leading, spacing: 4) { Text(verbatim: info.bitcoin_total.string) .foregroundColor(bitcoinColor) Text(verbatim: " ≈\(info.fiat_total.string)") .foregroundColor(fiatColor) } - .font(valueFont) - .gridCellAnchor(.leading) - - } // + } // .accessibilityLabel(accessibilityLabel_totalAmount(info)) } // @@ -323,14 +237,101 @@ struct PaymentDetails: View { @ViewBuilder @available(iOS 16.0, *) - func titleColumn(_ title: LocalizedStringKey, _ color: Color = Color.secondary) -> some View { + func titleColumn( + _ title: LocalizedStringKey, + _ color: Color = Color.secondary + ) -> some View { Text(title) .textCase(.uppercase) - .font(.subheadline) .foregroundColor(color) - .gridCellAnchor(.trailing) - // .frame(minWidth: 125, alignment: .trailing) // Todo: Replace hard-coded value + } + + @ViewBuilder + func valueColumn_offer_sendTo( + _ model: Scan.Model_OfferFlow + ) -> some View { + + if CONTACTS_ENABLED, let contact = parent.contact { + + HStack(alignment: VerticalAlignment.center, spacing: 4) { + Group { + if let photoUri = contact.photoUri, + let uiImage = UIImage(contentsOfFile: photoUri) + { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) // FILL ! + } else { + Image(systemName: "person.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.gray) + } + } + .frame(width: 32, height: 32) + .clipShape(Circle()) + + Text(contact.name) + } // + .onTapGesture { + parent.showManageContactSheet() + } + + } else { + + let offer: String = model.offer.encode() + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + Text(offer) + .lineLimit(2) + .truncationMode(.middle) + .contextMenu { + Button { + UIPasteboard.general.string = offer + } label: { + Text("Copy") + } + } + if CONTACTS_ENABLED { + Button { + parent.showManageContactSheet() + } label: { + HStack(alignment: VerticalAlignment.firstTextBaseline, spacing: 2) { + Image(systemName: "person") + Text("Add contact") + } + } + } + } // + } + } + + @ViewBuilder + func valueColumn_offer_message( + _ model: Scan.Model_OfferFlow + ) -> some View { + + Group { + let comment = parent.comment + if comment.isEmpty { + Button { + parent.showCommentSheet() + } label: { + Text("Attach a message") + } + } else { + HStack(alignment: VerticalAlignment.firstTextBaseline, spacing: 4) { + Text(comment) + .lineLimit(3) + .truncationMode(.tail) + Button { + parent.showCommentSheet() + } label: { + Image(systemName: "square.and.pencil").font(.body) + } + } + } + } // } // -------------------------------------------------- @@ -349,23 +350,32 @@ struct PaymentDetails: View { return trimmedMessage.isEmpty ? nil : trimmedMessage } - func requestDescription() -> String { + func requestDescription() -> String? { + + let sanitize = { (input: String?) -> String? in + + if let trimmedInput = input?.trimmingCharacters(in: .whitespacesAndNewlines) { + if !trimmedInput.isEmpty { + return trimmedInput + } + } + return nil + } - var desc: String? = nil if let invoice = parent.bolt11Invoice() { - desc = invoice.desc_() + return sanitize(invoice.desc_()) } else if let offer = parent.bolt12Offer() { - desc = offer.description_ + return sanitize(offer.description_) } else if let lnurlPay = parent.lnurlPay() { - desc = lnurlPay.metadata.plainText + return sanitize(lnurlPay.metadata.plainText) } else if let lnurlWithdraw = parent.lnurlWithdraw() { - desc = lnurlWithdraw.defaultDescription + return sanitize(lnurlWithdraw.defaultDescription) } - return desc ?? String(localized: "No description") + return nil } func accessibilityLabel_tipAmount(_ info: PaymentSummaryStrings) -> String { @@ -416,10 +426,53 @@ struct PaymentDetails: View { } } +@available(iOS 16.0, *) +struct GridRowWrapper: View { + + let gridWidth: CGFloat? + let keyColumn: KeyColumn + let valueColumn: ValueColumn + + init( + gridWidth: CGFloat?, + @ViewBuilder keyColumn keyColumnBuilder: () -> KeyColumn, + @ViewBuilder valueColumn valueColumnBuilder: () -> ValueColumn + ) { + self.gridWidth = gridWidth + self.keyColumn = keyColumnBuilder() + self.valueColumn = valueColumnBuilder() + } + + @ViewBuilder + var body: some View { + + GridRow(alignment: VerticalAlignment.firstTextBaseline) { + keyColumn + .font(.subheadline) + .frame(maxWidth: columnWidth, alignment: Alignment.topTrailing) + .gridCellAnchor(.topTrailing) + + valueColumn + .font(.subheadline) + .frame(minWidth: columnWidth, alignment: Alignment.leading) + .gridCellAnchor(.leading) + } + } + + var columnWidth: CGFloat? { + guard let gridWidth else { + return nil + } + return (gridWidth / 2.0) - (grid_horizontalSpacing / 2.0) + } +} + // -------------------------------------------------- // MARK: - // -------------------------------------------------- +/// Workaround for iOS 15, where `Grid` isn't available. +/// fileprivate struct PaymentDetails_Grid: InfoGridView { let parent: PaymentDetails @@ -452,12 +505,15 @@ fileprivate struct PaymentDetails_Grid: InfoGridView { private let horizontalSpacingBetweenColumns: CGFloat = 8 @ViewBuilder - func keyColumn(_ title: LocalizedStringKey) -> some View { + func keyColumn( + _ title: LocalizedStringKey, + _ color: Color = Color.secondary + ) -> some View { Text(title) .textCase(.uppercase) .font(.subheadline) - .foregroundColor(.secondary) + .foregroundColor(color) } @ViewBuilder @@ -469,6 +525,14 @@ fileprivate struct PaymentDetails_Grid: InfoGridView { ) { if let model = grandparent.mvi.model as? Scan.Model_OnChainFlow { rows_onChain(model) + } else if parent.requestDescription() != nil { + row_description() + } + if let model = grandparent.mvi.model as? Scan.Model_OfferFlow { + rows_offer(model) + } + if let paymentSummary = grandparent.paymentStrings() { + rows_paymentSummary(paymentSummary) } } } @@ -477,14 +541,62 @@ fileprivate struct PaymentDetails_Grid: InfoGridView { func rows_onChain(_ model: Scan.Model_OnChainFlow) -> some View { if parent.bitcoinUriMessage() != nil { - onChain_type() + row_onChain_type() + } + row_onChain_description() + row_onChain_sendTo(model) + } + + @ViewBuilder + func rows_offer( + _ model: Scan.Model_OfferFlow + ) -> some View { + + row_offer_sendTo(model) + row_offer_message(model) + } + + @ViewBuilder + func rows_paymentSummary( + _ info: PaymentSummaryStrings + ) -> some View { + + if info.hasTip { + row_paymentSummary_tip(info) + } + if info.hasLightningFee || info.isEmpty { + row_paymentSummary_lightningFee(info) } - onChain_description() - onChain_sendTo(model) + if info.hasMinerFee { + row_paymentSummary_minerFee(info) + } + if info.hasTip || info.hasLightningFee || info.hasMinerFee || info.isEmpty { + row_paymentSummary_total(info) + } + } + + @ViewBuilder + func row_description() -> some View { + let identifier: String = #function + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + keyColumn("Desc") + } valueColumn: { + + Text(parent.requestDescription() ?? "") + .font(.subheadline) + + } // } @ViewBuilder - func onChain_type() -> some View { + func row_onChain_type() -> some View { let identifier: String = #function InfoGridRow( @@ -504,7 +616,7 @@ fileprivate struct PaymentDetails_Grid: InfoGridView { } @ViewBuilder - func onChain_description() -> some View { + func row_onChain_description() -> some View { let identifier: String = #function InfoGridRow( @@ -529,7 +641,7 @@ fileprivate struct PaymentDetails_Grid: InfoGridView { } @ViewBuilder - func onChain_sendTo(_ model: Scan.Model_OnChainFlow) -> some View { + func row_onChain_sendTo(_ model: Scan.Model_OnChainFlow) -> some View { let identifier: String = #function InfoGridRow( @@ -555,4 +667,165 @@ fileprivate struct PaymentDetails_Grid: InfoGridView { } // } + + @ViewBuilder + func row_offer_sendTo(_ model: Scan.Model_OfferFlow) -> some View { + let identifier: String = #function + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + keyColumn("Send To") + } valueColumn: { + parent.valueColumn_offer_sendTo(model) + } // + } + + @ViewBuilder + func row_offer_message(_ model: Scan.Model_OfferFlow) -> some View { + let identifier: String = #function + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + keyColumn("Message") + } valueColumn: { + parent.valueColumn_offer_message(model) + } // + } + + @ViewBuilder + func row_paymentSummary_tip(_ info: PaymentSummaryStrings) -> some View { + let identifier: String = #function + + let titleColor = info.isEmpty ? Color.clear : Color.secondary + let bitcoinColor = info.isEmpty ? Color.clear : Color.primary + let fiatColor = info.isEmpty ? Color.clear : Color(UIColor.systemGray2) + let percentColor = info.isEmpty ? Color.clear : Color.secondary + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + keyColumn("tip", titleColor) + } valueColumn: { + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + Text(verbatim: info.bitcoin_tip.string) + .foregroundColor(bitcoinColor) + Text(verbatim: " ≈\(info.fiat_tip.string)") + .foregroundColor(fiatColor) + Text(verbatim: info.percent_tip) + .foregroundColor(percentColor) + } + } // + .accessibilityLabel(parent.accessibilityLabel_tipAmount(info)) + } + + @ViewBuilder + func row_paymentSummary_lightningFee(_ info: PaymentSummaryStrings) -> some View { + let identifier: String = #function + + let titleColor = info.isEmpty ? Color.clear : Color.secondary + let bitcoinColor = info.isEmpty ? Color.clear : Color.primary + let fiatColor = info.isEmpty ? Color.clear : Color(UIColor.systemGray2) + let percentColor = info.isEmpty ? Color.clear : Color.secondary + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + keyColumn("lightning fee", titleColor) + } valueColumn: { + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + Text(verbatim: info.bitcoin_lightningFee.string) + .foregroundColor(bitcoinColor) + Text(verbatim: " ≈\(info.fiat_lightningFee.string)") + .foregroundColor(fiatColor) + Text(verbatim: info.percent_lightningFee) + .foregroundColor(percentColor) + } + } // + .accessibilityLabel(parent.accessibilityLabel_lightningFeeAmount(info)) + } + + @ViewBuilder + func row_paymentSummary_minerFee(_ info: PaymentSummaryStrings) -> some View { + let identifier: String = #function + + let titleColor = info.isEmpty ? Color.clear : Color.secondary + let bitcoinColor = info.isEmpty ? Color.clear : Color.primary + let fiatColor = info.isEmpty ? Color.clear : Color(UIColor.systemGray2) + let percentColor = info.isEmpty ? Color.clear : Color.secondary + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + keyColumn("miner fee", titleColor) + } valueColumn: { + HStack(alignment: VerticalAlignment.center, spacing: 4) { + + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + Text(verbatim: info.bitcoin_minerFee.string) + .foregroundColor(bitcoinColor) + Text(verbatim: " ≈\(info.fiat_minerFee.string)") + .foregroundColor(fiatColor) + Text(verbatim: info.percent_minerFee) + .foregroundColor(percentColor) + } // + + Button { + grandparent.showMinerFeeSheet() + } label: { + Image(systemName: "square.and.pencil").font(.body) + } + + } // + } // + .accessibilityLabel(parent.accessibilityLabel_minerFeeAmount(info)) + } + + @ViewBuilder + func row_paymentSummary_total(_ info: PaymentSummaryStrings) -> some View { + let identifier: String = #function + + let titleColor = info.isEmpty ? Color.clear : Color.secondary + let bitcoinColor = info.isEmpty ? Color.clear : Color.primary + let fiatColor = info.isEmpty ? Color.clear : Color(UIColor.systemGray2) + + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + keyColumn("total", titleColor) + } valueColumn: { + VStack(alignment: HorizontalAlignment.leading, spacing: 4) { + Text(verbatim: info.bitcoin_total.string) + .foregroundColor(bitcoinColor) + Text(verbatim: " ≈\(info.fiat_total.string)") + .foregroundColor(fiatColor) + } + } // + .accessibilityLabel(parent.accessibilityLabel_totalAmount(info)) + } } diff --git a/phoenix-ios/phoenix-ios/views/send/SendView.swift b/phoenix-ios/phoenix-ios/views/send/SendView.swift index fc4e85a39..97c32b998 100644 --- a/phoenix-ios/phoenix-ios/views/send/SendView.swift +++ b/phoenix-ios/phoenix-ios/views/send/SendView.swift @@ -86,7 +86,8 @@ struct SendView: MVIView { switch mvi.model { case _ as Scan.Model_Ready, _ as Scan.Model_BadRequest, - _ as Scan.Model_LnurlServiceFetch: + _ as Scan.Model_LnurlServiceFetch, + _ as Scan.Model_ResolvingBip353: ScanView(location: location, mvi: mvi, toast: toast) .zIndex(4) @@ -215,7 +216,14 @@ struct SendView: MVIView { "You've already paid this invoice. Paying it again could result in stolen funds.", comment: "Error message - scanning lightning invoice" ) + + case is Scan.BadRequestReason_InvalidBip353: + msg = NSLocalizedString( + "Invalid BIP353 DNS address", + comment: "Error message - dns record contains an invalid offer" + ) + case let serviceError as Scan.BadRequestReason_ServiceError: let remoteFailure: LnurlError.RemoteFailure = serviceError.error diff --git a/phoenix-ios/phoenix-ios/views/transactions/TxHistoryExporter.swift b/phoenix-ios/phoenix-ios/views/transactions/TxHistoryExporter.swift index fa5cb1619..e0ce8023f 100644 --- a/phoenix-ios/phoenix-ios/views/transactions/TxHistoryExporter.swift +++ b/phoenix-ios/phoenix-ios/views/transactions/TxHistoryExporter.swift @@ -428,12 +428,10 @@ struct TxHistoryExporter: View { exportedCount = 0 let databaseManager = Biz.business.databaseManager - let peerManager = Biz.business.peerManager let fetcher = Biz.business.paymentsManager.fetcher do { let paymentsDb = try await databaseManager.paymentsDb() - let peer = try await peerManager.getPeer() let config = CsvWriter.Configuration( includesFiat: includeFiat, diff --git a/phoenix-legacy/build.gradle.kts b/phoenix-legacy/build.gradle.kts index 6249eb955..dd7e86793 100644 --- a/phoenix-legacy/build.gradle.kts +++ b/phoenix-legacy/build.gradle.kts @@ -30,7 +30,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { - val libCode = 83 + val libCode = 84 getByName("debug") { resValue("string", "CHAIN", chain) buildConfigField("String", "CHAIN", chain)