You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
395 lines
13 KiB
395 lines
13 KiB
//
|
|
// SubscriptionView.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Laurent Morvillier on 13/02/2024.
|
|
//
|
|
|
|
import SwiftUI
|
|
import StoreKit
|
|
import LeStorage
|
|
|
|
extension Product.SubscriptionPeriod.Unit {
|
|
var label: String {
|
|
switch self {
|
|
case .day: return "jour"
|
|
case .week: return "semaine"
|
|
case .month: return "mois"
|
|
case .year: return "année"
|
|
@unknown default: return "inconnu"
|
|
}
|
|
}
|
|
}
|
|
extension Product {
|
|
var item: StoreItem {
|
|
return StoreItem(rawValue: self.id)!
|
|
}
|
|
var formattedPrice: String {
|
|
if let period = self.subscription?.subscriptionPeriod {
|
|
return self.displayPrice + " / " + period.unit.label
|
|
}
|
|
return self.displayPrice
|
|
}
|
|
}
|
|
|
|
class SubscriptionModel: ObservableObject, StoreDelegate {
|
|
|
|
var storeManager: StoreManager? = nil
|
|
@Published var error: Error? = nil
|
|
|
|
@Published var isLoading: Bool = false
|
|
@Published var selectedProduct: Product? = nil {
|
|
didSet {
|
|
self._computePrice()
|
|
}
|
|
}
|
|
@Published var quantity: Int = 1 {
|
|
didSet {
|
|
self._computePrice()
|
|
self.selectedProduct = self.products.first(where: { $0.id == StoreItem.unit.rawValue })
|
|
}
|
|
}
|
|
@Published var products: [Product] = []
|
|
@Published var totalPrice: String = ""
|
|
|
|
init() {
|
|
Logger.log("SubscriptionModel init ")
|
|
}
|
|
|
|
func load() {
|
|
self.isLoading = true
|
|
if self.storeManager == nil {
|
|
self.storeManager = StoreManager(delegate: self)
|
|
}
|
|
}
|
|
|
|
func productsReceived(products: [Product]) {
|
|
self.isLoading = false
|
|
self.products = products
|
|
Logger.log("products received = \(products.count)")
|
|
}
|
|
|
|
func errorDidOccur(error: Error) {
|
|
self.isLoading = false
|
|
self.error = error
|
|
}
|
|
|
|
func purchase() async throws -> Bool {
|
|
Logger.log("start purchase...")
|
|
guard let product: Product = self.selectedProduct, let storeManager = self.storeManager else {
|
|
Logger.w("missing product or store manager")
|
|
return false
|
|
}
|
|
|
|
if product.item.isConsumable {
|
|
if let _ = try await storeManager.purchase(product, quantity: self.quantity) {
|
|
return true
|
|
}
|
|
} else {
|
|
let _ = try await storeManager.purchase(product)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fileprivate func _computePrice() {
|
|
|
|
if let product = self.selectedProduct, let item = StoreItem(rawValue: product.id) {
|
|
if item.isConsumable {
|
|
let price = NSDecimalNumber(decimal: product.price).multiplying(by: NSDecimalNumber(integerLiteral: self.quantity))
|
|
self.totalPrice = product.priceFormatStyle.format(price.decimalValue)
|
|
} else {
|
|
self.totalPrice = product.displayPrice
|
|
}
|
|
} else {
|
|
self.totalPrice = ""
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct SubscriptionView: View {
|
|
|
|
@ObservedObject var model: SubscriptionModel = SubscriptionModel()
|
|
|
|
@Binding var isPresented: Bool
|
|
|
|
var showLackOfPlanMessage: Bool = false
|
|
@State var isRestoring: Bool = false
|
|
@State var showLoginView: Bool = false
|
|
@State var isPurchasing: Bool = false
|
|
@State var showSuccessfulPurchaseView: Bool = false
|
|
|
|
init(isPresented: Binding<Bool>, showLackOfPlanMessage: Bool = false) {
|
|
self._isPresented = isPresented
|
|
self.showLackOfPlanMessage = showLackOfPlanMessage
|
|
|
|
Logger.log(">>> SubscriptionView init")
|
|
|
|
}
|
|
|
|
var body: some View {
|
|
|
|
VStack {
|
|
if self.showLoginView {
|
|
LoginView { _ in
|
|
self.showLoginView = false
|
|
self._purchase()
|
|
}
|
|
} else {
|
|
|
|
if self.showLackOfPlanMessage {
|
|
SubscriptionDetailView()
|
|
.clipShape(.rect(cornerRadius: 16.0))
|
|
.padding()
|
|
}
|
|
|
|
List {
|
|
|
|
if self.model.products.count > 0 {
|
|
|
|
Section {
|
|
|
|
ForEach(self.model.products) { product in
|
|
let isSelected = self.model.selectedProduct == product
|
|
ProductView(product: product,
|
|
quantity: self.$model.quantity,
|
|
selected: isSelected)
|
|
.foregroundStyle(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(Color.master)
|
|
.listRowBackground(Color.clear)
|
|
|
|
.onTapGesture {
|
|
self.model.selectedProduct = product
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Sélectionnez une offre")
|
|
}
|
|
|
|
if let product = self.model.selectedProduct {
|
|
|
|
Section {
|
|
|
|
Button {
|
|
if Store.main.hasToken() {
|
|
self._purchase()
|
|
} else {
|
|
self.showLoginView = true
|
|
}
|
|
} label: {
|
|
HStack {
|
|
if self.isPurchasing {
|
|
Spacer()
|
|
ProgressView().tint(.white)
|
|
Spacer()
|
|
} else {
|
|
Text("Acheter")
|
|
Spacer()
|
|
Text(self.model.totalPrice)
|
|
}
|
|
}
|
|
.padding(8.0)
|
|
.fontWeight(.bold)
|
|
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.orange)
|
|
.listRowBackground(Color.clear)
|
|
|
|
} footer: {
|
|
if product.item.isConsumable == false {
|
|
SubscriptionFooterView()
|
|
.foregroundStyle(Color(white: 0.8))
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
if self.model.isLoading {
|
|
ProgressView()
|
|
} else {
|
|
HStack {
|
|
if let plan = Guard.main.currentPlan {
|
|
Image(systemName: plan.systemImage)
|
|
} else {
|
|
Image(systemName: "questionmark.diamond.fill")
|
|
}
|
|
Text("Il n'y a pas de produits à vous proposer")
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
.listStyle(.grouped)
|
|
.scrollContentBackground(.hidden)
|
|
.background(.logoBackground)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
if self.isRestoring {
|
|
ProgressView()
|
|
} else {
|
|
Button("Restaurer") {
|
|
self._restore()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.preferredColorScheme(.dark)
|
|
.navigationTitle("Abonnements")
|
|
.onAppear {
|
|
self.model.load()
|
|
}
|
|
}
|
|
|
|
fileprivate func _purchase() {
|
|
|
|
self.isPurchasing = true
|
|
|
|
Task {
|
|
do {
|
|
let success = try await self.model.purchase()
|
|
DispatchQueue.main.async {
|
|
self.isPurchasing = false
|
|
self.showSuccessfulPurchaseView = true
|
|
if success {
|
|
self.isPresented = false
|
|
}
|
|
}
|
|
} catch {
|
|
Logger.error(error)
|
|
DispatchQueue.main.async {
|
|
self.isPurchasing = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func _restore() {
|
|
|
|
Task {
|
|
do {
|
|
self.isRestoring = true
|
|
try await Guard.main.refreshPurchasedAppleProducts()
|
|
self.isRestoring = false
|
|
} catch {
|
|
self.isRestoring = false
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct ProductView: View {
|
|
|
|
var product: Product
|
|
@Binding var quantity: Int
|
|
var selected: Bool
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: self._image)
|
|
.font(.system(size: 36.0))
|
|
.foregroundColor(.orange)
|
|
VStack(alignment: .leading) {
|
|
Text(product.displayName)
|
|
Text(product.formattedPrice)
|
|
.foregroundColor(.accentColor)
|
|
if self._isConsumable {
|
|
StepperView(count: self.$quantity, minimum: 1).font(.callout)
|
|
}
|
|
}
|
|
Spacer()
|
|
if self.selected {
|
|
Image(systemName: "checkmark")
|
|
.foregroundColor(.accentColor)
|
|
}
|
|
}
|
|
.contentShape(.rect)
|
|
|
|
}
|
|
|
|
fileprivate var _item: StoreItem? {
|
|
return StoreItem(rawValue: self.product.id)
|
|
}
|
|
|
|
fileprivate var _isConsumable: Bool {
|
|
return self._item?.isConsumable ?? false
|
|
}
|
|
|
|
fileprivate var _image: String {
|
|
if let item = self._item {
|
|
return item.systemImage
|
|
} else {
|
|
return "gift.circle.fill"
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct SubscriptionNoProductView: View {
|
|
|
|
@ObservedObject var model: SubscriptionModel
|
|
|
|
var body: some View {
|
|
VStack(alignment: .center) {
|
|
|
|
if let error = self.model.error {
|
|
Text(error.localizedDescription)
|
|
} else {
|
|
Text("Aucun produit disponible")
|
|
}
|
|
|
|
if self.model.isLoading {
|
|
ProgressView()
|
|
} else {
|
|
Button(action: {
|
|
self.model.load()
|
|
}, label: {
|
|
Image(systemName: "arrow.clockwise.circle.fill")
|
|
.font(.system(size: 64.0))
|
|
})
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
struct SubscriptionFooterView: View {
|
|
var body: some View {
|
|
Text("Conditions d’utilisations concernant l’abonnement:\n- Le paiement sera facturé sur votre compte Apple.\n- L’abonnement est renouvelé automatiquement chaque mois, à moins d’avoir été désactivé au moins 24 heures avant la fin de la période de l’abonnement.\n- L’abonnement peut être géré par l’utilisateur et désactivé en allant dans les réglages de son compte après s’être abonné.\n- Le compte sera facturé pour le renouvellement de l'abonnement dans les 24 heures précédent la fin de la période d’abonnement.\n- Un abonnement en cours ne peut être annulé.\n- Toute partie inutilisée de l'offre gratuite, si souscrite, sera abandonnée lorsque l'utilisateur s'abonnera, dans les cas applicables.")
|
|
.padding(.top)
|
|
}
|
|
}
|
|
|
|
struct SubscriptionDetailView: View {
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: "exclamationmark.bubble.fill")
|
|
//.foregroundStyle(Color.accentColor)
|
|
.font(.title)
|
|
Text("Vous ne disposez malheureusement plus d'offre pour continuer votre tournoi. Voici ce que nous proposons:")
|
|
.fontWeight(.semibold)
|
|
}
|
|
.padding()
|
|
.background(.orange)
|
|
.foregroundStyle(.black)
|
|
}
|
|
|
|
}
|
|
|
|
#Preview {
|
|
SubscriptionDetailView()
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
SubscriptionView(isPresented: .constant(true), showLackOfPlanMessage: false)
|
|
}
|
|
}
|
|
|