Check subscription status
To decide whether to show paid content or a paywall, check the user's access level in their Xee profile. You do not check product IDs in your code: every product in Xee unlocks the premium access level.
Before you start
A profile learns about a subscription in two ways:
- From the SDK. When the app reports a purchase or a restore, Xee asks Apple about it with your app's In-App Purchase key and records the answer, so
premiumis on right after the purchase. This needs the key and nothing else. - From Apple's notifications. Apple sends signed App Store Server Notifications to Xee for everything that happens later while the app may be closed: renewals, expiry, cancellation, grace period and billing retry, and refunds. Without them, a renewal or a cancellation never reaches the profile.
Set up both:
- In-App Purchase key. In App Store Connect, go to Users and Access, Integrations, In-App Purchase and generate a key. Add it in Settings, Apple keys with its Issuer ID, Key ID and
.p8file, then choose it for the app in App settings, Apple keys, under In-App Purchase. With Claude connected,list_appsshowsin_app_purchase_key: trueonce it is set. - Notification URL. Open App settings, iOS SDK in the Xee dashboard and copy the Notification URL. In App Store Connect, open your app, App Information, App Store Server Notifications, paste the URL for both Production and Sandbox, and choose Version 2.
The bundle ID in App settings, iOS SDK must match your app: Xee rejects notifications, and answers from Apple, for any other bundle.
Get the profile
Call Xee.getProfile() when you need the current state, for example at launch or before opening a paid screen. It asks the server each time. If the server cannot be reached, it returns the cached profile instead.
do {
let profile = try await Xee.getProfile()
if profile.accessLevels["premium"]?.isActive ?? false {
// unlock paid features
}
} catch {
// error is a XeeError
}accessLevels is keyed by access level ID. Xee has one, premium, and a profile with no access has no entry for it, so treat a missing entry as no access.
What an access level tells you
profile.accessLevels["premium"] is a XeeProfile.AccessLevel. The fields Xee fills:
| Property | Meaning |
|---|---|
isActive | Whether the user has access now. Check this one to unlock content. |
vendorProductId | The App Store product that gave the access. |
activatedAt | When Apple signed the notification or the transaction behind this state. |
expiresAt | When access ends, if Apple gave an end date. |
willRenew | Whether the subscription will renew. false once the user turns off auto-renew. |
unsubscribedAt | Set when auto-renew is off. |
isInGracePeriod | Whether Apple is still retrying a failed payment while access stays on. |
billingIssueDetectedAt | Set when Apple reported a billing problem. |
isRefund | Whether the purchase was refunded or revoked. |
A user who turns off auto-renew keeps isActive true until expiresAt. The profile also has subscriptions, keyed by product ID, with the same kind of detail per product, plus isSandbox. It has an entry for every subscription Xee has recorded for the profile, expired ones included, so a missing key means Xee has no record of that product for this user. profileId and customerUserId identify the profile.
Listen for profile updates
To keep the latest profile without asking for it, set a delegate. The SDK calls didLoadLatestProfile(_:) whenever it loads a newer profile, including after a purchase and on its regular refresh while the app is open (about once a minute). A renewal, a cancellation or a refund reaches the profile through Apple's notification, and a later refresh brings it to the app.
import Xee
final class SubscriptionManager: XeeDelegate {
func didLoadLatestProfile(_ profile: XeeProfile) {
let hasAccess = profile.accessLevels["premium"]?.isActive ?? false
// store hasAccess and update your UI
}
}
// After Xee.activate has finished:
Xee.delegate = subscriptionManagerXeeDelegate requires a class that is Sendable. Xee.delegate holds it strongly, so you can set it once and leave it. The method runs on the main queue unless you set another with .with(callbackDispatchQueue:); see Install & configure iOS SDK.
Decide what to show
Check access where the decision is made: at launch, when the user opens a paid section, or before you show paid content. If there is no access, show your paywall; see Get a paywall and its values.
func hasPremium() async -> Bool {
do {
let profile = try await Xee.getProfile()
return profile.accessLevels["premium"]?.isActive ?? false
} catch {
return false
}
}
// In your view:
.task {
if !(await hasPremium()) {
showPaywall = true
}
}The cached profile
The SDK keeps the last profile it loaded on the device. getProfile() falls back to it when the server cannot be reached, and the delegate receives it at launch. You cannot read the cache directly; it is refreshed whenever the SDK loads the profile from the server.
Next steps
- Identify users so purchases are linked to their profile.
- Test purchases in the sandbox to see a profile change end to end.