Files
Reflect/Shared/Views/PurchaseButtonView.swift
Trey T e7648ddd8a Add missing accessibility identifiers to all interactive UI elements
Audit found ~50+ interactive elements (buttons, toggles, pickers, alerts,
links) missing accessibility identifiers across 13 view files. Added
centralized ID definitions and applied them to every entry detail button,
guided reflection control, settings toggle, paywall unlock button,
subscription/IAP button, lock screen control, and photo action dialog.
2026-03-26 07:59:52 -05:00

213 lines
7.0 KiB
Swift

//
// PurchaseButtonView.swift
// Reflect
//
// 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
private var textColor: Color { theme.currentTheme.labelColor }
@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) {
ReflectSubscriptionStoreView(source: "purchase_button")
}
.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)
}
.accessibilityIdentifier(AccessibilityID.Purchase.manageSubscriptionButton)
// 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)
}
.accessibilityIdentifier(AccessibilityID.Purchase.changePlanButton)
}
}
}
private var subscriptionStatusBadge: some View {
Group {
switch iapManager.state {
case .subscribed(_, let willAutoRenew):
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)
}
case .billingRetry, .gracePeriod:
Text("Payment Issue")
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.orange)
.cornerRadius(4)
default:
EmptyView()
}
}
}
// 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(source: "purchase_button")
}
} label: {
Text(String(localized: "purchase_view_restore"))
.font(.body)
.foregroundColor(.blue)
}
.accessibilityIdentifier(AccessibilityID.Purchase.restorePurchasesButton)
}
}
private var trialStatusView: some View {
HStack {
Image(systemName: "clock")
.foregroundColor(.orange)
if iapManager.daysLeftInTrial > 0 {
Text("\(Text(String(localized: "purchase_view_trial_expires_in")).foregroundColor(textColor)) \(Text("\(iapManager.daysLeftInTrial) days").foregroundColor(.orange).bold())")
} else {
Text(String(localized: "purchase_view_trial_expired"))
.foregroundColor(.orange)
.bold()
}
}
.font(.body)
}
}
#Preview {
PurchaseButtonView(iapManager: IAPManager())
}