work on subscriptions

main
Laurent 3 years ago
parent e40f9d672f
commit 0ab3149980
  1. 4
      LeCountdown.xcodeproj/project.pbxproj
  2. 4
      LeCountdown/Subscription/AppGuard.swift
  3. 111
      LeCountdown/Subscription/Store.swift
  4. 129
      LeCountdown/Subscription/StoreView.swift
  5. 1
      LeCountdown/fr.lproj/Localizable.strings

@ -165,6 +165,7 @@
C4E5D67829B88BB5008E7465 /* DelaySoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D67329B88734008E7465 /* DelaySoundPlayer.swift */; };
C4E5D67A29B8C5A1008E7465 /* VolumeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D67929B8C5A1008E7465 /* VolumeView.swift */; };
C4E5D67C29B8D4A5008E7465 /* Low_Tom_Disto_Earth.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4E5D67B29B8D4A5008E7465 /* Low_Tom_Disto_Earth.wav */; };
C4E5D68029B8FD93008E7465 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D67F29B8FD93008E7465 /* Store.swift */; };
C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; };
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; };
C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; };
@ -379,6 +380,7 @@
C4E5D67329B88734008E7465 /* DelaySoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelaySoundPlayer.swift; sourceTree = "<group>"; };
C4E5D67929B8C5A1008E7465 /* VolumeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeView.swift; sourceTree = "<group>"; };
C4E5D67B29B8D4A5008E7465 /* Low_Tom_Disto_Earth.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Low_Tom_Disto_Earth.wav; sourceTree = "<group>"; };
C4E5D67F29B8FD93008E7465 /* Store.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; };
C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; };
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
@ -679,6 +681,7 @@
isa = PBXGroup;
children = (
C4BA2B56299FFA4F00CB4FBA /* AppGuard.swift */,
C4E5D67F29B8FD93008E7465 /* Store.swift */,
C4BA2B5E299FFC8300CB4FBA /* StoreView.swift */,
);
path = Subscription;
@ -997,6 +1000,7 @@
C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */,
C4BA2B6329A3C34600CB4FBA /* Stat.swift in Sources */,
C438C80F29828B8600BF3EF9 /* ActivitiesView.swift in Sources */,
C4E5D68029B8FD93008E7465 /* Store.swift in Sources */,
C4F8B187298AC234005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */,
C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */,

@ -1,6 +1,6 @@
//
// Guard.swift
// Poker Analytics 6
// AppGuard.swift
// LeCountdown
//
// Created by Laurent Morvillier on 20/04/2022.
//

@ -0,0 +1,111 @@
//
// Store.swift
// Poker Analytics 6
//
// Created by Laurent Morvillier on 20/04/2022.
//
import Foundation
import StoreKit
//public enum StoreError: Error {
// case failedVerification
//}
protocol StoreDelegate {
func productsReceived()
func errorDidOccur(error: Error)
}
//extension Notification.Name {
// static let StoreEventHappened = Notification.Name("storePurchaseSucceeded")
//}
class Store: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var purchasedTransactions = Set<StoreKit.Transaction>()
var delegate: StoreDelegate? = nil
var updateListenerTask: Task<Void, Error>? = nil
init() {
self.updateListenerTask = listenForTransactions()
Task {
//Initialize the store by starting a product request.
await self.requestProducts()
}
}
deinit {
self.updateListenerTask?.cancel()
}
func indexOf(identifier: String) -> Int? {
return self.products.map { $0.id }.firstIndex(of: identifier)
}
@MainActor
func requestProducts() async {
do {
let currentPlan = AppGuard.main.currentPlan
let identifiers: [String] = [StorePlan.unlimited.rawValue]
products = try await Product.products(for: identifiers)
Logger.log("products = \(self.products.count)")
self.delegate?.productsReceived()
} catch {
self.delegate?.errorDidOccur(error: error)
Logger.error(error)
}
}
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
//Iterate through any transactions which didn't come from a direct call to `purchase()`.
for await result in Transaction.updates {
do {
let transaction = try await AppGuard.main.processTransactionResult(result)
//Always finish a transaction.
await transaction.finish()
} catch {
self.delegate?.errorDidOccur(error: error)
//StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
func purchase(_ product: Product) async throws -> StoreKit.Transaction? {
// Begin a purchase.
let result = try await product.purchase()
switch result {
case .success(let verificationResult):
let transaction = try await AppGuard.main.processTransactionResult(verificationResult)
// Always finish a transaction.
await transaction.finish()
DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 200000), execute: {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
})
return transaction
case .userCancelled, .pending:
return nil
default:
return nil
}
}
}

@ -6,15 +6,138 @@
//
import SwiftUI
import StoreKit
struct StoreView: View {
fileprivate enum Feature: Int, Identifiable, CaseIterable {
case unlimitedTimers
case allSounds
case longSounds
case allStats
var id: Int { self.rawValue }
var localizedString: String {
switch self {
case .unlimitedTimers: return NSLocalizedString("Unlimited timers and stopwatches", comment: "")
case .allSounds: return NSLocalizedString("Access all the sound library", comment: "")
case .longSounds: return NSLocalizedString("Access long version of sounds", comment: "")
case .allStats: return NSLocalizedString("See all your activities in detail", comment: "")
}
}
}
struct StoreView: View, StoreDelegate {
@StateObject var store: Store = Store()
@State var errorMessage: String? = nil
init() {
self.store.delegate = self
// if SKPaymentQueue.canMakePayments() {
// self._store = Store(delegate: self)
// } else {
// self.errorMessage = NSLocalizedString("In-app purchase disabled", comment: "")
// }
}
var body: some View {
if let product = self.store.products.first {
PlanView(productName: product.displayName,
price: product.displayPrice) {
self._purchase()
}
} else {
ProgressView()
.progressViewStyle(.circular)
}
}
fileprivate func _purchase() {
if let product = self.store.products.first {
Task {
try await store.purchase(product)
}
}
}
// MARK: - StoreDelegate
func productsReceived() {
}
func errorDidOccur(error: Error) {
}
}
struct PlanView: View {
var productName: String
var price: String
var actionHandler: () -> ()
var body: some View {
Text("Hello Store!")
VStack {
Text(productName)
.font(.title)
.fontWeight(.bold)
.padding()
Group {
ForEach(Feature.allCases) { feature in
HStack {
Text(feature.localizedString)
Spacer()
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.accentColor)
}.fontWeight(.medium)
}
}
.padding(.horizontal, 24.0)
.padding(.vertical, 2.0)
Spacer()
Button {
self.actionHandler()
} label: {
HStack {
Spacer()
VStack {
Text("Purchase")
Text(self.price).font(.title3)
}
Spacer()
}
}
.font(.title)
.buttonStyle(.borderedProminent).fontWeight(.medium)
}.padding()
}
}
struct StoreView_Previews: PreviewProvider {
static var previews: some View {
StoreView()
PlanView(productName: "Pro version",
price: "$0.99 / month",
actionHandler: {})
}
}

@ -246,3 +246,4 @@
"Timer %@ started" = "Le minuteur %@ a démarré";
"The timer has not been found in the app" = "Le minuteur n'a pas été trouvé dans l'app";
"In-app purchase disabled" = "Les achats in-app sont désactivés. Veuillez les activer si vous souhaitez vous abonner";

Loading…
Cancel
Save