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.
76 lines
2.3 KiB
Swift
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
|