Model+Payment.swift (27299B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import Foundation 9 import taler_swift 10 import AnyCodable 11 //import SymLog 12 13 typealias I18nDict = [String: String] // two-char language code, e.g. "de", "en" 14 extension I18nDict { 15 var localized: String? { 16 if !self.isEmpty { 17 for code in Locale.preferredLanguageCodes { 18 if let i18n = self[code] { 19 return i18n 20 } 21 } 22 } 23 return nil 24 } 25 } 26 // MARK: - ContractTerms 27 28 struct TokenIssuePublicKey: Codable { 29 let cipher: String // "RSA", "CS" 30 31 // RSA public key 32 let rsaPub: String? // RSA public key converted to Crockford Base32 33 34 // CS public key 35 let csPub: String? // 32-byte value representing a point on Curve25519 36 37 // Start time of this key's signatures validity period 38 let signatureValidityStart: Timestamp 39 40 // End time of this key's signatures validity period 41 let signatureValidityEnd: Timestamp 42 43 enum CodingKeys: String, CodingKey { 44 case cipher 45 case rsaPub = "rsa_pub" 46 case csPub = "cs_pub" 47 case signatureValidityStart = "signature_validity_start" 48 case signatureValidityEnd = "signature_validity_end" 49 } 50 } 51 52 struct ContractTokenDetails: Codable { 53 let clazz: String // "subscription", "discount" 54 55 // Array of domain names where this subscription can be safely used 56 // (e.g. the issuer warrants that these sites will re-issue tokens of this type 57 // if the respective contract says so). May contain "*" for any domain or subdomain. 58 let trustedDomains: [String]? // only for subscription 59 60 // Array of domain names where this discount token is intended to be used. 61 // May contain "*" for any domain or subdomain. Users should be warned about sites 62 // proposing to consume discount tokens of this type that are not in this list that 63 // the merchant is accepting a coupon from a competitor and thus may be attaching 64 // different semantics (like get 20% discount for my competitors 30% discount token). 65 let expectedDomains: [String]? // only for discount 66 67 enum CodingKeys: String, CodingKey { 68 case clazz = "class" 69 case trustedDomains = "trusted_domains" 70 case expectedDomains = "expected_domains" 71 } 72 } 73 74 struct ContractTokenFamily: Codable { 75 // Human-readable name of the token family. 76 let name: String 77 78 // Human-readable description of the semantics of this token family (for display). 79 let description: String 80 81 // Map from IETF BCP 47 language tags to localized descriptions. 82 let descriptionI18n: I18nDict? 83 84 // Public keys used to validate tokens issued by this token family. 85 let keys: [TokenIssuePublicKey] 86 87 // Kind-specific information of the token 88 let details: ContractTokenDetails 89 90 // Must a wallet understand this token type to 91 // process contracts that use or issue it? 92 let critical: Bool 93 94 enum CodingKeys: String, CodingKey { 95 case name, description 96 case descriptionI18n = "description_i18n" 97 case keys, details, critical 98 } 99 } 100 101 struct ContractInput: Codable, Hashable { 102 let type: String // "token" 103 104 // Slug of the token family in the token_families map on the order 105 let tokenFamilySlug: String? 106 107 // Number of tokens of this type required. 108 // Defaults to one if the field is not provided. 109 let count: Int? 110 111 enum CodingKeys: String, CodingKey { 112 case type, count 113 case tokenFamilySlug = "token_family_slug" 114 } 115 } 116 117 struct ContractOutput: Codable, Hashable { 118 let type: String // "token" 119 120 // Slug of the token family in the token_families map on the order 121 let tokenFamilySlug: String? 122 123 // Number of tokens of this type required. 124 // Defaults to one if the field is not provided. 125 let count: Int? 126 127 // Index of the public key for this output token 128 // in the ContractTokenFamily keys array. 129 let keyIndex: Int 130 131 enum CodingKeys: String, CodingKey { 132 case type, count 133 case tokenFamilySlug = "token_family_slug" 134 case keyIndex = "key_index" 135 } 136 } 137 138 struct ContractOutputTaxReceipt: Codable, Hashable { 139 let type: String // "tax-receipt" 140 141 // Array of base URLs of donation authorities that can be 142 // used to issue the tax receipts. The client must select one. 143 let donauUrls: [String] 144 145 // Total amount that will be on the tax receipt. 146 let amount: Amount? 147 148 enum CodingKeys: String, CodingKey { 149 case type, amount 150 case donauUrls = "donau_urls" 151 } 152 } 153 154 /// wallet-core sends a union discriminated on "type" - a tax-receipt output has neither 155 /// "key_index" nor "token_family_slug", so decoding it as ContractOutput throws and takes 156 /// the whole getChoicesForPayment response with it, leaving nothing for the user to confirm. 157 enum ContractOutputAny: Codable, Hashable { 158 case token(ContractOutput) 159 case taxReceipt(ContractOutputTaxReceipt) 160 case unknown(String) // a type added after this was written 161 162 private enum TypeKey: String, CodingKey { 163 case type 164 } 165 166 init(from decoder: Decoder) throws { 167 let container = try decoder.container(keyedBy: TypeKey.self) 168 switch try container.decode(String.self, forKey: .type) { 169 case "token": self = .token(try ContractOutput(from: decoder)) 170 case "tax-receipt": self = .taxReceipt(try ContractOutputTaxReceipt(from: decoder)) 171 case let other: self = .unknown(other) 172 } 173 } 174 175 func encode(to encoder: Encoder) throws { 176 switch self { 177 case .token(let output): try output.encode(to: encoder) 178 case .taxReceipt(let output): try output.encode(to: encoder) 179 case .unknown(let type): 180 var container = encoder.container(keyedBy: TypeKey.self) 181 try container.encode(type, forKey: .type) 182 } 183 } 184 } 185 186 struct ContractChoice: Codable, Hashable { 187 let amount: Amount // Total amount payable 188 let maxFee: Amount // Maximum deposit fee covered by the merchant 189 let description: String? // 190 let descriptionI18n: I18nDict? // " localized " 191 let inputs: [ContractInput] 192 let outputs: [ContractOutputAny] 193 194 enum CodingKeys: String, CodingKey { 195 case amount, description 196 case maxFee = "max_fee" 197 case descriptionI18n = "description_i18n" 198 case inputs, outputs 199 } 200 201 var locDescription: String? { 202 self.descriptionI18n?.localized ?? self.description 203 } 204 } 205 206 struct MerchantContractTerms: Codable { 207 let version: Int? // v0 doesn't know this 208 209 // ContractTermsV0 210 let amount: Amount? // Total amount payable 211 let maxFee: Amount? // Maximum deposit fee covered by the merchant 212 213 // ContractTermsCommon 214 let summary: String // Human-readable short summary of the contract 215 let summaryI18n: I18nDict? // " localized " 216 let orderID: String // uniquely identify the purchase within one merchant instance 217 let publicReorderURL: String? // URL meant to share the shopping cart 218 let fulfillmentURL: String? // Fulfillment URL to view the product or delivery status 219 let fulfillmentMessage: String? // Plain text fulfillment message in the merchant's default language 220 let fulfillmentMessageI18n: I18nDict?// " localized ", keyed by BCP-47 tag 221 let products: [ProductSold]? // Products that are sold in this contract 222 let timestamp: Timestamp // Time when the contract was generated by the merchant 223 let refundDeadline: Timestamp? // Deadline for refunds 224 let payDeadline: Timestamp // Deadline to pay for the contract 225 let wireTransferDeadline: Timestamp?// Deadline for the wire transfer 226 let merchantPub: String // Public key of the merchant 227 let merchantBaseURL: String // Base URL of the merchant's backend 228 let merchant: MerchantInfo 229 let hWire: String // Hash of the merchant's wire details 230 let wireMethod: String // merchant wants to use 231 let exchanges: [ExchangeForPay] 232 let deliveryLocation: Location? // Delivery location for (all!) products 233 let deliveryDate: Timestamp? // indicating when the order should be delivered 234 let nonce: String // used to ensure freshness 235 let autoRefund: Duration? 236 let extra: Extra? // Extra data, interpreted by the merchant only 237 let minimumAge: Int? 238 239 let defaultMoneyPot: Int? 240 241 // deprecated let auditors: [Auditor]? 242 243 // ContractTermsV1 244 let choices: [ContractChoice]? 245 // Map of storing metadata and issue keys of 246 // token families referenced in this contract. 247 // @since protocol **vSUBSCRIBE** 248 let tokenFamilies: [String: ContractTokenFamily]? // token_family_slug: String 249 250 enum CodingKeys: String, CodingKey { 251 case version 252 case amount 253 case maxFee = "max_fee" 254 255 case summary 256 case summaryI18n = "summary_i18n" 257 case orderID = "order_id" 258 case publicReorderURL = "public_reorder_url" 259 case fulfillmentURL = "fulfillment_url" 260 case fulfillmentMessage = "fulfillment_message" 261 case fulfillmentMessageI18n = "fulfillment_message_i18n" 262 case products 263 case timestamp 264 case refundDeadline = "refund_deadline" 265 case payDeadline = "pay_deadline" 266 case wireTransferDeadline = "wire_transfer_deadline" 267 case merchantPub = "merchant_pub" 268 case merchantBaseURL = "merchant_base_url" 269 case merchant 270 case hWire = "h_wire" 271 case wireMethod = "wire_method" 272 case exchanges 273 case deliveryLocation = "delivery_location" 274 case deliveryDate = "delivery_date" 275 case nonce 276 case autoRefund = "auto_refund" 277 case extra 278 case minimumAge = "minimum_age" 279 case defaultMoneyPot = "default_money_pot" 280 281 // case auditors 282 case choices 283 case tokenFamilies = "token_families" 284 } 285 286 func timeToPay(_ debug: Bool) -> Int? { 287 if let milliseconds = try? self.payDeadline.milliseconds() { 288 let date = Date(milliseconds: milliseconds) 289 let now = Date.now 290 let timeInterval = now.timeIntervalSince(date) 291 if timeInterval < 0 { 292 let result = Int(-timeInterval) 293 if debug { 294 // symLog?.log("\(timeInterval) seconds left to pay") 295 print("\(result) seconds left to pay") 296 } 297 return result 298 } else if debug { 299 // symLog?.log("\(date) - \(now) = \(timeInterval)") 300 print("\(date) - \(now) = \(timeInterval)") 301 } 302 return 0 303 } else if debug { 304 // symLog?.log("no milliseconds") 305 print("no milliseconds in payDeadline") 306 } 307 return nil 308 } 309 310 var locSummary: String { 311 self.summaryI18n?.localized ?? self.summary 312 } 313 } 314 // MARK: - Auditor 315 struct Auditor: Codable { 316 let name: String 317 let auditorPub: String 318 let url: String 319 320 enum CodingKeys: String, CodingKey { 321 case name 322 case auditorPub = "auditor_pub" 323 case url 324 } 325 } 326 // MARK: - Exchange 327 struct ExchangeForPay: Codable { 328 let url: String 329 let masterPub: String 330 331 enum CodingKeys: String, CodingKey { 332 case url 333 case masterPub = "master_pub" 334 } 335 } 336 // MARK: - Extra 337 struct Extra: Codable { 338 let articleName: String? 339 340 enum CodingKeys: String, CodingKey { 341 case articleName = "article_name" 342 } 343 } 344 // MARK: - 345 enum PreparePayResultType: String, Codable { 346 case paymentPossible = "payment-possible" 347 case alreadyConfirmed = "already-confirmed" 348 case insufficientBalance = "insufficient-balance" 349 case choiceSelection = "choice-selection" 350 } 351 352 struct ExchangeFeeGapEstimate: Codable { 353 let balanceAvailable: Amount 354 let balanceMaterial: Amount 355 let balanceExchangeDepositable: Amount 356 let balanceAgeAcceptable: Amount 357 let balanceReceiverAcceptable: Amount 358 let balanceReceiverDepositable: Amount 359 let maxEffectiveSpendAmount: Amount 360 } 361 362 struct PerScopeDetails: Codable { 363 let scopeInfo: ScopeInfo 364 } 365 366 /// The result from PreparePayForUri2 and preparePayForTemplate2 367 struct PreparePayResult2: Codable { 368 let transactionId: String 369 } 370 /// A request to get an exchange's payment contract terms. 371 fileprivate struct PreparePayForUri: WalletBackendFormattedRequest { 372 typealias Response = PreparePayResult2 373 var operation: String { "preparePayForUriV2" } 374 func args() -> Args { Args(talerPayUri: talerPayUri) } // No progressToken! use waitTransactionState 375 376 var talerPayUri: String 377 struct Args: Encodable { 378 var talerPayUri: String 379 } 380 } 381 382 /** 383 * Forced coin selection for deposits/payments. 384 */ 385 struct ValueContribution: Codable { 386 var value: Amount 387 var contribution: Amount 388 } 389 390 struct ForcedCoinSel: Codable { 391 var coins: [ValueContribution] 392 } 393 394 enum ChoiceSelectionDetailStatus: String, Codable { 395 case paymentPossible = "payment-possible" 396 case insufficientBalance = "insufficient-balance" 397 } 398 399 enum TokenAvailabilityHint: String, Codable { 400 case walletTokensAvailableInsufficient = "wallet-tokens-available-insufficient" 401 case merchantUnexpected = "merchant-unexpected" 402 case merchantUntrusted = "merchant-untrusted" 403 404 } 405 406 struct TokenFamily: Codable, Hashable, Equatable { 407 var causeHint: TokenAvailabilityHint? 408 var requested: Int 409 var available: Int 410 var unexpected: Int 411 var untrusted: Int 412 } 413 414 struct TokenDetails: Codable, Hashable, Equatable { 415 var tokensRequested: Int 416 var tokensAvailable: Int 417 var tokensUnexpected: Int 418 var tokensUntrusted: Int 419 var perTokenFamily: [String: TokenFamily] 420 } 421 422 struct ChoiceSelectionDetail: Codable, Hashable, Sendable { 423 var status: ChoiceSelectionDetailStatus 424 var amountRaw: Amount 425 var scopeInfo: ScopeInfo? // only if wallet-core has the info 426 var amountEffective: Amount? // only if possible 427 var tokenDetails: TokenDetails? // only if possible 428 var balanceDetails: PaymentInsufficientBalanceDetails? // only if insufficient 429 } 430 431 typealias ChoiceTriple = (ChoiceSelectionDetail, ContractChoice, Int) 432 typealias ChoicesTuple = (String?, ChoicesForPayment?) // txID 433 434 struct ChoicesForPayment: Codable { 435 var choices: [ChoiceSelectionDetail] 436 /** 437 * Index of the choice in @e choices array to present to the user as default. 438 * Won´t be set if no default selection is configured or no choice is payable, 439 * otherwise, it will always be 0 for v0 orders. 440 */ 441 var defaultChoiceIndex: Int? 442 /** 443 * Whether the choice referenced by @e automaticExecutableIndex 444 * should be confirmed automatically without user interaction. 445 * 446 * If true, the wallet should call `confirmPay' immediately afterwards 447 * If false, the user should be first prompted to select and confirm a choice. 448 * Undefined when no choices are payable. 449 */ 450 var automaticExecution: Bool? 451 var automaticExecutableIndex: Int? 452 453 var contractTerms: MerchantContractTerms 454 455 func choiceTriple() -> ([ChoiceTriple], Bool)? { 456 let terms = self.contractTerms 457 if let ctChoices = terms.choices { 458 let combined = Array(zip(choices, ctChoices, ctChoices.indices)) 459 return (combined, true) 460 } else if let amount = terms.amount { // V0 = only 1 option 461 let maxFee = terms.maxFee ?? Amount.zero(currency: amount.currencyStr) 462 let ctChoice = ContractChoice(amount: amount, 463 maxFee: maxFee, 464 description: terms.summary, 465 descriptionI18n: terms.summaryI18n, 466 inputs: [], 467 outputs: []) 468 let combined = Array(zip(choices, [ctChoice], [0])) 469 return (combined, false) 470 } 471 // symLog.log(" ❗️Yikes, neither choices nor amount in contractTerms!\n\(stack)") 472 return nil 473 } 474 } 475 476 /// A request to get an exchange's payment contract terms. 477 fileprivate struct GetChoicesForPayment: WalletBackendFormattedRequest { 478 typealias Response = ChoicesForPayment 479 var operation: String { "getChoicesForPayment" } 480 func args() -> Args { Args(transactionId: transactionId, forcedCoinSel: forcedCoinSel) } 481 482 var transactionId: String 483 var forcedCoinSel: ForcedCoinSel? 484 struct Args: Encodable { 485 var transactionId: String 486 var forcedCoinSel: ForcedCoinSel? 487 } 488 } 489 490 struct TemplateParams: Codable { 491 let amount: Amount? // Total amount payable 492 let summary: String? // Human-readable short summary of the contract 493 } 494 495 /// A request to get an exchange's payment contract terms. 496 fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest { 497 typealias Response = PreparePayResult2 498 var operation: String { "preparePayForTemplateV2" } 499 func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams, 500 progressToken: talerPayTemplateUri) } 501 502 var talerPayTemplateUri: String 503 var templateParams: TemplateParams 504 struct Args: Encodable { 505 var talerPayTemplateUri: String 506 var templateParams: TemplateParams 507 var progressToken: String 508 } 509 } 510 // MARK: - 511 enum TemplateType: String, Codable { 512 case fixedOrder = "fixed-order" 513 case inventoryCart = "inventory-cart" 514 case paivana 515 } 516 517 struct TemplateContractDetails: Codable { 518 let summary: String? // Human-readable short summary of the contract. Editable if nil 519 let currency: String? // specify currency when amount is nil - unspecified if nil 520 let payDuration: Duration? 521 let maxPickupDuration: Duration? 522 let minimumAge: Int? 523 let requestTip: Bool? 524 525 let templateType: TemplateType? 526 let amount: Amount? // Total amount payable. Fixed if this field exists, editable if nil 527 let scopeInfo: ScopeInfo? // unfortunately this DOESN'T exist - but it should 528 let websiteRegex: String? 529 let choices: [OrderChoice]? 530 531 enum CodingKeys: String, CodingKey { 532 case summary, currency, amount 533 case scopeInfo, choices 534 case minimumAge = "minimum_age" 535 case requestTip = "request_tip" 536 case payDuration = "pay_duration" 537 case maxPickupDuration = "max_pickup_duration" 538 case websiteRegex = "website_regex" 539 case templateType = "template_type" 540 } 541 } 542 543 struct OrderChoice: Codable { 544 let amount: Amount 545 let tip: Amount? 546 let description: String? 547 let descriptionI18n: I18nDict? 548 let inputs: [OrderInput]? 549 let outputs: [OrderOutput]? // TODO: OrderOutputTaxReceipt 550 let maxFee: Amount? 551 552 enum CodingKeys: String, CodingKey { 553 case amount, tip, description 554 case descriptionI18n = "description_i18n" 555 case inputs, outputs 556 case maxFee = "max_fee" 557 } 558 } 559 560 struct OrderInput: Codable { // see ContractInput 561 let type: String // "token" 562 563 // Token family slug as configured in the merchant backend. 564 // Slug is unique across all configured tokens of a merchant. 565 let tokenFamilySlug: String? 566 567 // How many units of the input are required. 568 // Defaults to 1 if not specified. 569 // Output with count == 0 are ignored by the merchant backend. 570 let count: Int? 571 572 enum CodingKeys: String, CodingKey { 573 case type, count 574 case tokenFamilySlug = "token_family_slug" 575 } 576 } 577 578 struct OrderOutput: Codable { // TODO: ContractOutput 579 let type: String // "token" 580 581 // Token family slug as configured in the merchant backend. 582 // Slug is unique across all configured tokens of a merchant. 583 let tokenFamilySlug: String? 584 585 // How many units of the output are issued by the merchant. 586 // Defaults to 1 if not specified. 587 // Output with count == 0 are ignored by the merchant backend. 588 let count: Int? 589 590 // When should the output token be valid. Can be specified if the 591 // desired validity period should be in the future (like selling 592 // a subscription for the next month). Optional. If not given, 593 // the validity is supposed to be "now" (time of order creation). 594 let validAt: Timestamp? 595 596 enum CodingKeys: String, CodingKey { 597 case type, count 598 case tokenFamilySlug = "token_family_slug" 599 case validAt = "valid_at" 600 } 601 } 602 603 struct OrderOutputTaxReceipt: Codable { 604 let type: String // "tax-receipt" 605 } 606 607 struct TemplateContractDetailsDefaults: Codable { 608 let summary: String? // Default 'Human-readable summary' when editable: empty if nil 609 let currency: String? // Default currency when unspecified: any if nil (e.g. donations) 610 let amount: Amount? // Default amount when editable: unspecified if nil 611 } 612 613 struct TalerMerchantTemplateDetails: Codable { 614 let templateContract: TemplateContractDetails 615 let editableDefaults: TemplateContractDetailsDefaults? 616 let requiredCurrency: String? 617 enum CodingKeys: String, CodingKey { 618 case templateContract = "template_contract" 619 case editableDefaults = "editable_defaults" 620 case requiredCurrency = "required_currency" 621 } 622 } 623 624 /// The result from checkPayForTemplate 625 struct WalletTemplateDetails: Codable { 626 let templateDetails: TalerMerchantTemplateDetails 627 let supportedCurrencies: [String] 628 } 629 /// A request to get an exchange's payment contract terms. 630 fileprivate struct CheckPayForTemplate: WalletBackendFormattedRequest { 631 typealias Response = WalletTemplateDetails 632 var operation: String { "checkPayForTemplate" } 633 func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, progressToken: talerPayTemplateUri) } 634 635 var talerPayTemplateUri: String 636 struct Args: Encodable { 637 var talerPayTemplateUri: String 638 var progressToken: String 639 } 640 } 641 // MARK: - 642 /// The result from confirmPayForUri 643 struct ConfirmPayResult: Decodable { 644 var type: String? // done || pending 645 var contractTerms: MerchantContractTerms? // only if type==done 646 var transactionId: String 647 var lastError: TalerErrorDetail? // might, but only if type==pending 648 } 649 650 /// A request to get an exchange's payment details. 651 fileprivate struct ConfirmPayForUri: WalletBackendFormattedRequest { 652 typealias Response = ConfirmPayResult 653 var operation: String { "confirmPay" } 654 func args() -> Args { Args(transactionId: transactionId, noWait: true, 655 choiceIndex: choiceIndex) } 656 var transactionId: String 657 var choiceIndex: Int? 658 struct Args: Encodable { 659 var transactionId: String 660 var noWait: Bool? 661 var useDonau: Bool? 662 var sessionId: String? 663 var forcedCoinSel: ForcedCoinSel? 664 /** 665 * Whether token selection should be forced 666 * e.g. use tokens with non-matching `expected_domains' 667 * 668 * Only applies to v1 orders. 669 */ 670 var forcedTokenSel: Bool? 671 /** 672 * Only applies to v1 orders. 673 */ 674 var choiceIndex: Int? 675 } 676 } 677 678 struct UnclaimPaymentResult: Decodable { 679 var talerPayUri: String 680 } 681 682 fileprivate struct UnclaimPayment: WalletBackendFormattedRequest { 683 typealias Response = UnclaimPaymentResult 684 var operation: String { "unclaimPayment" } 685 func args() -> Args { Args(transactionId: transactionId, progressToken: transactionId) } 686 687 var transactionId: String 688 struct Args: Encodable { 689 var transactionId: String 690 var progressToken: String? 691 } 692 } 693 // MARK: - 694 extension WalletModel { 695 /// load payment details. Networking involved 696 nonisolated func checkPayForTemplate(_ talerPayTemplateUri: String, viewHandles: Bool = false) 697 async throws -> WalletTemplateDetails { 698 let request = CheckPayForTemplate(talerPayTemplateUri: talerPayTemplateUri) 699 let controller = Controller.shared 700 controller.progressOperation = request.operation 701 controller.progressToken = talerPayTemplateUri 702 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 703 return response 704 } 705 706 nonisolated func preparePayForTemplate(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false) 707 async throws -> PreparePayResult2 { 708 let templateParams = TemplateParams(amount: amount, summary: summary) 709 let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) 710 let controller = Controller.shared 711 controller.progressOperation = request.operation 712 controller.progressToken = talerPayTemplateUri 713 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 714 return response 715 } 716 717 nonisolated func getChoicesForPayment(_ transactionId: String, viewHandles: Bool = false) 718 async throws -> ChoicesForPayment { 719 let request = GetChoicesForPayment(transactionId: transactionId, forcedCoinSel: nil) 720 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 721 return response 722 } 723 724 nonisolated func preparePayForUri(_ talerPayUri: String, viewHandles: Bool = false) 725 async throws -> PreparePayResult2 { 726 let request = PreparePayForUri(talerPayUri: talerPayUri) 727 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 728 return response 729 } 730 731 nonisolated func confirmPay(_ transactionId: String, choiceIndex: Int?, viewHandles: Bool = false) 732 async throws -> ConfirmPayResult { 733 let request = ConfirmPayForUri(transactionId: transactionId, 734 choiceIndex: choiceIndex) 735 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 736 return response 737 } 738 739 nonisolated func unclaimPayment(_ transactionId: String, viewHandles: Bool = false) 740 async throws -> String { 741 let request = UnclaimPayment(transactionId: transactionId) 742 let controller = Controller.shared 743 controller.progressOperation = request.operation 744 controller.progressToken = transactionId 745 let (response, id) = try await sendRequest(request, viewHandles: viewHandles) 746 return response.talerPayUri 747 } 748 }