This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Run
scripts/setup.shto initialize the development environment - installs SwiftLint and sets up git hooks - Install
xcodegenif not already installed:brew install xcodegen - Pre-commit hooks automatically run
xcodegenand update podspec version from Constants.swift
- Build:
- Xcode: Open
SuperwallKit.xcodeprojin Xcode (auto-generated fromproject.yml) - Command Line: Use
scripts/build.shto build the framework via xcodebuild (automatically runs xcodegen)
- Xcode: Open
- Tests:
- Xcode: Run tests using the
SuperwallKitTestsscheme in Xcode - Command Line: Use
scripts/test.shto run tests via xcodebuild (automatically runs xcodegen)
- Xcode: Run tests using the
- Linting: Use
scripts/lint.shto run SwiftLint with configuration from.swiftlint.yml - Project Generation: Run
xcodegento regenerate Xcode project fromproject.yml
- Swift Package Manager: Primary dependency management via
Package.swift - CocoaPods: Also supported via
SuperwallKit.podspec - Dependencies:
superscript-ios-nextat exact version 1.0.15 (slim binary-target distribution; replaces the legacySuperscript-iOSrepo whose committed xcframework bloated clones)
SuperwallKit is an iOS SDK for remote paywall configuration and A/B testing. The architecture follows a dependency injection pattern centered around DependencyContainer.
- Superwall.swift: Main SDK entry point and public API
- DependencyContainer: Central dependency injection container managing all core services
- ConfigManager: Handles remote configuration from Superwall dashboard
- PaywallManager: Manages paywall presentation and caching
- StoreKitManager: Handles App Store purchases and transactions
- IdentityManager: Manages user identity and attributes
- NetworkManager: API communication with Superwall backend
Sources/SuperwallKit/: Main SDK source codeSources/SuperwallKit/Paywall/: Paywall presentation, caching, and web view handlingSources/SuperwallKit/StoreKit/: Purchase flow and transaction managementSources/SuperwallKit/Config/: Remote configuration and feature flagsSources/SuperwallKit/Analytics/: Event tracking and attributionSources/SuperwallKit/Dependencies/: Dependency injection frameworkTests/SuperwallKitTests/: Unit tests with mocks and test utilities
- SDK configuration happens through
Superwall.configure() - Remote config is fetched and managed by
ConfigManager - Paywall requests go through
PaywallRequestManager->PaywallManager - Purchases are handled by
StoreKitManagerwith automatic retry logic - Events are tracked through the analytics system
- Xcode 26 / Swift 6.2 (
swift-tools-version:6.2), iOS 15, macOS 12, watchOS 8. tvOS isn't supported (WebKit, SafariServices and Superscript have no tvOS build). CI builds iOS (tests), Mac Catalyst and visionOS (build-platforms.yml). Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. - Don't add
#if compiler(...)checks for anything below 6.2; only gate APIs newer than that (e.g.compiler(>=6.3.2)for iOS 26.4 StoreKit APIs). - Don't add
@available/#availablechecks or fallback branches for iOS 15 or older (StoreKit 2, sheet detents,UIMenubuttons and friends are always there). - The package pins
swiftLanguageModes: [.v5]. Moving to Swift 6 language mode is a separate change. - Keep
Package.swift,SuperwallKit.podspecandproject.ymldeployment targets in sync.
- 2-space indentation (enforced by SwiftLint)
- Prefer
Loggeroverprintstatements (enforced by custom lint rule) - Force unwrapping allowed but discouraged
- Extensive use of protocol factories for dependency injection
- Uses both StoreKit 1 and StoreKit 2 APIs with abstraction layer
When bumping the version, update all three files:
Sources/SuperwallKit/Misc/Constants.swift(line 21)SuperwallKit.podspec(s.version)CHANGELOG.md(add new version entry at top)
- Follows semantic versioning
- Never use an
## Unreleasedheading inCHANGELOG.md. Unreleased changes always live under the next concrete version number (e.g.## 4.16.4). - To pick that number, compare the version on
developwith the version onmaster:- If develop's version is above master's, a release is already staged — add your entries to that existing top section. Do not bump again.
- If develop's version equals master's, start the next release: add a new version section and bump all three files together (patch/minor/major per the change).
- Mock objects follow naming pattern
*Mock.swift - Tests are organized to mirror source structure
- Uses combine publishers for async testing
- Core Data testing uses in-memory store
- This project uses Swift's Testing framework (not XCTest) for all unit tests.
- Always use the Testing framework when writing new tests.
- When making changes to the SDK, always write a unit test for the new functionality.
- Make sure to run the tests, ensuring they pass.
- ALWAYS run
scripts/lint.shafter making any code changes to check for formatting issues. - ALWAYS run
swiftlint --fixon any files that have linting violations. - Remember: No trailing whitespace is allowed on any lines.
- Update CHANGELOG.md for customer-facing changes: Include new API additions, bug fixes, and crash fixes. Focus on what the change does for developers, not internal implementation details. For example: "Added
setIntegrationAttribute()method to enable setting individual attribution provider IDs" or "Fixed crash when handling expired subscriptions".
When creating PRs, always include the checklist from .github/PULL_REQUEST_TEMPLATE.md:
- All unit tests pass.
- All UI tests pass.
- Demo project builds and runs on iOS.
- Demo project builds and runs on Mac Catalyst.
- Demo project builds and runs on visionOS.
- I added/updated tests or detailed why my change isn't tested.
- I added an entry to the
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes. - I have run
swiftlintin the main directory and fixed any issues. - I have updated the SDK documentation as well as the online docs.
- I have reviewed the contributing guide
DeviceIPCollector owns session-local, timestamped IP observations. Collection is for the
MMP and stays off unless the backend's attributionOptions.mmp.enabled is on; SuperwallKit's privacy
manifest doesn't declare it, so apps that turn the MMP on declare it themselves. Keep collection
independent of enrichment success and purchase/configuration latency, preserve each family
separately, and filter stale cached ipV4/ipV6 fields before exposing device attributes.
Do not add customer attributes or authentication headers to the public IP lookup requests.
The IPv6 lookup must stay on a connection that may only use IPv6 (NWConnection with the IP
version set): its host is reachable over both, so URLSession could quietly report IPv4.
AttributionFetcher refreshes IDFV/IDFA/ATT when setting integration attributes and
on app activation after an integration has been configured. Compare the complete
refreshed snapshot, not just provider IDs. Sync device values into user attributes
(the server integration router reads those) only when that snapshot changes, since
every sync costs a user_attributes event; explicit nulls clear stale IDs after
ATT revocation. ATT is serialized as a numeric string in integration attributes.
idfv, idfa and attStatus are SDK-owned user-attribute keys — document any
change to that set in the changelog and the online docs. Don't gate the IDFA on
the ATT status: identifierForAdvertisers already filters the all-zero id.