Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
7fff295913
commit
d62cc6e88e
@ -606,15 +606,25 @@ For apps with both onboarding and in-app paywalls, handle both cases:
|
||||
|
||||
When a user completes a purchase, RevenueCat updates `Purchases.shared.cachedCustomerInfo` with the new subscription status. However, SwiftUI's `@Observable` macro doesn't automatically detect changes to this shared singleton.
|
||||
|
||||
This causes a common bug: **after purchasing from a paywall triggered outside of onboarding (e.g., from Settings), the UI doesn't reflect the new premium status** even though the purchase succeeded.
|
||||
**This affects the in-app purchase flow (Use Case 2), not onboarding (Use Case 1):**
|
||||
|
||||
### Why It Happens
|
||||
| Use Case | Affected? | Why |
|
||||
|----------|-----------|-----|
|
||||
| Onboarding | No | Success callback explicitly completes onboarding and transitions to main app |
|
||||
| In-App (Settings) | **Yes** | UI must detect the change to show unlocked features |
|
||||
|
||||
1. Your `SettingsViewModel` (or similar) has a `PremiumManager` instance
|
||||
2. `isPremiumUnlocked` is computed from `premiumManager.isPremiumUnlocked`
|
||||
3. `PremiumManager.isPremiumUnlocked` reads from `Purchases.shared.cachedCustomerInfo`
|
||||
4. After purchase, the cached customer info IS updated
|
||||
5. But SwiftUI doesn't know to re-render because no `@Observable` property changed
|
||||
### Why In-App Purchases Don't Update the UI
|
||||
|
||||
1. User opens Settings (which has a `SettingsViewModel` with its own `PremiumManager`)
|
||||
2. User taps a premium feature, triggering the paywall
|
||||
3. User completes purchase successfully
|
||||
4. Paywall dismisses, returning to Settings
|
||||
5. `isPremiumUnlocked` still returns `false` in the UI even though purchase succeeded
|
||||
|
||||
**Technical reason:**
|
||||
- `isPremiumUnlocked` reads from `Purchases.shared.cachedCustomerInfo`
|
||||
- The cached customer info IS updated after purchase
|
||||
- But SwiftUI doesn't re-render because no `@Observable` property changed
|
||||
|
||||
### Solution: Refresh Token Pattern
|
||||
|
||||
@ -646,28 +656,65 @@ final class SettingsViewModel {
|
||||
|
||||
### Triggering the Refresh
|
||||
|
||||
Call `refreshPremiumStatus()` when the paywall sheet dismisses:
|
||||
Always call `refreshPremiumStatus()` when the paywall sheet dismisses:
|
||||
|
||||
```swift
|
||||
struct ContentView: View {
|
||||
.sheet(isPresented: $showPaywall, onDismiss: {
|
||||
// Force UI to re-check premium status after paywall closes
|
||||
// This handles both successful purchases and user cancellations
|
||||
settings.refreshPremiumStatus()
|
||||
}) {
|
||||
PaywallPresenter()
|
||||
}
|
||||
```
|
||||
|
||||
**Why use `onDismiss` instead of the success callback?**
|
||||
- `onDismiss` is called for all dismissal reasons (purchase, cancel, swipe down)
|
||||
- It ensures the UI always reflects the current state
|
||||
- Works for both onboarding and in-app contexts
|
||||
|
||||
### Complete Integration Example
|
||||
|
||||
Handling both onboarding and in-app purchase flows:
|
||||
|
||||
```swift
|
||||
struct RootView: View {
|
||||
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
||||
@State private var settings = SettingsViewModel()
|
||||
@State private var showPaywall = false
|
||||
|
||||
var body: some View {
|
||||
// ... your content ...
|
||||
Group {
|
||||
if hasCompletedOnboarding {
|
||||
MainAppView(settings: settings, showPaywall: $showPaywall)
|
||||
} else {
|
||||
OnboardingView(showPaywall: $showPaywall, onComplete: completeOnboarding)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showPaywall, onDismiss: {
|
||||
// Force UI to re-check premium status after paywall closes
|
||||
// ALWAYS refresh after paywall closes (for in-app purchases)
|
||||
settings.refreshPremiumStatus()
|
||||
}) {
|
||||
PaywallPresenter()
|
||||
PaywallPresenter {
|
||||
// Only for onboarding: complete it on successful purchase
|
||||
if !hasCompletedOnboarding {
|
||||
completeOnboarding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func completeOnboarding() {
|
||||
withAnimation {
|
||||
hasCompletedOnboarding = true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative: Shared PremiumManager
|
||||
### Alternative: Shared PremiumManager via Environment
|
||||
|
||||
Instead of each view model having its own `PremiumManager`, you can use a single shared instance passed through the environment:
|
||||
Instead of each view model having its own `PremiumManager`, use a single shared instance:
|
||||
|
||||
```swift
|
||||
@main
|
||||
@ -676,7 +723,7 @@ struct YourApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
RootView()
|
||||
.environment(premiumManager)
|
||||
.task {
|
||||
await premiumManager.checkSubscriptionStatus()
|
||||
@ -686,7 +733,7 @@ struct YourApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
// In views:
|
||||
// In any view:
|
||||
struct SettingsView: View {
|
||||
@Environment(PremiumManager.self) private var premiumManager
|
||||
|
||||
@ -698,14 +745,31 @@ struct SettingsView: View {
|
||||
}
|
||||
```
|
||||
|
||||
This approach ensures all views observe the same instance and receive updates together.
|
||||
This approach ensures all views observe the same instance.
|
||||
|
||||
### Key Takeaways
|
||||
### Testing Checklist
|
||||
|
||||
1. **Always refresh after paywall dismisses** - Use `onDismiss` on the sheet
|
||||
2. **Use a refresh token** for computed properties that read from external sources
|
||||
3. **Consider a shared instance** if premium status is checked in many places
|
||||
4. **Test the full flow** - Skip onboarding, trigger paywall from Settings, complete purchase, verify UI updates
|
||||
Test both purchase flows to ensure they work correctly:
|
||||
|
||||
**Onboarding Flow:**
|
||||
- [ ] Skip to paywall step in onboarding
|
||||
- [ ] Complete a purchase
|
||||
- [ ] Verify: App transitions to main content immediately
|
||||
- [ ] Verify: Premium features are unlocked
|
||||
|
||||
**In-App Purchase Flow:**
|
||||
- [ ] Complete onboarding WITHOUT purchasing (tap "Maybe Later")
|
||||
- [ ] Open Settings
|
||||
- [ ] Tap a premium feature to trigger paywall
|
||||
- [ ] Complete a purchase
|
||||
- [ ] Verify: Paywall dismisses
|
||||
- [ ] Verify: Settings shows premium features as unlocked
|
||||
- [ ] Verify: No need to restart app or reopen Settings
|
||||
|
||||
**Restore Purchases:**
|
||||
- [ ] Test restore from onboarding soft paywall
|
||||
- [ ] Test restore from in-app paywall
|
||||
- [ ] Verify: UI updates correctly after restore
|
||||
|
||||
---
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user