1.1.5: conversion funnel overhaul

Paywall:
- Redesigned PremiumView with yearly/monthly/lifetime side by side
- 7-day free trial badge with savings % vs monthly
- Social proof line, BEST VALUE highlight on yearly
- StoreManager: new yearlyProduct, lifetimeProduct, yearlySavingsPercent, hasTrialAvailable

Funnel:
- Paywall step added to onboarding (PaywallStepView)
- Contextual modal at dish limit (replaces silent block)
- Visible "X/15 dishes" counter in DishListView
- BottomPromoBanner rotates AdMob with internal "Remove ads" CTA (1 in 4)
- Free dish limit lowered 20 → 15
- Review prompt earlier: 2 weeks → 1 week

Analytics:
- paywall_viewed/dismissed with source + seconds_on_screen
- dish_limit_hit, week_limit_hit, free_trial_started events
- Every PremiumView call site now passes its source

StoreKit:
- MealMood.storekit: added yearly $19.99 + 7d trial, monthly trial, lifetime $39.99
- AppStoreConnect-1.1.5-setup.md with full manual setup instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-06-11 22:21:36 +02:00
parent 72770801d3
commit b45cb2a530
31 changed files with 1067 additions and 104 deletions
+58 -14
View File
@@ -9,7 +9,7 @@ struct DishFormView: View {
@Query private var allSettings: [AppSettings]
@StateObject private var viewModel = DishViewModel()
@State private var showPremium = false
@State private var showDishLimitUpsell = false
@State private var showDishLimitModal = false
@State private var showSavedFeedback = false
var dish: Dish?
@@ -154,14 +154,6 @@ struct DishFormView: View {
.background(Color.mealMoodSurface)
.cornerRadius(14)
if showDishLimitUpsell && reachedFreeDishLimit {
PremiumUpsellBanner(
messageKey: "premium_limit_dishes",
actionTitleKey: "premium_subscribe"
) {
showPremium = true
}
}
// Delete button (edit mode only)
if viewModel.isEditing {
@@ -195,10 +187,8 @@ struct DishFormView: View {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
guard !reachedFreeDishLimit else {
withAnimation(.easeInOut(duration: 0.2)) {
showDishLimitUpsell = true
}
showPremium = true
AnalyticsService.logDishLimitHit()
showDishLimitModal = true
HapticManager.shared.notification(type: .warning)
return
}
@@ -226,9 +216,22 @@ struct DishFormView: View {
}
.sheet(isPresented: $showPremium) {
if let settings = allSettings.first {
NavigationStack { PremiumView(settings: settings) }
NavigationStack { PremiumView(settings: settings, source: "dish_limit") }
}
}
.sheet(isPresented: $showDishLimitModal) {
DishLimitModal(
onSeePremium: {
showDishLimitModal = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
showPremium = true
}
},
onDismiss: { showDishLimitModal = false }
)
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
.alert("dish_delete_title", isPresented: $viewModel.showDeleteAlert) {
Button("dish_cancel", role: .cancel) {}
Button("dish_delete", role: .destructive) {
@@ -255,6 +258,47 @@ struct DishFormView: View {
}
// Simple flow layout for tags
private struct DishLimitModal: View {
let onSeePremium: () -> Void
let onDismiss: () -> Void
var body: some View {
VStack(spacing: 20) {
Image(systemName: "sparkles")
.font(.system(size: 44))
.foregroundColor(.mealMoodCoral)
.padding(.top, 24)
VStack(spacing: 8) {
Text("dish_limit_modal_title")
.font(.mealMoodH2)
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.center)
Text("dish_limit_modal_message")
.font(.mealMoodBody)
.foregroundColor(.mealMoodTextSecondary)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 24)
Spacer()
VStack(spacing: 10) {
PrimaryButton(title: String(localized: "dish_limit_modal_cta"), action: onSeePremium)
Button(action: onDismiss) {
Text("dish_limit_modal_dismiss")
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextSecondary)
}
}
.padding(.horizontal, 24)
.padding(.bottom, 24)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.mealMoodBackground.ignoresSafeArea())
}
}
struct FlowLayout: Layout {
var spacing: CGFloat = 8
+53 -1
View File
@@ -10,6 +10,7 @@ struct DishListView: View {
@State private var showDishForm = false
@State private var editingDish: Dish?
@State private var showDeleteBlockedAlert = false
@State private var showPremium = false
private var language: AppLanguage {
(allSettings.first?.languageEnum ?? .system).resolved()
@@ -31,7 +32,11 @@ struct DishListView: View {
.padding(.horizontal, 60)
}
} else {
List {
VStack(spacing: 0) {
if let settings = allSettings.first, !settings.isPremium {
dishCounterBanner
}
List {
ForEach(dishes, id: \.id) { dish in
let dishTags = tags.filter { dish.tagIds.contains($0.id) }
Button {
@@ -65,6 +70,7 @@ struct DishListView: View {
.onDelete(perform: deleteDishes)
}
.scrollContentBackground(.hidden)
}
}
}
.navigationTitle("home_my_dishes")
@@ -78,6 +84,11 @@ struct DishListView: View {
}
}
}
.sheet(isPresented: $showPremium) {
if let settings = allSettings.first {
NavigationStack { PremiumView(settings: settings, source: "dish_counter") }
}
}
.sheet(isPresented: $showDishForm) {
DishFormView()
}
@@ -91,6 +102,47 @@ struct DishListView: View {
}
}
private var dishCounterBanner: some View {
let count = dishes.count
let limit = PremiumAccess.freeDishLimit
let remaining = max(0, limit - count)
let isNearLimit = remaining <= 2
let accent: Color = isNearLimit ? .mealMoodCoral : .mealMoodTextSecondary
return Button {
showPremium = true
} label: {
HStack(spacing: 10) {
Image(systemName: isNearLimit ? "exclamationmark.circle.fill" : "star.circle")
.foregroundColor(accent)
VStack(alignment: .leading, spacing: 2) {
Text("\(count) / \(limit) \(String(localized: "dish_counter_label"))")
.font(.mealMoodSmall.weight(.semibold))
.foregroundColor(.mealMoodTextPrimary)
Text("dish_counter_upgrade_hint")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12))
.foregroundColor(.mealMoodTextSecondary)
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(Color.mealMoodSurface)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(accent.opacity(isNearLimit ? 0.5 : 0.15), lineWidth: 1)
)
}
.buttonStyle(.plain)
.padding(.horizontal, 16)
.padding(.top, 12)
.padding(.bottom, 4)
}
private func deleteDishes(at offsets: IndexSet) {
let currentWeekStart = Date().startOfWeek()
let currentWeekPlan = weekPlans.first { $0.weekStartDate == currentWeekStart }
+10 -2
View File
@@ -13,6 +13,7 @@ struct HomeView: View {
@StateObject private var viewModel = HomeViewModel()
@State private var editingDish: Dish?
@State private var showPremiumFromExport: Bool = false
@State private var paywallSource: String = "unknown"
@State private var showMonthlyHistory: Bool = false
@State private var showStats: Bool = false
@State private var showWeekLimitUpsell: Bool = false
@@ -111,6 +112,8 @@ struct HomeView: View {
withAnimation(.easeInOut(duration: 0.2)) {
showWeekLimitUpsell = true
}
AnalyticsService.logWeekLimitHit()
paywallSource = "week_limit"
showPremiumFromExport = true
HapticManager.shared.notification(type: .warning)
}
@@ -218,6 +221,7 @@ struct HomeView: View {
messageKey: "premium_limit_future_weeks",
actionTitleKey: "premium_subscribe"
) {
paywallSource = "week_limit_banner"
showPremiumFromExport = true
}
.padding(.horizontal, 16)
@@ -267,7 +271,7 @@ struct HomeView: View {
.frame(maxHeight: .infinity, alignment: .top)
.safeAreaInset(edge: .bottom, spacing: 0) {
if !settings.isPremium && !isRunningOnMac {
AdBannerView()
BottomPromoBanner(settings: settings)
}
}
.alert("reset_title", isPresented: $viewModel.showResetAlert) {
@@ -312,6 +316,7 @@ struct HomeView: View {
.alert("onboarding_premium_prompt_title", isPresented: $showPostOnboardingPremiumPrompt) {
Button("onboarding_premium_prompt_cta") {
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey)
paywallSource = "post_onboarding"
showPremiumFromExport = true
}
Button("reset_cancel", role: .cancel) {
@@ -459,7 +464,7 @@ struct HomeView: View {
}
.sheet(isPresented: $showPremiumFromExport) {
NavigationStack {
PremiumView(settings: settings)
PremiumView(settings: settings, source: paywallSource)
}
}
.sheet(isPresented: $showExportStylePicker) {
@@ -727,6 +732,8 @@ struct HomeView: View {
withAnimation(.easeInOut(duration: 0.2)) {
showWeekLimitUpsell = true
}
AnalyticsService.logWeekLimitHit()
paywallSource = "week_picker"
showPremiumFromExport = true
HapticManager.shared.notification(type: .warning)
}
@@ -819,6 +826,7 @@ struct HomeView: View {
.buttonStyle(.plain)
} else {
Button {
paywallSource = "share_week"
showPremiumFromExport = true
} label: {
Label("share_week_button", systemImage: "star.fill")
@@ -63,13 +63,19 @@ struct OnboardingView: View {
viewModel: viewModel,
tags: tags,
onFinish: {
if viewModel.completeOnboarding(context: context) {
onComplete()
}
_ = viewModel.completeOnboarding(context: context)
viewModel.nextStep()
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.tag(4)
PaywallStepView(
onSkip: { onComplete() },
onConverted: { onComplete() }
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.tag(5)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.animation(.easeInOut(duration: 0.3), value: viewModel.currentStep)
@@ -0,0 +1,126 @@
import SwiftUI
import SwiftData
struct PaywallStepView: View {
@Environment(\.modelContext) private var context
@Query private var allSettings: [AppSettings]
@StateObject private var storeManager = StoreManager()
var onSkip: () -> Void
var onConverted: () -> Void
@State private var purchaseStatusMessageKey: String?
var body: some View {
ZStack {
Color.mealMoodBackground.ignoresSafeArea()
VStack(spacing: 24) {
AppIconPlaceholder(size: 84)
.padding(.top, 20)
VStack(spacing: 8) {
Text("onboarding_paywall_title")
.font(.mealMoodH1)
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.center)
Text("onboarding_paywall_subtitle")
.font(.mealMoodBody)
.foregroundColor(.mealMoodTextSecondary)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 24)
VStack(alignment: .leading, spacing: 10) {
benefitRow(text: String(localized: "premium_unlimited_dishes"))
benefitRow(text: String(localized: "premium_advanced_rules"))
benefitRow(text: String(localized: "premium_future_weeks"))
benefitRow(text: String(localized: "premium_no_ads"))
}
.padding(16)
.background(Color.mealMoodSurface)
.clipShape(RoundedRectangle(cornerRadius: 14))
.padding(.horizontal, 24)
Text("premium_social_proof")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
Spacer(minLength: 0)
VStack(spacing: 12) {
PrimaryButton(
title: trialButtonTitle,
action: { Task { await startTrial() } },
isEnabled: !storeManager.isLoading
)
Button(action: skipPaywall) {
Text("onboarding_paywall_cta_skip")
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextSecondary)
}
}
.padding(.horizontal, 24)
.padding(.bottom, 24)
}
}
.task { await storeManager.loadProducts() }
.onAppear { AnalyticsService.logPaywallViewed(source: "onboarding") }
.alert("premium_title", isPresented: Binding(
get: { purchaseStatusMessageKey != nil },
set: { if !$0 { purchaseStatusMessageKey = nil } }
)) {
Button("dish_delete_blocked_ok", role: .cancel) {}
} message: {
Text(LocalizedStringKey(purchaseStatusMessageKey ?? ""))
}
}
private var trialButtonTitle: String {
if let yearly = storeManager.yearlyProduct {
let format = String(localized: "premium_trial_note")
let priceLabel = yearly.displayPrice + " / " + String(localized: "premium_year")
return String(localized: "onboarding_paywall_cta_trial") + "" + String(format: format, priceLabel)
}
return String(localized: "onboarding_paywall_cta_trial")
}
@ViewBuilder
private func benefitRow(text: String) -> some View {
HStack(spacing: 10) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.mealMoodSuccess)
Text(text)
.font(.mealMoodBody)
.foregroundColor(.mealMoodTextPrimary)
Spacer()
}
}
private func startTrial() async {
let candidate = storeManager.yearlyProduct ?? storeManager.monthlyProduct
guard let product = candidate else {
purchaseStatusMessageKey = "premium_products_not_found"
return
}
AnalyticsService.logPremiumUpgradeTapped(source: "onboarding")
let result = await storeManager.purchase(product)
switch result {
case .success:
if let settings = allSettings.first {
settings.isPremium = true
try? context.save()
}
AnalyticsService.logFreeTrialStarted(plan: product.id)
onConverted()
case .pending: purchaseStatusMessageKey = "premium_purchase_pending"
case .cancelled: purchaseStatusMessageKey = "premium_purchase_cancelled"
case .failed: purchaseStatusMessageKey = "premium_purchase_failed"
}
}
private func skipPaywall() {
AnalyticsService.logPaywallDismissed(source: "onboarding", secondsOnScreen: 0, didConvert: false)
onSkip()
}
}
+216 -32
View File
@@ -5,16 +5,24 @@ struct PremiumView: View {
@Environment(\.modelContext) private var context
@StateObject private var storeManager = StoreManager()
@State private var purchaseStatusMessageKey: String?
@State private var appearedAt: Date = Date()
@State private var didConvert: Bool = false
@State private var selectedPlan: PlanChoice = .yearly
let settings: AppSettings
var source: String = "unknown"
enum PlanChoice {
case monthly, yearly, lifetime
}
var body: some View {
ZStack {
Color.mealMoodBackground.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
VStack(spacing: 16) {
VStack(spacing: 22) {
VStack(spacing: 12) {
AppIconPlaceholder(size: 84)
Text("premium_title")
@@ -25,10 +33,28 @@ struct PremiumView: View {
.font(.mealMoodBody)
.foregroundColor(.mealMoodTextSecondary)
.multilineTextAlignment(.center)
Text("premium_social_proof")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
.padding(.top, 2)
}
.padding(.top, 28)
.padding(.top, 24)
.padding(.horizontal, 24)
if settings.isPremium {
Label("premium_active", systemImage: "checkmark.seal.fill")
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodSuccess)
.padding(.horizontal, 24)
} else {
planSelector
.padding(.horizontal, 24)
ctaButton
.padding(.horizontal, 24)
}
VStack(alignment: .leading, spacing: 12) {
BenefitItem(text: String(localized: "premium_no_ads"))
BenefitItem(text: String(localized: "premium_unlimited_dishes"))
@@ -37,39 +63,14 @@ struct PremiumView: View {
BenefitItem(text: String(localized: "premium_share_week"))
BenefitItem(text: String(localized: "premium_family_sharing"))
}
.padding(20)
.padding(16)
.background(Color.mealMoodSurface)
.cornerRadius(16)
.padding(.horizontal, 24)
if settings.isPremium {
Label("premium_active", systemImage: "checkmark.seal.fill")
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodSuccess)
if !settings.isPremium && storeManager.monthlyProduct == nil && storeManager.yearlyProduct == nil && storeManager.lifetimeProduct == nil {
productLoadFallback
.padding(.horizontal, 24)
} else if let monthly = storeManager.monthlyProduct {
PlanCard(
title: String(localized: "premium_monthly"),
price: monthly.displayPrice + " / " + String(localized: "premium_month"),
caption: String(localized: "premium_price_note"),
isLoading: storeManager.isLoading
) {
Task { await purchase(monthly) }
}
.padding(.horizontal, 24)
} else {
let fallbackText = fallbackContent(for: storeManager.productLoadState)
PlanCard(
title: String(localized: "premium_monthly"),
price: fallbackText.price,
caption: fallbackText.hint,
buttonTitle: String(localized: "premium_retry_products"),
isLoading: storeManager.isLoadingProducts
) {
Task { await storeManager.loadProducts() }
}
.padding(.horizontal, 24)
#if DEBUG
let hasStoreKitFileInBundle = Bundle.main.url(forResource: "MealMood", withExtension: "storekit") != nil
@@ -128,6 +129,14 @@ struct PremiumView: View {
.task {
await storeManager.loadProducts()
}
.onAppear {
appearedAt = Date()
AnalyticsService.logPaywallViewed(source: source)
}
.onDisappear {
let seconds = Int(Date().timeIntervalSince(appearedAt))
AnalyticsService.logPaywallDismissed(source: source, secondsOnScreen: seconds, didConvert: didConvert)
}
.onChange(of: storeManager.isPremium) { _, isPremium in
if settings.isPremium != isPremium {
settings.isPremium = isPremium
@@ -146,12 +155,187 @@ struct PremiumView: View {
}
}
@ViewBuilder
private var planSelector: some View {
VStack(spacing: 10) {
HStack(spacing: 10) {
planChip(.yearly)
planChip(.monthly)
}
if storeManager.lifetimeProduct != nil {
planChip(.lifetime, fullWidth: true)
}
}
}
@ViewBuilder
private func planChip(_ plan: PlanChoice, fullWidth: Bool = false) -> some View {
let isSelected = selectedPlan == plan
let product: Product? = {
switch plan {
case .monthly: return storeManager.monthlyProduct
case .yearly: return storeManager.yearlyProduct
case .lifetime: return storeManager.lifetimeProduct
}
}()
let titleKey: LocalizedStringKey = {
switch plan {
case .monthly: return "premium_monthly"
case .yearly: return "premium_yearly"
case .lifetime: return "premium_lifetime"
}
}()
let priceSuffix: String = {
switch plan {
case .monthly: return " / " + String(localized: "premium_month")
case .yearly: return " / " + String(localized: "premium_year")
case .lifetime: return " · " + String(localized: "premium_one_time")
}
}()
Button {
selectedPlan = plan
HapticManager.shared.impact(style: .light)
} label: {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(titleKey)
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
Spacer()
if plan == .yearly, let savings = storeManager.yearlySavingsPercent {
Text(String(format: String(localized: "premium_save_badge"), "\(savings)%"))
.font(.system(size: 10, weight: .bold))
.foregroundColor(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.mealMoodCoral)
.clipShape(Capsule())
}
if plan == .lifetime {
Text("premium_lifetime_badge")
.font(.system(size: 10, weight: .bold))
.foregroundColor(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.mealMoodTextPrimary)
.clipShape(Capsule())
}
}
if let product = product {
Text(product.displayPrice + priceSuffix)
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextSecondary)
} else {
Text("premium_loading_products")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
}
if plan == .yearly {
Text("premium_best_value")
.font(.system(size: 9, weight: .bold))
.foregroundColor(.mealMoodCoral)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
.background(isSelected ? Color.mealMoodCoral.opacity(0.08) : Color.mealMoodSurface)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(isSelected ? Color.mealMoodCoral : Color(hex: "#E0E0E0"), lineWidth: isSelected ? 2 : 1)
)
}
.buttonStyle(.plain)
}
@ViewBuilder
private var ctaButton: some View {
let selectedProduct = currentSelectedProduct
VStack(spacing: 8) {
if selectedHasTrial {
Text("premium_trial_badge")
.font(.mealMoodCaption.weight(.semibold))
.foregroundColor(.mealMoodCoral)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(Color.mealMoodCoral.opacity(0.12))
.clipShape(Capsule())
}
PrimaryButton(
title: ctaTitle,
action: { if let p = selectedProduct { Task { await purchase(p) } } },
isEnabled: !storeManager.isLoading && selectedProduct != nil
)
if let product = selectedProduct, selectedHasTrial {
Text(String(format: String(localized: "premium_trial_note"),
product.displayPrice + " / " + String(localized: selectedPlan == .yearly ? "premium_year" : "premium_month")))
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
.multilineTextAlignment(.center)
}
}
}
private var currentSelectedProduct: Product? {
switch selectedPlan {
case .monthly: return storeManager.monthlyProduct
case .yearly: return storeManager.yearlyProduct
case .lifetime: return storeManager.lifetimeProduct
}
}
private var selectedHasTrial: Bool {
guard selectedPlan != .lifetime else { return false }
return storeManager.hasTrialAvailable
}
private var ctaTitle: String {
if storeManager.isLoading {
return String(localized: "premium_processing")
}
if selectedPlan == .lifetime {
return String(localized: "premium_buy_lifetime")
}
if selectedHasTrial {
return String(localized: "onboarding_paywall_cta_trial")
}
return String(localized: "premium_subscribe")
}
@ViewBuilder
private var productLoadFallback: some View {
let text = fallbackContent(for: storeManager.productLoadState)
VStack(spacing: 10) {
Text(text.price)
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.center)
Text(text.hint)
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
.multilineTextAlignment(.center)
PrimaryButton(
title: String(localized: "premium_retry_products"),
action: { Task { await storeManager.loadProducts() } },
isEnabled: !storeManager.isLoadingProducts
)
}
.padding(16)
.background(Color.mealMoodSurface)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
private func purchase(_ product: Product) async {
AnalyticsService.logPremiumUpgradeTapped(source: "premium_view")
AnalyticsService.logPremiumUpgradeTapped(source: source)
let result = await storeManager.purchase(product)
switch result {
case .success:
settings.isPremium = true
didConvert = true
if storeManager.hasTrialAvailable {
AnalyticsService.logFreeTrialStarted(plan: product.id)
}
try? context.save()
case .pending:
purchaseStatusMessageKey = "premium_purchase_pending"
+1 -1
View File
@@ -181,7 +181,7 @@ struct SettingsView: View {
.foregroundColor(.mealMoodTextSecondary)
}
NavigationLink(destination: PremiumView(settings: settings)) {
NavigationLink(destination: PremiumView(settings: settings, source: "settings")) {
Text("settings_remove_ads")
}
} header: {
+1 -1
View File
@@ -203,7 +203,7 @@ struct TagRulesEditView: View {
}
.sheet(isPresented: $showPremium) {
if let settings = allSettings.first {
NavigationStack { PremiumView(settings: settings) }
NavigationStack { PremiumView(settings: settings, source: "tag_rules") }
}
}
}