Files

120 lines
4.4 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import RevenueCatUI
import SwiftUI
import TermindDomain
import TermindInfrastructure
import TermindPresentation
struct ProPaywallView: View {
@Environment(AppModel.self) private var app
@Environment(\.dismiss) private var dismiss
@Environment(\.locale) private var locale
@State private var ignoresPurchaseDismissal = false
private var pro: ProAccessController { app.proAccess }
var body: some View {
VStack(spacing: 0) {
if pro.status == .loading {
ProgressView(AppStrings.Pro.contactingTheAppStoreProgress.swiftUIKey)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(alignment: .topLeading) { closeButton }
} else if pro.usesRevenueCat {
PaywallView(displayCloseButton: true)
.customPaywallVariables(paywallVariables)
.onPurchaseInitiated { [pro] _, resume in
Task { @MainActor in resume(shouldProceed: !pro.hasProAccess) }
}
.onPurchaseCompleted { _ in
ignoresPurchaseDismissal = true
Task { await pro.completePaywallOperation() }
}
.onRestoreCompleted { _ in Task { await pro.completePaywallOperation() } }
.onRequestedDismissal {
// RevenueCat requests dismissal after purchase; keep the account variant visible.
if ignoresPurchaseDismissal {
ignoresPurchaseDismissal = false
} else {
dismiss()
}
}
} else {
ContentUnavailableView {
Label(AppStrings.Pro.termindPro, systemImage: "cart")
} description: {
Text(AppStrings.Pro.productUnavailable)
} actions: {
Button(AppStrings.Common.close) { dismiss() }
}
}
}
.frame(width: 604, height: 720)
.termindInputAwareFocusEffect()
.onChange(of: pro.isPaywallPresented) { if !pro.isPaywallPresented { dismiss() } }
}
private var paywallVariables: [String: CustomVariableValue] {
let account = ProPaywallState(status: pro.status)
return [
"pro": .bool(account.hasProAccess),
"manage": .bool(account.canManageSubscription),
"state": .string(account.state),
"family": .bool(account.isFamilyShared),
"dated": .bool(account.expiresAt != nil),
"date": .string(
account.expiresAt?.formatted(
.dateTime.year().month().day().hour().minute().locale(locale)) ?? ""),
]
}
private var closeButton: some View {
Button {
dismiss()
} label: {
Label(AppStrings.Common.close, systemImage: "xmark")
.labelStyle(.iconOnly)
.font(.system(size: 20, weight: .medium))
.frame(width: 44, height: 44)
.contentShape(.rect)
}
.buttonStyle(.plain)
.padding(16)
}
}
struct ProPaywallPresenter: ViewModifier {
let app: AppModel
let window: WindowModel
func body(content: Content) -> some View {
content
.onChange(of: app.proAccess.isPaywallPresented, initial: true) {
app.mainWindows.synchronizePaywall()
}
.sheet(
isPresented: Binding(
get: {
app.proAccess.isPaywallPresented
&& app.mainWindows.paywallOwnerID == ObjectIdentifier(window)
},
set: { presented in
guard app.mainWindows.paywallOwnerID == ObjectIdentifier(window) else { return }
app.proAccess.isPaywallPresented = presented
app.mainWindows.synchronizePaywall()
}
)
) {
ProPaywallView().environment(app)
.allowsApplicationTerminationWhenModal()
}
}
}
extension View {
func proPaywallPresenter(app: AppModel, window: WindowModel) -> some View {
modifier(ProPaywallPresenter(app: app, window: window))
}
}