Untitled
unknown
swift
2 years ago
8.3 kB
10
Indexable
//
// SubscriptionsManager.swift
// YinYang (iOS)
//
// Created by Zeng on 3/28/24.
//
import Foundation
import StoreKit
import Amplify
struct MembershipInfo: Decodable {
let MembershipTypeId: String
let MembershipTypeName: String
let ChangeType: String
let PreviousMembershipType: String?
let NeedsUpgrade: Bool
let UserSessionsForTheMonth: Int
let CreationDate: Int?
}
@MainActor
class SubscriptionsManager: NSObject, ObservableObject {
// MARK: - Properties
let productIDs = ["com.zeng.YinYang.growth", "com.zeng.YinYang.premium"]
@Published var products: [Product] = []
@Published var currentSubscriptionTier = "Starter"
@Published var membershipInfo: MembershipInfo?
let subscriptionNameToIDMapping: [String: String] = [
"Growth": "com.zeng.YinYang.growth",
"Premium": "com.zeng.YinYang.premium"
]
var transactionListener: Task<Void, Error>? = nil
override init() {
super.init()
SKPaymentQueue.default().add(self)
transactionListener = listenForTransactions()
print("SubscriptionsManager initialized")
Task {
await loadProducts()
await fetchLatestMembershipInfo()
await updateCurrentEntitlements()
}
}
deinit {
transactionListener?.cancel()
SKPaymentQueue.default().remove(self)
}
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
for await result in Transaction.updates {
await self.handle(transactionVerification: result)
}
}
}
private func updateCurrentEntitlements() async {
for await result in Transaction.currentEntitlements {
await self.handle(transactionVerification: result)
}
}
@MainActor
private func handle(transactionVerification result: VerificationResult <Transaction> ) async {
do {
// Log the receipt of a transaction update
print("Received a transaction update: \(result)")
let transaction = try self.verifyPurchase(result)
// Log the verified transaction
print("Verified transaction: \(transaction)")
let productId = transaction.productID
let newSubscriptionTier = self.subscriptionName(for: productId)
let action = self.determinePlanChangeAction(from: self.currentSubscriptionTier, to: newSubscriptionTier)
// Log the determined action
print("Determined action from \(self.currentSubscriptionTier) to \(newSubscriptionTier): \(action)")
await self.changeMembershipTo(newMembershipTypeId: productId, action: action)
await transaction.finish()
// Log the completion of transaction processing
print("Processed and finished transaction for product ID: \(productId)")
} catch {
// Log any errors during transaction processing
print("Transaction didn't pass verification - ignoring purchase. Error: \(error)")
}
}
private func verifyPurchase<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw SubscriptionsManagerError.failedVerification
case .verified(let safe):
return safe
}
}
enum SubscriptionsManagerError: Error {
case failedVerification
}
func fetchLatestMembershipInfo() async {
let userId = UserManager.shared.userSub
let request = RESTRequest(path: "/membership/\(userId)")
do {
let data = try await Amplify.API.get(request: request)
let decoder = JSONDecoder()
let fetchedMembershipInfo = try decoder.decode(MembershipInfo.self, from: data)
DispatchQueue.main.async {
self.membershipInfo = fetchedMembershipInfo
self.currentSubscriptionTier = fetchedMembershipInfo.MembershipTypeName
}
} catch {
print("Failed to fetch membership info for user ID \(userId): \(error)")
}
}
// MARK: - StoreKit2 API
func loadProducts() async {
do {
self.products = try await Product.products(for: productIDs)
.sorted(by: { $0.price > $1.price })
} catch {
print("Failed to fetch products: \(error)")
}
}
func buyProduct(_ product: Product) async {
do {
print("Attempting to purchase product: \(product.id)")
let result = try await product.purchase()
await handlePurchaseResult(result)
} catch {
print("Failed to purchase product: \(product.id), Error: \(error)")
}
}
private func handlePurchaseResult(_ result: Product.PurchaseResult) async {
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
print("Purchase verified for transaction: \(transaction)")
await transaction.finish()
case .unverified(_, let error):
print("Unverified purchase: \(error)")
@unknown default:
print("Unknown verification result.")
}
case .userCancelled:
print("User cancelled the purchase")
case .pending:
print("Purchase pending for product.")
@unknown default:
print("Unexpected purchase result")
}
}
func changeMembershipTo(newMembershipTypeId: String, action: String) async {
let userId = UserManager.shared.userSub
// Log the beginning of the membership change process
print("Attempting to change membership. UserID: \(userId), NewMembershipTypeID: \(newMembershipTypeId), Action: \(action)")
let requestBody: [String: Any] = ["UserId": userId, "NewMembershipTypeId": newMembershipTypeId, "Action": action]
do {
// Attempt to serialize the request body to JSON and log potential serialization failure
guard let requestBodyData = try? JSONSerialization.data(withJSONObject: requestBody) else {
print("Failed to serialize request body to JSON for UserID: \(userId), NewMembershipTypeID: \(newMembershipTypeId)")
return
}
let request = RESTRequest(path: "/membership/change", body: requestBodyData)
// Log the API request details
print("Sending API request to change membership. Path: \(request.path), Body: \(requestBody)")
_ = try await Amplify.API.post(request: request)
// Log successful membership change
print("Successfully changed membership for UserID: \(userId) to MembershipTypeID: \(newMembershipTypeId)")
await fetchLatestMembershipInfo()
} catch {
// Log any errors that occur during the API request
print("Failed to change membership for user ID \(userId): \(error.localizedDescription). Request Body: \(requestBody)")
}
}
func determinePlanChangeAction(from currentPlan: String, to newPlan: String) -> String {
let tierHierarchy = ["Starter": 0, "Growth": 1, "Premium": 2]
let currentPlanIndex = tierHierarchy[currentPlan] ?? -1
let newPlanIndex = tierHierarchy[newPlan] ?? -1
return newPlanIndex > currentPlanIndex ? "upgrade" : "downgrade"
}
func subscriptionName(for productId: String) -> String {
return subscriptionNameToIDMapping.first(where: { $0.value == productId })?.key ?? "Starter"
}
}
extension SubscriptionsManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// Handle transaction updates if necessary
}
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
return true
}
}
Editor is loading...
Leave a Comment