Skip to content

Commit

Permalink
COIOS-774: Pay by Bank (#1878)
Browse files Browse the repository at this point in the history
# Summary
- Adds support for additional information when showing a stored pay by
bank payment method

# Demo


![image](https://github.com/user-attachments/assets/8d099b0e-1a87-485c-89f8-28abe8632bc1)

# Release notes

<new>

- Pay by Bank in the US now supports stored payment methods.

</new>

<changed>

- For Pay by Bank in the US, when you use
`PaymentMethodType.paybybank_AIS_DD`, Drop-in now show the logos of the
supported banks and a confirmation screen before redirecting to the
issuer

</changed>

# Ticket

<ticket>COIOS-774</ticket>

---------

Co-authored-by: Alex Guretzki <[email protected]>
  • Loading branch information
goergisn and Alex Guretzki authored Nov 25, 2024
1 parent 74d3d0c commit adb4adf
Show file tree
Hide file tree
Showing 18 changed files with 689 additions and 19 deletions.
50 changes: 50 additions & 0 deletions Adyen.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Adyen/Assets/Generated/LocalizationKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ public struct LocalizationKey {
public static let paybybankTitle = LocalizationKey(key: "adyen.paybybank.title")
/// Search…
public static let searchPlaceholder = LocalizationKey(key: "adyen.search.placeholder")
/// Use Pay by Bank to pay instantly from any bank account.
public static let payByBankAISDDDisclaimerHeader = LocalizationKey(key: "adyen.payByBankAISDD.disclaimer.header")
/// By connecting your bank account you are authorizing debits to your account for any amount owed for use of our services and/or purchase of our products, until this authorization is revoked.
public static let payByBankAISDDDisclaimerBody = LocalizationKey(key: "adyen.payByBankAISDD.disclaimer.body")
/// Continue to Pay by Bank
public static let payByBankAISDDSubmit = LocalizationKey(key: "adyen.payByBankAISDD.submit")
/// + more
public static let payByBankAISDDMore = LocalizationKey(key: "adyen.payByBankAISDD.more")
/// How would you like to use UPI?
public static let upiModeSelection = LocalizationKey(key: "adyen.upi.modeSelection")
/// Enter a correct virtual payment address
Expand Down
3 changes: 3 additions & 0 deletions Adyen/Core/Core Protocols/PaymentComponentBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public protocol PaymentComponentBuilder: AdyenContextAware {

/// Builds a certain `PaymentComponent` based on a `StoredTwintPaymentMethod`.
func build(paymentMethod: StoredTwintPaymentMethod) -> PaymentComponent?

/// Builds a certain `PaymentComponent` based on a `PayByBankUSPaymentMethod`.
func build(paymentMethod: PayByBankUSPaymentMethod) -> PaymentComponent?

/// Builds a certain `PaymentComponent` based on any `PaymentMethod`, as a default case.
func build(paymentMethod: PaymentMethod) -> PaymentComponent?
Expand Down
6 changes: 5 additions & 1 deletion Adyen/Core/Payment Methods/Abstract/AnyPaymentMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal enum AnyPaymentMethod: Codable {
case storedPayPal(StoredPayPalPaymentMethod)
case storedBCMC(StoredBCMCPaymentMethod)
case storedBlik(StoredBLIKPaymentMethod)
case storedPayByBankUS(StoredPayByBankUSPaymentMethod)
case storedAchDirectDebit(StoredACHDirectDebitPaymentMethod)
case storedCashAppPay(StoredCashAppPayPaymentMethod)
case storedTwint(StoredTwintPaymentMethod)
Expand Down Expand Up @@ -41,6 +42,7 @@ internal enum AnyPaymentMethod: Codable {
case upi(UPIPaymentMethod)
case cashAppPay(CashAppPayPaymentMethod)
case twint(TwintPaymentMethod)
case payByBankUS(PayByBankUSPaymentMethod)

case none

Expand Down Expand Up @@ -79,12 +81,14 @@ internal enum AnyPaymentMethod: Codable {
case let .upi(paymentMethod): return paymentMethod
case let .cashAppPay(paymentMethod): return paymentMethod
case let .twint(paymentMethod): return paymentMethod
case let .storedPayByBankUS(paymentMethod): return paymentMethod
case let .payByBankUS(paymentMethod): return paymentMethod
case .none: return nil
}
}

// MARK: - Decoding

internal init(from decoder: Decoder) throws {
self = AnyPaymentMethodDecoder.decode(from: decoder)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ internal enum AnyPaymentMethodDecoder {
.onlineBankingSK: OnlineBankingPaymentMethodDecoder(),
.upi: UPIPaymentMethodDecoder(),
.cashAppPay: CashAppPayPaymentMethodDecoder(),
.twint: TwintPaymentMethodDecoder()
.twint: TwintPaymentMethodDecoder(),
.payByBankAISDD: PayByBankUSPaymentMethodDecoder()
]

private static var defaultDecoder: PaymentMethodDecoder = InstantPaymentMethodDecoder()
Expand Down Expand Up @@ -341,6 +342,26 @@ private struct BLIKPaymentMethodDecoder: PaymentMethodDecoder {
}
}

private struct PayByBankUSPaymentMethodDecoder: PaymentMethodDecoder {
func decode(from decoder: Decoder, isStored: Bool) throws -> AnyPaymentMethod {
if isStored {
return try .storedPayByBankUS(.init(from: decoder))
} else {
return try .payByBankUS(.init(from: decoder))
}
}

func anyPaymentMethod(from paymentMethod: any PaymentMethod) -> AnyPaymentMethod? {
if let method = paymentMethod as? StoredPayByBankUSPaymentMethod {
return .storedPayByBankUS(method)
}
if let method = paymentMethod as? PayByBankUSPaymentMethod {
return .payByBankUS(method)
}
return nil
}
}

private struct DokuPaymentMethodDecoder: PaymentMethodDecoder {
func decode(from decoder: Decoder, isStored: Bool) throws -> AnyPaymentMethod {
try .doku(DokuPaymentMethod(from: decoder))
Expand Down
4 changes: 4 additions & 0 deletions Adyen/Core/Payment Methods/Abstract/PaymentMethodType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public enum PaymentMethodType: RawRepresentable, Hashable, Codable {
case upi
case cashAppPay
case twint
case payByBankAISDD
case other(String)

// Unsupported
Expand Down Expand Up @@ -129,6 +130,7 @@ public enum PaymentMethodType: RawRepresentable, Hashable, Codable {
case "cashapp": self = .cashAppPay
case "bizum": self = .bizum
case "twint": self = .twint
case "paybybank_AIS_DD": self = .payByBankAISDD
default: self = .other(rawValue)
}
}
Expand Down Expand Up @@ -191,6 +193,7 @@ public enum PaymentMethodType: RawRepresentable, Hashable, Codable {
case .cashAppPay: return "cashapp"
case .bizum: return "bizum"
case .twint: return "twint"
case .payByBankAISDD: return "paybybank_AIS_DD"
case let .other(value): return value
}
}
Expand Down Expand Up @@ -260,6 +263,7 @@ extension PaymentMethodType {
case .cashAppPay: return "cash app"
case .bizum: return "bizum"
case .twint: return "twint"
case .payByBankAISDD: return "Pay By Bank Direct Debit"
case let .other(name): return name.replacingOccurrences(of: "_", with: " ")
}
}
Expand Down
44 changes: 44 additions & 0 deletions Adyen/Core/Payment Methods/PayByBankUSPaymentMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

/// PayByBank US payment method.
public struct PayByBankUSPaymentMethod: PaymentMethod {
public let type: PaymentMethodType

public var name: String

public var merchantProvidedDisplayInformation: MerchantCustomDisplayInformation?

@_spi(AdyenInternal)
public static var logoNames: [String] {
["US-1", "US-2", "US-3", "US-4"]
}

public func defaultDisplayInformation(using parameters: LocalizationParameters?) -> DisplayInformation {
.init(
title: name,
subtitle: nil,
logoName: type.rawValue,
trailingInfo: .logos(
named: Self.logoNames,
trailingText: "+"
),
accessibilityLabel: name
)
}

@_spi(AdyenInternal)
public func buildComponent(using builder: PaymentComponentBuilder) -> PaymentComponent? {
builder.build(paymentMethod: self)
}

private enum CodingKeys: String, CodingKey {
case type
case name
}
}
55 changes: 55 additions & 0 deletions Adyen/Core/Payment Methods/StoredPayByBankUSPaymentMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

/// Stored PayByBank US payment method.
public struct StoredPayByBankUSPaymentMethod: StoredPaymentMethod {

public let type: PaymentMethodType

public let name: String

public let label: String?

public var merchantProvidedDisplayInformation: MerchantCustomDisplayInformation?

public let identifier: String

public let supportedShopperInteractions: [ShopperInteraction]

@_spi(AdyenInternal)
public func buildComponent(using builder: PaymentComponentBuilder) -> PaymentComponent? {
builder.build(paymentMethod: self)
}

@_spi(AdyenInternal)
public func defaultDisplayInformation(using parameters: LocalizationParameters?) -> DisplayInformation {
let title: String
let subtitle: String?

if let label {
title = label
subtitle = name
} else {
title = name
subtitle = nil
}

return DisplayInformation(title: title, subtitle: subtitle, logoName: type.rawValue)
}

// MARK: - Decoding

private enum CodingKeys: String, CodingKey {
case type
case name
case label
case identifier = "id"
case supportedShopperInteractions
}

}
3 changes: 2 additions & 1 deletion Adyen/Helpers/UIImageViewHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public extension AdyenScope where Base: UIImageView {
/// Applies given `ImageStyle` to the UIImageView
/// Sets `translatesAutoresizingMaskIntoConstraints` to `false`
/// - Parameter style: `ImageStyle` to be applied
internal func apply(_ style: ImageStyle) {
@_spi(AdyenInternal)
func apply(_ style: ImageStyle) {
round(using: style.cornerRounding)
base.layer.borderColor = style.borderColor?.cgColor
base.layer.borderWidth = style.borderWidth
Expand Down
3 changes: 2 additions & 1 deletion Adyen/Helpers/UILabelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public extension AdyenScope where Base: UILabel {
/// Applies given `TextStyle` to the UILabel
/// Sets `adjustsFontForContentSizeCategory` to `true`
/// - Parameter style: `TextStyle` to be applied
internal func apply(_ style: TextStyle) {
@_spi(AdyenInternal)
func apply(_ style: TextStyle) {
base.font = style.font
base.textColor = style.color
base.textAlignment = style.textAlignment
Expand Down
11 changes: 7 additions & 4 deletions Adyen/UI/Views/SupportedPaymentMethodsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import AdyenNetworking
import UIKit

internal class SupportedPaymentMethodLogosView: UIView {
@_spi(AdyenInternal)
public class SupportedPaymentMethodLogosView: UIView {

struct Style: ViewStyle {
public struct Style: ViewStyle {
public var backgroundColor: UIColor = .clear

public var images: ImageStyle = .init(
Expand All @@ -24,6 +25,8 @@ internal class SupportedPaymentMethodLogosView: UIView {
font: .preferredFont(forTextStyle: .callout),
color: UIColor.Adyen.componentSecondaryLabel
)

public init() {}
}

internal let imageSize: CGSize
Expand All @@ -44,7 +47,7 @@ internal class SupportedPaymentMethodLogosView: UIView {

@AdyenDependency(\.imageLoader) private var imageLoader

internal init(
public init(
imageSize: CGSize = .init(width: 24, height: 16),
imageUrls: [URL],
trailingText: String?,
Expand All @@ -60,7 +63,7 @@ internal class SupportedPaymentMethodLogosView: UIView {
self.setContentHuggingPriority(.required, for: .horizontal)
}

override internal func willMove(toSuperview newSuperview: UIView?) {
override public func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)

if newSuperview != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

@_spi(AdyenInternal) import Adyen

extension PayByBankUSComponent {

public struct Configuration: AnyBasicComponentConfiguration {

/// The UI style of the component.
public var style: PayByBankUSComponent.Style

public var localizationParameters: LocalizationParameters?

/// Initializes a new instance of `PayByBankUSComponent.Configuration`
///
/// - Parameters:
/// - style: The form style.
/// - localizationParameters: The localization parameters.
public init(
style: PayByBankUSComponent.Style = .init(),
localizationParameters: LocalizationParameters? = nil
) {
self.style = style
self.localizationParameters = localizationParameters
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation
import UIKit
@_spi(AdyenInternal) import Adyen

extension PayByBankUSComponent.ConfirmationViewController {

internal class Model {

internal let headerImageUrl: URL
internal let supportedBankLogoURLs: [URL]
internal let supportedBanksMoreText: String
internal let title: String
internal let subtitle: String
internal let message: String
internal let submitTitle: String

internal let style: PayByBankUSComponent.Style
internal let headerImageViewSize = CGSize(width: 80, height: 52)

internal let continueHandler: () -> Void

private let imageLoader: ImageLoading = ImageLoaderProvider.imageLoader()
private var imageLoadingTask: AdyenCancellable? {
willSet { imageLoadingTask?.cancel() }
}

internal init(
title: String,
headerImageUrl: URL,
supportedBankLogoNames: [String],
style: PayByBankUSComponent.Style,
localizationParameters: LocalizationParameters?,
logoUrlProvider: LogoURLProvider,
continueHandler: @escaping () -> Void
) {
self.headerImageUrl = headerImageUrl
self.supportedBankLogoURLs = supportedBankLogoNames.map { logoUrlProvider.logoURL(withName: $0) }
self.supportedBanksMoreText = localizedString(.payByBankAISDDMore, localizationParameters)
self.title = title
self.subtitle = localizedString(.payByBankAISDDDisclaimerHeader, localizationParameters)
self.message = localizedString(.payByBankAISDDDisclaimerBody, localizationParameters)
self.submitTitle = localizedString(.payByBankAISDDSubmit, localizationParameters)
self.style = style
self.continueHandler = continueHandler
}

internal func loadHeaderImage(for imageView: UIImageView) {
imageLoadingTask = imageView.load(url: headerImageUrl, using: imageLoader)
}
}
}
Loading

0 comments on commit adb4adf

Please sign in to comment.