Untitled

 avatar
unknown
swift
a year ago
8.3 kB
9
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