Skip to content

Commit

Permalink
fix(POM-423): UI modules cancellation (#359)
Browse files Browse the repository at this point in the history
Ensure that UI module lifecycle is ended properly (meaning completion
invocation and related events) when screen is dismissed "externally"
(e.g. interactively by user or programatically). Fix affects
following screens: card tokenization, card update and native
alternative payment.

Minor improvements:
* Accept full card number when resolving issuer information.
* Resolve actor related warnings in UI module
* Prevent errors from redundant fonts registration in generated code
  • Loading branch information
andrii-vysotskyi-cko authored Oct 2, 2024
1 parent 744f05d commit 92ff6b2
Show file tree
Hide file tree
Showing 32 changed files with 761 additions and 609 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ final class AlternativePaymentsViewModel: ObservableObject {
let configuration = PONativeAlternativePaymentConfiguration(
invoiceId: invoice.id,
gatewayConfigurationId: gatewayConfigurationId,
secondaryAction: .cancel(),
secondaryAction: .cancel(
confirmation: .init()
),
paymentConfirmation: .init(
showProgressIndicatorAfter: 5,
confirmButton: .init(),
Expand Down
2 changes: 1 addition & 1 deletion Sources/ProcessOut/Sources/Generated/Fonts+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal enum FontFamily {
}
internal static let allCustomFonts: [FontConvertible] = [WorkSans.all].flatMap { $0 }
internal static func registerAllCustomFonts() {
allCustomFonts.forEach { $0.register() }
allCustomFonts.forEach { $0.registerIfNeeded() }
}
}
// swiftlint:enable identifier_name line_length type_body_length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ extension POCardsService {
/// Allows to retrieve card issuer information based on IIN.
///
/// - Parameters:
/// - iin: Card issuer identification number. Corresponds to the first 6 or 8 digits of the main card number.
/// - iin: Card issuer identification number. Length should be at least 6 otherwise error is thrown.
@discardableResult
public func issuerInformation(
iin: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ final class DefaultCardsService: POCardsService {

// MARK: - POCardsService

func issuerInformation(iin: String) async throws -> POCardIssuerInformation {
try await repository.issuerInformation(iin: iin)
func issuerInformation(iin cardNumber: String) async throws -> POCardIssuerInformation {
// todo(andrii-vysotskyi): indicate in method arguments that method accepts full card number
let iin = try issuerIdentificationNumber(of: cardNumber)
return try await repository.issuerInformation(iin: iin)
}

func tokenize(request: POCardTokenizationRequest) async throws -> POCard {
Expand Down Expand Up @@ -62,4 +64,18 @@ final class DefaultCardsService: POCardsService {
private let applePayAuthorizationSession: ApplePayAuthorizationSession
private let applePayCardTokenizationRequestMapper: ApplePayCardTokenizationRequestMapper
private let applePayErrorMapper: POPassKitPaymentErrorMapper

// MARK: - Private Methods

private func issuerIdentificationNumber(of cardNumber: String) throws -> String {
let iinLength: Int, filteredNumber = cardNumber.filter(\.isNumber)
if filteredNumber.count >= 8 {
iinLength = 8
} else if filteredNumber.count >= 6 {
iinLength = 6
} else {
throw POFailure(message: "IIN must have at least 6 digits.", code: .generic(.mobile))
}
return String(filteredNumber.prefix(iinLength))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public protocol POCardsService: POService { // sourcery: AutoCompletion
/// Allows to retrieve card issuer information based on IIN.
///
/// - Parameters:
/// - iin: Card issuer identification number. Corresponds to the first 6 or 8 digits of the main card number.
/// - iin: Card issuer identification number. Length should be at least 6 otherwise error is thrown.
func issuerInformation(iin: String) async throws -> POCardIssuerInformation

/// Tokenizes a card. You can use the card for a single payment by creating a card token with it. If you want
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@ public protocol PONativeAlternativePaymentMethodDelegate: AnyObject {
for parameters: [PONativeAlternativePaymentMethodParameter], completion: @escaping ([String: String]) -> Void
)
}

extension PONativeAlternativePaymentMethodDelegate {

/// Method provides an ability to supply default values for given parameters. It is not mandatory
/// to provide defaults for all parameters.
///
/// - Returns: Dictionary where key is a parameter key, and value is desired default.
@_spi(PO)
@MainActor
public func nativeAlternativePayment(
defaultValuesFor parameters: [PONativeAlternativePaymentMethodParameter]
) async -> [String: String] {
await withCheckedContinuation { continuation in
nativeAlternativePaymentMethodDefaultValues(for: parameters) { continuation.resume(returning: $0) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class BaseInteractor<State>: Interactor {
var didChange: (() -> Void)?
var willChange: ((State) -> Void)?

@MainActor
func start() {
// Does nothing
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Andrii Vysotskyi on 19.10.2023.
//

@MainActor
protocol Interactor<State>: AnyObject {

associatedtype State
Expand Down
12 changes: 12 additions & 0 deletions Sources/ProcessOutUI/Sources/Core/ViewModel/AnyViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ final class AnyViewModel<State>: ViewModel {
base.start()
}

func stop() {
base.stop()
}

var state: State {
get { base.state }
set { base.state = newValue }
Expand All @@ -50,6 +54,10 @@ private class ViewModelBox<T>: AnyViewModelBase<T.State> where T: ViewModel {
base.start()
}

override func stop() {
base.stop()
}

override var state: T.State {
get { base.state }
set { base.state = newValue }
Expand All @@ -64,6 +72,10 @@ private class AnyViewModelBase<State>: ViewModel {
fatalError("Not implemented")
}

func stop() {
fatalError("Not implemented")
}

var state: State {
get { fatalError("Not implemented") }
set { fatalError("Not implemented") }
Expand Down
11 changes: 11 additions & 0 deletions Sources/ProcessOutUI/Sources/Core/ViewModel/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Combine

@MainActor
protocol ViewModel<State>: ObservableObject {

associatedtype State
Expand All @@ -16,4 +17,14 @@ protocol ViewModel<State>: ObservableObject {

/// Starts view model.
func start()

/// Cancels view model.
func stop()
}

extension ViewModel {

func stop() {
// Ignored
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import ProcessOut

@MainActor
protocol CardTokenizationInteractor: Interactor<CardTokenizationInteractorState> {

/// Delegate.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,43 @@ import ProcessOut

enum CardTokenizationInteractorState {

typealias ParameterId = WritableKeyPath<Started, Parameter>
struct Started {

struct Parameter {
/// Number of the card.
var number: Parameter

/// Parameter identifier
let id: ParameterId
/// Expiry date of the card.
var expiration: Parameter

/// Actual parameter value.
var value: String = ""
/// Card Verification Code of the card.
var cvc: Parameter

/// Indicates whether parameter is valid.
var isValid = true
/// Name of cardholder.
var cardholderName: Parameter

/// Boolean flag indicating whether parameter should be collected.
var shouldCollect = true // todo(andrii-vysotskyi): consider migrating to optional parameters
/// Card issuer information based on number.
var issuerInformation: POCardIssuerInformation?

/// Available parameter values.
var availableValues: [ParameterValue] = []
/// Preferred scheme.
var preferredScheme: POCardScheme?

/// Formatter that can be used to format this parameter.
var formatter: Formatter?
/// Billing address parameters.
var address: AddressParameters

/// Indicates whether card should be saved for future payments.
var shouldSaveCard: Bool = false

/// The most recent error message.
var recentErrorMessage: String?
}

struct ParameterValue: Decodable, Hashable {
struct Tokenizing {

/// Display name of value.
let displayName: String
/// Started state snapshot.
let snapshot: Started

/// Actual parameter value.
let value: String
/// Tokenization task.
let task: Task<Void, Never>
}

struct AddressParameters {
Expand All @@ -66,60 +73,68 @@ enum CardTokenizationInteractorState {
var specification: AddressSpecification
}

struct Started {

/// Number of the card.
var number: Parameter

/// Expiry date of the card.
var expiration: Parameter

/// Card Verification Code of the card.
var cvc: Parameter
struct Parameter {

/// Name of cardholder.
var cardholderName: Parameter
/// Parameter identifier
let id: ParameterId

/// Card issuer information based on number.
var issuerInformation: POCardIssuerInformation?
/// Actual parameter value.
var value: String = ""

/// Preferred scheme.
var preferredScheme: POCardScheme?
/// Indicates whether parameter is valid.
var isValid = true

/// Billing address parameters.
var address: AddressParameters
/// Boolean flag indicating whether parameter should be collected.
var shouldCollect = true // todo(andrii-vysotskyi): consider migrating to optional parameters

/// Indicates whether card should be saved for future payments.
var shouldSaveCard: Bool = false
/// Available parameter values.
var availableValues: [ParameterValue] = []

/// The most recent error message.
var recentErrorMessage: String?
/// Formatter that can be used to format this parameter.
var formatter: Formatter?
}

struct Tokenized {
struct ParameterValue: Decodable, Hashable {

/// Tokenized card.
let card: POCard
/// Display name of value.
let displayName: String

/// Full card number.
let cardNumber: String
/// Actual parameter value.
let value: String
}

typealias ParameterId = WritableKeyPath<Started, Parameter>

case idle

/// Interactor has started and is ready.
case started(Started)

/// Card information is currently being tokenized.
case tokenizing(snapshot: Started)
case tokenizing(Tokenizing)

/// Card was successfully tokenized. This is a sink state.
case tokenized(Tokenized)
case tokenized

/// Card tokenization did end with unrecoverable failure. This is a sink state.
case failure(POFailure)
}

extension CardTokenizationInteractorState {

/// Boolean variable that indicates whether the current state is a sink state.
///
/// A sink state is a special kind of state where, once entered, no other state transitions are possible.
var isSink: Bool {
switch self {
case .tokenized, .failure:
return true
default:
return false
}
}
}

extension CardTokenizationInteractorState.AddressParameters {

/// Boolean value that allows to determine whether all parameters are valid.
Expand Down
Loading

0 comments on commit 92ff6b2

Please sign in to comment.