Paywall & Premium Features
Relevant source files
The following files were used as context for generating this wiki page:
- Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallView.swift
- Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallViewModel.swift
- Packages/Domain/Sources/Domain/PaywallSource.swift
- Features/AppFeature/Tests/AppFeatureTests/AppRootViewModelTests.swift
- Features/SettingsFeature/Tests/SettingsFeatureTests/Views/Paywall/PaywallViewModelTests.swift
- README.md
The Paywall and Premium Features module manages the monetization logic and gated access within the Trackurr application. It leverages StoreKit 2 to provide users with multiple subscription tiers (Monthly, Annual) and a Lifetime purchase option, unlocking enhanced capabilities such as unlimited subscription tracking and custom categories.
This system is built using a modular architecture, separating the UI layer (PaywallView), the business logic layer (PaywallViewModel), and the service layer (PurchaseService). It utilizes an event-driven approach to trigger paywall presentations from various entry points across the app, ensuring a consistent user experience when encountering premium limits.
Sources: README.md:23-35, Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallView.swift:10-20
Architecture and Data Flow
The premium system follows a reactive pattern where specific user actions (e.g., reaching a subscription limit) trigger a ShowPaywallEvent. This event is handled by the AppRootViewModel, which manages the global state for the paywall's visibility.
Component Relationship
The following diagram illustrates the relationship between the Paywall UI components and the underlying services.
Purchase Sequence
When a user initiates a purchase, the PaywallViewModel coordinates with the PurchaseService to execute the transaction and update the local entitlement state.
Premium Plans and Entitlements
Trackurr offers three distinct premium tiers. These plans are defined in the PremiumPlan enumeration and mapped to specific products retrieved from the App Store.
| Plan | Billing Cycle | Feature Focus |
|---|---|---|
| Monthly | Recurring Monthly | Full access with flexible billing |
| Annual | Recurring Yearly | Full access with a cost saving (33%) |
| Lifetime | One-time purchase | Permanent access without recurring fees |
Sources: Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallView.swift:115-125, Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallView.swift:145-155
Premium Features
Gated features check for the entitlementStatus before allowing user interaction:
- Unlimited Subscriptions: Removes the cap on the number of recurring expenses tracked.
- Custom Categories: Allows users to create personalized categories beyond the system defaults.
- Data Export: Enables exporting user data as JSON for backup or external analysis.
Sources: Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallView.swift:85-102
Paywall Trigger Sources
The paywall can be presented from multiple contexts within the application. The PaywallSource enum tracks these entry points for analytics and contextual messaging.
| Source | Description |
|---|---|
.settings | Triggered manually by the user from the Settings menu. |
.subscriptionLimit | Triggered when a free-tier user attempts to exceed the subscription count limit. |
.customCategory | Triggered when attempting to create a new category. |
.dataExport | Triggered when attempting to access the data export feature. |
Sources: Packages/Domain/Sources/Domain/PaywallSource.swift:2-7, Features/AppFeature/Tests/AppFeatureTests/AppRootViewModelTests.swift:220-240
Implementation Details
Product Loading Logic
The PaywallViewModel loads products asynchronously upon view appearance. It also logs an analytics event to track paywall impressions.
func loadProducts() async {
await purchaseService.loadProducts()
products = purchaseService.products
appAnalytics.logEvent(AppAnalyticsEvent.paywallShown, parameters: [
AppAnalyticsParam.paywallSource: context.source.rawValue,
])
}
Sources: Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallViewModel.swift:45-52
Restore Purchases
The system allows users to restore previous purchases, which is mandatory for App Store compliance. If a premium entitlement is found during restoration, the purchaseSucceeded flag is set to true, dismissing the paywall.
func restorePurchases() async {
isRestoring = true
defer { isRestoring = false }
do {
try await purchaseService.restorePurchases()
if purchaseService.entitlementStatus.isPremium {
purchaseSucceeded = true
} else {
restoreMessage = "No previous purchases found."
showRestoreMessage = true
}
} catch {
logger.error("Restore failed", error: error, category: "Paywall")
errorMessage = error.localizedDescription
showError = true
}
}
Sources: Features/SettingsFeature/Sources/SettingsFeature/Views/Paywall/PaywallViewModel.swift:70-89
Conclusion
The Paywall & Premium Features module provides a robust foundation for application monetization. By centralizing purchase logic in the PurchaseService and using a dedicated PaywallViewModel to manage state, the system ensures that premium entitlements are reliably enforced across the app's various functional modules.