Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Example/Example/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,16 @@
}
}
},
"dynamic-checkout.3ds-service" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "3DS Service"
}
}
}
},
"dynamic-checkout.error-message" : {
"localizations" : {
"en" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ final class CardPaymentViewModel: ObservableObject {

init(invoicesService: POInvoicesService) {
self.invoicesService = invoicesService
commonInit()
}

// MARK: -

@Published
var state: CardPaymentViewModelState! // swiftlint:disable:this implicitly_unwrapped_optional
var state = CardPaymentViewModelState()

func pay() {
state.message = nil
Expand All @@ -37,13 +36,6 @@ final class CardPaymentViewModel: ObservableObject {

// MARK: - Private Methods

private func commonInit() {
state = .init(
authenticationService: .init(sources: [.test, .checkout, .netcetera], id: \.self, selection: .netcetera),
cardTokenization: nil
)
}

private func setCardTokenizationItem() {
let configuration = POCardTokenizationConfiguration(
cardholderName: nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ struct CardPaymentViewModelState {
var invoice = InvoiceViewModel()

/// 3DS service.
var authenticationService: PickerData<AuthenticationService, AuthenticationService>
var authenticationService = PickerData<AuthenticationService, AuthenticationService>(
sources: [.test, .checkout, .netcetera], id: \.self, selection: .netcetera
)

/// Card tokenization.
var cardTokenization: CardTokenization?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ extension LocalizedStringResource {
/// Title.
static let title = LocalizedStringResource("dynamic-checkout.title")

/// 3DS service.
static let threeDSService = LocalizedStringResource("dynamic-checkout.3ds-service")

/// Continue button.
static let pay = LocalizedStringResource("dynamic-checkout.pay")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ struct DynamicCheckoutView: View {
MessageView(viewModel: viewModel)
}
InvoiceView(viewModel: $viewModel.state.invoice)
Section {
Picker(data: $viewModel.state.authenticationService) { service in
Text(service.rawValue.capitalized)
} label: {
Text(.DynamicCheckout.threeDSService)
}
}
Button(String(localized: .DynamicCheckout.pay)) {
viewModel.pay()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import PassKit
import SwiftUI
@_spi(PO) import ProcessOut
@_spi(PO) import ProcessOutUI
import ProcessOutCheckout3DS
import ProcessOutNetcetera3DS
import Checkout3DS

@MainActor
final class DynamicCheckoutViewModel: ObservableObject {
Expand Down Expand Up @@ -112,7 +115,16 @@ extension DynamicCheckoutViewModel: PODynamicCheckoutDelegate {
willAuthorizeInvoiceWith request: inout POInvoiceAuthorizationRequest,
using paymentMethod: PODynamicCheckoutPaymentMethod
) async -> any PO3DS2Service {
POTest3DSService()
switch state.authenticationService.selection {
case .test:
return POTest3DSService()
case .checkout:
let delegate = DefaultCheckout3DSDelegate()
return POCheckout3DSService(delegate: delegate, environment: .sandbox)
case .netcetera:
let configuration = PONetcetera3DS2ServiceConfiguration(returnUrl: Constants.returnUrl)
return PONetcetera3DS2Service(configuration: configuration)
}
}

func dynamicCheckout(willAuthorizeInvoiceWith request: PKPaymentRequest) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ import ProcessOut

struct DynamicCheckoutViewModelState {

enum AuthenticationService: String, Hashable {

/// Test service.
case test

/// Checkout service.
case checkout

/// Netcetera 3DS SDK.
case netcetera
}

struct DynamicCheckout: Identifiable {

let id: String
Expand All @@ -28,6 +40,11 @@ struct DynamicCheckoutViewModelState {
/// Invoice details.
var invoice = InvoiceViewModel()

/// 3DS service.
var authenticationService = PickerData<AuthenticationService, AuthenticationService>(
sources: [.test, .checkout, .netcetera], id: \.self, selection: .netcetera
)

/// Dynamic checkout.
var dynamicCheckout: DynamicCheckout?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,22 @@ final class UrlSessionHttpConnector: HttpConnector {
private func convertToFailure(urlError error: Error) -> Failure {
let code: Failure.Code
switch error {
case URLError.appTransportSecurityRequiresSecureConnection,
URLError.secureConnectionFailed,
URLError.serverCertificateHasBadDate,
URLError.serverCertificateHasUnknownRoot,
URLError.serverCertificateNotYetValid,
URLError.serverCertificateUntrusted,
URLError.clientCertificateRejected,
URLError.clientCertificateRequired:
code = .security
case URLError.cancelled:
code = .cancelled
case URLError.notConnectedToInternet, URLError.networkConnectionLost:
case URLError.notConnectedToInternet,
URLError.networkConnectionLost,
URLError.internationalRoamingOff,
URLError.dataNotAllowed,
URLError.callIsActive:
code = .networkUnreachable
case URLError.timedOut:
code = .timeout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ struct HttpConnectorFailure: Error, Sendable {
/// Cancellation error.
case cancelled

/// An attempt to establish a secure connection failed.
case security

/// Internal error.
case `internal`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ final class DefaultHttpConnectorFailureMapper: HttpConnectorFailureMapper {
message = "Request was cancelled."
code = .Mobile.cancelled
invalidFields = nil
case .security:
message = "An attempt to establish a secure connection failed."
code = .Mobile.connectionSecurity
invalidFields = nil
case let .server(error, _):
message = error.message
code = .init(rawValue: error.errorType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,9 @@ extension POFailureCode {
/// Cancellation error.
public static let cancelled = POFailureCode(rawValue: "processout-mobile.cancelled")

/// An attempt to establish a secure connection failed.
public static let connectionSecurity = POFailureCode(rawValue: "processout-mobile.connection-security")

/// Internal error.
public static let `internal` = POFailureCode(rawValue: "processout-mobile.internal")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,23 @@ final class DefaultAlternativePaymentsService: POAlternativePaymentsService {
throw POFailure(message: message, code: .generic(.mobile), underlyingError: nil)
}
let queryItems = components.queryItems ?? []
if let gatewayToken = queryItems.queryItemValue(name: "token") {
if gatewayToken.isEmpty {
logger.debug("Gateway 'token' is empty in \(url), this may be an error.")
}
let tokenId = queryItems.queryItemValue(name: "token_id")
if let customerId = queryItems.queryItemValue(name: "customer_id"), let tokenId {
return .init(
gatewayToken: gatewayToken, customerId: customerId, tokenId: tokenId, returnType: .createToken
)
}
return .init(gatewayToken: gatewayToken, customerId: nil, tokenId: tokenId, returnType: .authorization)
}
if let errorCode = queryItems.queryItemValue(name: "error_code") {
throw POFailure(code: .init(rawValue: errorCode))
}
let gatewayToken = queryItems.queryItemValue(name: "token") ?? ""
if gatewayToken.isEmpty {
logger.debug("Gateway 'token' is not set in \(url), this may be an error.")
}
let tokenId = queryItems.queryItemValue(name: "token_id")
if let customerId = queryItems.queryItemValue(name: "customer_id"), let tokenId {
return .init(gatewayToken: gatewayToken, customerId: customerId, tokenId: tokenId, returnType: .createToken)
}
return .init(gatewayToken: gatewayToken, customerId: nil, tokenId: tokenId, returnType: .authorization)
logger.warn("Both token and error_code are not set in \(url).")
return .init(gatewayToken: "", customerId: nil, tokenId: nil, returnType: .authorization)
}

// MARK: -
Expand Down Expand Up @@ -154,14 +159,17 @@ final class DefaultAlternativePaymentsService: POAlternativePaymentsService {
throw POFailure(message: message, code: .Mobile.generic, underlyingError: nil)
}
let queryItems = components.queryItems ?? []
if let gatewayToken = queryItems.queryItemValue(name: "token") {
if gatewayToken.isEmpty {
logger.debug("Gateway 'token' is empty in \(url), this may be an error.")
}
return .init(gatewayToken: gatewayToken)
}
if let errorCode = queryItems.queryItemValue(name: "error_code") {
throw POFailure(code: .init(rawValue: errorCode))
}
let gatewayToken = queryItems.queryItemValue(name: "token") ?? ""
if gatewayToken.isEmpty {
logger.debug("Gateway 'token' is not set in \(url), this may be an error.")
}
return .init(gatewayToken: gatewayToken)
logger.warn("Both token and error_code are not set in \(url).")
return .init(gatewayToken: "")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,14 +599,16 @@ final class NativeAlternativePaymentDefaultInteractor:
) async throws -> NativeAlternativePaymentResolvedElement.Group {
var resolvedInstructions: [NativeAlternativePaymentResolvedElement.Instruction] = []
for instruction in group.instructions {
resolvedInstructions.append(try await resolve(customerInstruction: instruction))
if let resolvedInstruction = try await resolve(customerInstruction: instruction) {
resolvedInstructions.append(resolvedInstruction)
}
}
return .init(label: group.label, instructions: resolvedInstructions)
}

private func resolve(
customerInstruction: PONativeAlternativePaymentCustomerInstructionV2
) async throws -> NativeAlternativePaymentResolvedElement.Instruction {
) async throws -> NativeAlternativePaymentResolvedElement.Instruction? {
switch customerInstruction {
case .barcode(let barcode):
let minimumSize = CGSize(width: 250, height: 250)
Expand All @@ -618,8 +620,9 @@ final class NativeAlternativePaymentDefaultInteractor:
case .message(let message):
return .message(.init(label: message.label, value: message.value))
case .image(let image):
guard let image = await imagesRepository.image(resource: image.value) else {
throw POFailure(message: "Unable to prepare customer instruction image.", code: .Mobile.internal)
guard let image = await imagesRepository.image(resource: image.value) else { // Treated as decoration
logger.debug("Unable to prepare customer instruction image.")
return nil
}
return .image(image)
default:
Expand Down Expand Up @@ -653,12 +656,15 @@ final class NativeAlternativePaymentDefaultInteractor:
}
return .form(form)
case .customerInstruction(let instruction):
return .instruction(try await resolve(customerInstruction: instruction))
if let resolvedInstruction = try await resolve(customerInstruction: instruction) {
return .instruction(resolvedInstruction)
}
case .group(let group):
return .group(try await resolve(group: group))
case .unknown:
return nil
}
return nil
}

// MARK: - Payment Method Utils
Expand Down