Skip to content

Instantly share code, notes, and snippets.

@joshdholtz
Last active March 29, 2024 23:27
Show Gist options
  • Save joshdholtz/48aa8be3d139381b5eee1c370f407fd8 to your computer and use it in GitHub Desktop.
Save joshdholtz/48aa8be3d139381b5eee1c370f407fd8 to your computer and use it in GitHub Desktop.
Super basic SwiftUI app (70 lines of code) with paywall using RevenueCat

Super basic SwiftUI with paywall using RevenueCat

I wanted to show how to make a SwiftUI app that can easily get updated customer info (when purchases are made) to update the user interface to show the newly unlockd content.

  • Listens for CustomerInfo updates using Purchases.shared.customerInfoStream (an AsyncSequence)
  • Updates a @StateObject that is passed as an environment object to children views
  • ContentView will...
    • Display paywall from current Offering if no active entitlements
    • Show a Text (unlocked content) if active entitlements
    • Will update automatically when the customer info environment object is updated

Demo

⚠️ This demo is using an account that is used for lots of sandbox testing so the two "lifetime" packages isn't a bug 😛 Just something I need to clean up 🤷‍♂️

RocketSim_Recording_iPhone_12_Pro_2022-06-23_10.53.54.mp4
import SwiftUI
import RevenueCat
struct Constants {
static let apiKey = "<your_api_key>" // Will look like: appl_bunchofotherstuffhere
static let entitlementName = "<your_entitlement_name>" // I use something like "pro"
}
@main
struct ATinySampleApp: App {
@StateObject private var revenueCatCustomerData = RevenueCatCustomerData()
init() {
// Configure Purchases
Purchases.logLevel = .debug
Purchases.configure(with: Configuration
.builder(withAPIKey: Constants.apiKey)
.with(usesStoreKit2IfAvailable: true)
.build())
}
var body: some Scene {
WindowGroup {
ContentView()
// Passes updated customer info to children views
.environmentObject(self.revenueCatCustomerData)
.task {
// Listens to AsyncSequence for customer info updates
for await customerInfo in Purchases.shared.customerInfoStream {
self.revenueCatCustomerData.customerInfo = customerInfo
self.revenueCatCustomerData.appUserID = Purchases.shared.appUserID
}
}
}
}
}
// Observable object that holds RevenueCat customer data
// Any updates to this object will trigger updates to SwiftUI views
class RevenueCatCustomerData: ObservableObject {
@Published var appUserID: String? = nil
@Published var customerInfo: RevenueCat.CustomerInfo? = nil
var isUnlocked: Bool {
return customerInfo?.entitlements.active[Constants.entitlementName] != nil
}
}
import SwiftUI
import RevenueCat
struct ContentView: View {
@EnvironmentObject var revenueCatCustomerData: RevenueCatCustomerData
@State var currentOffering: Offering?
var body: some View {
VStack {
if revenueCatCustomerData.isUnlocked {
// User has purchased a product with the entitlement and unlocked content
Text("Has Pro!")
} else if let offering = self.currentOffering {
// Iterate over packages in current offering
// Display buttons to purchase package
ForEach(offering.availablePackages) { package in
Button {
Task {
try await Purchases.shared.purchase(package: package)
}
} label: {
Text(package.storeProduct.localizedTitle)
.padding()
.border(Color.accentColor)
}
}
}
}.task {
do {
self.currentOffering = try await Purchases.shared.offerings().current
} catch {
// Handle
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment