Skip to content

feature: Checkout Components#1212

Draft
borisprimer wants to merge 448 commits intomasterfrom
bn/feature/checkout-components
Draft

feature: Checkout Components#1212
borisprimer wants to merge 448 commits intomasterfrom
bn/feature/checkout-components

Conversation

@borisprimer
Copy link
Copy Markdown
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

@borisprimer borisprimer self-assigned this Jun 30, 2025
@borisprimer borisprimer requested review from a team as code owners June 30, 2025 22:33
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 30, 2025

Warnings
⚠️ > Pull Request size seems relatively large. If this Pull Request contains multiple changes, please split each into separate PR will helps faster, easier review.
⚠️ PR is classed as Work in Progress

Generated by 🚫 Danger Swift against 3505a00

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jul 1, 2025

@borisprimer borisprimer force-pushed the bn/feature/checkout-components branch 2 times, most recently from 1ae303a to 744fa40 Compare July 28, 2025 17:43
Comment thread package-lock.json Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This shouldnt be here, perhaps a rogue npm install from our friend Claude?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are these changes necessary?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As below with NetworkResponseFactory - are you intending to remove these logs?

@@ -75,6 +75,13 @@ extension PrimerTheme {
var darkHex: String?
var lightHex: String?

/// Convert to UIColor based on current appearance mode
var uiColor: UIColor? {
let isDarkMode = UIScreen.isDarkModeEnabled
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Open question - is this sufficient to determine Dark mode? In apps which allow users to manually set the theme, we may want to extract this to something overridable in PrimerSettings?
Doesnt need to be actioned here, but please log it if you agree

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for raising this. Looking at the usage, the uiColor property is only being used to convert server-provided theme colors (darkHex/lightHex/coloredHex) to UIColor based on the current appearance.

The current implementation uses UIScreen.main.traitCollection.userInterfaceStyle which reflects the system's appearance setting. For apps with manual theme overrides (e.g., "Always Dark" regardless of system setting, this could indeed select the wrong color variant.

Since this is a computed property on a data model that comes from the server, adding theme override support would require a more significant architectural change - we'd need to pass the app's theme preference through to this conversion point.

I'll log this as technical debt for future consideration. For now, the SDK will follow system appearance settings. Here is the Jira ticket.

@@ -1,6 +1,6 @@
import Foundation

internal enum PrimerPaymentMethodType: String, Codable, CaseIterable, Equatable, Hashable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you explain this change from internal->public. In what way should this enum be used by a merchant going forward?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This enum was made public to support the CheckoutComponents API. Merchants can now use it to access payment method scopes with compile-time safety and auto-completion:

  // Merchant usage example
  if let cardFormScope: DefaultCardFormScope = checkoutScope.getPaymentMethodScope(for: .paymentCard) {
      // Customize card form fields
      cardFormScope.cardNumberInput = { field in
          // Custom UI
      }
  }

Benefits for merchants:

  • Type safety: .paymentCard instead of string "PAYMENT_CARD"
  • Discoverability: Auto-completion shows all available payment methods
  • Compile-time checks: Typos caught at build time

Alternative (if we keep it internal):

We'd need to remove the enum-based method from PrimerCheckoutScope and merchants would use:

// Type-safe but less discoverable
checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self)

OR

  // Sstring-based (prone to typos)
  checkoutScope.getPaymentMethodScope(for: "PAYMENT_CARD")

The enum approach provides the best developer experience, though it does expose our payment method types as public API.

@@ -322,7 +322,8 @@ extension [CardNetwork]: LogReporter {
logger.warn(message: "Expected allowed networks to be present in client session")
return []
}
return networkStrings.compactMap { CardNetwork(rawValue: $0) }
let networks = networkStrings.compactMap { CardNetwork(rawValue: $0) }//.filter { $0 != .amex }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this change be here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nope, thanks.

}
}

/// Refactored validation method with reduced cyclomatic complexity.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment can be removed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you confirm this method is covered by Unit tests?


public var stringValue: String {
switch self {
// Existing cases
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Existing/New comments throughout arent relevant here (useful for review, not useful in the code going forward)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should all the logs in this file be commented out?

@NQuinn27
Copy link
Copy Markdown
Contributor

As a first pass I have reviewed all changes to /PrimerSDK code paths and added some comments @BorisNikolic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changes related to adding the CC demos.

Comment on lines +124 to +141
case .countryCode:
break
case .firstName:
break
case .lastName:
break
case .addressLine1:
break
case .addressLine2:
break
case .city:
break
case .state:
break
case .all:
break
case .email:
break
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I implemented the BillingAddress feature on the Headless example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

UIKit wrapper for the SwiftUI-based CheckoutComponents.

Here's how it works:

Architecture Overview

  1. SwiftUI Core: The actual CheckoutComponents implementation is in SwiftUI (PrimerCheckout.swift)
  2. Bridge Layer: PrimerSwiftUIBridgeViewController wraps SwiftUI views in a UIHostingController
  3. UIKit API: CheckoutComponentsPrimer provides a familiar UIKit-style API for iOS developers

Key Components

CheckoutComponentsPrimer.swift:342-395 creates and presents the SwiftUI content:
// Creates a bridge controller that embeds SwiftUI
let bridgeController = PrimerSwiftUIBridgeViewController.createForCheckoutComponents(
clientToken: clientToken,
settings: PrimerSettings.current,
diContainer: DIContainer.shared,
navigator: CheckoutNavigator(),
presentationContext: .direct,
onCompletion: { /* ... */ }
)

// Presents it modally like any UIViewController
viewController.present(bridgeController, animated: true)

PrimerSwiftUIBridgeViewController.swift:28-30 embeds SwiftUI:
init<Content: View>(swiftUIView: Content) {
self.hostingController = UIHostingController(rootView: AnyView(swiftUIView))
super.init()
}

Why This Design?

  1. Backward Compatibility: Many iOS apps still use UIKit primarily
  2. Familiar API: Matches the existing Primer SDK patterns (delegate-based)
  3. Easy Migration: Developers can adopt CheckoutComponents without rewriting their app in SwiftUI
  4. Modal Presentation: Works seamlessly with UIKit navigation patterns (there is some issues with dynamic height calculation, there is a ticket for it)

Usage Comparison

Pure SwiftUI approach:
PrimerCheckout(clientToken: token)
.onPaymentSuccess { result in /* ... */ }

UIKit wrapper approach:
CheckoutComponentsPrimer.shared.delegate = myDelegate
CheckoutComponentsPrimer.presentCheckout(
with: clientToken,
from: viewController
)

The wrapper handles all the complexity of embedding SwiftUI content into UIKit, managing sizing, and translating SwiftUI callbacks to UIKit delegate patterns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The changes related to CheckoutComponentsDelegate implementation should be revisited. I built this as a PoC to make sure everything works in UIKit, and left it there for the future. Because it is not defined how this should behave, or if we even should care about it (we may want to leave it to merchants to embed SwiftUI CC into their UIKit app the way they want), consider this part and related files as SPIKE. Let's talk about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file is related to this comment. I suggest skipping the review for now until we define what we want to do regarding UIKit support and how it should look.

borisprimer and others added 9 commits March 26, 2026 14:29
Three changes that together eliminate the deadlock:

1. Container.resolveSync: Task {} → Task.detached {} so the resolution
   task runs on the cooperative pool instead of inheriting @mainactor
   from the caller (which is blocked by the semaphore).

2. HeadlessRepositoryImpl.init: marked nonisolated since it only assigns
   stored properties and needs no actor-isolated work.

3. ComposableContainer: removed MainActor.run wrapper from the
   HeadlessRepository factory — no longer needed with nonisolated init,
   and it was causing the detached task to hop back to main.
Remove unnecessary await on same-actor calls, fix @preconcurrency import
for PassKit, replace deprecated primerHeadlessUniveraslCheckout typo call,
add @sendable to Factory closure, fix implicit coercion and var-to-let,
and clean up unused values.
Make PaymentMethodProtocol.createScope async, replacing resolveSync
(semaphore-based) with proper async DI resolution. Pre-load all payment
method scopes during checkout init so getPaymentMethodScope (public API)
stays synchronous via cache reads. Remove nonisolated init from 3
repository classes and @unchecked Sendable from 13 @mainactor classes.
# Conflicts:
#	.github/actions/sdk-tests/action.yml
- Make paymentMethodScopeCache internal on DefaultCheckoutScope so tests
  can pre-populate it, fixing async race where createView() was called
  before the background Task populated the cache
- Restore deprecated primerHeadlessUniveraslCheckoutUIDidDismissPaymentMethod
  call in PrimerDelegate dismiss methods for backward compatibility
Apply fixes from 3 rounds of code review (157 findings total):

Critical: Handle .checkoutComponents in PrimerDelegate, fix RetentionPolicyTests
shadow mock, fix Google Pay icon mapping, add KVC crash guard, retain card
payment handler, add card payment timeout, fix VoiceOver on card inputs,
add DesignTokens bounds check, custom Equatable for PrimerCardFormState.

High: Remove public from Internal types, fix DismissalMechanism encoding,
cancel vault timeout on success, log errors in catch blocks, resolve QRCode
interactor from DI, fix WebRedirect try? to try, remove dead registrations,
guard 500ms sleep, guard terminal dismissed state, fix analytics encoding,
wrap registry reset in DEBUG.

Medium/Low: Add Sendable to state structs, add enum doc comments, extract
PrimerFormFieldState to own file, add scope typealiases, replace UIAccessibility
with service, replace onTapGesture with Button, remove unused properties,
fix doc comments, replace Color.pink with design token.
…-components

# Conflicts:
#	Sources/PrimerSDK/Classes/Core/Analytics/AnalyticsEvent.swift
@borisprimer borisprimer marked this pull request as draft April 3, 2026 18:09
@borisprimer borisprimer force-pushed the bn/feature/checkout-components branch from 22316f2 to 6ff43c2 Compare April 6, 2026 11:57
borisprimer and others added 18 commits April 7, 2026 15:50
* feat: Implement ADYEN_KLARNA payment method in CheckoutComponents

Add Klarna via Adyen integration with payment option selection,
redirect flow, and polling for CheckoutComponents (iOS 15+).

- Add PrimerPaymentMethodType.adyenKlarna enum case
- Add AdyenKlarnaSessionInfo for tokenization
- Add API endpoint for fetching Klarna payment types
- Create AdyenKlarnaRepository with redirect+poll flow
- Create ProcessAdyenKlarnaPaymentInteractor
- Create PrimerAdyenKlarnaScope public protocol and state
- Create DefaultAdyenKlarnaScope with full state machine
- Create AdyenKlarnaScreen matching web SDK design
- Add accessibility identifiers for all UI elements
- Add localized strings in all 62 supported languages
- Register in PaymentMethodRegistry and DI container
- Add unit tests for interactor flow

Ticket: ACC-7020

* fix: Refactor nested closures in AdyenKlarnaRepositoryImpl

Replace 3-level nested closure in openDeepLink with @mainactor
async/await to resolve SonarCloud nesting violation.

* fix: Use @testable import in Debug App for SPM build

CustomPaymentSelectionDemo references DefaultCardFormScope which is
internal. Use @testable import PrimerSDK to access it in the Debug App.

* test: Add comprehensive tests for AdyenKlarna CheckoutComponents

Add tests for AdyenKlarnaPaymentMethod (12 tests), AdyenKlarnaRepository
(7 tests), and DefaultAdyenKlarnaScope (13 tests) to improve coverage
above 80% threshold for new code.

* Remove missing files and folders from the project

* test: Improve AdyenKlarnaRepositoryImpl test coverage

Add tests for tokenize flow (nil token, no required action, full success),
openWebAuthentication, and resumePayment after tokenize. Coverage now
covers init, fetchPaymentOptions, tokenize, openWebAuthentication,
resumePayment, and cancelPolling paths.

* fix: Localize Klarna payment option names in AdyenKlarnaScreen

Map raw API option names (klarna, klarna_account, klarna_paynow) to
localized display strings (Pay later, Pay over time, Pay now) matching
web SDK translations across all 57 supported languages.

* fix: Stabilize flaky tests with race-condition-safe assertions

Replace exact call count assertions (== 1) with relative assertions
(greaterThan countBeforeCall) in refreshVaultedPaymentMethods tests,
and increase sleep + use greaterThanOrEqual in selectCardNetwork test.
These tests were flaky due to async operations firing during scope init.
* feat: Add MOLLIE_GIFTCARD payment method type

Register Mollie Gift Card in PrimerPaymentMethodType enum so it is
recognized by the SDK. The actual payment flow is handled automatically
by the existing WebRedirect infrastructure when the backend returns
this type with implementationType WEB_REDIRECT.

* fix: Stabilize flaky tests with race-condition-safe assertions

Replace exact call count assertions (== 1) with relative assertions
(greaterThan countBeforeCall) in refreshVaultedPaymentMethods tests,
and increase sleep + use greaterThanOrEqual in selectCardNetwork test.
These tests were flaky due to async operations firing during scope init.

* fix: Use @testable import in Debug App for SPM build

CustomPaymentSelectionDemo references DefaultCardFormScope which is
internal. Use @testable import PrimerSDK to access it in the Debug App.
…#1689)

fix: Hide "choose other payment method" on error screen for single PM

When only one payment method is available, the error screen no longer
shows the "Choose other payment method" button since there are no
alternatives to choose from.

ACC-6849
…1694)

* fix: Make PaymentMethodRegistry.reset() available in Release builds

reset() was guarded by #if DEBUG, leaving stale payment method entries
in production when merchants re-initialize checkout with different API
configs. Now reset() runs unconditionally and is called at the start of
registerPaymentMethods() to clear previous registrations.

* fix: Reorder test registration to run after scope creation

DefaultCheckoutScope.init calls registerPaymentMethods() which now
calls reset(), clearing any test-registered payment methods. Move
test .register() calls to after scope creation so they aren't wiped.
The selectCountry computed property was lazily creating and caching a
DefaultSelectCountryScope on first access, mutating private state in a
getter. Replace with idiomatic lazy var for identical behavior without
the side effect.
* refactor: Break down DefaultCheckoutScope into focused types

Extract 3 concerns from DefaultCheckoutScope (582→484 lines):

- CheckoutNavigationState: standalone enum for navigation states
- CheckoutAnalyticsTracker: analytics event tracking and metadata
- VaultedPaymentMethodManager: vaulted payment method state management

Tests migrated to dedicated files (86 tests, 0 failures).

* style: Move onSelectionChanged above private(set) properties

Address review feedback to place higher-access stored property ahead of
private(set) ones per the member ordering convention.
CheckoutComponents test classes write into the global `DIContainer.shared`
but do not reset it between tests, so singleton registrations and
resolution state leak across tests. Under varying test-run orderings this
manifests as `circularDependency`, stale mock call counts, and tests
asserting against state left by a previous class. Four recent PRs
(#1685, #1686, #1691, #1705) have been flaking on this.

Changes:
- Add `ContainerTestHelpers.resetSharedContainer()` and call it from
  `setUp` and `tearDown` of the 12 test files that use `DIContainer.shared`.
- Add `ContainerTestHelpers.createSettledCheckoutScope()` — creates a
  `DefaultCheckoutScope` with a pre-wired test container and returns only
  after its async init leaves `.initializing`. Use in the three
  `DefaultPaymentMethodSelectionScope*Tests` classes so downstream
  `loadPaymentMethods()` no longer waits forever on a scope stuck in
  `.initializing`.
- Fix `test_onDismiss_setsNavigationStateToDismissed` race: build the SUT
  with `isInitScreenEnabled: false` so the async init task cannot
  overwrite `.dismissed` with `.loading`; drop the 500 ms sleep and the
  `|| == .loading` hedge — both `updateState` calls on the dismiss path
  are synchronous.
- Fix `test_deleteVaultedPaymentMethod_whenDeleteThrows_propagatesErrorWithoutRefresh`:
  capture a post-init baseline of `fetchVaultedPaymentMethodsCallCount`
  and assert the failed delete did not increment it, instead of asserting
  the absolute count is zero (the settled scope now legitimately refreshes
  once during init).
* fix: Pass correct paymentMethodType to QRCode interactor via Factory pattern

The QRCode interactor was registered in the DI container with an empty
string for paymentMethodType, causing all QR code payments to send ""
to the backend. Introduce QRCodePaymentInteractorFactory using the
existing Factory<Product, Params> protocol so the correct type is
passed at scope-creation time.

* style: Remove unused typealiases and @unchecked Sendable from QRCode factory
…er (#1705)

* fix: Propagate critical DI registration failures in ComposableContainer

configure() now throws and runs a production-level validation step that
resolves every critical dependency after registration, so a broken
container fails loudly at init instead of masking as silent resolve
failures at payment time.

- Split register helpers: criticalRegister (rethrows) for infrastructure
  and data repositories, guardedRegister (log-and-swallow) for optional
  payment method wiring
- Move DIContainer.setContainer after validation so a partially
  configured container is never published
- Add ComposableContainerTests for happy-path and publish-ordering
- Update existing DI/Settings tests to try await configure()

* refactor: Narrow validateCriticalDependencies to private

Addresses review feedback — no callers outside the type, so fileprivate
widens access beyond what is needed.
…onents (#1691)

* feat: Implement ADYEN_AFFIRM billing address redirect in CheckoutComponents

Add a new BillingAddressRedirect scope for payment methods that
require billing address collection before redirect (Affirm via Adyen).

New scope type:
- PrimerBillingAddressRedirectScope (public protocol)
- PrimerBillingAddressRedirectState (public state)
- DefaultBillingAddressRedirectScope (implementation)
- BillingAddressRedirectScreen (billing form + submit)
- BillingAddressRedirectPaymentMethod (registration)

Reuses existing validation rules (AddressRule, CityRule, StateRule,
PostalCodeRule) and ClientSessionActionsModule for billing address
submission. The billing address is sent to the backend before the
redirect to Affirm's hosted page.

Required fields: country, address line 1, postal code, city, state.
Address line 2 is optional.

* fix: Exclude ADYEN_AFFIRM from WebRedirect dynamic registration

ADYEN_AFFIRM has implementationType WEB_REDIRECT in the backend
but needs BillingAddressRedirect scope (with billing form). Without
this filter, WebRedirect would overwrite the dedicated registration
since the registry uses last-write-wins.

* chore: Remove ADYEN_AFFIRM WebRedirect exclusion

Platform PR primer-io/platform#7233 changes adyen_affirm.yml
implementation-type from WEB_REDIRECT to NATIVE_SDK, so the
webRedirect filter already excludes it — the dedicated exclusion
set becomes dead code.

* test: Add coverage for BillingAddressRedirect scope lifecycle

Cover start (idempotency), cancel, onBack (both contexts),
presentationContext, init with paymentMethod/surcharge,
validation edge cases, and submit happy + failure state
transitions. Raises DefaultBillingAddressRedirectScope new
coverage above 80%.
…CC-7135] (#1711)

* refactor: Decouple card form field views from DefaultCardFormScope [ACC-7135]

Introduce internal protocol CardFormFieldScopeInternal refining
PrimerCardFormScope with the FieldValidationStates-keyed validation hook.
Retype the 9 card form field components to accept the internal protocol,
dropping all 33 `as? DefaultCardFormScope` guards that previously silently
skipped validation for any scope other than DefaultCardFormScope
(including MockCardFormScope used in previews).

Trim the public PrimerCardFormScope protocol of two internal-only
validation methods (updateValidationState(cardNumber:cvv:expiry:cardholderName:)
and updateValidationStateIfNeeded(for:isValid:)) that were leftover plumbing
from an earlier pre-public-API iteration. Neither was documented in
API_REFERENCE.md.

Rename updateValidationState(_ field:) -> updateValidationState(keyPath:)
on DefaultCardFormScope+Validation for clarity at call sites.

Follow-ups (ACC-7171, ACC-7172, ACC-7173) filed separately to address
the remaining concrete-type couplings uncovered during the broader audit.

* fix: Interpolate keyPath in MockCardFormScope validation log [ACC-7135]

SonarCloud flagged the keyPath parameter as unused. Use it in the log
message for parity with the sibling updateValidationStateIfNeeded method,
making the preview log actually informative about which field was updated.

* refactor: Drop redundant keyPath: label on updateValidationState [ACC-7135]

Per review feedback — the strongly-typed WritableKeyPath parameter
self-documents at call sites, so the explicit label added nothing. Drop
the external label while keeping the internal name for readability in
the method body.
…-components

# Conflicts:
#	.cz.toml
#	BuildTools/.swiftformat
#	CHANGELOG.md
#	Debug App/Podfile.lock
#	PrimerSDK.podspec
#	Sources/PrimerSDK/Classes/Core/Analytics/AnalyticsEvent.swift
#	Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/Payment Method Managers/RawDataManager.swift
#	Sources/PrimerSDK/Classes/Data Models/PrimerConfiguration.swift
#	Sources/PrimerSDK/Classes/PCI/Services/API/Primer/PrimerAPI.swift
#	Sources/PrimerSDK/Classes/User Interface/TokenizationViewModels/PaymentMethodTokenizationViewModel+Logic.swift
#	Sources/PrimerSDK/Classes/version.swift
#	Tests/Klarna/PrimerHeadlessKlarnaComponentTests.swift
#	Tests/Utilities/Mocks/Services/MockAPIClient.swift
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 4, 2026

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.

7 participants