Skip to content
Open
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
55 changes: 51 additions & 4 deletions ios/TheTin/Sources/UI/Browse/CardDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,42 @@ struct CardDetailView: View {
return CardVariant.defaultFor(rarity: rarity).row(in: variants) ?? variants.first
}

/// What the history chart says about printings. The catalog keeps ONE weekly series per card:
/// PPT's `priceHistory.conditions`, which is its primary printing's history (byte-identical to
/// `priceHistory.variants[primaryPrinting]`, checked against Sceptile 2026-10-06), and
/// `price_latest.raw_printing` names that printing. So on a multi-printing card the chart can
/// be a different printing from the one the menu and the % badge are about — a reverse holo up
/// 400% drawn as a flat holofoil line. Until per-printing history ships, the chart says which
/// printing it is, and says so loudly when that isn't the selected one.
enum ChartPrintingNote: Equatable {
/// One printing: there is nothing to disambiguate.
case onePrinting
case matches(String)
case differs(chart: String, selected: String)
/// The catalog doesn't name a printing the card is priced in.
case unknown(selected: String)

var chart: String? {
switch self {
case .matches(let p), .differs(let p, _): p
case .onePrinting, .unknown: nil
}
}
}

/// `raw_printing` is trusted only when it names one of the card's own priced printings —
/// exactly, since both sides carry PPT's key. Older artifacts filled it from a TCGdex-derived
/// label ("Normal" for a card PPT only prices as "Holofoil"), and naming that would be wrong.
static func chartPrintingNote(chartPrinting: String?, selected: VariantPrice?,
variants: [VariantPrice]) -> ChartPrintingNote {
guard variants.count > 1, let selected else { return .onePrinting }
guard let chart = chartPrinting, variants.contains(where: { $0.printing == chart }) else {
return .unknown(selected: selected.printing)
}
return chart == selected.printing ? .matches(chart)
: .differs(chart: chart, selected: selected.printing)
}

var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Expand Down Expand Up @@ -827,11 +863,22 @@ struct CardDetailView: View {
Spacer()
if model.tier == .expert { overlayPickers }
}
Text("Weekly market price").font(.caption).foregroundStyle(.secondary)
let note = Self.chartPrintingNote(chartPrinting: model.price?.rawPrinting,
selected: currentPrinting, variants: model.variants)
Text(note.chart.map { "Weekly market price · \($0)" } ?? "Weekly market price")
.font(.caption).foregroundStyle(.secondary)
PriceHistoryChart(series: series)
if currentPrinting != nil {
Text("History is for the card overall — PPT has no per-printing history.")
.font(.caption2).foregroundStyle(.tertiary)
switch note {
case .differs(let chart, let selected):
Label("This chart is \(chart), not \(selected). History for other printings isn't in the catalog yet.",
systemImage: "info.circle")
.font(.caption).foregroundStyle(.secondary)
case .unknown(let selected):
Label("This chart is the card's main printing, which may not be \(selected).",
systemImage: "info.circle")
.font(.caption).foregroundStyle(.secondary)
case .onePrinting, .matches:
EmptyView()
}
}
case .empty where model.tier == .casual:
Expand Down
35 changes: 35 additions & 0 deletions ios/TheTin/Tests/CardDetailRoutingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,39 @@ final class CardDetailRoutingTests: XCTestCase {
XCTAssertEqual(h?.condition, .lp)
XCTAssertNil(CardHighlight(entry: entry(variant: nil, condition: nil)))
}

// MARK: Which printing the history chart is

private func note(chart: String?, selected: String?, _ variants: [VariantPrice]? = nil)
-> CardDetailView.ChartPrintingNote {
let vs = variants ?? printings
return CardDetailView.chartPrintingNote(chartPrinting: chart,
selected: vs.first { $0.printing == selected },
variants: vs)
}

/// The Sceptile report: the reverse holo is selected and up, the chart is PPT's primary
/// printing. The caption has to say the line isn't the printing above it.
func testAChartOfAnotherPrintingSaysWhichItIs() {
XCTAssertEqual(note(chart: "Holofoil", selected: "Reverse Holofoil"),
.differs(chart: "Holofoil", selected: "Reverse Holofoil"))
}

func testAChartOfTheSelectedPrintingIsLabelledQuietly() {
XCTAssertEqual(note(chart: "Holofoil", selected: "Holofoil"), .matches("Holofoil"))
}

/// An older artifact's TCGdex-derived label ("Normal" on a card PPT prices only as foils)
/// must not be presented as the chart's printing.
func testARawPrintingTheCardIsntPricedInIsNotNamed() {
XCTAssertEqual(note(chart: "Normal", selected: "Holofoil"), .unknown(selected: "Holofoil"))
XCTAssertEqual(note(chart: nil, selected: "Holofoil"), .unknown(selected: "Holofoil"))
}

/// One printing: no menu, so nothing for the chart to disagree with.
func testASinglePrintingCardsChartNeedsNoNote() {
let one = [VariantPrice(printing: "Holofoil", usd: 40)]
XCTAssertEqual(note(chart: "Holofoil", selected: nil, one), .onePrinting)
XCTAssertNil(note(chart: "Holofoil", selected: nil, one).chart)
}
}
Loading