120 lines
4.4 KiB
Swift
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))
|
|
}
|
|
}
|