commit d694c26b83d4319eda1c487d8102361b028c389c
parent 2525799c8a334fa0267ad07a35205cda2f41baba
Author: Daniel D’Aquino <daniel@daquino.me>
Date: Wed, 14 Feb 2024 21:31:59 +0000
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>
Diffstat:
5 files changed, 123 insertions(+), 68 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -442,6 +442,7 @@
D7100C582B76FC8400C59298 /* MarketingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C572B76FC8400C59298 /* MarketingContentView.swift */; };
D7100C5A2B76FD5100C59298 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C592B76FD5100C59298 /* LogoView.swift */; };
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5B2B77016700C59298 /* IAPProductStateView.swift */; };
+ D7100C5E2B7709ED00C59298 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; };
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
D723411A2B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
@@ -1344,6 +1345,7 @@
D7100C572B76FC8400C59298 /* MarketingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketingContentView.swift; sourceTree = "<group>"; };
D7100C592B76FD5100C59298 /* LogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoView.swift; sourceTree = "<group>"; };
D7100C5B2B77016700C59298 /* IAPProductStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPProductStateView.swift; sourceTree = "<group>"; };
+ D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleStoreKitManager.swift; sourceTree = "<group>"; };
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; };
D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleEnvironment.swift; sourceTree = "<group>"; };
D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; };
@@ -2670,6 +2672,7 @@
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */,
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */,
D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */,
+ D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */,
);
path = Purple;
sourceTree = "<group>";
@@ -3370,6 +3373,7 @@
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4C5D5C9A2A6AF8F80024563C /* NdbTagIterator.swift in Sources */,
+ D7100C5E2B7709ED00C59298 /* PurpleStoreKitManager.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
diff --git a/damus/Models/Purple/DamusPurple.swift b/damus/Models/Purple/DamusPurple.swift
@@ -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,
diff --git a/damus/Models/Purple/PurpleStoreKitManager.swift b/damus/Models/Purple/PurpleStoreKitManager.swift
@@ -0,0 +1,86 @@
+//
+// PurpleStoreKitManager.swift
+// damus
+//
+// Created by Daniel D’Aquino 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)
+}
diff --git a/damus/Views/Purple/DamusPurpleView.swift b/damus/Views/Purple/DamusPurpleView.swift
@@ -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 {
diff --git a/damus/Views/Purple/Detail/IAPProductStateView.swift b/damus/Views/Purple/Detail/IAPProductStateView.swift
@@ -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 {