Displaying Products
AppActor organizes products into a three-level hierarchy: Offerings โ Packages โ Products. This structure lets you control what products to show without app updates.
Hierarchyโ
Rollout Workflow (Payment Mode)โ
This is the key benefit of offerings: your paywall catalog is managed from the backend, while the app always reads offerings.current.
- Offerings โ A container of all available offerings. Has a
currentoffering that the user should see. - Offering โ A named group of packages (e.g., "default", "holiday_sale", "premium")
- Package โ Pairs a type (monthly, annual, lifetime, etc.) with a product
- Product โ The actual StoreKit/Play Store product with pricing information
Offering Strategyโ
Use a small set of clearly named offerings:
| Offering Type | Purpose | Example ID |
|---|---|---|
| Primary | Your default paywall | default |
| Campaign | Time-boxed promotion | holiday_sale |
| Experiment | Test an alternative paywall mix | paywall_v2 |
Safety Noteโ
Changing which packages appear in an offering changes what users see, not what they already own. Access remains entitlement-driven based on purchased product IDs.
Fetching Offeringsโ
- iOS
- Android
- Flutter
- React Native
// Payment mode: fetch server-driven offerings
let offerings = try await AppActor.shared.offerings()
let current = offerings.current
print("Current offering: \(current?.displayName ?? "none")")
// Cached snapshot (no network call)
if let cached = AppActor.shared.cachedOfferings {
print("Cached offerings: \(cached.all.count)")
}
In payment mode, offerings() uses cache + conditional requests internally. You can call it safely on app launch and paywall open.
Accessing Packagesโ
Each offering provides convenience accessors for common package types:
- iOS
- Android
- Flutter
- React Native
let offerings = try await AppActor.shared.offerings()
guard let offering = offerings.current else { return }
// Convenience accessors
let monthly = offering.monthly // monthly
let annual = offering.annual // annual
let weekly = offering.weekly // weekly
let sixMonth = offering.sixMonth // sixMonth
let threeMonth = offering.threeMonth // threeMonth
let twoMonth = offering.twoMonth // twoMonth
let lifetime = offering.lifetime // lifetime
// All packages
for package in offering.packages {
print("\(package.packageType): \(package.localizedPrice)")
}
Package Typesโ
| Type | Description |
|---|---|
weekly | Weekly subscription |
monthly | Monthly subscription |
twoMonth | 2-month subscription |
threeMonth | 3-month subscription |
sixMonth | 6-month subscription |
annual | Annual subscription |
lifetime | One-time lifetime purchase |
custom | Custom package type |
Displaying Pricesโ
Products include localized pricing from the App Store:
- iOS
- Android
- Flutter
- React Native
let offerings = try await AppActor.shared.offerings()
guard let package = offerings.current?.monthly else { return }
// Localized price string (e.g., "$9.99", "โฌ8,99", "ยฅ980")
let price = package.localizedPrice
print("Name: \(package.productName)")
print("Description: \(package.productDescription)")
print("Type: \(package.productType)")
// Optional direct StoreKit access
if let product = package.storeProduct {
print("StoreKit price: \(product.displayPrice)")
}
SwiftUI Exampleโ
- iOS
- Android
- Flutter
- React Native
struct PaywallView: View {
@State private var offerings: AppActorOfferings?
@State private var isLoading = false
var body: some View {
VStack(spacing: 16) {
if let offering = offerings?.current {
Text(offering.displayName)
.font(.title)
ForEach(offering.packages) { package in
Button {
Task {
let result = try await AppActor.shared.purchase(package: package)
switch result {
case .success:
break
case .pendingServerValidation:
break
case .failedServerValidation:
break
case .cancelled, .pending:
break
}
}
} label: {
HStack {
Text(package.packageType.description)
Spacer()
Text(package.localizedPrice)
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
}
}
} else {
ProgressView("Loading offerings...")
}
}
.padding()
.task {
guard offerings == nil, !isLoading else { return }
isLoading = true
defer { isLoading = false }
offerings = try? await AppActor.shared.offerings()
}
}
}
Offerings in Local Modeโ
In local mode, offerings are defined in your configuration and products are fetched from StoreKit:
AppActor.configure(
projectKey: "myapp",
defaultOfferingID: "default", // Which offering to set as .current
offerings: {
AppActorOfferingDefinition("default", displayName: "Standard") {
AppActorPackageDefinition(.monthly, productID: "com.myapp.monthly")
AppActorPackageDefinition(.annual, productID: "com.myapp.annual")
}
},
entitlements: { /* ... */ }
)
Next Stepsโ
- Making Purchases โ Execute purchases and handle results
- Subscription Status โ Check entitlement state