Skip to content

Commit

Permalink
feat(POM-398): support saving card during tokenization (#318)
Browse files Browse the repository at this point in the history
* Conditionally display save card checkbox during card tokenization.
* Expose savingAllowed within dynamic checkout API bindings.
* Add checkbox toggle style.
* Use monospaced numbers for list markers and dynamic checkout's
express payments.
* Remove unused UIKit extensions.
* Interactively dismiss keyboard during DC.
  • Loading branch information
andrii-vysotskyi-cko authored Jul 31, 2024
1 parent b232a2d commit 9a341ed
Show file tree
Hide file tree
Showing 37 changed files with 679 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ public enum PODynamicCheckoutPaymentMethod {
/// Indicates whether should collect cardholder name.
public let cardholderNameRequired: Bool

/// Indicates whether the UI should display a control (such as a checkbox) that allows
/// the user to choose whether to save their card details for future payments.
public let savingAllowed: Bool

/// Card billing address collection configuration.
public let billingAddress: BillingAddressConfiguration
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
- ``SwiftUI/EnvironmentValues/isRadioButtonSelected``
- ``PORadioButtonStyle/radio``

### Checkbox

- ``POCheckboxToggleStyle``
- ``POCheckboxToggleStateStyle``
- ``SwiftUI/ToggleStyle/poCheckbox``

### Actions Container

- ``POActionsContainerStyle``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ struct AttributedStringBuilder {
/// Allows to alter font with the specified symbolic traits.
var fontSymbolicTraits: UIFontDescriptor.SymbolicTraits = []

/// Font feature settings.
var fontFeatures = POFontFeaturesSettings()

/// The text tab objects that represent the paragraph’s tab stops.
var tabStops: [NSTextTab] = []

Expand All @@ -59,11 +62,11 @@ struct AttributedStringBuilder {
guard let typography else {
preconditionFailure("Typography must be set.")
}
let font = font(typography: typography, symbolicTraits: fontSymbolicTraits, maximumFontSize: maximumFontSize)
let font = font(typography: typography)
var attributes: [NSAttributedString.Key: Any] = [:]
let lineHeightMultiple = typography.lineHeight / typography.font.lineHeight
attributes[.font] = font
attributes[.baselineOffset] = baselineOffset(font: font, lineHeightMultiple: lineHeightMultiple)
attributes[.baselineOffset] = Self.baselineOffset(font: font, lineHeightMultiple: lineHeightMultiple)
attributes[.foregroundColor] = color
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.maximumLineHeight = font.lineHeight * lineHeightMultiple
Expand All @@ -80,7 +83,7 @@ struct AttributedStringBuilder {

// MARK: - Private Methods

private func baselineOffset(font: UIFont, lineHeightMultiple: CGFloat) -> CGFloat {
private static func baselineOffset(font: UIFont, lineHeightMultiple: CGFloat) -> CGFloat {
let offset = (font.lineHeight * lineHeightMultiple - font.capHeight) / 2 + font.descender
if #available(iOS 16, *) {
return offset
Expand All @@ -90,9 +93,7 @@ struct AttributedStringBuilder {
return offset < 0 ? offset : offset / 2
}

private func font(
typography: POTypography, symbolicTraits: UIFontDescriptor.SymbolicTraits, maximumFontSize: CGFloat?
) -> UIFont {
private func font(typography: POTypography) -> UIFont {
var font = typography.font
if let textStyle = typography.textStyle {
let uiSizeCategory = sizeCategory ?? UITraitCollection.current.preferredContentSizeCategory
Expand All @@ -104,10 +105,10 @@ struct AttributedStringBuilder {
if let maximumFontSize, font.pointSize > maximumFontSize {
font = font.withSize(maximumFontSize)
}
if !symbolicTraits.isEmpty, let descriptor = font.fontDescriptor.withSymbolicTraits(symbolicTraits) {
if !fontSymbolicTraits.isEmpty, let descriptor = font.fontDescriptor.withSymbolicTraits(fontSymbolicTraits) {
font = UIFont(descriptor: descriptor, size: 0)
}
return font
return font.addingFeatures(fontFeatures)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ final class AttributedStringMarkdownVisitor: MarkdownVisitor {
String(repeating: Constants.tab, count: level * 2 + 1) +
textList.marker(forItemNumber: textList.startingItemNumber + offset) +
Constants.tab
let attributedMarker = builder.with { $0.text = .plain(marker) }.build()
let attributedMarker = builder
.with { builder in
builder.fontFeatures.numberSpacing = .monospaced
builder.text = .plain(marker)
}
.build()
return [attributedMarker, attributedItem].joined()
}
.joined(separator: itemsSeparator)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ConstrainedRectangle.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 30.07.2024.
//

import SwiftUI

/// A rectangular shape that automatically adjusts its size and position to
/// satisfy minimum size requirement.
struct ConstrainedRectangle: Shape {

let minSize: CGSize

// MARK: - Shape

func path(in rect: CGRect) -> Path {
let adjustedRect = rect.insetBy(
dx: min(rect.width - minSize.width, 0) / 2, dy: min(rect.height - minSize.height, 0) / 2
)
return Path(roundedRect: adjustedRect, cornerSize: .zero)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Shape+ConstrainedRectangle.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 30.07.2024.
//

import SwiftUI

extension Shape where Self == ConstrainedRectangle {

/// A rectangle shape with a minimum size of 44x44 points, ensuring it meets the standard
/// clickable area requirements according to Human Interface Guidelines (HIG).
static var standardHittableRect: ConstrainedRectangle {
ConstrainedRectangle(minSize: CGSize(width: 44, height: 44))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// FontNumberSpacing.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 30.07.2024.
//

import CoreText

@_spi(PO)
public struct POFontNumberSpacing {

let rawValue: Int
}

extension POFontNumberSpacing {

/// Uniform width numbers, useful for displaying in columns.
public static let monospaced = Self(rawValue: kMonospacedNumbersSelector)

/// Numbers whose widths vary.
public static let proportional = Self(rawValue: kProportionalNumbersSelector)

/// Thin numerals.
public static let thirdWidth = Self(rawValue: kThirdWidthNumbersSelector)

/// Very thin numerals.
public static let quarterWidth = Self(rawValue: kQuarterWidthNumbersSelector)
}

extension POFontNumberSpacing: FontFeatureSetting {

var featureType: Int {
kNumberSpacingType
}

var featureSelector: Any {
rawValue
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// FontFeatureSetting.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 30.07.2024.
//

protocol FontFeatureSetting {

/// Indicates a general class of effect (e.g., ligatures).
var featureType: Int { get }

/// Indicates the specific effect (e.g., rare ligature).
var featureSelector: Any { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// POFontFeaturesSettings.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 30.07.2024.
//

@_spi(PO)
public struct POFontFeaturesSettings {

/// The number spacing feature type specifies a choice for the appearance of digits.
public var numberSpacing: POFontNumberSpacing = .proportional
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// UIFont+FontFeaturesSettings.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 30.07.2024.
//

import UIKit

extension UIFont {

func addingFeatures(_ settings: POFontFeaturesSettings) -> UIFont {
let settings = [
settings.numberSpacing
]
let rawSettings = settings.map { setting -> [UIFontDescriptor.FeatureKey: Any] in
[.featureIdentifier: setting.featureType, .typeIdentifier: setting.featureSelector]
}
let newDescriptor = fontDescriptor.addingAttributes(
[.featureSettings: rawSettings]
)
return UIFont(descriptor: newDescriptor, size: 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,4 @@ extension UIView {
UIView.performWithoutAnimation(actions)
}
}

/// Setting the value of this property to true hides the receiver and setting it to false shows
/// the receiver. The default value is false.
///
/// - Warning: UIKit has a known bug when changing `isHidden` on a subview of
/// UIStackView does not always work. It seems to be caused by fact that `isHidden`
/// is cumulative in `UIStackView`, so we have to ensure to not set it the same value
/// twice http://www.openradar.me/25087688
func setHidden(_ isHidden: Bool) {
if isHidden != self.isHidden {
self.isHidden = isHidden
}
}

/// Adds transition animation to receiver's layer. Method must be called inside animation block
/// to make sure that timing properties are properly set.
func addTransitionAnimation(type: CATransitionType = .fade, subtype: CATransitionSubtype? = nil) {
let transition = CATransition()
transition.type = type
transition.subtype = subtype
if let animation = layer.action(forKey: "backgroundColor") as? CAAnimation {
transition.duration = animation.duration
transition.timingFunction = animation.timingFunction
} else {
return
}
layer.add(transition, forKey: "transition")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// CheckboxButtonStyle.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 29.07.2024.
//

import SwiftUI

@available(iOS 14.0, *)
struct CheckboxButtonStyle: ButtonStyle {

/// Defines whether checkbox is selected.
let isSelected: Bool

/// Toggle style.
let style: POCheckboxToggleStyle

// MARK: - ButtonStyle

func makeBody(configuration: Configuration) -> some View {
ContentView { isInvalid, isEnabled, colorScheme in
let style = resolveStyle(isInvalid: isInvalid, isEnabled: isEnabled)
Label(
title: {
configuration.label
.textStyle(style.value)
.frame(maxWidth: .infinity, alignment: .leading)
},
icon: {
CheckboxView(isSelected: isSelected, style: style.checkmark)
}
)
.brightness(
brightnessAdjustment(isPressed: configuration.isPressed, colorScheme: colorScheme)
)
.contentShape(.standardHittableRect)
.animation(.default, value: isSelected)
.animation(.default, value: isEnabled)
}
.backport.geometryGroup()
}

// MARK: - Private Methods

private func resolveStyle(isInvalid: Bool, isEnabled: Bool) -> POCheckboxToggleStateStyle {
if !isEnabled {
return style.disabled
}
if isInvalid {
return style.error
}
if isSelected {
return style.selected
}
return style.normal
}

// MARK: - Private Methods

private func brightnessAdjustment(isPressed: Bool, colorScheme: ColorScheme) -> Double {
guard isPressed else {
return 0
}
return colorScheme == .dark ? -0.08 : 0.15 // Darken if color is light or brighten otherwise
}
}

// Environments are not propagated directly to ButtonStyle in any iOS before 14.
private struct ContentView<Content: View>: View {

@ViewBuilder
let content: (_ isInvalid: Bool, _ isEnabled: Bool, _ colorScheme: ColorScheme) -> Content

var body: some View {
content(isInvalid, isEnabled, colorScheme)
}

// MARK: - Private Properties

@Environment(\.isControlInvalid)
private var isInvalid

@Environment(\.isEnabled)
private var isEnabled

@Environment(\.colorScheme)
private var colorScheme
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// CheckboxView.swift
// ProcessOutCoreUI
//
// Created by Andrii Vysotskyi on 29.07.2024.
//

import SwiftUI

struct CheckboxView: View {

/// Determines whether the checkbox is selected or not.
let isSelected: Bool

/// Resolved style.
let style: POCheckboxToggleStateStyle.Checkmark

// MARK: - View

var body: some View {
CheckmarkShape()
.trim(from: 0, to: isSelected ? 1.0 : 0)
.stroke(
style.color,
style: StrokeStyle(lineWidth: style.width, lineCap: .round, lineJoin: .round)
)
.frame(width: Constants.checkmarkSize, height: Constants.checkmarkSize)
.frame(width: Constants.size, height: Constants.size)
.background(style.backgroundColor)
.border(style: style.border)
}

// MARK: - Private Nested Types

private enum Constants {
static let size: CGFloat = 22
static let checkmarkSize = Constants.size * 0.75
}
}
Loading

0 comments on commit 9a341ed

Please sign in to comment.