Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate card scanning - part 1 #2036

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d2e9843
Coios 855 3ds2 cancel update (#1960)
erenbesel Jan 22, 2025
8a511bc
Add accessibility identifier to Apple Pay pre-page root view (#1963)
atmamont Jan 27, 2025
cc16b93
Clean workflows (#1967)
goergisn Jan 27, 2025
1a10839
Prepare Release Workflow (#1962)
goergisn Jan 27, 2025
153ce0a
Fixed VoucherComponent doc
neelSharma12 Jan 28, 2025
af2bdfd
chore: updating documentation
neelSharma12 Jan 29, 2025
c8bb12e
Update publish-demo-app.yml
neelSharma12 Jan 29, 2025
e20c2b1
Update publish_podspec.yml
neelSharma12 Jan 29, 2025
5d9bef9
Restoring Compatibility Tests (#1975)
goergisn Jan 30, 2025
f2ff686
Trigger release workflow (#1988)
goergisn Jan 30, 2025
3dba511
Release note workflow escaping (#1999)
goergisn Jan 31, 2025
a1ff623
chore(deps): update adyen/adyen-swift-public-api-diff action to v0.8.…
renovate[bot] Feb 11, 2025
b7faf20
Removing release_notes.md (#2011)
goergisn Feb 13, 2025
b0cf9cc
Updating AdyenNetworking to 3.0.0 (#2016)
goergisn Feb 18, 2025
1f5f1b1
Update create_release.yml (#2018)
goergisn Feb 19, 2025
5faddc2
Fix prepare release script (#2019)
goergisn Feb 19, 2025
f366064
Create new accessory view for card scanner button
atmamont Mar 4, 2025
985a4ca
Propagate CardViewController closure to open card scanner to `FormCar…
atmamont Mar 4, 2025
914fec3
Merge branch 'COIOS-826_OCR_feature' of github.com:Adyen/adyen-ios in…
atmamont Mar 4, 2025
d9bec10
Rename file
atmamont Mar 4, 2025
d524833
Make card scan handler optional (iOS<13 support)
atmamont Mar 4, 2025
76e4ba8
Make “Scan your card” button optional relying on the logic in the `Fo…
atmamont Mar 5, 2025
b86d765
Add todo for localization
atmamont Mar 5, 2025
5b5395e
Speed up integration tests
atmamont Mar 5, 2025
17e3443
Fix tests target
atmamont Mar 5, 2025
8a39a0d
Add missing UIKit import
atmamont Mar 5, 2025
9de565f
Remove unused code
atmamont Mar 5, 2025
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
4 changes: 4 additions & 0 deletions Adyen.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@
A0F4559B295F0F58001742C7 /* MealVoucherPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F4559A295F0F58001742C7 /* MealVoucherPaymentMethod.swift */; };
A0F455A12968472B001742C7 /* PartialPaymentMethodDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F455A02968472B001742C7 /* PartialPaymentMethodDetails.swift */; };
A0FA143F26D65A5300627127 /* InstallmentPickerElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0FA143E26D65A5300627127 /* InstallmentPickerElement.swift */; };
B62A075C2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62A075B2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift */; };
B62D48B42BBE8DBE001EF01A /* AnalyticsFlavorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C16AB280702B200534419 /* AnalyticsFlavorTests.swift */; };
B62D48B52BBE8DBE001EF01A /* AnalyticsEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C16A928059A5A00534419 /* AnalyticsEventTests.swift */; };
B62D48B72BBE8DBE001EF01A /* AnalyticsProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C0005B280468E100CE2EEC /* AnalyticsProviderMock.swift */; };
Expand Down Expand Up @@ -1839,6 +1840,7 @@
A0F4559A295F0F58001742C7 /* MealVoucherPaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealVoucherPaymentMethod.swift; sourceTree = "<group>"; };
A0F455A02968472B001742C7 /* PartialPaymentMethodDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialPaymentMethodDetails.swift; sourceTree = "<group>"; };
A0FA143E26D65A5300627127 /* InstallmentPickerElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallmentPickerElement.swift; sourceTree = "<group>"; };
B62A075B2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FormCardNumberItemView+ScanCard.swift"; sourceTree = "<group>"; };
B62D48AC2BBE8D79001EF01A /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
B62D48C22BBE8ED6001EF01A /* XCTestCase+Wait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Wait.swift"; sourceTree = "<group>"; };
B62D48CA2BBE9059001EF01A /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3975,6 +3977,7 @@
A04BD78126D510E1008E270F /* Installments */,
E2B317A42265D76600C1BB30 /* FormCardNumberItem.swift */,
E2B317A62265D7B700C1BB30 /* FormCardNumberItemView.swift */,
B62A075B2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift */,
E76EC67F241125E0009C6E2F /* FormCardSecurityCodeItem.swift */,
E76EC681241125F5009C6E2F /* FormCardSecurityCodeItemView.swift */,
A01DCF0A26BD67BB00BC35B3 /* FormCardExpiryDateItem.swift */,
Expand Down Expand Up @@ -7513,6 +7516,7 @@
F92329DD25AD049E002C5BC4 /* FallbackCardBrandProvider.swift in Sources */,
E7798569268CE1B900CECA95 /* KCPDetails.swift in Sources */,
B6C9DA792D0C3F62005D65C7 /* DualBrandView.swift in Sources */,
B62A075C2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift in Sources */,
F92CBA072811754400367820 /* GiftCardConfirmationPaymentMethod.swift in Sources */,
E9E3DACD221EEAB200697074 /* CardSecurityCodeValidator.swift in Sources */,
F96286B5256BDEA400043FE3 /* CardBundleExtension.swift in Sources */,
Expand Down
55 changes: 39 additions & 16 deletions AdyenCard/Components/Card/CardViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ internal class CardViewController: FormViewController {

internal lazy var items = {

ItemsProvider(
let scanCardHandler: (() -> Void)?
if #available(iOS 13.0, *) {
scanCardHandler = { [weak self] in self?.openCardScanner() }
} else {
scanCardHandler = nil
}

return ItemsProvider(
formStyle: formStyle,
payment: payment,
configuration: configuration,
Expand All @@ -48,7 +55,8 @@ internal class CardViewController: FormViewController {
localizationParameters: localizationParameters,
addressViewModelBuilder: DefaultAddressViewModelBuilder(),
presenter: self,
addressMode: configuration.billingAddress.mode
addressMode: configuration.billingAddress.mode,
scanCardHandler: scanCardHandler
)
}()

Expand Down Expand Up @@ -100,6 +108,7 @@ internal class CardViewController: FormViewController {
override internal func viewDidLoad() {
setupView()
setupViewRelations()
setupCardScanning()
observeNumberItem()
super.viewDidLoad()
}
Expand Down Expand Up @@ -252,32 +261,32 @@ extension CardViewController {
items.socialSecurityNumberItem.isHidden.wrappedValue = shouldHideSocialSecurityItem(with: brand)
items.installmentsItem?.update(cardType: brand?.type)
}

// MARK: Private methods

private func setupView() {
append(items.numberContainerItem)

if configuration.showsSecurityCodeField {
let splitTextItem = FormSplitItem(items: items.expiryDateItem, items.securityCodeItem, style: formStyle.textField)
append(splitTextItem)
} else {
append(items.expiryDateItem)
}

if configuration.showsHolderNameField {
append(items.holderNameItem)
}

if configuration.koreanAuthenticationMode != .hide {
append(items.additionalAuthCodeItem)
append(items.additionalAuthPasswordItem)
}

if configuration.socialSecurityNumberMode != .hide {
append(items.socialSecurityNumberItem)
}

if let installmentsItem = items.installmentsItem {
append(installmentsItem)
}
Expand All @@ -290,7 +299,7 @@ extension CardViewController {
if let billingAddressItem {
append(billingAddressItem)
}

if configuration.showsSubmitButton {
append(FormSpacerItem())
append(items.button)
Expand All @@ -303,7 +312,7 @@ extension CardViewController {
switch configuration.billingAddress.mode {
case .lookup:
return items.billingAddressPickerItem

case .full:
return items.billingAddressPickerItem

Expand All @@ -314,18 +323,18 @@ extension CardViewController {
return nil
}
}

private func prefill() {
guard let shopperInformation else { return }

shopperInformation.billingAddress.map { billingAddress in
items.billingAddressPickerItem?.value = billingAddress
billingAddress.postalCode.map { items.postalCodeItem.value = $0 }
}
shopperInformation.card.map { items.holderNameItem.value = $0.holderName }
shopperInformation.socialSecurityNumber.map { items.socialSecurityNumberItem.value = $0 }
}

private func setupViewRelations() {
observe(items.numberContainerItem.numberItem.publisher) { [weak self] in self?.didChange(pan: $0) }
observe(items.numberContainerItem.numberItem.$binValue) { [weak self] in self?.didChange(bin: $0) }
Expand All @@ -334,7 +343,7 @@ extension CardViewController {
cardDelegate?.didSelectSubmitButton()
}
}

private func didChange(pan: String) {
items.securityCodeItem.selectedCard = supportedCardTypes.adyen.type(forCardNumber: pan)
cardDelegate?.didChange(pan: pan)
Expand Down Expand Up @@ -366,7 +375,6 @@ extension CardViewController {
return !brand.showsSocialSecurityNumber
}
}

}

internal protocol CardViewControllerDelegate: AnyObject {
Expand Down Expand Up @@ -398,6 +406,21 @@ extension CardViewController: CardViewControllerProtocol {
}
}

// MARK: - Card scanner

extension CardViewController {
private func setupCardScanning() {
if #available(iOS 13.0, *) {
// items.numberContainerItem.numberItem.view.inputAccessoryView = ...

}
}

private func openCardScanner() {
// TODO: Use CardScanning module
}
}

extension CardBrand {

internal var securityCodeItemDisplayMode: FormCardSecurityCodeItem.DisplayMode {
Expand Down
18 changes: 6 additions & 12 deletions AdyenCard/Components/Card/CardViewControllerItemsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,17 @@ extension CardViewController {
internal final class ItemsProvider {

private let formStyle: FormComponentStyle

private let amount: Amount?

private var localizationParameters: LocalizationParameters?

private let configuration: CardComponent.Configuration

private let shopperInformation: PrefilledShopperInformation?

private let cardLogos: [FormCardLogosItem.CardTypeLogo]

private let scope: String

private let initialCountry: String

private let addressViewModelBuilder: AddressViewModelBuilder

private let presenter: WeakReferenceViewControllerPresenter

private let addressMode: CardComponent.AddressFormType
private let scanCardHandler: (() -> Void)?

/// Closure that is called when an event is triggered via the field items.
internal var onDidTriggerInfoEvent: ((InfoEventData) -> Void)?
Expand All @@ -53,7 +44,8 @@ extension CardViewController {
localizationParameters: LocalizationParameters?,
addressViewModelBuilder: AddressViewModelBuilder,
presenter: ViewControllerPresenter,
addressMode: CardComponent.AddressFormType
addressMode: CardComponent.AddressFormType,
scanCardHandler: (() -> Void)?
) {
self.formStyle = formStyle
self.amount = payment?.amount
Expand All @@ -66,6 +58,7 @@ extension CardViewController {
self.addressViewModelBuilder = addressViewModelBuilder
self.presenter = .init(presenter)
self.addressMode = addressMode
self.scanCardHandler = scanCardHandler
}

internal lazy var billingAddressPickerItem: FormAddressPickerItem? = {
Expand Down Expand Up @@ -111,7 +104,8 @@ extension CardViewController {
cardTypeLogos: cardLogos,
showsSupportedCardLogos: configuration.showsSupportedCardLogos,
style: formStyle.textField,
localizationParameters: localizationParameters
localizationParameters: localizationParameters,
scanCardHandler: scanCardHandler
)
item.identifier = ViewIdentifierBuilder.build(scopeInstance: scope, postfix: "numberContainerItem")

Expand Down
9 changes: 7 additions & 2 deletions AdyenCard/Form/FormCardNumberContainerItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ internal final class FormCardNumberContainerItem: FormItem, AdyenObserver {
internal let showsSupportedCardLogos: Bool

private let localizationParameters: LocalizationParameters?

private let scanCardHandler: (() -> Void)?

internal lazy var subitems: [FormItem] = {
var subItems: [FormItem] = [numberItem]
Expand All @@ -35,7 +37,8 @@ internal final class FormCardNumberContainerItem: FormItem, AdyenObserver {
let item = FormCardNumberItem(
cardTypeLogos: cardTypeLogos,
style: style,
localizationParameters: localizationParameters
localizationParameters: localizationParameters,
scanCardHandler: scanCardHandler
)
item.identifier = ViewIdentifierBuilder.build(scopeInstance: self, postfix: "numberItem")
return item
Expand All @@ -51,12 +54,14 @@ internal final class FormCardNumberContainerItem: FormItem, AdyenObserver {
cardTypeLogos: [FormCardLogosItem.CardTypeLogo],
showsSupportedCardLogos: Bool = true,
style: FormTextItemStyle,
localizationParameters: LocalizationParameters?
localizationParameters: LocalizationParameters?,
scanCardHandler: (() -> Void)?
) {
self.cardTypeLogos = cardTypeLogos
self.showsSupportedCardLogos = showsSupportedCardLogos
self.localizationParameters = localizationParameters
self.style = style
self.scanCardHandler = scanCardHandler

if showsSupportedCardLogos {
observe(numberItem.$isActive) { [weak self] _ in
Expand Down
9 changes: 7 additions & 2 deletions AdyenCard/Form/FormCardNumberItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ internal final class FormCardNumberItem: FormTextItem, AdyenObserver {

private let localizationParameters: LocalizationParameters?

internal let scanCardHandler: (() -> Void)?

internal var supportsCardScanning: Bool { scanCardHandler != nil }

/// Returns the initial brand for single brand cases
/// or `selectedDualBrand` for dual brand cases
internal var currentBrand: CardBrand? {
Expand All @@ -57,7 +61,8 @@ internal final class FormCardNumberItem: FormTextItem, AdyenObserver {
internal init(
cardTypeLogos: [FormCardLogosItem.CardTypeLogo],
style: FormTextItemStyle = FormTextItemStyle(),
localizationParameters: LocalizationParameters? = nil
localizationParameters: LocalizationParameters? = nil,
scanCardHandler: (() -> Void)?
) {
// these 4 US debit brands are not to be displayed
// but should be supported so it's done here for now
Expand All @@ -68,8 +73,8 @@ internal final class FormCardNumberItem: FormTextItem, AdyenObserver {
logo.type != .nyce
}
self.supportedCardTypes = cardTypeLogos.map(\.type)

self.localizationParameters = localizationParameters
self.scanCardHandler = scanCardHandler

super.init(style: style)

Expand Down
29 changes: 29 additions & 0 deletions AdyenCard/Form/FormCardNumberItemView+ScanCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

extension FormCardNumberItemView {
func makeCardScanAccessoryView(_ selector: Selector) -> UIView {
let accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44))
accessoryView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)

let scanButton = UIButton(type: .system)
// TODO: Localization
scanButton.setTitle("Scan your card", for: .normal)
scanButton.addTarget(self, action: selector, for: .touchUpInside)
scanButton.translatesAutoresizingMaskIntoConstraints = false

accessoryView.addSubview(scanButton)

NSLayoutConstraint.activate([
scanButton.centerXAnchor.constraint(equalTo: accessoryView.centerXAnchor),
scanButton.centerYAnchor.constraint(equalTo: accessoryView.centerYAnchor)
])

return accessoryView
}
}
8 changes: 8 additions & 0 deletions AdyenCard/Form/FormCardNumberItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ internal final class FormCardNumberItemView: FormTextItemView<FormCardNumberItem
internal required init(item: FormCardNumberItem) {
super.init(item: item)
accessory = .customView(detectedBrandsView)
if item.supportsCardScanning {
textField.inputAccessoryView = makeCardScanAccessoryView(#selector(openCardScanner))
}
textField.textContentType = .creditCardNumber
textField.returnKeyType = .default
textField.allowsEditingActions = false
Expand Down Expand Up @@ -95,4 +98,9 @@ internal final class FormCardNumberItemView: FormTextItemView<FormCardNumberItem
cardTypeLogosView.backgroundColor = item.style.backgroundColor
return cardTypeLogosView
}()

@objc private func openCardScanner() {
guard let scanCardHandler = item.scanCardHandler else { return }
scanCardHandler()
}
}
Loading