diff --git a/Packages/ShowRecorderKit/Sources/Recording/License.swift b/Packages/ShowRecorderKit/Sources/Recording/License.swift index 4a31a6c6..99307ddc 100644 --- a/Packages/ShowRecorderKit/Sources/Recording/License.swift +++ b/Packages/ShowRecorderKit/Sources/Recording/License.swift @@ -1,4 +1,5 @@ import Foundation +import MixerLink /// What the operator may do right now (ADR 0004, CONTEXT.md): every Pro, Free and Trial question is answered here, /// from the tier and whether Pro is offered. It is read from what the App Store last said and never waits on it, and @@ -88,9 +89,13 @@ public struct ProStatus: Equatable, Sendable { return knownTier(now: now) != before || purchases.ownedPacks != packsBefore } - public func license(proOffered: Bool, now: Date) -> License { - License(proOffered: proOffered, tier: knownTier(now: now), trialStarted: purchases.trialStartedAt != nil) + /// - Parameter armedFamily: the Armed input's mixer family, so a pack for it counts as Pro (#705); nil for any other + /// input, or where nothing is Armed. + public func license(proOffered: Bool, armedFamily: MixerFamily? = nil, now: Date) -> License { + License(proOffered: proOffered, tier: knownTier(now: now, armedFamily: armedFamily), trialStarted: purchases.trialStartedAt != nil) } - private func knownTier(now: Date) -> Tier? { tierKnown ? purchases.tier(now: now) : nil } + private func knownTier(now: Date, armedFamily: MixerFamily? = nil) -> Tier? { + tierKnown ? purchases.tier(now: now, armedFamily: armedFamily) : nil + } } diff --git a/Packages/ShowRecorderKit/Sources/Recording/PackOffer.swift b/Packages/ShowRecorderKit/Sources/Recording/PackOffer.swift index e40b493e..f4523006 100644 --- a/Packages/ShowRecorderKit/Sources/Recording/PackOffer.swift +++ b/Packages/ShowRecorderKit/Sources/Recording/PackOffer.swift @@ -42,6 +42,25 @@ public enum PackOffer { return [.pro] } + /// How Settings names the plan (#705): a pack owner's plan is their pack ("X Air pack"), unless Pro is owned or a + /// Trial is running, which say more. Pass the tier without the Armed family (`license(proOffered:now:)`), so the + /// line doesn't change with the input and a running Trial isn't hidden by a pack on the Armed family. + public static func planTitle(tier: Tier?, ownsPro: Bool, ownedPacks: Set) -> String { + guard let tier else { return "Checking with the App Store…" } + if ownsPro { return Tier.pro.title } + if case .trial = tier { return tier.title } + if let pack = ownedPacks.sorted(by: { $0.name < $1.name }).first { return "\(pack.name) pack" } + return tier.title + } + + /// What Restore Purchases says once the App Store has synced (#705): nothing when Pro came back, the pack when only + /// a pack did, else that no purchase was found. + public static func restoreNote(ownsPro: Bool, ownedPacks: Set) -> String? { + if ownsPro { return nil } + if let pack = ownedPacks.sorted(by: { $0.name < $1.name }).first { return "Restored: \(pack.name) pack" } + return "No Pro purchase found for this Apple Account." + } + /// The pack headline (#705): the linked Mixer's model, else the family's name. public static func headline(family: MixerFamily, mixerModel: String?) -> String { let model = mixerModel?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" @@ -50,8 +69,8 @@ public enum PackOffer { } extension MixerFamily { - /// The family's name in the paywall (#705). - var name: String { + /// The family's name in copy: "X Air", "X32" (#705). + public var name: String { switch self { case .xAir: "X Air" case .x32: "X32" diff --git a/Packages/ShowRecorderKit/Tests/RecordingTests/PackOfferTests.swift b/Packages/ShowRecorderKit/Tests/RecordingTests/PackOfferTests.swift index 1f0dbb7b..3b039076 100644 --- a/Packages/ShowRecorderKit/Tests/RecordingTests/PackOfferTests.swift +++ b/Packages/ShowRecorderKit/Tests/RecordingTests/PackOfferTests.swift @@ -61,4 +61,32 @@ struct PackOfferTests { #expect(PackAvailability.isAvailable(isDebugBuild: true)) #expect(!PackAvailability.isAvailable(isDebugBuild: false)) } + + @Test("The license honors a pack on the Armed family: Pro on an X Air with the X Air pack, Free on an X32 (#705)") + func licenseWithFamily() { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let pack = OwnedPurchase(productID: ProPurchases.packProductIDs[.xAir]!, purchaseDate: now) + var status = ProStatus(lastKnown: nil) + _ = status.appStoreAnswered([pack], now: now) + #expect(status.license(proOffered: true, armedFamily: .xAir, now: now).tier == .pro) + #expect(status.license(proOffered: true, armedFamily: .x32, now: now).tier == .free) + #expect(status.license(proOffered: true, now: now).tier == .free, "no family: as before") + } + + @Test("Settings names a pack owner's plan after the pack, unless Pro or a running Trial says more (#705)") + func planTitle() { + #expect(PackOffer.planTitle(tier: .free, ownsPro: false, ownedPacks: [.xAir]) == "X Air pack") + #expect(PackOffer.planTitle(tier: .pro, ownsPro: false, ownedPacks: [.x32]) == "X32 pack", "Pro on its own family, still a pack") + #expect(PackOffer.planTitle(tier: .pro, ownsPro: true, ownedPacks: [.xAir]) == "Pro") + #expect(PackOffer.planTitle(tier: .trial(daysLeft: 3), ownsPro: false, ownedPacks: [.xAir]) == "14-day Trial, 3 days left") + #expect(PackOffer.planTitle(tier: .free, ownsPro: false, ownedPacks: []) == "Free") + #expect(PackOffer.planTitle(tier: nil, ownsPro: false, ownedPacks: []) == "Checking with the App Store…") + } + + @Test("Restore says what came back: Pro, a pack, or nothing (Eric, #705)") + func restoreNote() { + #expect(PackOffer.restoreNote(ownsPro: true, ownedPacks: [.xAir]) == nil) + #expect(PackOffer.restoreNote(ownsPro: false, ownedPacks: [.xAir]) == "Restored: X Air pack") + #expect(PackOffer.restoreNote(ownsPro: false, ownedPacks: []) == "No Pro purchase found for this Apple Account.") + } }