From 861256554131097327b4f9fa69d92a3b6afa0f5a Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 13 Sep 2024 15:48:18 +0200 Subject: [PATCH 1/7] Allow injecting invoice ID --- .../Example/Resources/Localizable.xcstrings | 10 ++++++++ .../LocalizedStringResource+Invoice.swift | 3 +++ .../UI/Modules/Invoice/View/InvoiceView.swift | 25 ++++++++++++++++--- .../Invoice/ViewModel/InvoiceViewModel.swift | 8 +++--- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Example/Example/Resources/Localizable.xcstrings b/Example/Example/Resources/Localizable.xcstrings index a92710ef2..221bddedb 100644 --- a/Example/Example/Resources/Localizable.xcstrings +++ b/Example/Example/Resources/Localizable.xcstrings @@ -451,6 +451,16 @@ } } }, + "invoice.id" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID (optional)" + } + } + } + }, "invoice.name" : { "localizations" : { "en" : { diff --git a/Example/Example/Sources/UI/Modules/Invoice/Symbols/LocalizedStringResource+Invoice.swift b/Example/Example/Sources/UI/Modules/Invoice/Symbols/LocalizedStringResource+Invoice.swift index f67234953..ced9d26d7 100644 --- a/Example/Example/Sources/UI/Modules/Invoice/Symbols/LocalizedStringResource+Invoice.swift +++ b/Example/Example/Sources/UI/Modules/Invoice/Symbols/LocalizedStringResource+Invoice.swift @@ -11,6 +11,9 @@ extension LocalizedStringResource { enum Invoice { + /// Invoice ID. + static let id = LocalizedStringResource("invoice.id") + /// Title. static let title = LocalizedStringResource("invoice.title") diff --git a/Example/Example/Sources/UI/Modules/Invoice/View/InvoiceView.swift b/Example/Example/Sources/UI/Modules/Invoice/View/InvoiceView.swift index 52e5b8f61..2093e641a 100644 --- a/Example/Example/Sources/UI/Modules/Invoice/View/InvoiceView.swift +++ b/Example/Example/Sources/UI/Modules/Invoice/View/InvoiceView.swift @@ -17,9 +17,25 @@ struct InvoiceView: View { var body: some View { Section { - TextField( - String(localized: .Invoice.name), text: $viewModel.name - ) + HStack { + TextField( + String(localized: .Invoice.id), text: $viewModel.id + ) + .keyboardType(.asciiCapable) + Button( + action: { + isScannerPresented = true + }, + label: { + Image(systemName: "qrcode.viewfinder") + } + ) + } + .sheet(isPresented: $isScannerPresented) { + ConfigurationScannerView { invoiceId in + viewModel.id = invoiceId + } + } TextField( String(localized: .Invoice.amount), value: $viewModel.amount, format: .number ) @@ -37,4 +53,7 @@ struct InvoiceView: View { @Binding private var viewModel: InvoiceViewModel + + @State + private var isScannerPresented = false } diff --git a/Example/Example/Sources/UI/Modules/Invoice/ViewModel/InvoiceViewModel.swift b/Example/Example/Sources/UI/Modules/Invoice/ViewModel/InvoiceViewModel.swift index 80e7ef861..d5a3cd75f 100644 --- a/Example/Example/Sources/UI/Modules/Invoice/ViewModel/InvoiceViewModel.swift +++ b/Example/Example/Sources/UI/Modules/Invoice/ViewModel/InvoiceViewModel.swift @@ -9,8 +9,8 @@ import Foundation struct InvoiceViewModel { - /// Invoice name. - var name: String + /// Invoice ID. + var id: String /// Invoice amount. var amount: Decimal @@ -23,8 +23,8 @@ extension InvoiceViewModel { /// Convenience init to create default view model. init() { - self.name = UUID().uuidString - self.amount = 100 + id = "" + amount = 100 currencyCode = "USD" } } From 86c805e3b0482ccb5df30626ac69762e27b985e7 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 13 Sep 2024 15:48:49 +0200 Subject: [PATCH 2/7] Unconditionally attach private key to requests --- .../DefaultHttpConnectorRequestMapper.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift index 865362a37..aafac1c9c 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift @@ -85,12 +85,10 @@ final class DefaultHttpConnectorRequestMapper: HttpConnectorRequestMapper { request: HttpConnectorRequest, configuration: HttpConnectorRequestMapperConfiguration ) -> String { var value = configuration.projectId + ":" - if request.requiresPrivateKey { - if let privateKey = configuration.privateKey { - value += privateKey - } else { - assertionFailure("Private key is required by '\(request)' request but not set") - } + if let privateKey = configuration.privateKey { + value += privateKey + } else if request.requiresPrivateKey { + assertionFailure("Private key is required by '\(request)' request but not set") } return "Basic " + Data(value.utf8).base64EncodedString() } From 956e780eb06ae15423f626be6e447f4b2e667ac6 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 13 Sep 2024 15:50:29 +0200 Subject: [PATCH 3/7] Respect custom invoice ID during alternative payment --- .../Interactor/AlternativePaymentsInteractor.swift | 9 +++++++-- .../ViewModel/AlternativePaymentsViewModel.swift | 12 +++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Example/Example/Sources/UI/Modules/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift b/Example/Example/Sources/UI/Modules/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift index 5e31b1b09..585abff2d 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift @@ -82,9 +82,14 @@ final class AlternativePaymentsInteractor { _ = await task.result } - func createInvoice(name: String, amount: Decimal, currencyCode: String) async throws -> POInvoice { + func invoice(id: String) async throws -> POInvoice { + let request = POInvoiceRequest(invoiceId: id) + return try await invoicesService.invoice(request: request) + } + + func createInvoice(amount: Decimal, currencyCode: String) async throws -> POInvoice { let request = POInvoiceCreationRequest( - name: name, + name: UUID().uuidString, amount: amount, currency: currencyCode, returnUrl: Example.Constants.returnUrl, diff --git a/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift b/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift index 83caaf156..a5345d6ea 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift @@ -134,11 +134,13 @@ final class AlternativePaymentsViewModel: ObservableObject { return } do { - let invoice = try await interactor.createInvoice( - name: state.invoice.name, - amount: state.invoice.amount, - currencyCode: state.invoice.currencyCode - ) + let invoice = if state.invoice.id.isEmpty { + try await interactor.createInvoice( + amount: state.invoice.amount, currencyCode: state.invoice.currencyCode + ) + } else { + try await interactor.invoice(id: state.invoice.id) + } var authorizationSource = gatewayConfigurationId if state.shouldTokenize { let token = try await interactor.tokenize(gatewayConfigurationId: gatewayConfigurationId) From 958a15d53e21ca1177eeff7d5b6835f98d6486f5 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 13 Sep 2024 15:51:22 +0200 Subject: [PATCH 4/7] Simplify configuration scanner integration --- .../Modules/Configuration/View/ConfigurationView.swift | 10 ++-------- .../ConfigurationScannerView.swift | 4 ++++ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Example/Example/Sources/UI/Modules/Configuration/View/ConfigurationView.swift b/Example/Example/Sources/UI/Modules/Configuration/View/ConfigurationView.swift index 42a13a070..23fbac182 100644 --- a/Example/Example/Sources/UI/Modules/Configuration/View/ConfigurationView.swift +++ b/Example/Example/Sources/UI/Modules/Configuration/View/ConfigurationView.swift @@ -63,15 +63,9 @@ struct ConfigurationView: View { } } .sheet(isPresented: $isScannerPresented) { - VStack { - ConfigurationScannerView { code in - viewModel.didScanConfiguration(code) - } - Spacer() + ConfigurationScannerView { code in + viewModel.didScanConfiguration(code) } - .presentationCornerRadius(16) - .presentationDragIndicator(.visible) - .presentationDetents([.fraction(0.5)]) } } .onReceive(viewModel.dismiss) { diff --git a/Example/Example/Sources/UI/Modules/ConfigurationScanner/ConfigurationScannerView.swift b/Example/Example/Sources/UI/Modules/ConfigurationScanner/ConfigurationScannerView.swift index 2fded87b5..6f884cab7 100644 --- a/Example/Example/Sources/UI/Modules/ConfigurationScanner/ConfigurationScannerView.swift +++ b/Example/Example/Sources/UI/Modules/ConfigurationScanner/ConfigurationScannerView.swift @@ -39,9 +39,13 @@ struct ConfigurationScannerView: View { .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(8) .fixedSize(horizontal: false, vertical: true) + Spacer() } .frame(maxWidth: .infinity) .padding(32) + .presentationCornerRadius(16) + .presentationDragIndicator(.visible) + .presentationDetents([.fraction(0.5)]) } // MARK: - Private Properties From 8a598b6a086a20f3db460e2a4c958993a5fe4b05 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 13 Sep 2024 15:51:47 +0200 Subject: [PATCH 5/7] Resolve invoice name internally --- .../UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift | 2 +- .../UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift | 2 +- .../DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift b/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift index 835577584..52801dadf 100644 --- a/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift +++ b/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift @@ -55,7 +55,7 @@ final class ApplePayViewModel: ObservableObject { do { var invoice: POInvoice! // swiftlint:disable:this implicitly_unwrapped_optional let invoiceCreationRequest = POInvoiceCreationRequest( - name: state.invoice.name, + name: UUID().uuidString, amount: state.invoice.amount, currency: state.invoice.currencyCode ) diff --git a/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift b/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift index 29c4d3865..e38b59053 100644 --- a/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift +++ b/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift @@ -71,7 +71,7 @@ extension CardPaymentViewModel: POCardTokenizationDelegate { func cardTokenization(didTokenizeCard card: POCard, shouldSaveCard save: Bool) async throws { let invoiceCreationRequest = POInvoiceCreationRequest( - name: state.invoice.name, + name: UUID().uuidString, amount: state.invoice.amount, currency: state.invoice.currencyCode, returnUrl: Constants.returnUrl diff --git a/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift b/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift index 812aedcb8..c56ba1a3d 100644 --- a/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift +++ b/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift @@ -38,7 +38,7 @@ final class DynamicCheckoutViewModel: ObservableObject { @MainActor private func startDynamicCheckout() async { let invoiceCreationRequest = POInvoiceCreationRequest( - name: state.invoice.name, + name: UUID().uuidString, amount: state.invoice.amount, currency: state.invoice.currencyCode, returnUrl: Constants.returnUrl, From a44c0387b713d726d766401146eaf092c5e5160b Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 13 Sep 2024 16:02:11 +0200 Subject: [PATCH 6/7] Respect injected invoice ID --- .../ViewModel/ApplePayViewModel.swift | 23 ++++++++++++------ .../ViewModel/CardPaymentViewModel.swift | 23 ++++++++++++------ .../ViewModel/DynamicCheckoutViewModel.swift | 24 ++++++++++++------- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift b/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift index 52801dadf..ac890edc8 100644 --- a/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift +++ b/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift @@ -53,14 +53,8 @@ final class ApplePayViewModel: ObservableObject { private func createInvoiceAndAuthorize(request: PKPaymentRequest) async { do { - var invoice: POInvoice! // swiftlint:disable:this implicitly_unwrapped_optional - let invoiceCreationRequest = POInvoiceCreationRequest( - name: UUID().uuidString, - amount: state.invoice.amount, - currency: state.invoice.currencyCode - ) + let invoice = try await createInvoice() let coordinator = ApplePayTokenizationCoordinator { [invoicesService] card in - invoice = try await invoicesService.createInvoice(request: invoiceCreationRequest) let authorizationRequest = POInvoiceAuthorizationRequest( invoiceId: invoice.id, source: card.id ) @@ -77,6 +71,21 @@ final class ApplePayViewModel: ObservableObject { } } + private func createInvoice() async throws -> POInvoice { + if state.invoice.id.isEmpty { + let request = POInvoiceCreationRequest( + name: UUID().uuidString, + amount: state.invoice.amount, + currency: state.invoice.currencyCode, + returnUrl: Constants.returnUrl + ) + return try await invoicesService.createInvoice(request: request) + } else { + let request = POInvoiceRequest(invoiceId: state.invoice.id) + return try await invoicesService.invoice(request: request) + } + } + private func setSuccessMessage(invoice: POInvoice, card: POCard) { let text = String(localized: .ApplePay.successMessage, replacements: invoice.id, card.id) state.message = .init(text: text, severity: .success) diff --git a/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift b/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift index e38b59053..874878437 100644 --- a/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift +++ b/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift @@ -65,18 +65,27 @@ final class CardPaymentViewModel: ObservableObject { ) state.cardTokenization = cardTokenizationItem } + + private func createInvoice() async throws -> POInvoice { + if state.invoice.id.isEmpty { + let request = POInvoiceCreationRequest( + name: UUID().uuidString, + amount: state.invoice.amount, + currency: state.invoice.currencyCode, + returnUrl: Constants.returnUrl + ) + return try await invoicesService.createInvoice(request: request) + } else { + let request = POInvoiceRequest(invoiceId: state.invoice.id) + return try await invoicesService.invoice(request: request) + } + } } extension CardPaymentViewModel: POCardTokenizationDelegate { func cardTokenization(didTokenizeCard card: POCard, shouldSaveCard save: Bool) async throws { - let invoiceCreationRequest = POInvoiceCreationRequest( - name: UUID().uuidString, - amount: state.invoice.amount, - currency: state.invoice.currencyCode, - returnUrl: Constants.returnUrl - ) - let invoice = try await invoicesService.createInvoice(request: invoiceCreationRequest) + let invoice = try await createInvoice() let invoiceAuthorizationRequest = POInvoiceAuthorizationRequest( invoiceId: invoice.id, source: card.id, diff --git a/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift b/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift index c56ba1a3d..4ce8a52da 100644 --- a/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift +++ b/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift @@ -37,15 +37,8 @@ final class DynamicCheckoutViewModel: ObservableObject { @MainActor private func startDynamicCheckout() async { - let invoiceCreationRequest = POInvoiceCreationRequest( - name: UUID().uuidString, - amount: state.invoice.amount, - currency: state.invoice.currencyCode, - returnUrl: Constants.returnUrl, - customerId: Constants.customerId - ) do { - let invoice = try await self.invoicesService.createInvoice(request: invoiceCreationRequest) + let invoice = try await createInvoice() continueDynamicCheckout(invoice: invoice) } catch { setMessage(with: error) @@ -87,6 +80,21 @@ final class DynamicCheckoutViewModel: ObservableObject { } state.message = .init(text: errorMessage ?? String(localized: .DynamicCheckout.errorMessage), severity: .error) } + + private func createInvoice() async throws -> POInvoice { + if state.invoice.id.isEmpty { + let request = POInvoiceCreationRequest( + name: UUID().uuidString, + amount: state.invoice.amount, + currency: state.invoice.currencyCode, + returnUrl: Constants.returnUrl + ) + return try await invoicesService.createInvoice(request: request) + } else { + let request = POInvoiceRequest(invoiceId: state.invoice.id) + return try await invoicesService.invoice(request: request) + } + } } extension DynamicCheckoutViewModel: PODynamicCheckoutDelegate { From da4f71b3c29b1f2fd989544b099d77d1c9aa68a8 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 13 Sep 2024 18:56:56 +0200 Subject: [PATCH 7/7] Fix tests --- .../Http/DefaultHttpConnectorRequestMapperTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift index f3e73f30a..9b2925546 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/DefaultHttpConnectorRequestMapperTests.swift @@ -139,7 +139,7 @@ final class DefaultHttpConnectorRequestMapperTests: XCTestCase { // Then let authorization = urlRequest.value(forHTTPHeaderField: "Authorization") - XCTAssertEqual(authorization, "Basic PElEPjo=") + XCTAssertEqual(authorization, "Basic PElEPjo8S0VZPg==") } func test_urlRequest_whenPrivateKeyIsRequired_addsAuthorization() async throws {