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/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) diff --git a/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift b/Example/Example/Sources/UI/Modules/ApplePay/ViewModel/ApplePayViewModel.swift index 835577584..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: state.invoice.name, - 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 29c4d3865..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: state.invoice.name, - 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/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 diff --git a/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift b/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift index 812aedcb8..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: state.invoice.name, - 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 { 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" } } 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() } 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 {