Skip to content
Draft
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 @@ -7,20 +7,55 @@
//

@_spi(STP) import StripeCore
import StripePaymentSheet
@_spi(AddressViewControllerSeparatorStylePreview) import StripePaymentSheet
import SwiftUI

@available(iOS 15.0, *)
public struct AddressElementExampleView: View {
@State private var showingAddressSheet = false
@State private var collectedAddress: AddressElement.AddressDetails?
@State private var selectedSeparatorStyle: SeparatorStyleOption = .divider

enum SeparatorStyleOption: String, CaseIterable, Identifiable {
case divider = "Divider Lines"
case spacing8 = "Spacing (8pt)"
case spacing12 = "Spacing (12pt)"
case spacing16 = "Spacing (16pt)"
case spacing20 = "Spacing (20pt)"

var id: String { rawValue }

var separatorStyle: AddressViewController.SeparatorDisplayStyle {
switch self {
case .divider:
return .divider
case .spacing8:
return .spacing(8.0)
case .spacing12:
return .spacing(12.0)
case .spacing16:
return .spacing(16.0)
case .spacing20:
return .spacing(20.0)
}
}
}

private func makeConfiguration() -> AddressElement.Configuration {
STPAPIClient.shared.publishableKey = "pk_test"

var config = AddressElement.Configuration()
config.allowedCountries = ["US", "CA", "GB", "AU"]
config.buttonTitle = "Save Address"
config.separatorStyle = selectedSeparatorStyle.separatorStyle

// TODO
if #available(iOS 26.0, *) {
config.appearance.applyLiquidGlass()
}
config.appearance.colors.componentBackground = .lightGray
config.appearance.colors.componentBorder = .lightGray
config.appearance.colors.selectedComponentBorder = .black

// Pre-populate with existing address if available
if let address = collectedAddress {
Expand All @@ -39,6 +74,23 @@ public struct AddressElementExampleView: View {
public var body: some View {
NavigationView {
VStack(spacing: 20) {
// Separator style picker
HStack(alignment: .center, spacing: 0) {
Text("Separator Style:")
.font(.subheadline)
.foregroundColor(.secondary)

Spacer(minLength: 8)

Picker("Separator Style", selection: $selectedSeparatorStyle) {
ForEach(SeparatorStyleOption.allCases) { option in
Text(option.rawValue).tag(option)
}
}
.pickerStyle(.menu)
}
.padding(.horizontal)

// Example: Using .sheet with isPresented
Button("Collect Address") {
showingAddressSheet = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import Foundation
@_spi(STP) import StripeUICore

public extension AddressViewController {
/// The style used for separators between address fields.
/// - See also: `StripeUICore.SeparatorDisplayStyle`
typealias SeparatorDisplayStyle = StripeUICore.SeparatorDisplayStyle

/// The customer data collected by `AddressViewController`
struct AddressDetails {
/// The customer's address
Expand Down Expand Up @@ -75,6 +79,7 @@ public extension AddressViewController {
additionalFields: AddressViewController.Configuration.AdditionalFields = .init(),
allowedCountries: [String] = [],
appearance: PaymentSheet.Appearance = PaymentSheet.Appearance.default,
separatorStyle: AddressViewController.SeparatorDisplayStyle = .divider,
buttonTitle: String? = nil,
title: String? = nil
) {
Expand All @@ -84,6 +89,7 @@ public extension AddressViewController {
self.appearance = appearance
self.buttonTitle = buttonTitle ?? .Localized.save_address
self.title = title ?? .Localized.shipping_address
self.separatorStyle = separatorStyle
}

/// Configuration related to the collection of additional fields beyond the physical address.
Expand Down Expand Up @@ -167,5 +173,8 @@ public extension AddressViewController {
/// When provided, shows a checkbox that allows customers to populate shipping fields with billing address data.
@_spi(STP) public var billingAddress: DefaultAddressDetails?

/// The style to use for separators between address fields.
/// Defaults to `.divider` which shows visible divider lines between fields.
@_spi(AddressViewControllerSeparatorStylePreview) public var separatorStyle: AddressViewController.SeparatorDisplayStyle = .divider
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ extension AddressViewController {
defaults: .init(from: defaultValues),
collectionMode: showFullForm ? .all(autocompletableCountries: configuration.autocompleteCountries) : .autoCompletable,
additionalFields: .init(from: configuration.additionalFields),
separatorStyle: configuration.separatorStyle,
theme: configuration.appearance.asElementsTheme,
presentAutoComplete: { [weak self] in
self?.presentAutocomplete()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,7 @@ extension PaymentSheet.Appearance {
colors.componentBackground = self.colors.componentBackground
colors.bodyText = self.colors.text
colors.border = self.colors.componentBorder
colors.selectedBorder = self.colors.selectedComponentBorder
colors.divider = self.colors.componentDivider
colors.textFieldText = self.colors.componentText
colors.secondaryText = self.colors.textSecondary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,14 @@ extension STPFormView {
let rows: [[STPFormInput]]
let title: String?
let accessoryButton: UIButton?
let separatorStyle: SeparatorDisplayStyle

init(rows: [[STPFormInput]], title: String? = nil, accessoryButton: UIButton? = nil, separatorStyle: SeparatorDisplayStyle = .divider) {
self.rows = rows
self.title = title
self.accessoryButton = accessoryButton
self.separatorStyle = separatorStyle
}

func contains(_ input: STPInputTextField) -> Bool {
for row in rows.compactMap({ $0 as? [STPInputTextField] }) {
Expand Down Expand Up @@ -571,9 +579,8 @@ extension STPFormView {
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = STPFormView.borderWidth
stackView.applySeparatorDisplayStyle(section.separatorStyle, defaultColor: InputFormColors.outlineColor)
stackView.borderWidth = STPFormView.borderWidth
stackView.separatorColor = InputFormColors.outlineColor
return stackView
}

Expand All @@ -584,9 +591,8 @@ extension STPFormView {

stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = STPFormView.borderWidth
stackView.applySeparatorDisplayStyle(section.separatorStyle, defaultColor: InputFormColors.outlineColor)
stackView.borderWidth = STPFormView.borderWidth
stackView.separatorColor = InputFormColors.outlineColor

stackView.drawBorder = true
stackView.borderCornerRadius = STPFormView.cornerRadius
Expand Down
18 changes: 18 additions & 0 deletions StripeUICore/StripeUICore/Source/Elements/ElementsUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ import UIKit
}
}

/// Defines how separators between elements are displayed
public enum SeparatorDisplayStyle {
/// Use visible divider lines.
case divider
/// Use spacing without visible divider lines.
case spacing(CGFloat)

var stackSpacing: CGFloat {
switch self {
case .divider:
return 12
case .spacing(let amount):
return amount
}
}
}

/// Describes the appearance of an Element
/// A superset of `StripePaymentSheet.PaymentSheetAppearance`. This exists b/c we can't see that type from `StripeUICore`, and we don't want to the public StripePaymentSheet API to be a typealias of this.
@_spi(STP) public struct ElementsAppearance {
Expand Down Expand Up @@ -122,6 +139,7 @@ import UIKit
public var componentBackground = ElementsUI.backgroundColor
public var disabledBackground = ElementsUI.disabledBackgroundColor
public var border = ElementsUI.fieldBorderColor
public var selectedBorder: UIColor?
public var divider = ElementsUI.fieldBorderColor
public var textFieldText = UIColor.label
public var bodyText = UIColor.label
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,29 @@ import UIKit
}

// MARK: Element protocol
public let elements: [Element]
public var elements: [Element] {
addressSections + [sameAsCheckbox]
}

public weak var delegate: ElementDelegate?

public lazy var view: UIView = {
let vStack = UIStackView(arrangedSubviews: [addressSection.view, sameAsCheckbox.view].compactMap { $0 })
let vStack = UIStackView()
vStack.axis = .vertical
vStack.spacing = 16
vStack.spacing = separatorStyle.stackSpacing
return vStack
}()

// MARK: Elements
let addressSection: SectionElement
var addressSections: [SectionElement] = []

/// For backward compatibility - returns a section containing all address fields
@available(*, deprecated, message: "Access addressSections directly")
var addressSection: SectionElement {
let allElements = addressSections.flatMap(\.elements)
return SectionElement(elements: allElements, separatorStyle: separatorStyle, theme: theme)
}

public let name: TextFieldElement?
public let phone: PhoneNumberElement?
public let email: TextFieldElement?
Expand Down Expand Up @@ -156,11 +168,37 @@ import UIKit

public let countryCodes: [String]
let addressSpecProvider: AddressSpecProvider
let separatorStyle: SeparatorDisplayStyle
let theme: ElementsAppearance
private(set) var defaults: AddressDetails
@_spi(STP) public var didTapAutocompleteButton: () -> Void
public var didUpdate: DidUpdateAddress?

/// The selection behavior used for individual field sections in `.spacing` mode.
/// Highlights the border of the focused field using the theme's selected border color.
private var spacingSelectionBehavior: SelectionBehavior {
let borderColor = theme.colors.selectedBorder ?? theme.colors.primary
// Determine the correct corner radius, accounting for Liquid Glass which uses 26pt corners
let cornerRadius: CGFloat
if let themeCornerRadius = theme.cornerRadius {
cornerRadius = themeCornerRadius
} else if LiquidGlassDetector.isEnabledInMerchantApp {
cornerRadius = 26.0
} else {
cornerRadius = ElementsUI.defaultCornerRadius
}
let params = UISpringTimingParameters(mass: 1.0, dampingRatio: 0.93, frequencyResponse: 0.22)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
animator.isInterruptible = true
let configuration = HighlightBorderConfiguration(
width: 2.0,
cornerRadius: cornerRadius,
color: borderColor,
animator: animator
)
return .highlightBorder(configuration: configuration)
}

// MARK: - Implementation
/**
Creates an address section with a country dropdown populated from the given list of countryCodes.
Expand All @@ -180,6 +218,7 @@ import UIKit
defaults: AddressDetails = .empty,
collectionMode: CollectionMode = .all(),
additionalFields: AdditionalFields = .init(),
separatorStyle: SeparatorDisplayStyle = .divider,
theme: ElementsAppearance = .default,
presentAutoComplete: @escaping () -> Void = { }
) {
Expand All @@ -196,6 +235,7 @@ import UIKit
)
self.defaults = defaults
self.addressSpecProvider = addressSpecProvider
self.separatorStyle = separatorStyle
self.theme = theme
self.didTapAutocompleteButton = presentAutoComplete

Expand Down Expand Up @@ -238,9 +278,8 @@ import UIKit
} else {
sameAsCheckbox.view.isHidden = true
}
addressSection = SectionElement(title: title, elements: [], theme: theme)
elements = ([addressSection, sameAsCheckbox] as [Element?]).compactMap { $0 }
elements.forEach { $0.delegate = self }

sameAsCheckbox.delegate = self

self.updateAddressFields(
for: initialCountry,
Expand Down Expand Up @@ -382,7 +421,32 @@ import UIKit
initialElements.append(autoCompleteLine)
let emailElement: [Element?] = [email]
let phoneElement: [Element?] = [phone]
addressSection.elements = (emailElement + phoneElement + initialElements + addressFields).compactMap { $0 }

// Create sections based on separator style:
// - .divider: Single section with all fields and divider lines between them
// - .spacing: Each field in its own bordered section
let allFields = (emailElement + phoneElement + initialElements + addressFields).compactMap { $0 }

addressSections = switch separatorStyle {
case .divider:
[SectionElement(elements: allFields, separatorStyle: .divider, theme: theme)]
case .spacing:
allFields.map { SectionElement(elements: [$0], selectionBehavior: spacingSelectionBehavior, theme: theme) }
}

addressSections.forEach { $0.delegate = self }
updateViewWithSections()
}

private func updateViewWithSections() {
guard let stackView = view as? UIStackView else { return }

stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
addressSections.forEach { stackView.addArrangedSubview($0.view) }

if !sameAsCheckbox.view.isHidden {
stackView.addArrangedSubview(sameAsCheckbox.view)
}
}

/// Returns `true` iff all **displayed** address fields match the given `address`, treating `nil` and "" as equal.
Expand Down
Loading
Loading