## StoreKit 2 Refactor - Rewrote IAPManager with clean enum-based state model (SubscriptionState) - Added native SubscriptionStoreView for iOS 17+ purchase UI - Subscription status now checked on every app launch - Synced subscription status to UserDefaults for widget access - Simplified PurchaseButtonView and IAPWarningView - Removed unused StatusInfoView ## Interactive Vote Widget - New FeelsVoteWidget with App Intents for mood voting - Subscribers can vote directly from widget, shows stats after voting - Non-subscribers see "Tap to subscribe" which opens subscription store - Added feels:// URL scheme for deep linking ## Firebase Removal - Commented out Firebase imports and initialization - EventLogger now prints to console in DEBUG mode only ## Other Changes - Added fallback for Core Data when App Group unavailable - Added new localization strings for subscription UI - Updated entitlements and Info.plist 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
201 lines
6.3 KiB
Swift
201 lines
6.3 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(String(localized: "purchase_view_trial_expires_in"))
|
|
.foregroundColor(textColor)
|
|
+
|
|
Text(" ")
|
|
+
|
|
Text(expirationDate, style: .relative)
|
|
.foregroundColor(.orange)
|
|
.bold()
|
|
}
|
|
}
|
|
.font(.body)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
PurchaseButtonView(iapManager: IAPManager())
|
|
}
|