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

feat: adds maskChange event on CVC, makes complete flag consistent, checks complete flag on tokenize requests #27

Merged
merged 6 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal protocol InternalElementProtocol {

internal protocol ElementReferenceProtocol {
func getValue() -> String?
var isValid: Bool? { get set }
var isComplete: Bool? { get set }
}

public enum ElementConfigError: Error {
Expand All @@ -32,11 +32,11 @@ public enum ElementConfigError: Error {

public class ElementValueReference: ElementReferenceProtocol {
var valueMethod: (() -> String)?
var isValid: Bool? = true
var isComplete: Bool? = true

init(valueMethod: (() -> String)?, isValid: Bool?) {
init(valueMethod: (() -> String)?, isComplete: Bool?) {
self.valueMethod = valueMethod
self.isValid = isValid
self.isComplete = isComplete
}

func getValue() -> String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ final public class BasisTheoryElements {
} else if let v = val as? ElementReferenceProtocol {
let textValue = v.getValue()

if !v.isValid! {
if !v.isComplete! {
throw TokenizingError.invalidInput
}
body[key] = textValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ final public class CardExpirationDateUITextField: TextElementUITextField {
}

public func month() -> ElementValueReference {
let monthReference = ElementValueReference(valueMethod: getMonthValue, isValid: self.isValid)
let monthReference = ElementValueReference(valueMethod: getMonthValue, isComplete: self.isComplete)
return monthReference
}

public func year() -> ElementValueReference {
let yearReference = ElementValueReference(valueMethod: getYearValue, isValid: self.isValid)
let yearReference = ElementValueReference(valueMethod: getYearValue, isComplete: self.isComplete)
return yearReference
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ final public class CardNumberUITextField: TextElementUITextField {
}

private func getCardElementEvent(text: String?, event: ElementEvent) -> ElementEvent {
let complete = cardBrand?.complete ?? false
let complete = cardBrand?.complete ?? false && event.valid
let brand = cardBrand?.bestMatchCardBrand?.cardBrandName != nil ? String(describing: cardBrand!.bestMatchCardBrand!.cardBrandName) : "unknown"
var details = [ElementEventDetails(type: "cardBrand", message: brand)]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,24 @@ final public class CardVerificationCodeUITextField: TextElementUITextField {

let brand = cardNumberUITextField?.cardBrand?.bestMatchCardBrand

if brand != nil {
if brand != nil && self.cvcMask?.count != brand?.cvcMaskInput.count {
self.cvcMask = brand?.cvcMaskInput
self.sendMaskChangeEvent()
}

return
}

private func sendMaskChangeEvent() {
let text = super.getValue()
let valid = validateCvc(text: text)
let maskSatisfied = text?.count == self.inputMask?.count
let complete = valid && maskSatisfied
let elementEvent = ElementEvent(type: "maskChange", complete: complete, empty: text?.isEmpty ?? false, valid: valid, details: [])

self.subject.send(elementEvent)
}

public override func setConfig(options: TextElementOptions?) throws {
throw ElementConfigError.configNotAllowed
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ struct ProxyHelpers {
} else {
json[key] = JSON.elementValueReference(ElementValueReference(valueMethod: {
String(describing: value)
}, isValid: true))
}, isComplete: true))
}
}
}
Expand All @@ -97,7 +97,7 @@ struct ProxyHelpers {
} else {
json[index] = JSON.elementValueReference(ElementValueReference(valueMethod: {
String(describing: value)
}, isValid: true))
}, isComplete: true))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public struct TextElementOptions {
}

public class TextElementUITextField: UITextField, InternalElementProtocol, ElementProtocol, ElementReferenceProtocol {
var isValid: Bool? = true
var isComplete: Bool? = true
var getElementEvent: ((String?, ElementEvent) -> ElementEvent)?
var validation: ((String?) -> Bool)?
var backspacePressed: Bool = false
Expand Down Expand Up @@ -59,7 +59,7 @@ public class TextElementUITextField: UITextField, InternalElementProtocol, Eleme
}

if let validation = validation {
self.isValid = validation(transform(text: newValue))
self.isComplete = self.isComplete ?? true && validation(transform(text: newValue))
}
}
get { nil }
Expand Down Expand Up @@ -167,24 +167,22 @@ public class TextElementUITextField: UITextField, InternalElementProtocol, Eleme
}

if let validation = validation {
self.isValid = validation(transform(text: text))
self.isComplete = self.isComplete ?? true && validation(transform(text: text))
}
}

@objc func textFieldDidChange() {
var maskComplete = true

if inputMask != nil {
if let inputMask = inputMask {
// dont conform on backspace pressed - just remove the value + check for backspace on empty
if (!backspacePressed || super.text != nil) {
super.text = conformToMask(text: super.text)
} else {
backspacePressed = false
}

if (super.text?.count != inputMask!.count ) {
maskComplete = false
}

maskComplete = super.text?.count == inputMask.count
}

let transformedTextValue = self.transform(text: super.text)
Expand All @@ -194,10 +192,10 @@ public class TextElementUITextField: UITextField, InternalElementProtocol, Eleme
valid = validation(transformedTextValue)
}

self.isValid = valid

let complete = valid && maskComplete

self.isComplete = complete

var elementEvent = ElementEvent(type: "textChange", complete: complete, empty: transformedTextValue.isEmpty , valid: valid, details: [])

if let getElementEvent = getElementEvent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,32 @@ final class SplitCardElementsIntegrationTesterUITests: XCTestCase {
XCTAssertEqual(cvcTextField.value as! String, "1234")
}
}

func testCvcInputDoesNotChangeWithoutUserActionWhenCardBrandChanges() throws {
let amExCardNumber = "378282246310005"

let cardNumberTextField = app.textFields["Card Number"]
cardNumberTextField.tap()
cardNumberTextField.typeText(amExCardNumber)

let amExCvc = "4321"

let cvcTextField = app.textFields["CVC"]
cvcTextField.tap()
cvcTextField.typeText(amExCvc)

XCTAssertEqual(cvcTextField.value as! String, amExCvc)

let visaCardNumber = "4242424242424242"

cardNumberTextField.doubleTap()
cardNumberTextField.typeText(visaCardNumber)

XCTAssertEqual(cvcTextField.value as! String, amExCvc) // CVC value doesn't change even though new mask is applied

cvcTextField.tap()
cvcTextField.typeText("5") // user tries to type in 5

XCTAssertEqual(cvcTextField.value as! String, "432") // mask is applied and truncates input after user action
}
}
13 changes: 3 additions & 10 deletions IntegrationTester/UnitTests/CardNumberUITextFieldTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,13 @@ final class CardNumberUITextFieldTests: XCTestCase {
XCTAssertEqual(brandDetails.message, "visa")

if (!incompleteNumberExpectationHasBeenFulfilled) {
XCTAssertEqual(message.complete, false)
XCTAssertEqual(message.complete, false) // mask incomplete and number is invalid
XCTAssertEqual(eventDetails.count, 1)
incompleteNumberExpectation.fulfill()
incompleteNumberExpectationHasBeenFulfilled = true
} else {
XCTAssertEqual(message.complete, true) // mask completed but number invalid

let last4Details = eventDetails[1]
let binDetails = eventDetails[2]

XCTAssertEqual(last4Details.type, "cardLast4")
XCTAssertEqual(binDetails.type, "cardBin")
XCTAssertEqual(last4Details.message, "5598")
XCTAssertEqual(binDetails.message, "412993")
XCTAssertEqual(message.complete, false) // mask completed but number invalid
XCTAssertEqual(eventDetails.count, 1)

luhnInvalidNumberExpectation.fulfill()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,61 @@ final class CardVerificationCodeUITextFieldTests: XCTestCase {

waitForExpectations(timeout: 1, handler: nil)
}

func testCvcEventIsEmittedWhenCardBrandChanges() throws {
let cardNumberTextField = CardNumberUITextField()
let cvcTextField = CardVerificationCodeUITextField()

cvcTextField.setConfig(options: CardVerificationCodeOptions(cardNumberUITextField: cardNumberTextField))

var cvcInitialMaskHasBeenSet = false
var cvcInitialTextHasBeenEntered = false
let cvcTextExpectation = self.expectation(description: "CVC input")
let cvcMaskChangeExpectation1 = self.expectation(description: "CVC mask change 1")
let cvcMaskChangeExpectation2 = self.expectation(description: "CVC mask change 2")
var cancellables = Set<AnyCancellable>()
cvcTextField.subject.sink { completion in
print(completion)
} receiveValue: { message in
if !cvcInitialMaskHasBeenSet {
cvcInitialMaskHasBeenSet = true

XCTAssertEqual(message.type, "maskChange")
XCTAssertEqual(message.valid, false)
XCTAssertEqual(message.complete, false)

cvcMaskChangeExpectation1.fulfill()
} else if !cvcInitialTextHasBeenEntered {
cvcInitialTextHasBeenEntered = true

XCTAssertEqual(message.type, "textChange")
XCTAssertEqual(message.valid, true)
XCTAssertEqual(message.complete, true)

cvcTextExpectation.fulfill()
} else {
XCTAssertEqual(message.type, "maskChange")
XCTAssertEqual(message.valid, true)
XCTAssertEqual(message.complete, false)

cvcMaskChangeExpectation2.fulfill()
}
}.store(in: &cancellables)

let amExCardNumber = "378282246310005"
let amExCvc = "4321"

cardNumberTextField.insertText(amExCardNumber)
cvcTextField.insertText(amExCvc)

let visaCardNumber = "4242424242424242"
let masterCardCardNumber = "5555555555554444"

cardNumberTextField.text = ""
cardNumberTextField.insertText(visaCardNumber)
cardNumberTextField.text = ""
cardNumberTextField.insertText(masterCardCardNumber)

waitForExpectations(timeout: 1, handler: nil)
}
}
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,21 @@ Events are triggered whenever a user types into an element textfield. An `Elemen
| valid | Whether the input is `valid` according to `validation` for each element. |
| details | An array of [ElementEventDetail](#elementeventdetail) describing more information about the element event. |

Available `type`'s include:
* `textChange` - when the text has changed
* `maskChange` - when the card brand has changed the CVC mask. This type is only emitted from the `CardExpirationDateUITextField` element.

### ElementEventDetail

| Property | Description |
| --- | --- |
| type | A `String` describing the type of detail. |
| message | A `String` containing the message for the detail. |

Available details include:
* `cardBrand` when the card number can be identified
* `cardLast4` when the card number is complete
* `cardBin` when the card number is complete
Available details include (All of the following can only be emitted by the `CardNumberUITextField`):
* `cardBrand` - when the card number can be identified
* `cardLast4` - when the card number is complete
* `cardBin` - when the card number is complete

## Element Instances

Expand Down