damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

PurpleStoreKitManager.swift (6125B)


      1 //
      2 //  PurpleStoreKitManager.swift
      3 //  damus
      4 //
      5 //  Created by Daniel D’Aquino on 2024-02-09.
      6 //
      7 
      8 import Foundation
      9 import StoreKit
     10 
     11 extension DamusPurple {
     12     class StoreKitManager { // Has to be a class to get around Swift-imposed limitations of mutations on concurrently executing code associated with the purchase update monitoring task.
     13         // The delegate is any object that wants to be notified of successful purchases. (e.g. A view that needs to update its UI)
     14         var delegate: DamusPurpleStoreKitManagerDelegate? = nil {
     15             didSet {
     16                 // Whenever the delegate is set, send it all recorded transactions to make sure it's up to date.
     17                 Task {
     18                     Log.info("Delegate changed. Try sending all recorded valid product transactions", for: .damus_purple)
     19                     guard let new_delegate = delegate else {
     20                         Log.info("Delegate is nil. Cannot send recorded product transactions", for: .damus_purple)
     21                         return
     22                     }
     23                     Log.info("Sending all %d recorded valid product transactions", for: .damus_purple, self.recorded_purchased_products.count)
     24                     
     25                     for purchased_product in self.recorded_purchased_products {
     26                         new_delegate.product_was_purchased(product: purchased_product)
     27                         Log.info("Sent StoreKit tx to delegate", for: .damus_purple)
     28                     }
     29                 }
     30             }
     31         }
     32         // Keep track of all recorded purchases so that we can send them to the delegate when it's set (whenever it's set)
     33         var recorded_purchased_products: [PurchasedProduct] = []
     34         
     35         // Helper struct to keep track of a purchased product and its transaction
     36         struct PurchasedProduct {
     37             let tx: StoreKit.Transaction
     38             let product: Product
     39         }
     40         
     41         // Singleton instance of StoreKitManager. To avoid losing purchase updates, there should only be one instance of StoreKitManager on the app.
     42         static let standard = StoreKitManager()
     43         
     44         init() {
     45             Log.info("Initiliazing StoreKitManager", for: .damus_purple)
     46             self.start()
     47         }
     48         
     49         func start() {
     50             Task {
     51                 try await monitor_updates()
     52             }
     53         }
     54         
     55         func get_products() async throws -> [Product] {
     56             return try await Product.products(for: DamusPurpleType.allCases.map({ $0.rawValue }))
     57         }
     58         
     59         // Use this function to manually and immediately record a purchased product update
     60         func record_purchased_product(_ purchased_product: PurchasedProduct) {
     61             self.recorded_purchased_products.append(purchased_product)
     62             self.delegate?.product_was_purchased(product: purchased_product)
     63         }
     64         
     65         // This function starts a task that monitors StoreKit updates and sends them to the delegate.
     66         // This function will run indefinitely (It should never return), so it is important to run this as a background task.
     67         private func monitor_updates() async throws {
     68             Log.info("Monitoring StoreKit updates", for: .damus_purple)
     69             // StoreKit.Transaction.updates is an async stream that emits updates whenever a purchase is verified.
     70             for await update in StoreKit.Transaction.updates {
     71                 switch update {
     72                     case .verified(let tx):
     73                         let products = try await self.get_products()
     74                         let prod = products.filter({ prod in tx.productID == prod.id }).first
     75                         
     76                         if let prod,
     77                            let expiration = tx.expirationDate,
     78                            Date.now < expiration
     79                         {
     80                             Log.info("Received valid transaction update from StoreKit", for: .damus_purple)
     81                             let purchased_product = PurchasedProduct(tx: tx, product: prod)
     82                             self.recorded_purchased_products.append(purchased_product)
     83                             self.delegate?.product_was_purchased(product: purchased_product)
     84                             Log.info("Sent tx to delegate (if exists)", for: .damus_purple)
     85                         }
     86                     case .unverified:
     87                         continue
     88                 }
     89             }
     90         }
     91         
     92         // Use this function to complete a StoreKit purchase
     93         // Specify the product and the app account token (UUID) to complete the purchase
     94         // The account token is used to associate with the user's account on the server.
     95         func purchase(product: Product, id: UUID) async throws -> Product.PurchaseResult {
     96             return try await product.purchase(options: [.appAccountToken(id)])
     97         }
     98     }
     99 }
    100 
    101 extension DamusPurple.StoreKitManager {
    102     // This helper struct is used to encapsulate StoreKit products, metadata, and supplement with additional information
    103     enum DamusPurpleType: String, CaseIterable {
    104         case yearly = "purpleyearly"
    105         case monthly = "purple"
    106         
    107         func non_discounted_price(product: Product) -> String? {
    108             switch self {
    109                 case .yearly:
    110                     return (product.price * 1.1984569224).formatted(product.priceFormatStyle)
    111                 case .monthly:
    112                     return nil
    113             }
    114         }
    115         
    116         func label() -> String {
    117             switch self {
    118                 case .yearly:
    119                     return NSLocalizedString("Annually", comment: "Annual renewal of purple subscription")
    120                 case .monthly:
    121                     return NSLocalizedString("Monthly", comment: "Monthly renewal of purple subscription")
    122             }
    123         }
    124     }
    125 }
    126 
    127 // This protocol is used to describe the delegate of the StoreKitManager, which will receive updates.
    128 protocol DamusPurpleStoreKitManagerDelegate {
    129     func product_was_purchased(product: DamusPurple.StoreKitManager.PurchasedProduct)
    130 }