Skip to content

Commit d07f6da

Browse files
StoredCard and Preselected screen amount based changes & tests (#2479)
# Summary [Required] - Amount can have 3 states, so added tests for them using existing copies(note: the copies may change but that will be a smaller change). The todo for this change will still remain until it is confirmed. - Fixing contraints for the width and duplicate constraints for the StoredView. - Passing CheckoutTheme to the StoredCardComponent - Removing unused code in the viewModel. ## Checklist [Required] <!-- Mark completed items with an [x] --> - [x] Tested changes locally - [x] Added/updated unit tests
2 parents 7710a7d + dd35727 commit d07f6da

File tree

11 files changed

+121
-79
lines changed

11 files changed

+121
-79
lines changed

Adyen/Assets/Generated/LocalizationKey.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public struct LocalizationKey {
7878
public static let cardCvcItemTitle = LocalizationKey(key: "adyen.card.cvcItem.title")
7979
/// Enter security code
8080
public static let cardSecurityCodeTitle = LocalizationKey(key: "adyen.card.securityCode.title")
81-
/// Enter the security code for %@ to complete the payment of %@
81+
/// Enter the security code for %@
8282
public static let cardSecurityCodeDescription = LocalizationKey(key: "adyen.card.securityCode.description")
8383
/// 123
8484
public static let cardCvcItemPlaceholder = LocalizationKey(key: "adyen.card.cvcItem.placeholder")
@@ -560,7 +560,7 @@ public struct LocalizationKey {
560560
public static let cardScannerCameraAccessDeniedAlertMessage = LocalizationKey(key: "adyen.card.scanner.camera.access.denied.alert.message")
561561
/// Open Settings
562562
public static let cardScannerCameraAccessDeniedAlertSettingsButtonTitle = LocalizationKey(key: "adyen.card.scanner.camera.access.denied.alert.settingsButton.title")
563-
/// Use %@ to pay %@
563+
/// Use %@
564564
public static let preselectedPaymentMethodSubtitle = LocalizationKey(key: "adyen.preselectedPaymentMethod.subtitle")
565565
/// Other payment options
566566
public static let preselectedPaymentMethodOtherOptions = LocalizationKey(key: "adyen.preselectedPaymentMethod.otherOptions")

Adyen/Assets/en-US.lproj/Localizable.strings

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"adyen.card.cvcItem.invalid" = "Invalid CVC / CVV format";
3434
"adyen.card.cvcItem.title" = "Security code";
3535
"adyen.card.cvcItem.placeholder" = "123";
36-
"adyen.card.securityCode.description" = "Enter the security code for %@ to complete the payment of %@";
36+
"adyen.card.securityCode.title" = "Enter security code";
37+
"adyen.card.securityCode.description" = "Enter the security code for %@";
3738
"adyen.card.stored.title" = "Verify your card";
3839
"adyen.card.stored.message" = "Please enter the CVC code for %@";
3940
"adyen.card.stored.expires" = "Expires %@";
@@ -273,5 +274,5 @@
273274
"adyen.card.scanner.camera.access.denied.alert.title" = "Allow camera access";
274275
"adyen.card.scanner.camera.access.denied.alert.message" = "Access was previously denied. To scan cards, please grant access from Settings.";
275276
"adyen.card.scanner.camera.access.denied.alert.settingsButton.title" = "Open Settings";
276-
"adyen.preselectedPaymentMethod.subtitle" = "Use %@ to pay %@";
277+
"adyen.preselectedPaymentMethod.subtitle" = "Use %@";
277278
"adyen.preselectedPaymentMethod.otherOptions" = "Other payment options";

AdyenCard/Components/Card/CardComponent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public class CardComponent: PresentableComponent,
150150
}
151151
// TODO: FIX StoredCard UI
152152
if configuration.stored.showsSecurityCodeField {
153-
let storedComponent = StoredCardComponent(storedCardPaymentMethod: paymentMethod, context: context)
153+
let storedComponent = StoredCardComponent(storedCardPaymentMethod: paymentMethod, context: context, theme: configuration.theme)
154154
storedComponent.localizationParameters = configuration.localizationParameters
155155
return storedComponent
156156
} else {

AdyenCard/Components/Stored Card/StoredCardComponent.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
@_spi(AdyenInternal) import Adyen
88
import Foundation
99
import UIKit
10+
#if canImport(AdyenUI)
11+
import AdyenUI
12+
#endif
1013

1114
/// A component that provides a form for stored card payments.
1215
@MainActor
@@ -26,13 +29,17 @@ package final class StoredCardComponent: StoredPaymentComponent, Localizable {
2629
package var localizationParameters: LocalizationParameters?
2730

2831
private let storedCardPaymentMethod: StoredCardPaymentMethod
29-
32+
33+
private let theme: CheckoutTheme
34+
3035
package init(
3136
storedCardPaymentMethod: StoredCardPaymentMethod,
32-
context: AdyenContext
37+
context: AdyenContext,
38+
theme: CheckoutTheme
3339
) {
3440
self.storedCardPaymentMethod = storedCardPaymentMethod
3541
self.context = context
42+
self.theme = theme
3643
}
3744

3845
package var viewController: UIViewController {

AdyenCard/Components/Stored Card/StoredCardInputView/StoredCardInputViewController.swift

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal class StoredCardInputViewController: UIViewController {
1919

2020
private enum Constants {
2121
static let chevronBackwardImage = "chevron.backward"
22-
static let contentPadding: CGFloat = 24
22+
static let contentPadding: CGFloat = 16
2323
static let distanceBetweenImageAndLabels: CGFloat = 12
2424
static let distanceFromButtonsToLabels: CGFloat = 24
2525
static let buttonsBottomPadding: CGFloat = 0
@@ -151,27 +151,22 @@ internal class StoredCardInputViewController: UIViewController {
151151
scrollView.addSubview(contentStackView)
152152

153153
[
154-
topContentStackView,
155-
securityCodeItemView,
156-
buttonsStackView
157-
].forEach(contentStackView.addArrangedSubview)
154+
titleLabel,
155+
subtitleLabel
156+
].forEach(labelsStackView.addArrangedSubview)
158157

159158
[
160159
cardImageView,
161160
labelsStackView
162161
].forEach(topContentStackView.addArrangedSubview)
163162

164-
[
165-
titleLabel,
166-
subtitleLabel
167-
].forEach(labelsStackView.addArrangedSubview)
163+
buttonsStackView.addArrangedSubview(primaryButton)
168164

169165
[
170-
titleLabel,
171-
subtitleLabel
172-
].forEach(labelsStackView.addArrangedSubview)
173-
174-
buttonsStackView.addArrangedSubview(primaryButton)
166+
topContentStackView,
167+
securityCodeItemView,
168+
buttonsStackView
169+
].forEach(contentStackView.addArrangedSubview)
175170

176171
configureConstraints()
177172
configureContent()
@@ -185,6 +180,7 @@ internal class StoredCardInputViewController: UIViewController {
185180
}
186181

187182
private func configureConstraints() {
183+
// TODO: Robert: StoredView: Auto layout Constraints breaks. This needs a separate investigation as this involves the FormCardSecurityCodeItemView
188184
NSLayoutConstraint.activate([
189185
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
190186
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
@@ -193,8 +189,8 @@ internal class StoredCardInputViewController: UIViewController {
193189

194190
contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
195191
contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: Constants.contentPadding),
196-
contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -Constants.contentPadding),
197-
contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -Constants.buttonsBottomPadding)
192+
contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -Constants.buttonsBottomPadding),
193+
contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -2 * Constants.contentPadding)
198194
])
199195
}
200196

AdyenCard/Components/Stored Card/StoredCardInputView/StoredCardInputViewModel.swift

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ internal final class StoredCardInputViewModel: StoredCardInputViewModelProtocol
5757

5858
/// This informs the status of the payment after submitting the security code.
5959
internal var cardDetailsCompletionHandler: Completion<Result<CardDetails, Error>>?
60-
internal var otherPaymentOptionsHandler: VoidCompletion?
6160
internal var closeHandler: VoidCompletion?
6261

6362
internal init(
@@ -103,39 +102,28 @@ internal final class StoredCardInputViewModel: StoredCardInputViewModelProtocol
103102
localizedString(.cardSecurityCodeTitle, localizationParameters)
104103
}
105104

106-
/// We construct something like - Enter the security code for BOLD[Visa •••• 4556] to complete the payment of BOLD[$140.98]
107105
// TODO: Robert: StoredView: This & the pay button title needs to change according to the amount.
106+
/// We construct something like - Enter the security code for BOLD[Visa •••• 4556]
108107
internal var subtitleText: NSAttributedString {
109108
let displayInformation = storedCardPaymentMethod.displayInformation(using: localizationParameters)
110109
let paymentMethodTitle = storedCardPaymentMethod.name + " " + displayInformation.title
111-
let localizedString = localizedString(.cardSecurityCodeDescription, localizationParameters, paymentMethodTitle, formattedAmount)
110+
let localizedString = localizedString(.cardSecurityCodeDescription, localizationParameters, paymentMethodTitle)
112111

113112
let attributed = NSMutableAttributedString(string: localizedString)
114113

115114
let range = (localizedString as NSString).range(of: paymentMethodTitle)
116115
attributed.addAttribute(.font, value: theme.elements.labels.bodyEmphasized.font, range: range)
117116
attributed.addAttribute(.foregroundColor, value: theme.elements.labels.bodyEmphasized.color, range: range)
118117

119-
let amountRange = (localizedString as NSString).range(of: formattedAmount)
120-
attributed.addAttribute(.font, value: theme.elements.labels.bodyEmphasized.font, range: amountRange)
121-
attributed.addAttribute(.foregroundColor, value: theme.elements.labels.bodyEmphasized.color, range: amountRange)
122-
123118
return attributed
124119
}
125120

126-
private var formattedAmount: String {
127-
guard let amount,
128-
let formatted = AmountFormatter.formatted(
129-
amount: amount.value,
130-
currencyCode: amount.currencyCode
131-
) else {
132-
return ""
133-
}
134-
return formatted
135-
}
136-
137121
internal var submitButtonTitle: String {
138-
localizedString(.submitButtonFormatted, localizationParameters, formattedAmount)
122+
localizedSubmitButtonTitle(
123+
with: amount,
124+
style: .immediate,
125+
localizationParameters
126+
)
139127
}
140128

141129
internal func viewDidLoad() {

AdyenCheckout/CheckoutComponentBuilder.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ internal enum CheckoutComponentBuilder {
8383
case let storedCard as StoredCardPaymentMethod:
8484
StoredCardComponent(
8585
storedCardPaymentMethod: storedCard,
86-
context: context
86+
context: context,
87+
theme: configuration.theme
8788
)
8889
#endif
8990

AdyenDropIn/Modules/PreselectedPaymentMethod/PreselectedPaymentMethodViewModel.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,16 @@ internal final class PreselectedPaymentMethodViewModel: PreselectedPaymentMethod
101101
return displayInformation.title
102102
}
103103

104-
private var formattedAmount: String {
105-
guard let amount = component.context.amount,
106-
let formatted = AmountFormatter.formatted(amount: amount.value, currencyCode: amount.currencyCode) else {
107-
return ""
108-
}
109-
return formatted
110-
}
111-
112104
internal var subtitleText: String {
113-
localizedString(.preselectedPaymentMethodSubtitle, localizationParameters, component.paymentMethod.name, formattedAmount)
105+
localizedString(.preselectedPaymentMethodSubtitle, localizationParameters, component.paymentMethod.name)
114106
}
115107

116108
internal var submitButtonTitle: String {
117-
localizedString(.submitButtonFormatted, localizationParameters, formattedAmount)
109+
localizedSubmitButtonTitle(
110+
with: component.context.amount,
111+
style: .immediate,
112+
localizationParameters
113+
)
118114
}
119115

120116
internal func submitPayment() {

Tests/IntegrationTests/Card Tests/StoredCardComponentTests.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
@_spi(AdyenInternal) @testable import Adyen
88
@testable @_spi(AdyenInternal) import AdyenCard
9+
@testable import AdyenUI
910
import XCTest
1011

1112
@MainActor
@@ -29,7 +30,7 @@ class StoredCardComponentTests: XCTestCase {
2930
// let payment = Payment(amount: Amount(value: 174, currencyCode: "EUR"), countryCode: "NL")
3031

3132
func testUIWithClientKey() throws {
32-
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context)
33+
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context, theme: CheckoutTheme())
3334

3435
presentOnRoot(sut.viewController)
3536

@@ -44,7 +45,7 @@ class StoredCardComponentTests: XCTestCase {
4445
}
4546

4647
func testPaymentSubmitWithValidPublicKey() throws {
47-
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context)
48+
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context, theme: CheckoutTheme())
4849

4950
let delegateExpectation = expectation(description: "expect delegate to be called.")
5051
let delegate = PaymentComponentDelegateMock()
@@ -93,7 +94,7 @@ class StoredCardComponentTests: XCTestCase {
9394
publicKey: "invalid_key",
9495
analyticsProvider: AnalyticsProviderMock()
9596
)
96-
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: contextWithInvalidKey)
97+
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: contextWithInvalidKey, theme: CheckoutTheme())
9798

9899
let delegate = PaymentComponentDelegateMock()
99100
delegate.onDidSubmit = { _, _ in
@@ -136,7 +137,7 @@ class StoredCardComponentTests: XCTestCase {
136137
expiryYear: "22",
137138
holderName: "holderName"
138139
)
139-
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context)
140+
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context, theme: CheckoutTheme())
140141

141142
presentOnRoot(sut.viewController)
142143

@@ -175,7 +176,7 @@ class StoredCardComponentTests: XCTestCase {
175176
}
176177

177178
func testCVCLimitForNonAMEX() throws {
178-
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context)
179+
let sut = StoredCardComponent(storedCardPaymentMethod: method, context: context, theme: CheckoutTheme())
179180

180181
presentOnRoot(sut.viewController)
181182

@@ -208,7 +209,8 @@ class StoredCardComponentTests: XCTestCase {
208209
let paymentMethod = storedCardPaymentMethod(brand: .masterCard)
209210
let sut = StoredCardComponent(
210211
storedCardPaymentMethod: paymentMethod,
211-
context: context
212+
context: context,
213+
theme: CheckoutTheme()
212214
)
213215

214216
// When

Tests/IntegrationTests/Card Tests/StoredCardInputViewModelTests.swift

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,47 @@ struct StoredCardInputViewModelTests {
6666
// MARK: - UI Text
6767

6868
@Test
69-
func textProperties() {
69+
func textUI_WhenAmountIsAvailable() {
7070
// Given
7171
let amount = Amount(value: 14098, currencyCode: "USD")
72+
let expectedTitle = "Enter security code"
73+
let expectedSubTitle = "Enter the security code for VISA •••• 4556"
74+
let expectedButtonTitle = "Pay $140.98"
7275
let sut = makeSUT(name: "VISA", lastFour: "4556", amount: amount)
7376

7477
// Then
75-
#expect(!sut.titleText.isEmpty)
78+
#expect(sut.titleText == expectedTitle)
79+
#expect(sut.subtitleText.string == expectedSubTitle)
80+
#expect(sut.submitButtonTitle == expectedButtonTitle)
81+
}
82+
83+
@Test
84+
func textUI_WhenAmountIsZero() {
85+
// Given
86+
let amount = Amount(value: 0, currencyCode: "USD")
87+
let expectedTitle = "Enter security code"
88+
let expectedSubTitle = "Enter the security code for VISA •••• 4556"
89+
let expectedButtonTitle = "Confirm preauthorization"
90+
let sut = makeSUT(name: "VISA", lastFour: "4556", amount: amount)
91+
92+
// Then
93+
#expect(sut.titleText == expectedTitle)
94+
#expect(sut.subtitleText.string == expectedSubTitle)
95+
#expect(sut.submitButtonTitle == expectedButtonTitle)
96+
}
97+
98+
@Test
99+
func textUI_WhenAmountIsNil() {
100+
// Given
101+
let expectedTitle = "Enter security code"
102+
let expectedSubTitle = "Enter the security code for VISA •••• 4556"
103+
let expectedButtonTitle = "Pay"
104+
let sut = makeSUT(name: "VISA", lastFour: "4556", amount: nil)
76105

77-
// subtitleText contains payment method info and formatted amount
78-
let subtitle = sut.subtitleText.string
79-
#expect(subtitle == "Enter the security code for VISA •••• 4556 to complete the payment of $140.98")
80-
// submitButtonTitle contains formatted amount
81-
#expect(sut.submitButtonTitle.contains("$140.98"))
106+
// Then
107+
#expect(sut.titleText == expectedTitle)
108+
#expect(sut.subtitleText.string == expectedSubTitle)
109+
#expect(sut.submitButtonTitle == expectedButtonTitle)
82110
}
83111

84112
@Test(arguments: StoredCardTestData.amounts)
@@ -104,7 +132,7 @@ struct StoredCardInputViewModelTests {
104132

105133
// Then
106134
#expect(closeHandlerCalled)
107-
#expect(sut.securityCodeItem.value == "")
135+
#expect(sut.securityCodeItem.value.isEmpty)
108136
}
109137

110138
@Test
@@ -113,20 +141,18 @@ struct StoredCardInputViewModelTests {
113141
let sut = makeSUT()
114142
sut.securityCodeItem.value = "999"
115143
sut.closeHandler = {}
116-
sut.otherPaymentOptionsHandler = {}
117144

118145
// When
119146
sut.dismiss()
120147

121148
// Then
122-
#expect(sut.securityCodeItem.value == "", "Security code should be cleared after dismiss")
149+
#expect(sut.securityCodeItem.value.isEmpty, "Security code should be cleared after dismiss")
123150
}
124151

125152
@Test func navigation_withoutHandlers_doesNotCrash() {
126153
// Given
127154
let sut = makeSUT()
128155
sut.closeHandler = nil
129-
sut.otherPaymentOptionsHandler = nil
130156

131157
// When / Then - no crash
132158
sut.dismiss()
@@ -274,7 +300,6 @@ enum StoredCardTestData {
274300

275301
static let amounts: [AmountData] = [
276302
AmountData(amount: Amount(value: 100, currencyCode: "EUR"), expectedFormatted: "€1.00"),
277-
AmountData(amount: Amount(value: 14098, currencyCode: "USD"), expectedFormatted: "$140.98"),
278-
AmountData(amount: Amount(value: 0, currencyCode: "GBP"), expectedFormatted: "£0.00")
303+
AmountData(amount: Amount(value: 14098, currencyCode: "USD"), expectedFormatted: "$140.98")
279304
]
280305
}

0 commit comments

Comments
 (0)