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

@@ -8,8 +8,6 @@
import SwiftUI
import StoreKit
fileprivate let damus_products = ["purpleyearly","purple"]
// MARK: - Helper structures
enum AccountInfoState {
@@ -19,25 +17,16 @@ enum AccountInfoState {
case error(message: String)
}
func non_discounted_price(_ product: Product) -> String {
return (product.price * 1.1984569224).formatted(product.priceFormatStyle)
}
enum DamusPurpleType: String {
case yearly = "purpleyearly"
case monthly = "purple"
}
// MARK: - Main view
struct DamusPurpleView: View {
struct DamusPurpleView: View, DamusPurpleStoreKitManagerDelegate {
let damus_state: DamusState
let keypair: Keypair
@State var my_account_info_state: AccountInfoState = .loading
@State var products: ProductState
@State var purchased: PurchasedProduct? = nil
@State var selection: DamusPurpleType = .yearly
@State var selection: DamusPurple.StoreKitManager.DamusPurpleType = .yearly
@State var show_welcome_sheet: Bool = false
@State var show_manage_subscriptions = false
@State private var shouldDismissView = false
@@ -48,6 +37,7 @@ struct DamusPurpleView: View {
self._products = State(wrappedValue: .loading)
self.damus_state = damus_state
self.keypair = damus_state.keypair
damus_state.purple.storekit_manager.delegate = self
}
// MARK: - Top level view
@@ -159,30 +149,10 @@ struct DamusPurpleView: View {
}
}
func handle_transactions(products: [Product]) async {
for await update in StoreKit.Transaction.updates {
switch update {
case .verified(let tx):
let prod = products.filter({ prod in tx.productID == prod.id }).first
if let prod,
let expiration = tx.expirationDate,
Date.now < expiration
{
self.purchased = PurchasedProduct(tx: tx, product: prod)
break
}
case .unverified:
continue
}
}
}
func load_products() async {
do {
let products = try await Product.products(for: damus_products)
let products = try await self.damus_state.purple.storekit_manager.get_products()
self.products = .loaded(products)
await handle_transactions(products: products)
print("loaded products", products)
} catch {
@@ -191,8 +161,13 @@ struct DamusPurpleView: View {
}
}
// For DamusPurple.StoreKitManager.Delegate conformance. This gets called by the StoreKitManager when a new product was purchased
func product_was_purchased(product: DamusPurple.StoreKitManager.PurchasedProduct) {
self.purchased = product
}
func subscribe(_ product: Product) async throws {
let result = try await product.purchase()
let result = try await self.damus_state.purple.make_iap_purchase(product: product)
switch result {
case .success(.verified(let tx)):
print("success \(tx.debugDescription)")
@@ -219,12 +194,6 @@ struct DamusPurpleView: View {
break
}
}
var product: Product? {
return self.products.products?.filter({
prod in prod.id == selection.rawValue
}).first
}
}
struct DamusPurpleView_Previews: PreviewProvider {

View File

@@ -11,6 +11,8 @@ import StoreKit
// MARK: - IAPProductStateView
extension DamusPurpleView {
typealias PurchasedProduct = DamusPurple.StoreKitManager.PurchasedProduct
struct IAPProductStateView: View {
let products: ProductState
let purchased: PurchasedProduct?
@@ -82,28 +84,20 @@ extension DamusPurpleView {
}
func price_description(product: Product) -> some View {
if product.id == "purpleyearly" {
return (
AnyView(
HStack(spacing: 10) {
Text(NSLocalizedString("Annually", comment: "Annual renewal of purple subscription"))
Spacer()
Text(verbatim: non_discounted_price(product)).strikethrough().foregroundColor(DamusColors.white.opacity(0.5))
Text(verbatim: product.displayPrice).fontWeight(.bold)
}
)
)
} else {
return (
AnyView(
HStack(spacing: 10) {
Text(NSLocalizedString("Monthly", comment: "Monthly renewal of purple subscription"))
Spacer()
Text(verbatim: product.displayPrice).fontWeight(.bold)
}
)
)
}
let purple_type = DamusPurple.StoreKitManager.DamusPurpleType(rawValue: product.id)
return (
HStack(spacing: 10) {
Text(purple_type?.label() ?? product.displayName)
Spacer()
if let non_discounted_price = purple_type?.non_discounted_price(product: product) {
Text(verbatim: non_discounted_price)
.strikethrough()
.foregroundColor(DamusColors.white.opacity(0.5))
}
Text(verbatim: product.displayPrice)
.fontWeight(.bold)
}
)
}
}
}
@@ -127,11 +121,6 @@ extension DamusPurpleView {
}
}
}
struct PurchasedProduct {
let tx: StoreKit.Transaction
let product: Product
}
}
#Preview {