Skip to content

Conversation

@borisprimer
Copy link
Contributor

@borisprimer borisprimer commented Jun 30, 2025

🚀 iOS CheckoutComponents SDK Implementation

Overview

This PR introduces the new CheckoutComponents SDK for iOS - a modern, SwiftUI-native payment integration framework that provides exact API parity with our Android SDK while leveraging iOS platform
strengths.

RFC Reference

https://www.notion.so/primerio/RFC-iOS-CheckoutComponents-SDK-23bca65dc30e8163bf68dea650baf9d9

Key Features Implemented

🏗️ Core Architecture

  • Scope-based API pattern matching Android SDK exactly for cross-platform consistency
  • SwiftUI-native implementation requiring iOS 15+
  • Actor-based dependency injection for thread-safe dependency management
  • AsyncStream state management (no Combine dependency)
  • Clean Architecture with clear separation of concerns

💳 Payment Components

  • Complete card payment form with real-time validation
  • Payment method selection screen
  • Country selection component
  • Billing address collection (backend-controlled)
  • 3DS authentication support
  • Co-badged card handling

🎨 Customization Capabilities

  • Field-level customization - Override individual input fields
  • Section-level customization - Replace entire form sections
  • Screen-level customization - Provide custom screens
  • Theme support via design tokens (light/dark modes)

📱 Debug App Integration

  • Comprehensive showcase demonstrating all customization patterns
  • Example implementations for common use cases
  • Interactive demos showing runtime property changes

Technical Highlights

Performance Optimizations

  • Validation caching with 70-85% cache hit rate using NSCache
  • Lazy view creation for optimal memory usage
  • State-driven navigation without NavigationView conflicts

Modern iOS Patterns

  // SwiftUI entry point
  PrimerCheckout(
      clientToken: "your-token",
      settings: PrimerSettings(),
      scope: { checkoutScope in
          // Customize components
      }
  )

  // UIKit bridge for legacy support
  CheckoutComponentsPrimer.presentCheckout(
      from: viewController,
      clientToken: token,
      settings: settings
  )

File Changes Summary

  • 92 new SwiftUI component files in Sources/PrimerSDK/Classes/CheckoutComponents/
  • Design tokens for theming (light/dark modes)
  • Debug app examples showcasing all customization patterns
  • Comprehensive documentation (README.md with 857 lines)

Testing

Production Readiness

This implementation is feature-complete but requires additional work before production release as outlined in the RFC:

Dependencies

  • iOS 15.0+ minimum deployment target
  • No external dependencies (pure Swift/SwiftUI)
  • Reuses existing PrimerSDK infrastructure

Breaking Changes

None - This is a new module that doesn't affect existing Drop-in UI functionality.

Next Steps

  1. Code review and feedback incorporation
  2. Begin/continue production readiness tasks from this epic

}
}

/// The main entry point for CheckoutComponents, providing a familiar API similar to the main Primer class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wont be relevant to all readers of the docs - going forward the majority of consumers wont be migrating, or be familiar with the main Primer class, which Im not sure what it refers to anyway.

// MARK: - Properties

/// The currently active checkout view controller
private weak var activeCheckoutController: UIViewController?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this always be accessible, even in pure SwiftUI?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NQuinn27 , great observation! You're right to question this. Let me clarify the architecture:

The separation is intentional:

  • CheckoutComponentsPrimer.swift - UIKit-only wrapper that provides presentCheckout(from: UIViewController) methods. It handles UIKit presentation concerns like modal presentation, sheet detents,
    and UIViewController lifecycle. This class always creates a PrimerSwiftUIBridgeViewController internally to host the SwiftUI content.
  • PrimerCheckout.swift - Pure SwiftUI View that can be directly embedded in SwiftUI apps without any UIKit dependencies. SwiftUI developers can use this directly in their view hierarchies.

Why this separation exists:

  1. Different paradigms: UIKit apps need imperative APIs (presentCheckout()) while SwiftUI apps need declarative views (PrimerCheckout())
  2. Clean separation: SwiftUI apps shouldn't need to import/know about UIKit presentation logic
  3. Developer experience: Each platform gets idiomatic APIs - UIKit devs get familiar delegate patterns and view controller presentation, SwiftUI devs get Views and modifiers

To answer your specific question: activeCheckoutController will always be accessible even in "pure SwiftUI" because when SwiftUI apps use CheckoutComponentsPrimer (the UIKit wrapper), it creates a
bridge controller. However, pure SwiftUI apps should use PrimerCheckout directly and wouldn't interact with CheckoutComponentsPrimer at all.

The architecture is:

  • UIKit apps → CheckoutComponentsPrimer → PrimerSwiftUIBridgeViewController → PrimerCheckout
  • SwiftUI apps → PrimerCheckout directly

I've updated the comments to make this distinction clearer. The key insight is that these serve different audiences - one for UIKit integration, one for pure SwiftUI.

Does this make sense to you? Am I overkilling it?

)
}

/// Present the card form directly without payment method selection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting usecase, generally a safe bet to assume that PAYMENT_CARD is available, what will happen if the response from /configuration doesnt include it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a remnant of an AI hallucination, removed.

@@ -0,0 +1,180 @@
// swiftlint:disable all
import SwiftUI
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import SwiftUICore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got this error:
Module 'SwiftUICore' is an implementation detail of 'SwiftUI'; import 'SwiftUI' instead

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah they made it private in Xcode 26 👍

}

/// Compare 3DS options for equality
private func areThreeDsOptionsEqual(_ lhs: PrimerThreeDsOptions?, _ rhs: PrimerThreeDsOptions?) -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not conform these types to equatable instead of these functions?

// MARK: - Properties

@Published private var internalState = PrimerSelectCountryState()
private weak var cardFormScope: DefaultCardFormScope?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private below public vars

// Handle all font cases
switch font {
case .title2:
self.init(descriptor: UIFont.preferredFont(forTextStyle: .title2).fontDescriptor, size: 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a comment here why the size is zero


import Foundation

public struct ContainerDiagnostics: Sendable, CustomStringConvertible {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of engineering here. I just wonder whether the value of them warrants the complexity.

My instinct would be that if our consumers are expected to use container diagnostics, our SDK is too complex for them to use.

If this is a nice to have, it could probably be introduced further in the future to cut down the complexity of the work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea was to create a proper DI library that could be reused across all the iOS SDKs in Primer. It might be overkill for now, but this can be part of the PrimerCore lib when we start modularising the SDK even further, WDYT? @henry-cooper-primer

BorisNikolic and others added 4 commits August 28, 2025 12:43
# Conflicts:
#	Debug App/Podfile.lock
#	Sources/PrimerSDK/Classes/Error Handler/PrimerInternalError.swift
#	Sources/PrimerSDK/Classes/PCI/Checkout Components/PrimerRawCardDataTokenizationBuilder.swift
# Conflicts:
#	Sources/PrimerSDK/Classes/PCI/Tokenization View Models/FormsTokenizationViewModel/CardFormPaymentMethodTokenizationViewModel.swift
# Conflicts:
#	Debug App/Primer.io Debug App SPM.xcodeproj/project.pbxproj
#	Sources/PrimerSDK/Classes/Core/BIN Data/CardValidationService.swift
#	Sources/PrimerSDK/Classes/PCI/Checkout Components/PrimerRawCardDataTokenizationBuilder.swift
borisprimer pushed a commit that referenced this pull request Sep 23, 2025
borisprimer and others added 12 commits September 23, 2025 14:26
…n and API clarity (#1358)

* Update DEMO files as per PR review

* swiftlint --fix --format

* Apply PR review feedback for CheckoutComponents code quality

  Changes per Henry Cooper's review:
  - Remove redundant internal access modifiers (Swift defaults to internal)
  - Replace OR operators with array.contains() for better readability
  - Consolidate similar switch case statements
  - Use Swift 5.7+ guard let shorthand syntax
  - Remove obvious/redundant code comments ("Default implementation", etc.)

  Improves code maintainability and follows Swift best practices.

* Address additional PR review feedback from Henry

  Architecture improvements:
  - Extract SDK initialization logic into CheckoutSDKInitializer service
  - Reduce InternalCheckout view responsibilities (SOLID principles)

  Code quality improvements:
  - Make InternalCheckout properties private (proper encapsulation)
  - Fix CheckoutNavigator to use @StateObject (ObservableObject handling)
  - Use State(wrappedValue:) instead of deprecated initialValue syntax
  - Remove redundant init?(coder:) from PrimerSwiftUIBridgeViewController
  - Remove unnecessary @mainactor annotations from View computed properties
  - Remove obvious/redundant property comments in PrimerFieldStyling
  - Clean up "Default implementation" comments throughout

  Type fixes:
  - Fix CheckoutCustomization type reference in examples view

  These changes improve separation of concerns, follow SwiftUI best practices,
  and reduce code noise from unnecessary annotations and comments.

* Address PR review feedback for CheckoutComponents

  - Make PrimerThreeDsOptions, PrimerStripeOptions, and PrimerLocaleData conform to Equatable
  - Replace custom comparison functions with Equatable conformance in SettingsObserver
  - Reorder properties in DefaultSelectCountryScope to place private below public
  - Add comment explaining size 0 usage in UIFont extension for Dynamic Type preservation

* remove all "Created by Claude" instances

* remove print statements

* remove obsolete file

* Create NetworkingUtils utility to eliminate repeated networking boilerplate across demo files

* Update comments for more clarity

* Add new file to SPM project

* Refactor CheckoutComponents customization closures to use any View instead of AnyView

Implemented Henry's hybrid approach to solve Swift existential type limitations while improving developer experience

* Update code as per comments

* Extract the PrimerCardholderNameField styling values to private vars

* Remove all the performance metrics and monitoring code from ValidationService.swift

---------

Co-authored-by: Boris Nikolic <[email protected]>
Fix dark mode support by correcting token reference resolution in
DesignTokensManager and implementing proper SwiftUI reactive pattern.

Changes:
- Fix reference resolution bug in resolveFlattenedReferences
- Simplify DesignTokensManager using functional patterns
- Use @StateObject for automatic UI updates on color scheme changes
- Remove redundant manual state synchronization
…#1369)

* Update DEMO files as per PR review

* swiftlint --fix --format

* Apply PR review feedback for CheckoutComponents code quality

  Changes per Henry Cooper's review:
  - Remove redundant internal access modifiers (Swift defaults to internal)
  - Replace OR operators with array.contains() for better readability
  - Consolidate similar switch case statements
  - Use Swift 5.7+ guard let shorthand syntax
  - Remove obvious/redundant code comments ("Default implementation", etc.)

  Improves code maintainability and follows Swift best practices.

* Address additional PR review feedback from Henry

  Architecture improvements:
  - Extract SDK initialization logic into CheckoutSDKInitializer service
  - Reduce InternalCheckout view responsibilities (SOLID principles)

  Code quality improvements:
  - Make InternalCheckout properties private (proper encapsulation)
  - Fix CheckoutNavigator to use @StateObject (ObservableObject handling)
  - Use State(wrappedValue:) instead of deprecated initialValue syntax
  - Remove redundant init?(coder:) from PrimerSwiftUIBridgeViewController
  - Remove unnecessary @mainactor annotations from View computed properties
  - Remove obvious/redundant property comments in PrimerFieldStyling
  - Clean up "Default implementation" comments throughout

  Type fixes:
  - Fix CheckoutCustomization type reference in examples view

  These changes improve separation of concerns, follow SwiftUI best practices,
  and reduce code noise from unnecessary annotations and comments.

* Address PR review feedback for CheckoutComponents

  - Make PrimerThreeDsOptions, PrimerStripeOptions, and PrimerLocaleData conform to Equatable
  - Replace custom comparison functions with Equatable conformance in SettingsObserver
  - Reorder properties in DefaultSelectCountryScope to place private below public
  - Add comment explaining size 0 usage in UIFont extension for Dynamic Type preservation

* remove all "Created by Claude" instances

* remove print statements

* remove obsolete file

* Create NetworkingUtils utility to eliminate repeated networking boilerplate across demo files

* Update comments for more clarity

* Add new file to SPM project

* Refactor CheckoutComponents customization closures to use any View instead of AnyView

Implemented Henry's hybrid approach to solve Swift existential type limitations while improving developer experience

* Update code as per comments

* Remove all hardcoded strings

* Update mismatched strings and improve the Splash screen flow

* Reuse existing translated string

* Update the working branch with the latest from checkout-components

* Add Swift Format Pass

* Update as per comments

---------

Co-authored-by: Boris Nikolic <[email protected]>
Co-authored-by: Henry Cooper <[email protected]>
@borisprimer borisprimer force-pushed the bn/feature/checkout-components branch from 91253f6 to b3ae3a0 Compare October 13, 2025 14:11
BorisNikolic and others added 7 commits October 13, 2025 16:18
…#1374)

feat: Implement dynamic sheet height for UIKit CheckoutComponents

Implement dynamic sheet sizing for CheckoutComponents presented via
UIKit bridge controller. The sheet now automatically adjusts its
height based on SwiftUI content size while respecting system
constraints.

Key changes:
- Add custom detent for iOS 16+ with dynamic height calculation
- Implement KVO observer to detect SwiftUI content size changes
- Add sheet presentation configuration with size constraints
- Include country selection modal with sheet presentation
- Integrate design tokens manager for proper theme support

Co-authored-by: Boris Nikolic <[email protected]>
* Implement analytics for checkout components

* Complete analytics event metadata per documentation requirements

  Enhanced all CheckoutComponents analytics events to include required
  and optional metadata fields according to the event definitions spec:

  - Add userLocale (Locale.current.identifier) to all analytics events
  - Extract paymentId and paymentMethod from PrimerError.paymentFailed
    for PAYMENT_FAILURE events
  - Include paymentMethod context in card form analytics events
    (PAYMENT_DETAILS_ENTERED, PAYMENT_SUBMITTED, PAYMENT_PROCESSING_STARTED)
  - Add paymentMethod to PAYMENT_THREEDS event from token data
  - Add userLocale to PAYMENT_REDIRECT_TO_THIRD_PARTY event

  Changed files:
  - DefaultCheckoutScope.swift: Enhanced state tracking with metadata
    extraction helper for failure cases
  - CheckoutSDKInitializer.swift: Added userLocale to SDK init events
  - DefaultCardFormScope.swift: Added metadata to card form events
  - DefaultPaymentMethodSelectionScope.swift: Added userLocale
  - HeadlessRepositoryImpl.swift: Enhanced 3DS and redirect event metadata

  Note: PAYMENT_REATTEMPTED event not implemented as retry feature
  does not exist yet.

* Remove code duplication, improve DI pattern implementation, and add more tests

* Discriminated union refactoring

* Remove obsolete code

* Inject analytics config provider into checkout initializer

* Use types

* Update as per Henry's comments

* Address PR review comments

- Remove unnecessary metadata variable assignment in DefaultAnalyticsInteractor
- Simplify AnalyticsEventBufferTests to use default nil metadata value
- Replace UUIDGenerator with existing String.uuid extension
- Extract duplicate device type checking logic into private helper method in UIDeviceExtension
- Add proper assertions to AnalyticsEventServiceTests"

* Improve test coverage

---------

Co-authored-by: Boris Nikolic <[email protected]>
# Conflicts:
#	Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Models/PrimerHeadlessUniversalCheckoutInputElement.swift
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
9.9% Coverage on New Code (required ≥ 80%)
9.5% Duplication on New Code (required ≤ 3%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

6 participants