iap: move StoreKit logic out of DamusPurpleView and into a new PurpleStoreKitManager

This commit moves most of StoreKit-specific logic that was embedded into
DamusPurpleView and places it into a new PurpleStoreKitManager struct,
to make code more reusable and readable by separating view concerns from
StoreKit-specific concerns.

Most of the code here should be in feature parity with the previous
behavior. However, a few logical improvements were made alongside this
refactoring:

- Improved StoreKit transaction update monitoring logic: Previously the
  view would stop listening for purchase updates after the first update.
  However, I made the program continuously listen for purchase updates,
  as recommended by Apple's documentation
  (https://developer.apple.com/documentation/storekit/transaction/3851206-updates)

- Improved/simplified logic around getting extra information from the
  products: Information and the handling of product information was
  spread in a few separate places. I incorporated those bits of
  information into central and uniform interfaces on DamusPurpleType, to
  simplify logic and future changes.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
Daniel D’Aquino
2024-02-14 21:31:59 +00:00
committed by William Casarin
parent 2525799c8a
commit d694c26b83
5 changed files with 123 additions and 68 deletions

View File

@@ -6,10 +6,12 @@
//
import Foundation
import StoreKit
class DamusPurple: StoreObserverDelegate {
let settings: UserSettingsStore
let keypair: Keypair
var storekit_manager: StoreKitManager
@MainActor
var account_cache: [Pubkey: Account]
@@ -18,6 +20,7 @@ class DamusPurple: StoreObserverDelegate {
self.settings = settings
self.keypair = keypair
self.account_cache = [:]
self.storekit_manager = .init()
}
// MARK: Functions
@@ -123,6 +126,10 @@ class DamusPurple: StoreObserverDelegate {
try await self.create_account(pubkey: pubkey)
}
func make_iap_purchase(product: Product) async throws -> Product.PurchaseResult {
return try await self.storekit_manager.purchase(product: product)
}
func send_receipt() async {
// Get the receipt if it's available.
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,

View File

@@ -0,0 +1,86 @@
//
// PurpleStoreKitManager.swift
// damus
//
// Created by Daniel DAquino on 2024-02-09.
//
import Foundation
import StoreKit
extension DamusPurple {
struct StoreKitManager {
var delegate: DamusPurpleStoreKitManagerDelegate? = nil
struct PurchasedProduct {
let tx: StoreKit.Transaction
let product: Product
}
init() {
self.start()
}
func start() {
Task {
try await monitor_updates()
}
}
func get_products() async throws -> [Product] {
return try await Product.products(for: DamusPurpleType.allCases.map({ $0.rawValue }))
}
private func monitor_updates() async throws {
for await update in StoreKit.Transaction.updates {
switch update {
case .verified(let tx):
let products = try await self.get_products()
let prod = products.filter({ prod in tx.productID == prod.id }).first
if let prod,
let expiration = tx.expirationDate,
Date.now < expiration
{
self.delegate?.product_was_purchased(product: PurchasedProduct(tx: tx, product: prod))
}
case .unverified:
continue
}
}
}
func purchase(product: Product) async throws -> Product.PurchaseResult {
return try await product.purchase(options: [])
}
}
}
extension DamusPurple.StoreKitManager {
enum DamusPurpleType: String, CaseIterable {
case yearly = "purpleyearly"
case monthly = "purple"
func non_discounted_price(product: Product) -> String? {
switch self {
case .yearly:
return (product.price * 1.1984569224).formatted(product.priceFormatStyle)
case .monthly:
return nil
}
}
func label() -> String {
switch self {
case .yearly:
return NSLocalizedString("Annually", comment: "Annual renewal of purple subscription")
case .monthly:
return NSLocalizedString("Monthly", comment: "Monthly renewal of purple subscription")
}
}
}
}
protocol DamusPurpleStoreKitManagerDelegate {
func product_was_purchased(product: DamusPurple.StoreKitManager.PurchasedProduct)
}