Unit tests und nudging screen

This commit is contained in:
2026-04-20 11:10:53 +02:00
parent e1aebdb916
commit 3858500a45
8 changed files with 1223 additions and 0 deletions
@@ -0,0 +1,160 @@
import SwiftUI
import StoreKit
/// Nudge sheet asking the user to support development.
/// Shown automatically 3 days after first launch, then every 6 months.
struct SupportNudgeView: View {
@Environment(MAStoreManager.self) private var storeManager
@Binding var isPresented: Bool
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 0) {
// MARK: - Hero
VStack(spacing: 16) {
ZStack {
Circle()
.fill(
LinearGradient(
colors: [Color.orange, Color.pink],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 80, height: 80)
.shadow(color: .orange.opacity(0.4), radius: 16, y: 6)
Image(systemName: "heart.fill")
.font(.system(size: 36))
.foregroundStyle(.white)
}
.padding(.top, 32)
Text("Keep Mobile MA Growing")
.font(.title2.weight(.bold))
.multilineTextAlignment(.center)
Text("Mobile MA is a free, passion-driven app. If it brings music to your life, a small donation helps keep it alive and growing.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
}
.padding(.bottom, 32)
// MARK: - Tiers
VStack(spacing: 12) {
if storeManager.products.isEmpty {
ProgressView()
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
} else {
ForEach(storeManager.products, id: \.id) { product in
TierRow(product: product, storeManager: storeManager, isPresented: $isPresented)
}
}
}
.padding(.horizontal, 20)
// MARK: - Dismiss
Button {
isPresented = false
} label: {
Text("Maybe Later")
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.vertical, 20)
}
.buttonStyle(.plain)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
isPresented = false
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
.font(.title3)
}
.buttonStyle(.plain)
}
}
}
.task {
if storeManager.products.isEmpty {
await storeManager.loadProducts()
}
}
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
}
// MARK: - Tier Row
private struct TierRow: View {
let product: Product
let storeManager: MAStoreManager
@Binding var isPresented: Bool
private var accentColor: Color {
switch product.id {
case "donatesong": return .teal
case "donatealbum": return .orange
case "donateanthology": return .purple
default: return .pink
}
}
var body: some View {
HStack(spacing: 14) {
// Icon
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(accentColor.opacity(0.15))
.frame(width: 48, height: 48)
Image(systemName: storeManager.iconName(for: product))
.font(.title3)
.foregroundStyle(accentColor)
}
// Info
VStack(alignment: .leading, spacing: 2) {
Text(storeManager.tierName(for: product))
.font(.body.weight(.medium))
Text(product.description)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer()
// Buy button
Button {
Task {
await storeManager.purchase(product)
if case .success = storeManager.purchaseResult {
isPresented = false
}
}
} label: {
Text(product.displayPrice)
.font(.subheadline.weight(.semibold))
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(accentColor.opacity(0.15))
.foregroundStyle(accentColor)
.clipShape(Capsule())
}
.buttonStyle(.plain)
.disabled(storeManager.isPurchasing)
}
.padding(14)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
}
}