unified ProAccessControllerError

This commit is contained in:
Kaede Akino committed 2026-09-02 07:34:14 +08:00
1 parent dc4bd34414
commit 54ea515750
3 files changed
+71 -25

No files matched your search

+2
View File
@@ -53,6 +53,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
DBE378B130478CFB00B16AA1 /* Termind.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = Termind.storekit; path = StoreKit/Termind.storekit; sourceTree = "<group>"; };
TRMD00000000000000000001 /* Termind.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Termind.app; sourceTree = BUILT_PRODUCTS_DIR; };
TRMD00000000000000020001 /* TermindTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TermindTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
TRMD00000000000000030002 /* termind-mcp */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "termind-mcp"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -128,6 +129,7 @@
TRMD00000000000000000030 = {
isa = PBXGroup;
children = (
DBE378B130478CFB00B16AA1 /* Termind.storekit */,
TRMD00000000000000000010 /* Termind */,
TRMD00000000000000020010 /* TermindTests */,
TRMD00000000000000030010 /* TermindMCPBridge */,
@@ -89,6 +89,13 @@
ReferencedContainer = "container:Termind.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
<AdditionalOption
key = "APP_DISTRIBUTOR_ID_OVERRIDE"
value = "com.apple.AppStore"
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
+62 -25
View File
@@ -19,7 +19,7 @@ final class ProAccessController {
private(set) var status: EntitlementStatus
private(set) var product: Product?
private(set) var message: String?
private(set) var error: ProAccessControllerError?
private(set) var workspaceCompositionGrantedForLaunch: Bool
private(set) var isReviewDemoModeEnabled: Bool
var isPaywallPresented = false
@@ -27,6 +27,7 @@ final class ProAccessController {
var displayPrice: String? { product?.displayPrice }
var hasProAccess: Bool { status.hasProAccess }
var message: String? { error?.localizedDescription }
static func live() -> ProAccessController {
ProAccessController(trialStore: TrialEligibilityStore(), reviewDefaults: .standard)
@@ -105,7 +106,7 @@ final class ProAccessController {
reviewDefaults?.set(true, forKey: Self.reviewDemoDefaultsKey)
workspaceCompositionGrantedForLaunch = false
requestedFeature = nil
message = nil
error = nil
apply(.trialExpired)
return true
}
@@ -119,7 +120,7 @@ final class ProAccessController {
if status == .unrestrictedForTesting { return }
refreshGeneration &+= 1
let generation = refreshGeneration
message = nil
error = nil
async let ownership = currentOwnership()
Task { [weak self] in await self?.loadProductMetadata(generation: generation) }
@@ -158,28 +159,29 @@ final class ProAccessController {
func startTrial() async {
guard let trialStore else { return }
refreshGeneration &+= 1
error = nil
do {
apply(Self.status(for: try await trialStore.start()))
if !status.hasProAccess {
message = String(localized: "This Apple account or Mac has already used the free trial.")
error = .trialAlreadyUsed
} else {
isPaywallPresented = false
}
} catch {
message = error.localizedDescription
} catch let startError {
error = .trialStartFailed(startError.localizedDescription)
}
}
func purchase() async {
refreshGeneration &+= 1
message = nil
error = nil
do {
let product = try await loadProduct()
switch try await product.purchase() {
case .success(let verification):
guard case .verified(let transaction) = verification else {
if !status.hasProAccess { apply(.verificationFailed) }
message = String(localized: "The App Store could not verify this purchase.")
error = .purchaseVerificationFailed
return
}
await transaction.finish()
@@ -188,36 +190,39 @@ final class ProAccessController {
isPaywallPresented = false
case .pending:
if !status.hasProAccess { apply(.purchasePending) }
message = String(localized: "The purchase is waiting for App Store approval.")
error = .purchasePending
case .userCancelled:
break
@unknown default:
break
}
} catch {
message = error.localizedDescription
} catch let purchaseError {
error = Self.proAccessError(
purchaseError,
fallback: .purchaseFailed(purchaseError.localizedDescription)
)
}
}
func restorePurchases() async {
message = nil
error = nil
do {
try await AppStore.sync()
await refresh()
if !status.hasProAccess {
message = String(localized: "No Termind Pro purchase was found for this Apple account.")
error = .purchaseNotFound
} else {
isPaywallPresented = false
}
} catch {
message = error.localizedDescription
} catch let restoreError {
error = .restoreFailed(restoreError.localizedDescription)
}
}
private func loadProduct() async throws -> Product {
if let product { return product }
guard let loaded = try await Product.products(for: [Self.productID]).first else {
throw ProPurchaseError.productUnavailable
throw ProAccessControllerError.productUnavailable
}
product = loaded
return loaded
@@ -230,9 +235,7 @@ final class ProAccessController {
product = loaded
} catch {
guard generation == refreshGeneration else { return }
message = String(
localized: "The App Store is temporarily unavailable. You can keep using already unlocked features."
)
self.error = .productMetadataUnavailable
}
}
@@ -277,12 +280,46 @@ final class ProAccessController {
case .expired, .recoveryRequired: .trialExpired
}
}
}
private enum ProPurchaseError: LocalizedError {
case productUnavailable
var errorDescription: String? {
String(localized: "Termind Pro is not available from the App Store right now.")
private static func proAccessError(
_ error: any Error,
fallback: @autoclosure () -> ProAccessControllerError
) -> ProAccessControllerError {
error as? ProAccessControllerError ?? fallback()
}
}
nonisolated enum ProAccessControllerError: LocalizedError, Equatable, Sendable {
case productUnavailable
case productMetadataUnavailable
case trialAlreadyUsed
case trialStartFailed(String)
case purchaseVerificationFailed
case purchasePending
case purchaseFailed(String)
case purchaseNotFound
case restoreFailed(String)
var errorDescription: String? {
switch self {
case .productUnavailable:
String(localized: "Termind Pro is not available from the App Store right now.")
case .productMetadataUnavailable:
String(
localized: "The App Store is temporarily unavailable. You can keep using already unlocked features."
)
case .trialAlreadyUsed:
String(localized: "This Apple account or Mac has already used the free trial.")
case .trialStartFailed(let description),
.purchaseFailed(let description),
.restoreFailed(let description):
description
case .purchaseVerificationFailed:
String(localized: "The App Store could not verify this purchase.")
case .purchasePending:
String(localized: "The purchase is waiting for App Store approval.")
case .purchaseNotFound:
String(localized: "No Termind Pro purchase was found for this Apple account.")
}
}
}