Yes it's possible to paywall a widget. What I did was:
- Design two Views, one which is a paywall view, the other is the actual Widget. (You could in theory also design more views, so that users on higher paying tiers get more widget functionality.)
Highly simplified example:
// Actual widget view
struct WidgetView: View {
var entry: WidgetEntry
var body: some View {
VStack(alignment: .leading) {
Spacer()
Text(entry.widgetText)
.font(.system(size: 12, weight: .semibold, design: .default))
.padding(.leading, 16)
.padding(.trailing, 16)
.padding(.bottom, 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.white)
}
}
}
// Paywall widget view
struct WidgetPaywallView: View {
var body: some View {
VStack(alignment: .leading) {
Spacer()
Text("Subscribe to use the widget")
.font(.system(size: 12, weight: .semibold, design: .default))
.padding(.leading, 16)
.padding(.trailing, 16)
.padding(.bottom, 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.white)
}
}
}
- In your widget timeline entry model, add a Boolean var such as "isPremiumSubscriber"
struct WidgetEntry: TimelineEntry {
let date: Date
let widgetText: String
var isPremiumSubscriber: Bool
}
- In getTimeline() in your Timeline Provider, check to see if the user is subscribed (or, in my case, I use RevenueCat to manage the in-app subscriptions as it's much easier that way). If they are subscribed, create your timeline entries with the timestamp when the widget will be updated (e.g., every 20 mins), the data you want to display at that time (in the code example I pasted, it's just a simple String), and then set the value of isPremiumSubscriber = true.
- Then, in your Widget view (which is called each time your widget is about to be refreshed), get the data from your entry, and if entry.isPremiumSubscriber == true, return the actual widget view with all the functionality; if it's false, then return the paywall widget view.
Note that with this approach, if the user's subscription status lapses midway through an already created timeline, then the user will still have widget functionality until the timeline runs out and getTimeline() is called again.
struct WidgetEntryView: View {
var entry: WidgetEntry
var body: some View {
// Display widget if user is subscribed, otherwise return the paywall view
if entry.isPremiumSubscriber {
WidgetView(entry: entry)
} else {
WidgetPaywallView()
}
}
}
- Finally, in your actual app, you also need to add a way to force refresh the widget if the user has just subscribed. You can use WidgetCenter.shared.reloadAllTimelines() for this. In my case, I have a global boolean in AppDelegate.swift which keeps track if the user is subscribed. I added a didSet that fires whenever the value changes-- and I add a check to see if the new value is different from the old value. If it's different, I call WidgetCenter.shared.reloadAllTimelines(), which will call getTimeline() in your widget. Make sure to import WidgetKit too before you use reloadAllTimelines().
var isPremiumSubscriber: Bool = false {
didSet {
if #available(iOS 14.0, *) {
if isPremiumSubscriber != oldValue {
// Update widgets because subscription status has changed
WidgetCenter.shared.reloadAllTimelines()
}
}
}
}