TokiTrainer/Toki Trainer/Views/ContributeView.swift

318 lines
9.3 KiB
Swift

//
// ContributeView.swift
// Toki Trainer
//
// Created by Madeline Pace on 12/16/23.
//
import SwiftUI
import StoreKit
typealias Transaction = StoreKit.Transaction
public enum StoreError: Error {
case failedVerification
}
// MARK: ContributeView
struct ContributeView: View {
private let supportString = """
Hi 👋
I'm Maddie, the primary developer for Toki Trainer.
This app is free and open source. If you find it \
useful, please consider supporting my development efforts.
I don't collect your data and am committed to providing \
a high-quality, ad-free experience. Learning toki pona \
easily and comfortably is the whole point, and ads would \
ruin that!
Taking a moment to add an App Store Review helps a ton. \
Please also consider donating financially if you can. 💕
"""
private var recurringIAPs = ["TierThree"]
@EnvironmentObject var transactions: TransactionObserver
var body: some View {
VStack {
Spacer()
if transactions.hasDonated {
ThankYouBannerView(donationHearts: $transactions.donationHearts)
}
Text(supportString)
.padding(16)
Spacer()
// Link("Source Code", destination: URL(string: "https://git.corrupt.link/maddiefuzz/TokiTrainer")!)
// .padding(12)
// .padding([.bottom], 12)
HStack {
ReviewButton()
SourceCodeButton()
}
HStack {
SingleDonationButton()
RecurringDonationButton()
}
Spacer()
}
}
}
// MARK: ThankYouBannerView
struct ThankYouBannerView: View {
@Binding var donationHearts: Int
var body: some View {
ZStack {
Rectangle()
.fill(.blue)
.cornerRadius(15)
VStack {
Text("Thank you for donating!")
.multilineTextAlignment(.center)
.font(.title)
Text("\(donationHearts) 💕")
.padding(10)
.font(.title3)
}
}
.frame(width: 250, height: 140)
}
}
struct SourceCodeButton: View {
var body: some View {
Link(destination: URL(string: "https://git.corrupt.link/maddiefuzz/TokiTrainer")!) {
VStack {
Image(systemName: "apple.terminal")
.font(.system(size: 24, weight: .regular))
.padding(2)
Text("Source Code")
}
}
.frame(width: 100)
.padding(8)
}
}
// MARK: ReviewButton
struct ReviewButton: View {
@Environment(\.requestReview) private var requestReview
var body: some View {
Button(action: {
print("Review requested")
presentReviewRequest()
}, label: {
VStack {
Image(systemName: "star.bubble")
.font(.system(size: 24, weight: .regular))
.padding(2)
Text("Write Review")
}
})
.frame(width: 100)
.padding(8)
}
func presentReviewRequest() {
Task {
await requestReview()
}
}
}
// Workaround for .sheet(item:) expecting an Identifiable
struct DonationProducts: Identifiable {
let id = UUID()
var products = [Product]()
}
// MARK: SingleDonationButton
struct SingleDonationButton: View {
@Environment(\.purchase) private var purchase: PurchaseAction
@EnvironmentObject var transactions: TransactionObserver
@State private var IAPs: DonationProducts?
@State private var productsFetched = false
@State private var disabledPurchaseButtons = false
private var singleIAPs = ["SingleTimeTipTierOne",
"SingleTimeTipTierTwo",
"SingleTimeTipTierThree",
"SingleTimeTipTierFour"]
var body: some View {
Button {
Task {
try await loadSingleProducts()
}
} label: {
VStack {
Image(systemName: "dollarsign.circle")
.font(.system(size: 24, weight: .regular))
.padding(2)
Text("Donate Once")
}
}
.frame(width: 100)
.padding(8)
.sheet(item: self.$IAPs) { IAPs in
if(productsFetched) {
VStack {
Spacer()
Text("One-Time Donation")
.font(.largeTitle)
Spacer()
ForEach(IAPs.products) { product in
HStack {
VStack {
Text(product.displayName)
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
Text(product.description)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.leading, 12)
Spacer()
Button {
print("Purchase this one: \(product)")
disabledPurchaseButtons = true
Task {
let _ = try? await purchase(product)
disabledPurchaseButtons = false
}
} label: {
Text(product.displayPrice)
.frame(minWidth: 50)
.padding(8)
.fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/)
}
.padding(.trailing, 12)
.buttonStyle(.bordered)
.disabled(disabledPurchaseButtons)
}
.padding(12)
}
Spacer()
LegalLinksView()
.padding(16)
.padding([.bottom], 20)
}
//.padding([.top], 60)
} else {
ProgressView()
}
}
}
private func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await transaction.finish()
print("Purchase success")
transactions.processConsumable(transaction.productID)
return transaction
case .userCancelled, .pending:
return nil
default:
return nil
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
//Check whether the JWS passes StoreKit verification.
switch result {
case .unverified:
//StoreKit parses the JWS, but it fails verification.
throw StoreError.failedVerification
case .verified(let safe):
//The result is verified. Return the unwrapped value.
return safe
}
}
private func loadSingleProducts() async throws {
self.IAPs = DonationProducts()
self.IAPs?.products = try await Product.products(for: singleIAPs).sorted(by: { p1, p2 in
p1.price < p2.price
})
self.productsFetched = true
}
}
// MARK: RecurringDonationButton
struct RecurringDonationButton: View {
@State var toggleSheet = false
private let groupID = "21424772"
var body: some View {
Button {
print("Subscription button pressed")
toggleSheet.toggle()
} label: {
VStack {
Image(systemName: "dollarsign.arrow.circlepath")
.font(.system(size: 24, weight: .regular))
.padding(2)
Text("Donate Monthly")
}
}
.frame(width: 100)
.padding(8)
.sheet(isPresented: $toggleSheet, content: {
VStack {
SubscriptionStoreView(groupID: self.groupID)
// HStack {
// Link("Terms of Use", destination: URL(string: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/")!)
// .padding(12)
// Link("Privacy Policy", destination: URL(string: "https://maddie.info/null_privacy_policy")!)
// }
LegalLinksView()
.padding(16)
.padding([.bottom], 20)
}
})
}
}
struct LegalLinksView: View {
var body: some View {
HStack {
Link("Terms of Use", destination: URL(string: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/")!)
.padding(12)
Link("Privacy Policy", destination: URL(string: "https://maddie.info/null_privacy_policy")!)
}
}
}
#Preview {
ContributeView().environmentObject(TransactionObserver())
}
#Preview {
@State var donationHearts = 5000
return ThankYouBannerView(donationHearts: $donationHearts)
}