diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 0000000..5c542da
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1,2 @@
+[test]
+pathIgnorePatterns = ["reference/**"]
diff --git a/content/docs/ios/changelog.mdx b/content/docs/ios/changelog.mdx
index 340dd14..74fd9f8 100644
--- a/content/docs/ios/changelog.mdx
+++ b/content/docs/ios/changelog.mdx
@@ -3,6 +3,18 @@ title: "Changelog"
description: "Release notes for the Superwall iOS SDK"
---
+## 4.15.0
+
+### Enhancements
+
+- Adds support for custom store products. This allows you to purchase products that are on stores outside of the App Store using the `PurchaseController`.
+- Adds `formUnion` override when unioning sets of `Entitlement` objects.
+
+### Fixes
+
+- Fixes issue where test mode products had trial price data missing.
+- Fixed computed period prices (`weeklyPrice`, `dailyPrice`, `monthlyPrice`, `yearlyPrice`) displaying incorrectly rounded values on StoreKit 2 in production. For example, a £4.99/week product could show as £5.00/week. This was caused by Apple's `priceFormatStyle` applying storefront-specific rounding to computed values.
+
## 4.14.2
### Enhancements
diff --git a/content/docs/ios/index.mdx b/content/docs/ios/index.mdx
index 81b152c..e97b936 100644
--- a/content/docs/ios/index.mdx
+++ b/content/docs/ios/index.mdx
@@ -50,6 +50,6 @@ If you have feedback on any of our docs, please leave a rating and message at th
If you have any issues with the SDK, please [open an issue on GitHub](https://github.com/superwall/superwall-ios/issues).
diff --git a/content/docs/ios/sdk-reference/NonSubscriptionTransaction.mdx b/content/docs/ios/sdk-reference/NonSubscriptionTransaction.mdx
index 28380ce..e43d083 100644
--- a/content/docs/ios/sdk-reference/NonSubscriptionTransaction.mdx
+++ b/content/docs/ios/sdk-reference/NonSubscriptionTransaction.mdx
@@ -48,8 +48,8 @@ Provides details about one-time purchases in [`CustomerInfo`](/ios/sdk-reference
/>
-### Store values (4.11.0+)
-`appStore`, `stripe`, `paddle`, `playStore`, `superwall`, `other`.
+### Store values
+`appStore`, `stripe`, `paddle`, `playStore`, `superwall`, `custom`, `other`.
## Usage
diff --git a/content/docs/ios/sdk-reference/PurchaseController.mdx b/content/docs/ios/sdk-reference/PurchaseController.mdx
index 77f5ad9..7512434 100644
--- a/content/docs/ios/sdk-reference/PurchaseController.mdx
+++ b/content/docs/ios/sdk-reference/PurchaseController.mdx
@@ -12,7 +12,7 @@ When implementing PurchaseController, you must manually update [`subscriptionSta
## Purpose
-Use this protocol only if you want complete control over purchase handling, such as when using RevenueCat or other third-party purchase frameworks.
+Use this protocol only if you want complete control over purchase handling, such as when using RevenueCat, another third-party purchase framework, or your own external billing flow.
## Signature
```swift
@@ -30,7 +30,7 @@ public protocol PurchaseController: AnyObject {
type={{
purchase: {
type: "product: StoreProduct",
- description: "Called when user initiates purchasing. Implement your purchase logic here. Returns `PurchaseResult`.",
+ description: "Called when user initiates purchasing. The `StoreProduct` may wrap an App Store product or, for custom paywall products in 4.15.0+, an API-backed product that must be purchased by your external billing system. Returns `PurchaseResult`.",
required: true,
},
restorePurchases: {
@@ -48,6 +48,11 @@ public protocol PurchaseController: AnyObject {
When using a PurchaseController, you must also manage [`subscriptionStatus`](/ios/sdk-reference/subscriptionStatus) yourself.
+## Handling Products
+- For App Store-backed products, use `product.sk1Product` or `product.sk2Product`, or pass the product into your existing purchase SDK.
+- For custom products introduced in `4.15.0`, Superwall will call your purchase controller with a `StoreProduct` that has no StoreKit backing product. In that case, use `product.productIdentifier` in your external billing system and return the matching `PurchaseResult`.
+- Do not call `Superwall.shared.purchase(product)` for custom products. That helper is for StoreKit-backed purchases only.
+
## Usage
For implementation examples and detailed guidance, see [Using RevenueCat](/ios/guides/using-revenuecat).
diff --git a/content/docs/ios/sdk-reference/SubscriptionTransaction.mdx b/content/docs/ios/sdk-reference/SubscriptionTransaction.mdx
index 864619a..4059da8 100644
--- a/content/docs/ios/sdk-reference/SubscriptionTransaction.mdx
+++ b/content/docs/ios/sdk-reference/SubscriptionTransaction.mdx
@@ -75,14 +75,14 @@ Provides details about a single subscription transaction returned from [`Custome
/>
-### Offer types (4.11.0+)
+### Offer types
- `trial` — introductory offer.
- `code` — offer redeemed with a promo code.
- `promotional` — StoreKit promotional offer.
- `winback` — win-back offer (iOS 17.2+ only).
-### Store values (4.11.0+)
-`appStore`, `stripe`, `paddle`, `playStore`, `superwall`, `other`.
+### Store values
+`appStore`, `stripe`, `paddle`, `playStore`, `superwall`, `custom`, `other`.
## Usage
diff --git a/content/docs/ios/sdk-reference/customerInfo.mdx b/content/docs/ios/sdk-reference/customerInfo.mdx
index fd6d113..15824cb 100644
--- a/content/docs/ios/sdk-reference/customerInfo.mdx
+++ b/content/docs/ios/sdk-reference/customerInfo.mdx
@@ -53,10 +53,6 @@ public var customerInfo: CustomerInfo { get }
/>
-
-Starting in 4.11.0, transactions include offer metadata (`offerType`), the `subscriptionGroupId`, and the `store` (`ProductStore`) that fulfilled the purchase to help you audit cross-store sales.
-
-
## Returns / State
Returns a `CustomerInfo` object containing the latest customer purchase and subscription data. This object is immutable and does not update automatically—you must access the property again to get the latest data.
diff --git a/content/docs/ios/sdk-reference/index.mdx b/content/docs/ios/sdk-reference/index.mdx
index 0233cc1..967a39a 100644
--- a/content/docs/ios/sdk-reference/index.mdx
+++ b/content/docs/ios/sdk-reference/index.mdx
@@ -16,6 +16,6 @@ If you have feedback on any of our docs, please leave a rating and message at th
If you have any issues with the SDK, please [open an issue on GitHub](https://github.com/superwall/superwall-ios/issues).
diff --git a/content/shared/advanced-configuration.mdx b/content/shared/advanced-configuration.mdx
index 6bdb91b..a4bea90 100644
--- a/content/shared/advanced-configuration.mdx
+++ b/content/shared/advanced-configuration.mdx
@@ -16,6 +16,8 @@ By default, Superwall handles basic subscription-related logic for you:
However, if you want more control, you can pass in a `PurchaseController` when configuring the SDK via `configure(apiKey:purchaseController:options:)` and manually set `Superwall.shared.subscriptionStatus` to take over this responsibility.
+Starting in `4.15.0`, a `PurchaseController` is also how you handle custom products attached to Superwall paywalls. Those products are not purchased with StoreKit, so your controller must route them through your own billing system.
+
### Step 1: Creating a `PurchaseController`
A `PurchaseController` handles purchasing and restoring via protocol methods that you implement.
@@ -34,7 +36,9 @@ final class MyPurchaseController: PurchaseController {
// 1
func purchase(product: StoreProduct) async -> PurchaseResult {
- // Use StoreKit or some other SDK to purchase...
+ // Use StoreKit, RevenueCat, or your own billing system to purchase...
+ // If `product.sk1Product` and `product.sk2Product` are both nil,
+ // this is a custom product that should be handled externally.
// Send Superwall the result.
return .purchased // .cancelled, .pending, .failed(Error)
}
@@ -76,6 +80,10 @@ final class MyPurchaseController: PurchaseController {
// ----
// Purchase via StoreKit, RevenueCat, Qonversion or however
// you like and return a valid SWKPurchaseResult
+ //
+ // Custom products introduced in 4.15.0 are not backed by StoreKit.
+ // Handle those with your external billing system using
+ // `product.productIdentifier`.
completion(SWKPurchaseResultPurchased, nil);
}
@@ -101,6 +109,17 @@ final class MyPurchaseController: PurchaseController {
import StoreKit
import SuperwallKit
+enum PurchaseControllerError: LocalizedError {
+ case customProductNotHandled(productId: String)
+
+ var errorDescription: String? {
+ switch self {
+ case .customProductNotHandled(let productId):
+ return "Custom product \(productId) must be handled by your external billing system."
+ }
+ }
+}
+
final class SWPurchaseController: PurchaseController {
// MARK: Sync Subscription Status
/// Makes sure that Superwall knows the customer's subscription status by
@@ -129,11 +148,20 @@ final class SWPurchaseController: PurchaseController {
}
// MARK: Handle Purchases
- /// Makes a purchase with Superwall and returns its result after syncing subscription status. This gets called when
- /// someone tries to purchase a product on one of your paywalls.
+ /// For App Store-backed products, delegate to `Superwall.shared.purchase(...)`.
+ /// Custom products from Superwall paywalls must be handled in your
+ /// external billing system using `product.productIdentifier`.
func purchase(product: StoreProduct) async -> PurchaseResult {
- let result = await Superwall.shared.purchase(product)
- return result
+ if product.sk1Product != nil || product.sk2Product != nil {
+ return await Superwall.shared.purchase(product)
+ }
+
+ // Replace this with your own external billing implementation.
+ return .failed(
+ PurchaseControllerError.customProductNotHandled(
+ productId: product.productIdentifier
+ )
+ )
}
// MARK: Handle Restores
@@ -147,6 +175,10 @@ final class SWPurchaseController: PurchaseController {
```
+
+
+For custom products in `4.15.0+`, `StoreProduct` does not contain an App Store purchase target. If `sk1Product` and `sk2Product` are unavailable, purchase the product in your own billing system using `product.productIdentifier`, then return `.purchased`, `.pending`, `.cancelled`, or `.failed(error)` from your controller. Do not call `Superwall.shared.purchase(product)` for that case.
+
:::
:::android
```kotlin Kotlin