[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-ios] 22/54: cleaned up P2P
From: |
gnunet |
Subject: |
[taler-taler-ios] 22/54: cleaned up P2P |
Date: |
Fri, 30 Jun 2023 22:33:54 +0200 |
This is an automated email from the git hooks/post-receive script.
marc-stibane pushed a commit to branch master
in repository taler-ios.
commit 78e7674feec48454bd9e1bf4791e14acc6b7fa4e
Author: Marc Stibane <marc@taler.net>
AuthorDate: Mon Jun 26 07:19:06 2023 +0200
cleaned up P2P
---
TalerWallet.xcodeproj/project.pbxproj | 4 +-
TalerWallet1/Model/Model+P2P.swift | 238 ++++++++++++++-------
TalerWallet1/Views/Peer2peer/SendAmount.swift | 2 +-
TalerWallet1/Views/Peer2peer/SendPurpose.swift | 2 +-
.../Views/Sheets/P2P_Sheets/P2pAcceptDone.swift | 2 +-
.../Views/Sheets/P2P_Sheets/P2pPayURIView.swift | 34 +--
.../Sheets/P2P_Sheets/P2pReceiveURIView.swift | 9 +-
7 files changed, 173 insertions(+), 118 deletions(-)
diff --git a/TalerWallet.xcodeproj/project.pbxproj
b/TalerWallet.xcodeproj/project.pbxproj
index a9580b8..ebfa133 100644
--- a/TalerWallet.xcodeproj/project.pbxproj
+++ b/TalerWallet.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 54;
+ objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
@@ -276,8 +276,8 @@
isa = PBXGroup;
children = (
4E3B4BC02A41E6C200CC88B8 /*
P2pReceiveURIView.swift */,
- 4E578E932A4822D500F21F1C /* P2pPayURIView.swift
*/,
4E3B4BC22A42252300CC88B8 /* P2pAcceptDone.swift
*/,
+ 4E578E932A4822D500F21F1C /* P2pPayURIView.swift
*/,
);
path = P2P_Sheets;
sourceTree = "<group>";
diff --git a/TalerWallet1/Model/Model+P2P.swift
b/TalerWallet1/Model/Model+P2P.swift
index f696400..a671673 100644
--- a/TalerWallet1/Model/Model+P2P.swift
+++ b/TalerWallet1/Model/Model+P2P.swift
@@ -8,21 +8,44 @@ import AnyCodable
//import SymLog
fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
-// MARK: - PeerContractTerms
+// MARK: common structures
struct PeerContractTerms: Codable {
let amount: Amount
let summary: String
let purse_expiration: Timestamp
}
-// MARK: -
-/// The result from CheckPeerPushDebit
+// MARK: - PeerPushDebit
+/// Check if initiating a peer push payment is possible, check fees
+struct AmountResponse: Codable {
+ let effectiveAmount: Amount
+ let rawAmount: Amount
+}
+fileprivate struct GetMaxPeerPushAmount: WalletBackendFormattedRequest {
+ typealias Response = AmountResponse
+ func operation() -> String { return "GetMaxPeerPushAmount" }
+ func args() -> Args { return Args(currency: currency) }
+
+ var currency: String
+ struct Args: Encodable {
+ var currency: String
+ }
+}
+extension WalletModel {
+ @MainActor
+ func getMaxPeerPushAmountM(_ currency: String) // M for MainActor
+ async throws -> AmountResponse {
+ let request = GetMaxPeerPushAmount(currency: currency)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // getMaxPeerPushAmountM
+// - - - - - -
struct CheckPeerPushDebitResponse: Codable {
let exchangeBaseUrl: String?
let amountRaw: Amount
let amountEffective: Amount
let maxExpirationDate: Timestamp? // TODO: limit expiration (30
days or 7 days)
}
-/// A request to check fees before sending coins to another wallet.
fileprivate struct CheckPeerPushDebit: WalletBackendFormattedRequest {
typealias Response = CheckPeerPushDebitResponse
func operation() -> String { return "checkPeerPushDebit" }
@@ -33,8 +56,50 @@ fileprivate struct CheckPeerPushDebit:
WalletBackendFormattedRequest {
var amount: Amount
}
}
-// MARK: -
-/// The result from CheckPeerPullCredit
+extension WalletModel {
+ @MainActor
+ func checkPeerPushDebitM(_ amount: Amount) // M for MainActor
+ async throws -> CheckPeerPushDebitResponse {
+ let request = CheckPeerPushDebit(amount: amount)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // checkPeerPushDebitM
+// - - - - - -
+/// Initiate an outgoing peer push payment, send coins
+struct InitiatePeerPushDebitResponse: Codable {
+ let contractPriv: String
+ let mergePriv: String
+ let pursePub: String
+ let exchangeBaseUrl: String
+ let talerUri: String
+ let transactionId: String
+}
+fileprivate struct InitiatePeerPushDebit: WalletBackendFormattedRequest {
+ typealias Response = InitiatePeerPushDebitResponse
+ func operation() -> String { return "initiatePeerPushDebit" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl,
+ partialContractTerms:
partialContractTerms) }
+
+ var exchangeBaseUrl: String?
+ var partialContractTerms: PeerContractTerms
+ struct Args: Encodable {
+ var exchangeBaseUrl: String?
+ var partialContractTerms: PeerContractTerms
+ }
+}
+extension WalletModel {
+ @MainActor
+ func initiatePeerPushDebitM(_ baseURL: String?, terms: PeerContractTerms)
// M for MainActor
+ async throws -> InitiatePeerPushDebitResponse {
+ let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL,
+ partialContractTerms: terms)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // initiatePeerPushDebitM
+// MARK: - PeerPullCredit
+/// Check fees before sending an invoice to another wallet
struct CheckPeerPullCreditResponse: Codable {
let scopeInfo: ScopeInfo?
let exchangeBaseUrl: String?
@@ -42,7 +107,6 @@ struct CheckPeerPullCreditResponse: Codable {
let amountEffective: Amount
var numCoins: Int? // Number of coins this
amountEffective will create
}
-/// A request to check fees before invoicing another wallet.
fileprivate struct CheckPeerPullCredit: WalletBackendFormattedRequest {
typealias Response = CheckPeerPullCreditResponse
func operation() -> String { return "checkPeerPullCredit" }
@@ -57,22 +121,26 @@ fileprivate struct CheckPeerPullCredit:
WalletBackendFormattedRequest {
var amount: Amount
}
}
-// MARK: -
-/// The result from InitiatePeerPushDebit
-struct InitiatePeerPushDebitResponse: Codable {
- let contractPriv: String
- let mergePriv: String
- let pursePub: String
- let exchangeBaseUrl: String
+extension WalletModel {
+ @MainActor
+ func checkPeerPullCreditM(_ amount: Amount, exchangeBaseUrl: String?)
// M for MainActor
+ async throws -> CheckPeerPullCreditResponse {
+ let request = CheckPeerPullCredit(exchangeBaseUrl: exchangeBaseUrl,
amount: amount)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // checkPeerPullCreditM
+// - - - - - -
+/// Initiate an outgoing peer pull payment, send an invoice
+struct InitiatePeerPullCreditResponse: Codable {
let talerUri: String
let transactionId: String
}
-/// A request to send coins to another wallet.
-fileprivate struct InitiatePeerPushDebit: WalletBackendFormattedRequest {
- typealias Response = InitiatePeerPushDebitResponse
- func operation() -> String { return "initiatePeerPushDebit" }
+fileprivate struct InitiatePeerPullCredit: WalletBackendFormattedRequest {
+ typealias Response = InitiatePeerPullCreditResponse
+ func operation() -> String { return "initiatePeerPullCredit" }
func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl,
- partialContractTerms:
partialContractTerms) }
+ partialContractTerms: partialContractTerms) }
var exchangeBaseUrl: String?
var partialContractTerms: PeerContractTerms
@@ -81,14 +149,25 @@ fileprivate struct InitiatePeerPushDebit:
WalletBackendFormattedRequest {
var partialContractTerms: PeerContractTerms
}
}
-// MARK: -
+extension WalletModel {
+ @MainActor
+ func initiatePeerPullCreditM(_ baseURL: String?, terms: PeerContractTerms)
// M for MainActor
+ async throws -> InitiatePeerPullCreditResponse {
+ let request = InitiatePeerPullCredit(exchangeBaseUrl: baseURL,
+ partialContractTerms: terms)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // initiatePeerPullCreditM
+// MARK: - PeerPushCredit
+/// Prepare an incoming peer push payment, receive coins
struct PreparePeerPushCreditResponse: Codable {
let contractTerms: PeerContractTerms
let amountRaw: Amount
let amountEffective: Amount
- let transactionId: String // after scanning the transaction
already is created in the local DB
+ // the dialog transaction is already created in the local DB - must either
accept or delete
+ let transactionId: String
}
-/// A request to receive coins from another wallet.
fileprivate struct PreparePeerPushCredit: WalletBackendFormattedRequest {
typealias Response = PreparePeerPushCreditResponse
func operation() -> String { return "preparePeerPushCredit" }
@@ -99,10 +178,20 @@ fileprivate struct PreparePeerPushCredit:
WalletBackendFormattedRequest {
var talerUri: String
}
}
-// MARK: -
-fileprivate struct ConfirmPeerPushCredit: WalletBackendFormattedRequest {
- struct Response: Decodable {}
- func operation() -> String { return "confirmPeerPushCredit" }
+extension WalletModel {
+ @MainActor
+ func preparePeerPushCreditM(_ talerUri: String) // M for MainActor
+ async throws -> PreparePeerPushCreditResponse {
+ let request = PreparePeerPushCredit(talerUri: talerUri)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // preparePeerPushCreditM
+// - - - - - -
+/// Accept an incoming peer push payment
+fileprivate struct AcceptPeerPushCredit: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "confirmPeerPushCredit" } // should be
"acceptPeerPushCredit"
func args() -> Args { return Args(transactionId: transactionId) }
var transactionId: String
@@ -110,78 +199,61 @@ fileprivate struct ConfirmPeerPushCredit:
WalletBackendFormattedRequest {
var transactionId: String
}
}
-// MARK: -
-/// The result from InitiatePeerPullCredit
-struct InitiatePeerPullCreditResponse: Codable {
- let talerUri: String
- let transactionId: String
-}
-/// A request to send a payment request to another wallet.
-fileprivate struct InitiatePeerPullCredit: WalletBackendFormattedRequest {
- typealias Response = InitiatePeerPullCreditResponse
- func operation() -> String { return "initiatePeerPullCredit" }
- func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl,
- partialContractTerms:
partialContractTerms) }
-
- var exchangeBaseUrl: String?
- var partialContractTerms: PeerContractTerms
- struct Args: Encodable {
- var exchangeBaseUrl: String?
- var partialContractTerms: PeerContractTerms
- }
-}
-// MARK: -
extension WalletModel {
- /// query exchange for fees (sending coins). Networking involved
- @MainActor
- func checkPeerPushDebitM(_ amount: Amount) // M for MainActor
- async throws -> CheckPeerPushDebitResponse {
- let request = CheckPeerPushDebit(amount: amount)
- let response = try await sendRequest(request, ASYNCDELAY)
- return response
- }
- /// generate peer-push. Networking involved
@MainActor
- func initiatePeerPushDebitM(_ baseURL: String?, terms: PeerContractTerms)
// M for MainActor
- async throws -> InitiatePeerPushDebitResponse {
- let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL,
- partialContractTerms: terms)
+ func acceptPeerPushCreditM(_ transactionId: String) // M for
MainActor
+ async throws -> Decodable {
+ let request = AcceptPeerPushCredit(transactionId: transactionId)
let response = try await sendRequest(request, ASYNCDELAY)
return response
}
+} // acceptPeerPushCreditM
+// MARK: - PeerPullDebit
+/// Prepare an incoming peer push invoice, pay coins
+struct PreparePeerPullDebitResponse: Codable {
+ let contractTerms: PeerContractTerms
+ let amountRaw: Amount
+ let amountEffective: Amount
+ // the dialog transaction is already created in the local DB - must either
accept or delete
+ let transactionId: String
+}
+fileprivate struct PreparePeerPullDebit: WalletBackendFormattedRequest {
+ typealias Response = PreparePeerPullDebitResponse
+ func operation() -> String { return "preparePeerPullDebit" }
+ func args() -> Args { return Args(talerUri: talerUri) }
- /// query exchange for fees (invoice coins). Networking involved
- @MainActor
- func checkPeerPullCreditM(_ amount: Amount, exchangeBaseUrl: String?)
// M for MainActor
- async throws -> CheckPeerPullCreditResponse {
- let request = CheckPeerPullCredit(exchangeBaseUrl: exchangeBaseUrl,
amount: amount)
- let response = try await sendRequest(request, ASYNCDELAY)
- return response
+ var talerUri: String
+ struct Args: Encodable {
+ var talerUri: String
}
- /// generate peer-pull. Networking involved
+}
+extension WalletModel {
@MainActor
- func initiatePeerPullCreditM(_ baseURL: String?, terms: PeerContractTerms)
// M for MainActor
- async throws -> InitiatePeerPullCreditResponse {
- let request = InitiatePeerPullCredit(exchangeBaseUrl: baseURL,
- partialContractTerms: terms)
+ func preparePeerPullDebitM(_ talerUri: String) // M for MainActor
+ async throws -> PreparePeerPullDebitResponse {
+ let request = PreparePeerPullDebit(talerUri: talerUri)
let response = try await sendRequest(request, ASYNCDELAY)
return response
}
+} // preparePeerPullDebitM
+// - - - - - -
+/// Confirm incoming peer push invoice and pay
+fileprivate struct ConfirmPeerPullDebit: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "confirmPeerPullDebit" }
+ func args() -> Args { return Args(transactionId: transactionId) }
- /// prepare peer-pull. Networking involved
- @MainActor
- func preparePeerPushCreditM(_ talerUri: String) // M for MainActor
- async throws -> PreparePeerPushCreditResponse {
- let request = PreparePeerPushCredit(talerUri: talerUri)
- let response = try await sendRequest(request, ASYNCDELAY)
- return response
+ var transactionId: String
+ struct Args: Encodable {
+ var transactionId: String
}
- /// confirm peer-pull. Networking involved
+}
+extension WalletModel {
@MainActor
- func confirmPeerPushCreditM(_ transactionId: String) // M for
MainActor
+ func confirmPeerPullDebitM(_ transactionId: String) // M for
MainActor
async throws -> Decodable {
- let request = ConfirmPeerPushCredit(transactionId: transactionId)
+ let request = ConfirmPeerPullDebit(transactionId: transactionId)
let response = try await sendRequest(request, ASYNCDELAY)
return response
}
-}
+} // confirmPeerPullDebitM
diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift
b/TalerWallet1/Views/Peer2peer/SendAmount.swift
index a845d6d..854ea04 100644
--- a/TalerWallet1/Views/Peer2peer/SendAmount.swift
+++ b/TalerWallet1/Views/Peer2peer/SendAmount.swift
@@ -10,7 +10,7 @@ import SymLog
struct SendAmount: View {
private let symLog = SymLogV()
- let amountAvailable: Amount
+ let amountAvailable: Amount // TODO: GetMaxPeerPushAmount
@Binding var centsToTransfer: UInt64
@Binding var summary: String
diff --git a/TalerWallet1/Views/Peer2peer/SendPurpose.swift
b/TalerWallet1/Views/Peer2peer/SendPurpose.swift
index a17d7f4..660c64f 100644
--- a/TalerWallet1/Views/Peer2peer/SendPurpose.swift
+++ b/TalerWallet1/Views/Peer2peer/SendPurpose.swift
@@ -93,7 +93,7 @@ struct SendPurpose: View {
.task {
symLog.log(".task")
do {
-// peerPushCheck = try await model.checkPeerPushDebitM(amount)
+
} catch { // TODO: error
symLog.log(error.localizedDescription)
}
diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
index abc91c1..5e96d06 100644
--- a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
+++ b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
@@ -44,7 +44,7 @@ struct P2pAcceptDone: View {
DebugViewC.shared.setSheetID(SHEET_RCV_P2P_ACCEPT)
}.task {
do {
- _ = try await model.confirmPeerPushCreditM(transactionId)
+ _ = try await model.acceptPeerPushCreditM(transactionId)
finished = true
playSound(success: true)
} catch { // TODO: error
diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
index 3fd431c..f244965 100644
--- a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
+++ b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
@@ -12,14 +12,14 @@ struct P2pPayURIView: View {
private let symLog = SymLogV()
let navTitle = String(localized: "Pay P2P Invoice")
- // the URL from the bank website
+ // the scanned URL
let url: URL
+
@EnvironmentObject private var model: WalletModel
@State private var peerPullDebitResponse: PreparePeerPullDebitResponse?
var body: some View {
- let badURL = "Error in URL: \(url)"
VStack {
if let peerPullDebitResponse {
List {
@@ -27,35 +27,21 @@ struct P2pPayURIView: View {
let effective = peerPullDebitResponse.amountEffective
let currency = raw.currencyStr
let fee = try! Amount.diff(raw, effective)
- let outColor = WalletColors().transactionColor(false)
- let inColor = WalletColors().transactionColor(true)
-
- ThreeAmountsView(topTitle: String(localized: "Amount to
receive:"),
+ ThreeAmountsView(topTitle: String(localized: "Amount to
pay:"),
topAmount: raw, fee: fee,
- bottomTitle: String(localized:
"\(currency) to be obtained:"),
+ bottomTitle: String(localized:
"\(currency) to be spent:"),
bottomAmount: effective,
- large: false, pending: false, incoming:
true,
+ large: false, pending: false, incoming:
false,
baseURL: nil)
}
.navigationTitle(navTitle)
- let tosAccepted = true // TODO:
https://bugs.gnunet.org/view.php?id=7869
- if tosAccepted {
- NavigationLink(destination: LazyView {
+
+ NavigationLink(destination: LazyView {
P2pAcceptDone(transactionId:
peerPullDebitResponse.transactionId)
}) {
- Text("Confirm Withdrawal") //
SHEET_WITHDRAW_ACCEPT
+ Text("Confirm Payment", comment:"pay P2P invoice")
// SHEET_PAY_P2P
}.buttonStyle(TalerButtonStyle(type: .prominent))
.padding()
- } else {
- NavigationLink(destination: LazyView {
- WithdrawTOSView(exchangeBaseUrl: nil,
- viewID: SHEET_RCV_P2P_TOS,
- acceptAction: nil) // pop back
to here
- }) {
- Text("Check Terms of Service")
- }.buttonStyle(TalerButtonStyle(type: .prominent))
- .padding()
- }
} else {
// Yikes no details or no baseURL
// WithdrawProgressView(message: url.host ??
badURL)
@@ -64,12 +50,12 @@ struct P2pPayURIView: View {
}
.onAppear() {
symLog.log("onAppear")
- DebugViewC.shared.setSheetID(SHEET_RCV_P2P)
+ DebugViewC.shared.setSheetID(SHEET_PAY_P2P)
}
.task {
do { // TODO: cancelled
symLog.log(".task")
- let ppDebitResponse = try await
model.preparePeerPushCreditM(url.absoluteString)
+ let ppDebitResponse = try await
model.preparePeerPullDebitM(url.absoluteString)
peerPullDebitResponse = ppDebitResponse
} catch { // TODO: error
symLog.log(error.localizedDescription)
diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
index 7d2c0fc..f2c1f20 100644
--- a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
+++ b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
@@ -12,14 +12,14 @@ struct P2pReceiveURIView: View {
private let symLog = SymLogV()
let navTitle = String(localized: "Accept P2P Receive")
- // the URL from the bank website
+ // the scanned URL
let url: URL
+
@EnvironmentObject private var model: WalletModel
@State private var peerPushCreditResponse: PreparePeerPushCreditResponse?
var body: some View {
- let badURL = "Error in URL: \(url)"
VStack {
if let peerPushCreditResponse {
List {
@@ -27,9 +27,6 @@ struct P2pReceiveURIView: View {
let effective = peerPushCreditResponse.amountEffective
let currency = raw.currencyStr
let fee = try! Amount.diff(raw, effective)
- let outColor = WalletColors().transactionColor(false)
- let inColor = WalletColors().transactionColor(true)
-
ThreeAmountsView(topTitle: String(localized: "Amount to
receive:"),
topAmount: raw, fee: fee,
bottomTitle: String(localized: "\(currency)
to be obtained:"),
@@ -43,7 +40,7 @@ struct P2pReceiveURIView: View {
NavigationLink(destination: LazyView {
P2pAcceptDone(transactionId:
peerPushCreditResponse.transactionId)
}) {
- Text("Confirm Withdrawal") //
SHEET_WITHDRAW_ACCEPT
+ Text("Accept Withdrawal") // SHEET_WITHDRAW_ACCEPT
}.buttonStyle(TalerButtonStyle(type: .prominent))
.padding()
} else {
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-taler-ios] 44/54: BalanceReloaded, (continued)
- [taler-taler-ios] 44/54: BalanceReloaded, gnunet, 2023/06/30
- [taler-taler-ios] 35/54: developerMode, gnunet, 2023/06/30
- [taler-taler-ios] 45/54: TransactionDetails, gnunet, 2023/06/30
- [taler-taler-ios] 39/54: Scrollview, gnunet, 2023/06/30
- [taler-taler-ios] 25/54: failTransaction, gnunet, 2023/06/30
- [taler-taler-ios] 23/54: Remove old view, gnunet, 2023/06/30
- [taler-taler-ios] 24/54: balance-change, gnunet, 2023/06/30
- [taler-taler-ios] 47/54: #available(iOS 17.0, *) only with Xcode 15, gnunet, 2023/06/30
- [taler-taler-ios] 46/54: ThreeAmountsSheet, gnunet, 2023/06/30
- [taler-taler-ios] 51/54: remove debugging, gnunet, 2023/06/30
- [taler-taler-ios] 22/54: cleaned up P2P,
gnunet <=
- [taler-taler-ios] 54/54: iOS: bump version to 0.9.3 (10), gnunet, 2023/06/30
- [taler-taler-ios] 49/54: ScrollViewReader, gnunet, 2023/06/30
- [taler-taler-ios] 50/54: withdrawalAmountDetails, gnunet, 2023/06/30
- [taler-taler-ios] 48/54: Demo Shop, reloading, gnunet, 2023/06/30
- [taler-taler-ios] 52/54: playSound, gnunet, 2023/06/30
- [taler-taler-ios] 53/54: ScrollViewReader needs Spacers if too few items, gnunet, 2023/06/30