summaryrefslogtreecommitdiff
path: root/DomainDig/ContentView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-22 12:39:23 -0500
committerChristian Cleberg <[email protected]>2026-04-22 12:39:23 -0500
commitdcf4135ec376d357b8fafc0101a75b674e76329a (patch)
tree498e0782b34778b6f6d871d2adc230437fe77904 /DomainDig/ContentView.swift
parent37b02e5ee360d61751951b77b7b556165a3af1fc (diff)
downloaddomain-dig-dcf4135ec376d357b8fafc0101a75b674e76329a.tar.gz
domain-dig-dcf4135ec376d357b8fafc0101a75b674e76329a.tar.bz2
domain-dig-dcf4135ec376d357b8fafc0101a75b674e76329a.zip
feat(v3​.1​.0): add ​Store​Kit integration and ​Pro tier
• 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
Diffstat (limited to 'DomainDig/ContentView.swift')
-rw-r--r--DomainDig/ContentView.swift114
1 files changed, 62 insertions, 52 deletions
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index 9802f77..e6e7a41 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -25,20 +25,21 @@ private struct WorkflowNavigationTarget: Hashable {
struct ContentView: View {
@Environment(\.appDensity) private var appDensity
@Bindable var viewModel: DomainViewModel
+ @State private var purchaseService = PurchaseService.shared
@State private var navigationPath = NavigationPath()
@FocusState private var domainFieldFocused: Bool
@State private var customPortInput = ""
@State private var customPortsExpanded = false
@State private var trackingNoteDraft = ""
@State private var editingTrackedDomain: TrackedDomain?
- @State private var showTrackLimitAlert = false
- @State private var featureGateMessage: String?
@State private var inputMode: LookupInputMode = .single
@State private var collapsedSections: Set<ResultSection> = [.network]
@State private var showingCurrentDomainWorkflowSheet = false
@State private var showingBatchWorkflowSheet = false
var body: some View {
+ let _ = purchaseService.currentTier
+
NavigationStack(path: $navigationPath) {
ScrollView(.vertical) {
VStack(spacing: 0) {
@@ -84,9 +85,7 @@ struct ContentView: View {
workflows: viewModel.currentDomainWorkflows,
trackingLimitMessage: viewModel.trackingLimitMessage,
onTrack: {
- if !viewModel.trackCurrentDomain() {
- showTrackLimitAlert = true
- }
+ _ = viewModel.trackCurrentDomain()
},
onTogglePinned: {
guard let trackedDomain = viewModel.currentTrackedDomain else { return }
@@ -248,23 +247,6 @@ struct ContentView: View {
navigationPath = NavigationPath()
domainFieldFocused = false
}
- .alert("Tracking limit reached", isPresented: $showTrackLimitAlert) {
- Button("OK", role: .cancel) {}
- } message: {
- Text(viewModel.trackingLimitMessage ?? "Tracking limit reached.")
- }
- .alert("Feature unavailable", isPresented: Binding(
- get: { featureGateMessage != nil },
- set: { newValue in
- if !newValue {
- featureGateMessage = nil
- }
- }
- )) {
- Button("OK", role: .cancel) {}
- } message: {
- Text(featureGateMessage ?? "")
- }
.sheet(item: $editingTrackedDomain) { trackedDomain in
NavigationStack {
Form {
@@ -346,6 +328,12 @@ struct ContentView: View {
.font(appDensity.font(.caption))
.foregroundStyle(.secondary)
+ if let batchAllowanceSummary = FeatureAccessService.batchAllowanceSummary() {
+ Text(batchAllowanceSummary)
+ .font(appDensity.font(.caption2))
+ .foregroundStyle(.secondary)
+ }
+
TextField(
"example.com\napple.com, openai.com",
text: $viewModel.bulkInput,
@@ -391,25 +379,25 @@ struct ContentView: View {
Menu {
if !viewModel.isCurrentDomainTracked {
Button("Track this domain") {
- if !viewModel.trackCurrentDomain() {
- showTrackLimitAlert = true
- }
+ _ = viewModel.trackCurrentDomain()
}
}
Button("Add to workflow") {
- if FeatureAccessService.hasAccess(to: .workflows) {
- showingCurrentDomainWorkflowSheet = true
- } else {
- featureGateMessage = FeatureAccessService.upgradeMessage(for: .workflows)
- }
+ showingCurrentDomainWorkflowSheet = true
}
- Button("Copy report JSON") {
- guard let json = viewModel.exportJSONString() else { return }
- AppClipboard.copy(json)
- AppHaptics.copy()
+ if FeatureAccessService.hasAccess(to: .advancedExports) {
+ Button("Copy report JSON") {
+ guard let json = viewModel.exportJSONString() else { return }
+ AppClipboard.copy(json)
+ AppHaptics.copy()
+ }
+ } else {
+ Button("Copy report JSON") {
+ viewModel.upgradePrompt = FeatureAccessService.upgradePrompt(for: .advancedExports)
+ }
}
Button("Export report") {
- shareSingleResults(format: .json)
+ shareSingleResults(format: .text)
}
} label: {
Image(systemName: "bolt.circle")
@@ -463,11 +451,7 @@ struct ContentView: View {
if !viewModel.currentBatchResultEntries.isEmpty {
Menu {
Button("Add to Workflow") {
- if FeatureAccessService.hasAccess(to: .workflows) {
- showingBatchWorkflowSheet = true
- } else {
- featureGateMessage = FeatureAccessService.upgradeMessage(for: .workflows)
- }
+ showingBatchWorkflowSheet = true
}
Divider()
Button("Export Batch TXT") {
@@ -2165,6 +2149,7 @@ private extension String {
struct SettingsView: View {
@Environment(\.appDensity) private var appDensity
@Bindable var viewModel: DomainViewModel
+ @State private var purchaseService = PurchaseService.shared
@AppStorage(DNSResolverOption.userDefaultsKey)
private var storedResolverURL = DNSResolverOption.defaultURLString
@AppStorage(AppDensity.userDefaultsKey)
@@ -2185,6 +2170,8 @@ struct SettingsView: View {
}
var body: some View {
+ let _ = purchaseService.currentTier
+
Form {
Section("Display") {
Picker("Density", selection: $storedDensity) {
@@ -2233,28 +2220,51 @@ struct SettingsView: View {
}
}
- Section("Features") {
- LabeledContent("Tier", value: FeatureAccessService.currentTier.title)
+ Section("Pro") {
+ LabeledContent("Status", value: purchaseService.currentTier.title)
- if FeatureAccessService.enabledFeatureLabels().isEmpty {
- Text("No features enabled.")
- .font(appDensity.font(.caption, design: .default))
- .foregroundStyle(.secondary)
+ if purchaseService.currentTier == .free {
+ Button("Upgrade to Pro") {
+ viewModel.isPaywallPresented = true
+ }
} else {
- ForEach(FeatureAccessService.enabledFeatureLabels(), id: \.self) { label in
- Text(label)
+ Button("Manage Subscription") {
+ Task {
+ await purchaseService.manageSubscription()
+ }
}
}
- Text("Workflows, batch operations, and advanced exports are prepared for future Pro unlocks. Extended historical datasets are reserved for Data+ scaffolding.")
- .font(appDensity.font(.caption, design: .default))
- .foregroundStyle(.secondary)
+ Button(purchaseService.isRestoring ? "Restoring…" : "Restore Purchases") {
+ Task {
+ await purchaseService.restorePurchases()
+ }
+ }
+ .disabled(purchaseService.isRestoring || purchaseService.isPurchasing)
+
+ 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("Features") {
+ ForEach(FeatureAccessService.enabledFeatureLabels(), id: \.self) { label in
+ Text(label)
+ }
}
Section("About") {
LabeledContent("Version", value: appVersion)
LabeledContent("Storage", value: "Local-only")
- LabeledContent("Report Schema", value: "3.0.0")
+ LabeledContent("Report Schema", value: "3.1.0")
}
}
.navigationTitle("Settings")