Exhaustive file-by-file audit of every Swift file in the project (iOS app, Watch app, Widget extension). Every interactive UI element — buttons, toggles, pickers, links, menus, tap gestures, text editors, color pickers, photo pickers — now has an accessibilityIdentifier for XCUITest automation. 46 files changed across Shared/, Onboarding/, Watch App/, and Widget targets. Added ~100 new ID definitions covering settings debug controls, export/photo views, sharing templates, customization subviews, onboarding flows, tip modals, widget voting buttons, and watch mood buttons.
214 lines
7.0 KiB
Swift
214 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)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityID.Purchase.subscribeButton)
|
|
|
|
// 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())
|
|
}
|