Widget Extension Fixes: - Create standalone WidgetDataProvider for widget data isolation - Add WIDGET_EXTENSION compiler flag for conditional compilation - Fix DataController references in widget-shared files - Sync widget version numbers with main app (23, 1.0.2) - Add WidgetBackground color to asset catalog Warning Resolutions: - Fix UIScreen.main deprecation in BGView and SharingListView - Fix Text '+' concatenation deprecation in PurchaseButtonView and SettingsTabView - Fix exhaustive switch in BiometricAuthManager (add .none case) - Fix var to let in ExportService (3 instances) - Fix unused result warning in NoteEditorView - Fix ForEach duplicate ID warnings in MonthView and YearView Code Quality Improvements: - Wrap bypassSubscription in #if DEBUG for security - Rename StupidAssCustomWidgetObservableObject to CustomWidgetStateViewModel - Add @MainActor to IconViewModel - Replace fatalError with graceful fallback in SharedModelContainer - Add [weak self] to closures in DayViewViewModel - Add OSLog-based AppLogger for production logging - Add ImageCache with NSCache for memory efficiency - Add AccessibilityHelpers with Reduce Motion support - Create DataControllerProtocol for dependency injection - Update .gitignore with secrets exclusions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
194 lines
6.2 KiB
Swift
194 lines
6.2 KiB
Swift
//
|
|
// PurchaseButtonView.swift
|
|
// Feels
|
|
//
|
|
// Subscription status and purchase view for settings.
|
|
//
|
|
|
|
import SwiftUI
|
|
import StoreKit
|
|
|
|
struct PurchaseButtonView: View {
|
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
|
|
|
@ObservedObject var iapManager: IAPManager
|
|
|
|
@State private var showSubscriptionStore = false
|
|
@State private var showManageSubscriptions = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
if iapManager.isLoading {
|
|
loadingView
|
|
} else if iapManager.isSubscribed {
|
|
subscribedView
|
|
} else {
|
|
notSubscribedView
|
|
}
|
|
}
|
|
.padding()
|
|
.background(theme.currentTheme.secondaryBGColor)
|
|
.cornerRadius(10)
|
|
.sheet(isPresented: $showSubscriptionStore) {
|
|
FeelsSubscriptionStoreView()
|
|
}
|
|
.manageSubscriptionsSheet(isPresented: $showManageSubscriptions)
|
|
}
|
|
|
|
// MARK: - Loading View
|
|
|
|
private var loadingView: some View {
|
|
VStack(spacing: 12) {
|
|
ProgressView()
|
|
Text(String(localized: "purchase_view_loading"))
|
|
.font(.body)
|
|
.foregroundColor(textColor)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
}
|
|
|
|
// MARK: - Subscribed View
|
|
|
|
private var subscribedView: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text(String(localized: "purchase_view_current_subscription"))
|
|
.font(.title3)
|
|
.bold()
|
|
.foregroundColor(textColor)
|
|
|
|
if let product = iapManager.currentProduct {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(product.displayName)
|
|
.font(.headline)
|
|
.foregroundColor(textColor)
|
|
Text(product.displayPrice)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
subscriptionStatusBadge
|
|
}
|
|
}
|
|
|
|
Divider()
|
|
|
|
// Manage subscription button
|
|
Button {
|
|
showManageSubscriptions = true
|
|
} label: {
|
|
Text(String(localized: "purchase_view_manage_subscription"))
|
|
.font(.body)
|
|
.foregroundColor(.blue)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
// Show other subscription options
|
|
if iapManager.sortedProducts.count > 1 {
|
|
Button {
|
|
showSubscriptionStore = true
|
|
} label: {
|
|
Text(String(localized: "purchase_view_change_plan"))
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var subscriptionStatusBadge: some View {
|
|
Group {
|
|
if case .subscribed(_, let willAutoRenew) = iapManager.state {
|
|
if willAutoRenew {
|
|
Text(String(localized: "subscription_status_active"))
|
|
.font(.caption)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Color.green)
|
|
.cornerRadius(4)
|
|
} else {
|
|
Text(String(localized: "subscription_status_expires"))
|
|
.font(.caption)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Color.orange)
|
|
.cornerRadius(4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Not Subscribed View
|
|
|
|
private var notSubscribedView: some View {
|
|
VStack(spacing: 16) {
|
|
Text(String(localized: "purchase_view_title"))
|
|
.font(.title3)
|
|
.bold()
|
|
.foregroundColor(textColor)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
// Trial status
|
|
if iapManager.shouldShowTrialWarning {
|
|
trialStatusView
|
|
} else if iapManager.shouldShowPaywall {
|
|
Text(String(localized: "purchase_view_trial_expired"))
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Text(String(localized: "purchase_view_current_why_subscribe"))
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
// Subscribe button
|
|
Button {
|
|
showSubscriptionStore = true
|
|
} label: {
|
|
Text(String(localized: "purchase_view_subscribe_button"))
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.pink)
|
|
.cornerRadius(10)
|
|
}
|
|
|
|
// Restore purchases
|
|
Button {
|
|
Task {
|
|
await iapManager.restore()
|
|
}
|
|
} label: {
|
|
Text(String(localized: "purchase_view_restore"))
|
|
.font(.body)
|
|
.foregroundColor(.blue)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var trialStatusView: some View {
|
|
HStack {
|
|
Image(systemName: "clock")
|
|
.foregroundColor(.orange)
|
|
|
|
if let expirationDate = iapManager.trialExpirationDate {
|
|
Text("\(Text(String(localized: "purchase_view_trial_expires_in")).foregroundColor(textColor)) \(Text(expirationDate, style: .relative).foregroundColor(.orange).bold())")
|
|
}
|
|
}
|
|
.font(.body)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
PurchaseButtonView(iapManager: IAPManager())
|
|
}
|