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)