Skip to content

Commit

Permalink
feat(POM0-395): improve dynamic checkout failure recovery (#308)
Browse files Browse the repository at this point in the history
* Allow recovering error within screen
* Allow changing native alternative payment methods
* Fix POBrandButtonStyle brand color brightness calculation
  • Loading branch information
andrii-vysotskyi-cko authored Jul 12, 2024
1 parent 87061b5 commit 479ec04
Show file tree
Hide file tree
Showing 24 changed files with 363 additions and 322 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ extension FeaturesViewModel: PODynamicCheckoutDelegate {
.init(label: "Test", amount: 100, type: .final)
]
}

func dynamicCheckout(newInvoiceFor invoice: POInvoice) async -> POInvoice? {
let request = POInvoiceCreationRequest(
name: "Example",
amount: invoice.amount.description,
currency: invoice.currency,
returnUrl: invoice.returnUrl,
customerId: Constants.customerId
)
return try? await invoicesService.createInvoice(request: request)
}
}

extension FeaturesViewModel: POPassKitPaymentAuthorizationControllerDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ public enum PODynamicCheckoutPaymentMethod {

public struct ApplePay: Decodable { // sourcery: AutoCodingKeys

/// Transient ID assigned to method during decoding.
/// Payment method ID.
@_spi(PO)
public let id = UUID().uuidString // sourcery:coding: skip
public var id: String { // sourcery:coding: skip
configuration.merchantId
}

/// Payment flow.
public let flow: Flow?
Expand Down Expand Up @@ -48,9 +50,11 @@ public enum PODynamicCheckoutPaymentMethod {

public struct NativeAlternativePayment: Decodable { // sourcery: AutoCodingKeys

/// Transient ID assigned to method during decoding.
/// Payment method ID.
@_spi(PO)
public let id = UUID().uuidString // sourcery:coding: skip
public var id: String { // sourcery:coding: skip
configuration.gatewayConfigurationId
}

/// Display information.
public let display: Display
Expand All @@ -69,9 +73,11 @@ public enum PODynamicCheckoutPaymentMethod {

public struct AlternativePayment: Decodable { // sourcery: AutoCodingKeys

/// Transient ID assigned to method during decoding.
/// Payment method ID.
@_spi(PO)
public let id = UUID().uuidString // sourcery:coding: skip
public var id: String { // sourcery:coding: skip
configuration.gatewayConfigurationId
}

/// Display information.
public let display: Display
Expand All @@ -85,6 +91,9 @@ public enum PODynamicCheckoutPaymentMethod {

public struct AlternativePaymentConfiguration: Decodable {

/// Gateway configuration ID.
public let gatewayConfigurationId: String

/// Redirect URL.
public let redirectUrl: URL
}
Expand All @@ -93,9 +102,9 @@ public enum PODynamicCheckoutPaymentMethod {

public struct Card: Decodable { // sourcery: AutoCodingKeys

/// Transient ID assigned to method during decoding.
/// Payment method ID.
@_spi(PO)
public let id = UUID().uuidString // sourcery:coding: skip
public let id = "card" // sourcery:coding: skip

/// Display information.
public let display: Display
Expand All @@ -119,6 +128,15 @@ public enum PODynamicCheckoutPaymentMethod {
public let billingAddress: BillingAddressConfiguration
}

public struct BillingAddressConfiguration: Decodable {

/// List of ISO country codes that is supported for the billing address. When nil, all countries are supported.
public let restrictToCountryCodes: Set<String>?

/// Billing address collection mode.
public let collectionMode: POBillingAddressCollectionMode
}

// MARK: - Unknown

public struct Unknown {
Expand All @@ -145,15 +163,6 @@ public enum PODynamicCheckoutPaymentMethod {
public private(set) var brandColor: UIColor
}

public struct BillingAddressConfiguration: Decodable {

/// List of ISO country codes that is supported for the billing address. When nil, all countries are supported.
public let restrictToCountryCodes: Set<String>?

/// Billing address collection mode.
public let collectionMode: POBillingAddressCollectionMode
}

public enum Flow: String, Decodable {
case express
}
Expand Down Expand Up @@ -218,6 +227,7 @@ extension PODynamicCheckoutPaymentMethod {
case .card(let method):
return method.id
case .unknown(let method):
assertionFailure("It is considered an error to request an ID for unknown payment method.")
return method.id
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ extension ButtonStyle where Self == POBrandButtonStyle {
/// Simple style that changes its appearance based on brand color ``SwiftUI/EnvironmentValues/poButtonBrandColor``.
@_disfavoredOverload
public static var brand: POBrandButtonStyle {
.init(title: .init(color: Color(.Text.inverse), typography: .button), border: .clear, shadow: .clear)
.init(title: .init(color: Color(.Text.primary), typography: .button), border: .clear, shadow: .clear)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import SwiftUI

/// Brand button style that only requires base style information and resolves
/// styling for other states automatically.
///
/// - NOTE: SDK uses light color variation with light brand colors and dark otherwise.
@available(iOS 14, *)
public struct POBrandButtonStyle: ButtonStyle {

Expand Down Expand Up @@ -36,7 +38,6 @@ public struct POBrandButtonStyle: ButtonStyle {
let isBrandColorLight = UIColor(brandColor).isLight() != false
configuration.label
.textStyle(title)
.colorScheme(isBrandColorLight ? .light : .dark)
.lineLimit(1)
.padding(Constants.padding)
.frame(maxWidth: .infinity, minHeight: Constants.minHeight)
Expand All @@ -48,6 +49,7 @@ public struct POBrandButtonStyle: ButtonStyle {
}
.border(style: border)
.shadow(style: shadow)
.colorScheme(isBrandColorLight ? .light : .dark)
.contentShape(.rect)
.animation(.default, value: isEnabled)
.animation(.default, value: AnyHashable(brandColor))
Expand Down Expand Up @@ -75,6 +77,7 @@ public struct POBrandButtonStyle: ButtonStyle {

// Environments are not propagated directly to ButtonStyle in any iOS before 14.5 workaround is
// to wrap content into additional view and extract them.
@available(iOS 14, *)
private struct ContentView<Content: View>: View {

@ViewBuilder
Expand All @@ -86,6 +89,20 @@ private struct ContentView<Content: View>: View {

// MARK: - Private Properties

@Environment(\.isEnabled) private var isEnabled
@Environment(\.poButtonBrandColor) private var brandColor
@Environment(\.isEnabled)
private var isEnabled

@Environment(\.poButtonBrandColor)
private var uiBrandColor

@Environment(\.colorScheme)
private var colorScheme

// MARK: - Private Methods

private var brandColor: Color {
let interfaceStyle = UIUserInterfaceStyle(colorScheme)
let traitCollection = UITraitCollection(userInterfaceStyle: interfaceStyle)
return Color(uiBrandColor.resolvedColor(with: traitCollection))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ import SwiftUI
extension View {

/// Adds a condition that controls whether button with a `POButtonStyle` should show loading indicator.
@_spi(PO) public func buttonBrandColor(_ color: Color) -> some View {
@_spi(PO) public func buttonBrandColor(_ color: UIColor) -> some View {
environment(\.poButtonBrandColor, color)
}
}

extension EnvironmentValues {

/// Brand color of associated third party service. Only ``POBrandButtonStyle`` responds to this property changes.
public var poButtonBrandColor: Color {
public var poButtonBrandColor: UIColor {
get { self[Key.self] }
set { self[Key.self] = newValue }
}

// MARK: - Private Nested Types

private struct Key: EnvironmentKey {
static let defaultValue = Color(.Button.Primary.Background.default)
static let defaultValue = UIColor(resource: .Button.Primary.Background.default)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ extension POColorResource {
/// The "Surface/Default" asset catalog color resource.
public static let `default` = POColorResource(.Surface.default)

/// The "Surface/Neutral" asset catalog color resource.
public static let neutral = POColorResource(.Surface.neutral)

/// The "Surface/Success" asset catalog color resource.
public static let success = POColorResource(.Surface.success)

Expand Down
68 changes: 0 additions & 68 deletions Sources/ProcessOutUI/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1633,40 +1633,6 @@
}
}
},
"dynamic-checkout.error.unavailable-method" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "طريقة الدفع المطلوبة غير متاحة، يرجى تجربة طريقة دفع أخرى."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The requested payment method is not available, please try another payment method."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Le mode de paiement demandé n'est pas disponible, veuillez essayer un autre mode de paiement."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wybrana metoda płatności nie jest obsługiwana. Proszę wybierz inną metodę."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "O método de pagamento escolhido não está disponível, por favor tente outro método de pagamento."
}
}
}
},
"dynamic-checkout.pay-button" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -1769,40 +1735,6 @@
}
}
},
"dynamic-checkout.unavailable-warning" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "طريقة الدفع هذه غير متاحة."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This payment method is unavailable."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ce mode de paiement est indisponible."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ta metoda płatności nie jest obsługiwana."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Este método de pagamento não está disponível."
}
}
}
},
"native-alternative-payment.cancel-button.title" : {
"localizations" : {
"ar" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ public protocol PODynamicCheckoutDelegate: AnyObject {
///
/// Your implementation may alter request parameters and return new request but make
/// sure that invoice id and source stay the same.
func dynamicCheckout(willAuthorizeInvoiceWith request: inout POInvoiceAuthorizationRequest) async -> PO3DSService
func dynamicCheckout(
willAuthorizeInvoiceWith request: inout POInvoiceAuthorizationRequest
) async -> PO3DSService

/// Asks delegate whether user should be allowed to continue after failure or module should complete.
/// Default implementation returns `true`.
func dynamicCheckout(shouldContinueAfter failure: POFailure) -> Bool

/// Your implementation could return new invoice to replace existing one to be able to recover from
/// normally unrecoverable payment failure.
func dynamicCheckout(newInvoiceFor invoice: POInvoice) async -> POInvoice?

// MARK: - Card Payment

/// Invoked when module emits event.
Expand Down Expand Up @@ -64,6 +70,10 @@ extension PODynamicCheckoutDelegate {
true
}

public func dynamicCheckout(newInvoiceFor invoice: POInvoice) async -> POInvoice? {
nil
}

public func dynamicCheckout(didEmitCardTokenizationEvent event: POCardTokenizationEvent) {
// Ignored
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ protocol DynamicCheckoutInteractorChildProvider {

/// Creates and returns card tokenization interactor.
func cardTokenizationInteractor(
configuration: PODynamicCheckoutPaymentMethod.CardConfiguration
invoiceId: String, configuration: PODynamicCheckoutPaymentMethod.CardConfiguration
) -> any CardTokenizationInteractor

/// Creates and returns native APM interactor..
func nativeAlternativePaymentInteractor(gatewayConfigurationId: String) -> any NativeAlternativePaymentInteractor
func nativeAlternativePaymentInteractor(
invoiceId: String, gatewayConfigurationId: String
) -> any NativeAlternativePaymentInteractor
}
Loading

0 comments on commit 479ec04

Please sign in to comment.