diff --git a/FuelBoard/SettingsView.swift b/FuelBoard/SettingsView.swift index 96f79b0..5308827 100644 --- a/FuelBoard/SettingsView.swift +++ b/FuelBoard/SettingsView.swift @@ -721,7 +721,7 @@ final class TipStore: ObservableObject { // (or restored transactions) are picked up. do { let fetched = try await Product.products(for: Self.tiers.map(\.id)) - products = Dictionary(uniqueKeysWithValues: fetched.map { ($0.id, $0) }) + merge(fetched) } catch { // No products yet (sideloaded build) — the buttons still show the // intended prices and report purchase attempts gracefully. @@ -729,24 +729,37 @@ final class TipStore: ObservableObject { } } + private func merge(_ fetched: [Product]) { + products.merge(Dictionary(uniqueKeysWithValues: fetched.map { ($0.id, $0) })) { _, new in new } + } + + private func product(for tier: TipTier) async throws -> Product? { + if let product = products[tier.id] { return product } + + // TestFlight/App Store Connect product availability can lag the Settings + // onAppear load. Re-query the tapped tier before falling back so a newly + // approved IAP opens the system purchase sheet instead of showing stale + // "not available" copy. + let fetched = try await Product.products(for: [tier.id]) + merge(fetched) + return fetched.first { $0.id == tier.id } + } + func purchase(_ tier: TipTier) async { guard !purchaseInProgress else { return } purchaseInProgress = true defer { purchaseInProgress = false } - // If the product hasn't loaded (e.g. not configured in App Store - // Connect yet), still allow the attempt so the user sees a clear - // outcome rather than a dead button. - guard let product = products[tier.id] else { - showOutcome( - NSLocalizedString("The tip isn't available in this build yet — check back after an App Store release.", comment: ""), - icon: "info.circle.fill", - tint: Color(.secondaryLabel) - ) - return - } - do { + guard let product = try await product(for: tier) else { + showOutcome( + NSLocalizedString("This tip isn't available from the App Store yet. Please try again shortly.", comment: ""), + icon: "info.circle.fill", + tint: Color(.secondaryLabel) + ) + return + } + let result = try await product.purchase() switch result { case .success(let verification):