IceCubes/IceCubesApp/App/Tabs/Settings/SupportAppView.swift

272 lines
7.6 KiB
Swift
Raw Normal View History

2023-01-07 13:44:13 +01:00
import DesignSystem
2023-01-17 11:36:01 +01:00
import Env
2023-01-07 13:44:13 +01:00
import RevenueCat
2023-01-10 08:24:05 +01:00
import Shimmer
2023-01-17 11:36:01 +01:00
import SwiftUI
2023-01-07 13:44:13 +01:00
struct SupportAppView: View {
2023-02-21 18:52:30 +01:00
enum Tip: String, CaseIterable {
case one, two, three, four, supporter
2023-01-17 11:36:01 +01:00
2023-01-07 13:44:13 +01:00
init(productId: String) {
self = .init(rawValue: String(productId.split(separator: ".")[2]))!
}
2023-01-17 11:36:01 +01:00
2023-01-07 13:44:13 +01:00
var productId: String {
"icecubes.tipjar.\(rawValue)"
}
2023-01-22 06:38:30 +01:00
var title: LocalizedStringKey {
2023-01-07 13:44:13 +01:00
switch self {
case .one:
return "settings.support.one.title"
2023-01-07 13:44:13 +01:00
case .two:
return "settings.support.two.title"
2023-01-07 13:44:13 +01:00
case .three:
return "settings.support.three.title"
2023-01-22 12:27:00 +01:00
case .four:
return "settings.support.four.title"
case .supporter:
return "settings.support.supporter.title"
2023-01-07 13:44:13 +01:00
}
}
2023-01-22 06:38:30 +01:00
var subtitle: LocalizedStringKey {
2023-01-07 13:44:13 +01:00
switch self {
case .one:
return "settings.support.one.subtitle"
2023-01-07 13:44:13 +01:00
case .two:
return "settings.support.two.subtitle"
2023-01-07 13:44:13 +01:00
case .three:
return "settings.support.three.subtitle"
2023-01-22 12:27:00 +01:00
case .four:
return "settings.support.four.subtitle"
case .supporter:
return "settings.support.supporter.subtitle"
2023-01-07 13:44:13 +01:00
}
}
}
2023-01-17 11:36:01 +01:00
2023-01-07 13:44:13 +01:00
@EnvironmentObject private var theme: Theme
2023-03-13 13:38:28 +01:00
@Environment(\.openURL) private var openURL
2023-01-17 11:36:01 +01:00
2023-01-07 13:44:13 +01:00
@State private var loadingProducts: Bool = false
@State private var products: [StoreProduct] = []
@State private var subscription: StoreProduct?
@State private var customerInfo: CustomerInfo?
2023-01-07 13:44:13 +01:00
@State private var isProcessingPurchase: Bool = false
@State private var purchaseSuccessDisplayed: Bool = false
@State private var purchaseErrorDisplayed: Bool = false
2023-01-17 11:36:01 +01:00
2023-01-07 13:44:13 +01:00
var body: some View {
Form {
aboutSection
subscriptionSection
tipsSection
restorePurchase
linksSection
2023-01-07 13:44:13 +01:00
}
.navigationTitle("settings.support.navigation-title")
2023-01-07 13:44:13 +01:00
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
2023-01-07 13:44:13 +01:00
}, message: {
Text("settings.support.alert.message")
2023-01-07 13:44:13 +01:00
})
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
2023-01-07 13:44:13 +01:00
}, message: {
Text("settings.support.alert.error.message")
2023-01-07 13:44:13 +01:00
})
.onAppear {
loadingProducts = true
fetchStoreProducts()
refreshUserInfo()
}
}
2023-03-13 13:38:28 +01:00
private func purchase(product: StoreProduct) async {
if !isProcessingPurchase {
isProcessingPurchase = true
do {
let result = try await Purchases.shared.purchase(product: product)
if !result.userCancelled {
purchaseSuccessDisplayed = true
2023-01-10 08:24:05 +01:00
}
} catch {
purchaseErrorDisplayed = true
}
isProcessingPurchase = false
}
}
2023-03-13 13:38:28 +01:00
private func fetchStoreProducts() {
Purchases.shared.getProducts(Tip.allCases.map { $0.productId }) { products in
self.subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId })
2023-03-13 13:38:28 +01:00
self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted(by: { $0.price < $1.price })
withAnimation {
loadingProducts = false
}
}
}
2023-03-13 13:38:28 +01:00
private func refreshUserInfo() {
Purchases.shared.getCustomerInfo { info, _ in
self.customerInfo = info
}
}
2023-03-13 13:38:28 +01:00
private func makePurchaseButton(product: StoreProduct) -> some View {
Button {
Task {
await purchase(product: product)
refreshUserInfo()
}
} label: {
if isProcessingPurchase {
ProgressView()
} else {
Text(product.localizedPriceString)
}
}
.buttonStyle(.bordered)
}
2023-03-13 13:38:28 +01:00
private var aboutSection: some View {
Section {
HStack(alignment: .top, spacing: 12) {
VStack(spacing: 18) {
Image("avatar")
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
Image("icon0")
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(4)
}
Text("settings.support.message-from-dev")
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
2023-03-13 13:38:28 +01:00
private var subscriptionSection: some View {
Section {
if loadingProducts {
loadingPlaceholder
} else if let subscription {
HStack {
2023-03-01 21:14:26 +01:00
if customerInfo?.entitlements["Supporter"]?.isActive == true {
Text(Image(systemName: "checkmark.seal.fill"))
.foregroundColor(theme.tintColor)
.baselineOffset(-1) +
2023-03-13 13:38:28 +01:00
Text("settings.support.supporter.subscribed")
.font(.scaledSubheadline)
} else {
VStack(alignment: .leading) {
Text(Image(systemName: "checkmark.seal.fill"))
.foregroundColor(theme.tintColor)
.baselineOffset(-1) +
2023-03-13 13:38:28 +01:00
Text(Tip.supporter.title)
.font(.scaledSubheadline)
Text(Tip.supporter.subtitle)
.font(.scaledFootnote)
.foregroundColor(.gray)
}
Spacer()
makePurchaseButton(product: subscription)
}
}
.padding(.vertical, 8)
}
} footer: {
if customerInfo?.entitlements.active.isEmpty == true {
Text("settings.support.supporter.subscription-info")
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
2023-03-13 13:38:28 +01:00
private var tipsSection: some View {
Section {
if loadingProducts {
loadingPlaceholder
} else {
ForEach(products, id: \.productIdentifier) { product in
let tip = Tip(productId: product.productIdentifier)
HStack {
VStack(alignment: .leading) {
Text(tip.title)
.font(.scaledSubheadline)
Text(tip.subtitle)
.font(.scaledFootnote)
.foregroundColor(.gray)
}
Spacer()
makePurchaseButton(product: product)
}
.padding(.vertical, 8)
}
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
2023-03-13 13:38:28 +01:00
private var restorePurchase: some View {
Section {
HStack {
Spacer()
Button {
Purchases.shared.restorePurchases { info, _ in
self.customerInfo = info
}
} label: {
Text("settings.support.restore-purchase.button")
}.buttonStyle(.bordered)
Spacer()
}
} footer: {
Text("settings.support.restore-purchase.explanation")
}
.listRowBackground(theme.secondaryBackgroundColor)
}
2023-03-13 13:38:28 +01:00
private var linksSection: some View {
Section {
VStack(alignment: .leading, spacing: 16) {
Button {
openURL(URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!)
} label: {
Text("settings.support.privacy-policy")
}
.buttonStyle(.borderless)
Button {
openURL(URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!)
} label: {
Text("settings.support.terms-of-use")
}
.buttonStyle(.borderless)
}
}
.listRowBackground(theme.secondaryBackgroundColor)
}
2023-03-13 13:38:28 +01:00
private var loadingPlaceholder: some View {
HStack {
VStack(alignment: .leading) {
Text("placeholder.loading.short.")
.font(.scaledSubheadline)
Text("settings.support.placeholder.loading-subtitle")
.font(.scaledFootnote)
.foregroundColor(.gray)
2023-01-07 13:44:13 +01:00
}
.padding(.vertical, 8)
2023-01-07 13:44:13 +01:00
}
.redacted(reason: .placeholder)
.shimmering()
2023-01-07 13:44:13 +01:00
}
}