Issue #346
We have FrontCard that contains number and expiration date, BackCard that contains CVC. CardView is used to contain front and back sides for flipping transition.
We leverage STPPaymentCardTextField
from Stripe for working input fields, then CardHandler
is used to parse STPPaymentCardTextField
content and update our UI.
For masked credit card numbers, we pad string to fit 16 characters with ●
symbol, then chunk into 4 parts and zip with labels to update.
For flipping animation, we use UIView.transition
with showHideTransitionViews
BackCard.swift
import UIKit
final class BackCard: UIView {
lazy var rectangle: UIView = {
let view = UIView()
view.backgroundColor = R.color.darkText
return view
}()
lazy var cvcLabel: UILabel = {
let label = UILabel()
label.font = R.customFont.medium(14)
label.textColor = R.color.darkText
label.textAlignment = .center
return label
}()
lazy var cvcBox: UIView = {
let view = UIView()
view.backgroundColor = R.color.lightText
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
private func setup() {
addSubviews([rectangle, cvcBox, cvcLabel])
NSLayoutConstraint.on([
rectangle.leftAnchor.constraint(equalTo: leftAnchor),
rectangle.rightAnchor.constraint(equalTo: rightAnchor),
rectangle.heightAnchor.constraint(equalToConstant: 52),
rectangle.topAnchor.constraint(equalTo: topAnchor, constant: 30),
cvcBox.rightAnchor.constraint(equalTo: rightAnchor, constant: -16),
cvcBox.topAnchor.constraint(equalTo: rectangle.bottomAnchor, constant: 16),
cvcBox.widthAnchor.constraint(equalToConstant: 66),
cvcBox.heightAnchor.constraint(equalToConstant: 30),
cvcLabel.centerXAnchor.constraint(equalTo: cvcBox.centerXAnchor),
cvcLabel.centerYAnchor.constraint(equalTo: cvcBox.centerYAnchor)
])
}
}
FrontCard.swift
import UIKit
final class FrontCard: UIView {
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
return stackView
}()
lazy var numberLabels: [UILabel] = Array(0..<4).map({ _ in return UILabel() })
lazy var expirationStaticLabel: UILabel = {
let label = UILabel()
label.font = R.customFont.regular(10)
label.textColor = R.color.darkText
return label
}()
lazy var expirationLabel: UILabel = {
let label = UILabel()
label.font = R.customFont.medium(14)
label.textColor = R.color.darkText
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
private func setup() {
addSubview(stackView)
numberLabels.forEach {
stackView.addArrangedSubview($0)
}
addSubviews([expirationStaticLabel, expirationLabel])
numberLabels.forEach {
$0.font = R.customFont.medium(16)
$0.textColor = R.color.darkText
$0.textAlignment = .center
}
NSLayoutConstraint.on([
stackView.heightAnchor.constraint(equalToConstant: 50),
stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 24),
stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -24),
stackView.topAnchor.constraint(equalTo: centerYAnchor),
expirationStaticLabel.topAnchor.constraint(equalTo: stackView.bottomAnchor),
expirationStaticLabel.leftAnchor.constraint(equalTo: rightAnchor, constant: -70),
expirationLabel.leftAnchor.constraint(equalTo: expirationStaticLabel.leftAnchor),
expirationLabel.topAnchor.constraint(equalTo: expirationStaticLabel.bottomAnchor)
])
}
}
CardView.swift
import UIKit
final class CardView: UIView {
let backCard = BackCard()
let frontCard = FrontCard()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
private func setup() {
addSubview(backCard)
addSubview(frontCard)
[backCard, frontCard].forEach {
NSLayoutConstraint.on([
$0.pinEdges(view: self)
])
$0.clipsToBounds = true
$0.layer.cornerRadius = 10
$0.backgroundColor = R.color.card.background
}
}
}
CardHandler.swift
import Foundation
import Stripe
final class CardHandler {
let cardView: CardView
init(cardView: CardView) {
self.cardView = cardView
}
func reset() {
cardView.frontCard.expirationStaticLabel.text = R.string.localizable.cardExpiration()
cardView.frontCard.expirationLabel.text = R.string.localizable.cardExpirationPlaceholder()
cardView.backCard.cvcLabel.text = R.string.localizable.cardCvcPlaceholder()
}
func showFront() {
flip(
from: cardView.backCard,
to: cardView.frontCard,
options: .transitionFlipFromLeft
)
}
func showBack() {
flip(
from: cardView.frontCard,
to: cardView.backCard,
options: .transitionFlipFromRight
)
}
func handle(_ textField: STPPaymentCardTextField) {
handle(number: textField.cardNumber ?? "")
handle(month: textField.formattedExpirationMonth, year: textField.formattedExpirationYear)
handle(cvc: textField.cvc)
}
private func handle(number: String) {
let paddedNumber = number.padding(
toLength: 16,
withPad: R.string.localizable.cardNumberPlaceholder(),
startingAt: 0
)
let chunkedNumbers = paddedNumber.chunk(by: 4)
zip(cardView.frontCard.numberLabels, chunkedNumbers).forEach { tuple in
tuple.0.text = tuple.1
}
}
private func handle(cvc: String?) {
if let cvc = cvc, !cvc.isEmpty {
cardView.backCard.cvcLabel.text = cvc
} else {
cardView.backCard.cvcLabel.text = R.string.localizable.cardCvcPlaceholder()
}
}
private func handle(month: String?, year: String?) {
guard
let month = month, let year = year,
!month.isEmpty
else {
cardView.frontCard.expirationLabel.text = R.string.localizable.cardExpirationPlaceholder()
return
}
let formattedYear = year.ifEmpty(replaceWith: "00")
cardView.frontCard.expirationLabel.text = "\(month)/\(formattedYear)"
}
private func flip(from: UIView, to: UIView, options: UIView.AnimationOptions) {
UIView.transition(
from: from,
to: to,
duration: 0.25,
options: [options, .showHideTransitionViews],
completion: nil
)
}
}
String+Extension.swift
extension String {
func ifEmpty(replaceWith: String) -> String {
return isEmpty ? replaceWith : self
}
func chunk(by length: Int) -> [String] {
return stride(from: 0, to: count, by: length).map {
let start = index(startIndex, offsetBy: $0)
let end = index(start, offsetBy: length, limitedBy: endIndex) ?? endIndex
return String(self[start..<end])
}
}
}
Updated at 2020-07-12 08:43:21