Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup
### Enhancements

- Adds support for "Test Mode", which allows you to simulate in-app purchases without involving StoreKit. Test Mode can be enabled through the Superwall dashboard by marking specific users as test store users, or activates automatically when a bundle ID mismatch is detected. When active, a configuration modal lets you select starting entitlements and override free trial availability. Purchases are simulated with a UI that lets users complete, abandon, or fail transactions, with all purchase events firing normally for end-to-end paywall testing.
- Adds Stripe checkout message handling for `stripe_checkout_start`, `stripe_checkout_submit`, `stripe_checkout_complete`, `stripe_checkout_fail`, and `stripe_checkout_abandon`.
- Adds SDK-side analytics tracking for Stripe checkout lifecycle events (`start`, `submit`, `complete`, `fail`) with `store` and `product_identifier` payload fields.

## 4.13.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,46 @@ enum InternalSuperwallEvent {
}
}

struct StripeCheckout: TrackableSuperwallEvent {
enum State {
case start
case submit
case complete
case fail
}
let state: State
let productId: String
let paywallInfo: PaywallInfo
let placementData: PlacementData?

var audienceFilterParams: [String: Any] {
return paywallInfo.audienceFilterParams()
}

var superwallEvent: SuperwallEvent {
switch state {
case .start:
return .stripeCheckoutStart(paywallInfo: paywallInfo)
case .submit:
return .stripeCheckoutSubmit(paywallInfo: paywallInfo)
case .complete:
return .stripeCheckoutComplete(paywallInfo: paywallInfo)
case .fail:
return .stripeCheckoutFail(paywallInfo: paywallInfo)
}
}

func getSuperwallParameters() async -> [String: Any] {
var params: [String: Any] = [
"is_triggered_from_event": placementData != nil,
"store": "STRIPE",
"product_identifier": productId
]
params += await paywallInfo.placementParams()
return params
}
}

enum ConfigCacheStatus: String {
case cached = "CACHED"
case notCached = "NOT_CACHED"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ public enum SuperwallEvent {
/// this won't be `nil`. However, it could be `nil` if you are using a ``PurchaseController``
/// and the transaction object couldn't be detected after you return `.purchased` in ``PurchaseController/purchase(product:)``.
case transactionComplete(
transaction: StoreTransaction?, product: StoreProduct, type: TransactionType, paywallInfo: PaywallInfo)
transaction: StoreTransaction?,
product: StoreProduct,
type: TransactionType,
paywallInfo: PaywallInfo
)

/// When the user successfully completes a transaction for a subscription product with no introductory offers.
case subscriptionStart(product: StoreProduct, paywallInfo: PaywallInfo)
Expand Down Expand Up @@ -259,6 +263,18 @@ public enum SuperwallEvent {
/// When paywall preloading completes.
case paywallPreloadComplete(paywallCount: Int)

/// When a Stripe checkout session starts.
case stripeCheckoutStart(paywallInfo: PaywallInfo)

/// When a Stripe checkout form is submitted.
case stripeCheckoutSubmit(paywallInfo: PaywallInfo)

/// When a Stripe checkout session completes.
case stripeCheckoutComplete(paywallInfo: PaywallInfo)

/// When a Stripe checkout session fails.
case stripeCheckoutFail(paywallInfo: PaywallInfo)

/// When the test mode modal is opened.
case testModeModalOpen

Expand Down Expand Up @@ -450,6 +466,14 @@ extension SuperwallEvent {
return .init(objcEvent: .paywallPreloadStart)
case .paywallPreloadComplete:
return .init(objcEvent: .paywallPreloadComplete)
case .stripeCheckoutStart:
return .init(objcEvent: .stripeCheckoutStart)
case .stripeCheckoutSubmit:
return .init(objcEvent: .stripeCheckoutSubmit)
case .stripeCheckoutComplete:
return .init(objcEvent: .stripeCheckoutComplete)
case .stripeCheckoutFail:
return .init(objcEvent: .stripeCheckoutFail)
case .testModeModalOpen:
return .init(objcEvent: .testModeModalOpen)
case .testModeModalClose:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ public enum SuperwallEventObjc: Int, CaseIterable {
/// When paywall preloading completes.
case paywallPreloadComplete

/// When a Stripe checkout session starts.
case stripeCheckoutStart

/// When a Stripe checkout form is submitted.
case stripeCheckoutSubmit

/// When a Stripe checkout session completes.
case stripeCheckoutComplete

/// When a Stripe checkout session fails.
case stripeCheckoutFail

/// When the test mode modal is opened.
case testModeModalOpen

Expand Down Expand Up @@ -395,6 +407,14 @@ public enum SuperwallEventObjc: Int, CaseIterable {
return "paywallPreload_start"
case .paywallPreloadComplete:
return "paywallPreload_complete"
case .stripeCheckoutStart:
return "stripeCheckout_start"
case .stripeCheckoutSubmit:
return "stripeCheckout_submit"
case .stripeCheckoutComplete:
return "stripeCheckout_complete"
case .stripeCheckoutFail:
return "stripeCheckout_fail"
case .testModeModalOpen:
return "testModeModal_open"
case .testModeModalClose:
Expand Down
12 changes: 8 additions & 4 deletions Sources/SuperwallKit/Dependencies/DependencyContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,10 +484,6 @@ extension DependencyContainer: ConfigManagerFactory {
deviceLocale: deviceInfo.locale
)
}

func makeConfigManager() -> ConfigManager? {
return configManager
}
}

// MARK: - StoreTransactionFactory
Expand Down Expand Up @@ -624,6 +620,14 @@ extension DependencyContainer: ConfigAttributesFactory {

// MARK: WebEntitlementFactory
extension DependencyContainer: WebEntitlementFactory {
/// Properties like `deviceHelper` are implicitly unwrapped optionals set after
/// init. Tests create a bare `DependencyContainer` without fully configuring it,
/// so background tasks in `WebEntitlementRedeemer` must check this before
/// accessing factory methods to avoid a nil dereference.
func makeIsContainerReady() -> Bool {
return configManager != nil
}

func makeDeviceId() -> String {
return "$SuperwallDevice:\(deviceHelper.vendorId)"
}
Expand Down
3 changes: 1 addition & 2 deletions Sources/SuperwallKit/Dependencies/FactoryProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ protocol ConfigManagerFactory: AnyObject {
withId paywallId: String?,
isDebuggerLaunched: Bool
) -> Paywall?

func makeConfigManager() -> ConfigManager?
}

protocol IdentityFactory: AnyObject {
Expand Down Expand Up @@ -166,6 +164,7 @@ protocol ConfigAttributesFactory: AnyObject {
}

protocol WebEntitlementFactory: AnyObject {
func makeIsContainerReady() -> Bool
func makeDeviceId() -> String
func makeAppUserId() -> String?
func makeAliasId() -> String
Expand Down
6 changes: 6 additions & 0 deletions Sources/SuperwallKit/Models/Web2App/RedeemRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ struct TransactionReceipt: Encodable {
let type = "IOS"
let jwsRepresentation: String
}

struct PollRedemptionResultRequest: Encodable {
let checkoutContextId: String
let deviceId: String
let appUserId: String?
}
14 changes: 13 additions & 1 deletion Sources/SuperwallKit/Models/Web2App/RedeemResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ import Foundation
/// An object return from the server acting as a source of truth for the redeemed codes
/// and web entitlements.
struct RedeemResponse: Codable {
enum PollRedemptionStatus: String, Codable {
case pending
case failed
case complete
}

var results: [RedemptionResult]
var customerInfo: CustomerInfo
var status: PollRedemptionStatus?

private enum CodingKeys: String, CodingKey {
case results = "codes"
case customerInfo
case status
}

var allCodes: Set<Redeemable> {
Expand All @@ -26,22 +34,26 @@ struct RedeemResponse: Codable {

init(
results: [RedemptionResult],
customerInfo: CustomerInfo
customerInfo: CustomerInfo,
status: PollRedemptionStatus? = nil
) {
self.results = results
self.customerInfo = customerInfo
self.status = status
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.results = try container.decode([RedemptionResult].self, forKey: .results)
self.customerInfo = try container.decodeIfPresent(CustomerInfo.self, forKey: .customerInfo) ?? .blank()
self.status = try container.decodeIfPresent(PollRedemptionStatus.self, forKey: .status)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(results, forKey: .results)
try container.encode(customerInfo, forKey: .customerInfo)
try container.encodeIfPresent(status, forKey: .status)
}
}

Expand Down
13 changes: 13 additions & 0 deletions Sources/SuperwallKit/Network/Endpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,19 @@ extension Endpoint where
method: .post
)
}

static func pollRedemptionResult(request: PollRedemptionResultRequest) -> Self {
let bodyData = try? JSONEncoder().encode(request)

return Endpoint(
components: Components(
host: .subscriptionsApi,
path: "checkout/session/poll-redemption-result",
bodyData: bodyData
),
method: .post
)
}
}

extension Endpoint where
Expand Down
7 changes: 7 additions & 0 deletions Sources/SuperwallKit/Network/Network.swift
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,13 @@ class Network {
)
}

func pollRedemptionResult(request: PollRedemptionResultRequest) async throws -> RedeemResponse {
return try await urlSession.request(
.pollRedemptionResult(request: request),
data: SuperwallRequestData(factory: factory)
)
}

func getEntitlements(
appUserId: String?,
deviceId: String
Expand Down
Loading