Files
Outpost/Outpost/Features/About/TipJarView.swift
T
Puranjay Savar Mattas 5de445daa5 feat(app): rewire account footer menu - App Store feedback, support link
Center-aligned TipJarView's "Support Outpost" heading and thank-you/
error text to match the rest of AboutInfoView (was VStack(alignment:
.leading), out of place among everything else there being centered).

AccountFooter's Documentation/API Documentation/Changelog links
pointed at Outpost's own repo or the signed-in Outline server's own
/developers page - not actually useful here, removed along with the
now-unused repositoryURL/issuesURL/apiDocumentationURL. Send Us
Feedback and Report a Bug (previously both just opening the Gitea
issues page) collapsed into one "Leave Us Feedback" wired to Apple's
native requestReview() prompt - there's no separate Apple-native
channel for "bug" vs "feedback", so one button covers both.

Added "Support Outpost" near the bottom of the same menu, alongside
Profile/Settings (same visual weight, not pinned to the top) - opens
Settings -> About, same navigation the app-menu's "About Outpost"
command already uses.

Also checking in the shared Xcode scheme (previously untracked/
nonexistent) now that it references Configuration.storekit, so the
StoreKit testing setup travels with the repo instead of being
machine-local.

Not compiler-verified - Outpost app target has no CLI build path.
2026-08-21 01:38:11 +01:00

76 lines
2.3 KiB
Swift

#if os(macOS)
import SwiftUI
import StoreKit
/// One button per consumable tip tier — no "restore purchases" (nothing to
/// restore, consumables aren't entitlements) and no manual retry: a failed
/// load just shows a message, tapping a tier again re-attempts naturally.
struct TipJarView: View {
@State private var store = TipJarStore()
var body: some View {
VStack(spacing: 8) {
Text("Support Outpost")
.font(.callout.weight(.semibold))
if store.isLoading && store.products.isEmpty {
ProgressView()
.controlSize(.small)
} else if !store.products.isEmpty {
HStack(spacing: 8) {
ForEach(store.products) { product in
tipButton(for: product)
}
}
}
switch store.purchaseState {
case .thankYou:
Label("Thank you!", systemImage: "heart.fill")
.font(.caption)
.foregroundStyle(.pink)
case .failed(let message):
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
case .idle, .purchasing:
EmptyView()
}
}
.task { await store.loadProductsIfNeeded() }
}
private func tipButton(for product: Product) -> some View {
Button {
Task { await store.purchase(product) }
} label: {
VStack(spacing: 2) {
if isPurchasing(product) {
ProgressView()
.controlSize(.small)
} else {
Text(product.displayPrice)
.font(.callout.weight(.semibold))
}
Text(product.displayName)
.font(.caption2)
.foregroundStyle(.secondary)
}
.frame(minWidth: 64)
.padding(.vertical, 6)
}
.buttonStyle(.bordered)
.disabled(isAnyPurchaseInFlight)
}
private func isPurchasing(_ product: Product) -> Bool {
store.purchaseState == .purchasing(product.id)
}
private var isAnyPurchaseInFlight: Bool {
if case .purchasing = store.purchaseState { return true }
return false
}
}
#endif