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:
committed by
William Casarin
parent
2525799c8a
commit
d694c26b83
@@ -442,6 +442,7 @@
|
|||||||
D7100C582B76FC8400C59298 /* MarketingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C572B76FC8400C59298 /* MarketingContentView.swift */; };
|
D7100C582B76FC8400C59298 /* MarketingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C572B76FC8400C59298 /* MarketingContentView.swift */; };
|
||||||
D7100C5A2B76FD5100C59298 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C592B76FD5100C59298 /* LogoView.swift */; };
|
D7100C5A2B76FD5100C59298 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C592B76FD5100C59298 /* LogoView.swift */; };
|
||||||
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5B2B77016700C59298 /* IAPProductStateView.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 */; };
|
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; };
|
||||||
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
|
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
|
||||||
D723411A2B6864F200E1E135 /* 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; };
|
||||||
@@ -2670,6 +2672,7 @@
|
|||||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */,
|
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */,
|
||||||
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */,
|
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */,
|
||||||
D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */,
|
D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */,
|
||||||
|
D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */,
|
||||||
);
|
);
|
||||||
path = Purple;
|
path = Purple;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3370,6 +3373,7 @@
|
|||||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
||||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
||||||
4C5D5C9A2A6AF8F80024563C /* NdbTagIterator.swift in Sources */,
|
4C5D5C9A2A6AF8F80024563C /* NdbTagIterator.swift in Sources */,
|
||||||
|
D7100C5E2B7709ED00C59298 /* PurpleStoreKitManager.swift in Sources */,
|
||||||
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */,
|
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */,
|
||||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
class DamusPurple: StoreObserverDelegate {
|
class DamusPurple: StoreObserverDelegate {
|
||||||
let settings: UserSettingsStore
|
let settings: UserSettingsStore
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
|
var storekit_manager: StoreKitManager
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var account_cache: [Pubkey: Account]
|
var account_cache: [Pubkey: Account]
|
||||||
@@ -18,6 +20,7 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.keypair = keypair
|
self.keypair = keypair
|
||||||
self.account_cache = [:]
|
self.account_cache = [:]
|
||||||
|
self.storekit_manager = .init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
@@ -123,6 +126,10 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
try await self.create_account(pubkey: pubkey)
|
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 {
|
func send_receipt() async {
|
||||||
// Get the receipt if it's available.
|
// Get the receipt if it's available.
|
||||||
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
|
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
|
||||||
|
|||||||
86
damus/Models/Purple/PurpleStoreKitManager.swift
Normal file
86
damus/Models/Purple/PurpleStoreKitManager.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import StoreKit
|
import StoreKit
|
||||||
|
|
||||||
fileprivate let damus_products = ["purpleyearly","purple"]
|
|
||||||
|
|
||||||
// MARK: - Helper structures
|
// MARK: - Helper structures
|
||||||
|
|
||||||
enum AccountInfoState {
|
enum AccountInfoState {
|
||||||
@@ -19,25 +17,16 @@ enum AccountInfoState {
|
|||||||
case error(message: String)
|
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
|
// MARK: - Main view
|
||||||
|
|
||||||
struct DamusPurpleView: View {
|
struct DamusPurpleView: View, DamusPurpleStoreKitManagerDelegate {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
|
|
||||||
@State var my_account_info_state: AccountInfoState = .loading
|
@State var my_account_info_state: AccountInfoState = .loading
|
||||||
@State var products: ProductState
|
@State var products: ProductState
|
||||||
@State var purchased: PurchasedProduct? = nil
|
@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_welcome_sheet: Bool = false
|
||||||
@State var show_manage_subscriptions = false
|
@State var show_manage_subscriptions = false
|
||||||
@State private var shouldDismissView = false
|
@State private var shouldDismissView = false
|
||||||
@@ -48,6 +37,7 @@ struct DamusPurpleView: View {
|
|||||||
self._products = State(wrappedValue: .loading)
|
self._products = State(wrappedValue: .loading)
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
self.keypair = damus_state.keypair
|
self.keypair = damus_state.keypair
|
||||||
|
damus_state.purple.storekit_manager.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Top level view
|
// 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 {
|
func load_products() async {
|
||||||
do {
|
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)
|
self.products = .loaded(products)
|
||||||
await handle_transactions(products: products)
|
|
||||||
|
|
||||||
print("loaded products", products)
|
print("loaded products", products)
|
||||||
} catch {
|
} 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 {
|
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 {
|
switch result {
|
||||||
case .success(.verified(let tx)):
|
case .success(.verified(let tx)):
|
||||||
print("success \(tx.debugDescription)")
|
print("success \(tx.debugDescription)")
|
||||||
@@ -219,12 +194,6 @@ struct DamusPurpleView: View {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var product: Product? {
|
|
||||||
return self.products.products?.filter({
|
|
||||||
prod in prod.id == selection.rawValue
|
|
||||||
}).first
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DamusPurpleView_Previews: PreviewProvider {
|
struct DamusPurpleView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import StoreKit
|
|||||||
// MARK: - IAPProductStateView
|
// MARK: - IAPProductStateView
|
||||||
|
|
||||||
extension DamusPurpleView {
|
extension DamusPurpleView {
|
||||||
|
typealias PurchasedProduct = DamusPurple.StoreKitManager.PurchasedProduct
|
||||||
|
|
||||||
struct IAPProductStateView: View {
|
struct IAPProductStateView: View {
|
||||||
let products: ProductState
|
let products: ProductState
|
||||||
let purchased: PurchasedProduct?
|
let purchased: PurchasedProduct?
|
||||||
@@ -82,28 +84,20 @@ extension DamusPurpleView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func price_description(product: Product) -> some View {
|
func price_description(product: Product) -> some View {
|
||||||
if product.id == "purpleyearly" {
|
let purple_type = DamusPurple.StoreKitManager.DamusPurpleType(rawValue: product.id)
|
||||||
return (
|
return (
|
||||||
AnyView(
|
HStack(spacing: 10) {
|
||||||
HStack(spacing: 10) {
|
Text(purple_type?.label() ?? product.displayName)
|
||||||
Text(NSLocalizedString("Annually", comment: "Annual renewal of purple subscription"))
|
Spacer()
|
||||||
Spacer()
|
if let non_discounted_price = purple_type?.non_discounted_price(product: product) {
|
||||||
Text(verbatim: non_discounted_price(product)).strikethrough().foregroundColor(DamusColors.white.opacity(0.5))
|
Text(verbatim: non_discounted_price)
|
||||||
Text(verbatim: product.displayPrice).fontWeight(.bold)
|
.strikethrough()
|
||||||
}
|
.foregroundColor(DamusColors.white.opacity(0.5))
|
||||||
)
|
}
|
||||||
)
|
Text(verbatim: product.displayPrice)
|
||||||
} else {
|
.fontWeight(.bold)
|
||||||
return (
|
}
|
||||||
AnyView(
|
)
|
||||||
HStack(spacing: 10) {
|
|
||||||
Text(NSLocalizedString("Monthly", comment: "Monthly renewal of purple subscription"))
|
|
||||||
Spacer()
|
|
||||||
Text(verbatim: product.displayPrice).fontWeight(.bold)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,11 +121,6 @@ extension DamusPurpleView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PurchasedProduct {
|
|
||||||
let tx: StoreKit.Transaction
|
|
||||||
let product: Product
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
Reference in New Issue
Block a user