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.
213 lines
7.0 KiB
Swift
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())
|
|
}
|