Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions Packages/ShowRecorderKit/Sources/Recording/License.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
}
23 changes: 21 additions & 2 deletions Packages/ShowRecorderKit/Sources/Recording/PackOffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<MixerFamily>) -> 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<MixerFamily>) -> 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) ?? ""
Expand All @@ -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"
Expand Down
28 changes: 28 additions & 0 deletions Packages/ShowRecorderKit/Tests/RecordingTests/PackOfferTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
}
Loading