From dcf4135ec376d357b8fafc0101a75b674e76329a Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 22 Apr 2026 12:39:23 -0500 Subject: feat(v3​.1​.0): add ​Store​Kit integration and ​Pro tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • implement Purchase​Service using StoreKit 2 • activate feature tiers based on entitlement • add minimal paywall and restore flow • enforce free-tier limits with upgrade paths • integrate Pro status into settings • ensure clean and non-intrusive monetization flow --- DomainDig/PaywallView.swift | 123 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 DomainDig/PaywallView.swift (limited to 'DomainDig/PaywallView.swift') diff --git a/DomainDig/PaywallView.swift b/DomainDig/PaywallView.swift new file mode 100644 index 0000000..a21448f --- /dev/null +++ b/DomainDig/PaywallView.swift @@ -0,0 +1,123 @@ +import StoreKit +import SwiftUI + +struct PaywallView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.appDensity) private var appDensity + @State private var purchaseService = PurchaseService.shared + + var body: some View { + NavigationStack { + List { + Section { + Text("Pro unlocks automation, larger runs, and export convenience while keeping the core lookup experience free.") + .font(appDensity.font(.body, design: .default)) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + Section("What Pro Unlocks") { + featureRow("Unlimited tracked domains") + featureRow("Workflows") + featureRow("Larger batch sizes") + featureRow("Advanced exports") + } + + Section("Subscription") { + if purchaseService.isLoadingProducts { + ProgressView("Loading pricing…") + } else if purchaseService.products.isEmpty { + Text("Pricing is unavailable right now.") + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.secondary) + + Button("Retry") { + Task { + await purchaseService.refreshProducts() + } + } + } else { + ForEach(purchaseService.products, id: \.id) { product in + Button { + Task { + await purchaseService.purchase(product) + } + } label: { + VStack(alignment: .leading, spacing: 6) { + Text(subscriptionTitle(for: product)) + .font(appDensity.font(.headline, design: .default, weight: .semibold)) + .foregroundStyle(.primary) + Text(product.displayPrice) + .font(appDensity.font(.callout, design: .default)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .disabled(purchaseService.isPurchasing || purchaseService.isRestoring) + } + } + + if let statusMessage = purchaseService.statusMessage { + Text(statusMessage) + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.secondary) + } + + if let errorMessage = purchaseService.errorMessage { + Text(errorMessage) + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.red) + } + } + + Section("Account") { + Button(purchaseService.isRestoring ? "Restoring…" : "Restore Purchases") { + Task { + await purchaseService.restorePurchases() + } + } + .disabled(purchaseService.isPurchasing || purchaseService.isRestoring) + + if purchaseService.hasProAccess { + Button("Manage Subscription") { + Task { + await purchaseService.manageSubscription() + } + } + } + } + } + .navigationTitle("DomainDig Pro") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { + dismiss() + } + } + } + } + .task { + await purchaseService.refreshProducts() + await purchaseService.refreshEntitlements() + } + .onDisappear { + purchaseService.clearMessages() + } + } + + private func featureRow(_ title: String) -> some View { + Text(title) + .font(appDensity.font(.body, design: .default)) + } + + private func subscriptionTitle(for product: Product) -> String { + switch product.id { + case PurchaseService.monthlyProductID: + return "Pro Monthly" + case PurchaseService.yearlyProductID: + return "Pro Yearly" + default: + return product.displayName + } + } +} -- cgit v1.2.3