So I am testing my in-app purchase implementation. I am currently testing Ask to buy
or interrupted purchases
using StoreKit config file
in Xcode
. Testing purchase errors is working good. When I am testing Ask to buy
or Interrupted Purchase
According to apples documentationUse updates to receive new transactions while the app is running. This sequence receives transactions that occur outside of the app, such as Ask to Buy transactions, subscription offer code redemptions, and purchases that customers make in the App Store
.
When I accept or decline the transaction in Xcode. Transaction updates
is not notified of the new transaction or failed transaction if I decline the transaction
This is my implementation of InappPurchase
enum PurchaseResult {
case success(Date)
case unverifiedResult
case pending
case userCancelled
case unknownError
}
final class IAPManager: ObservableObject {
private let userCache = UserCache.shared
private let productIDs = ["my_product_identifier"]
var updates: Task<Void, Never>? = nil
static let shared = IAPManager()
@Published var latestPurchaseTransaction: Transaction? = nil
private init() {
updates = newTransactionListenerTask()
Task {
await updateCurrentEntitlements()
}
}
deinit {
updates?.cancel()
}
private func newTransactionListenerTask() -> Task<Void, Never> {
Task(priority: .background) {
for await verificationResult in Transaction.updates {
print("Transaction updates: \(verificationResult)")
self.handle(updatedTransaction: verificationResult)
}
}
}
// This function fetches the lastest transactions the user has made
private func updateCurrentEntitlements() async {
guard let productID = productIDs.first, let verificationResult = await Transaction.latest(for: productID) else {
// The user hasn't purchased this product.
return
}
switch verificationResult {
case .verified(let transaction):
// Check the transaction and give the user access to purchased
// content as appropriate.
print("Latest transaction date: \(transaction.purchaseDate)")
print("Expired date: \(transaction.expirationDate)")
/*
1. Fetch the date
2. compare with saved date
*/
case .unverified(let transaction, let verificationError):
// Handle unverified transactions based
// on your business model.
Crashlytics.crashlytics().record(error: verificationError)
}
}
func fetchProducts() async throws -> [Product] {
return try await Product.products(for: productIDs)
}
func purchase(_ product: Product) async throws -> PurchaseResult {
let result = try await product.purchase()
switch result {
case .success(let verificationResult):
switch verificationResult {
case .verified(let transaction):
// Give the user access to purchased content.
// Complete the transaction after providing
// the user access to the content.
await transaction.finish()
return .success(transaction.purchaseDate)
case .unverified(let transaction, let verificationError):
// Handle unverified transactions based
// on your business model.
Crashlytics.crashlytics().record(error: verificationError)
return .unverifiedResult
}
case .pending:
// The purchase requires action from the customer.
// If the transaction completes,
// it's available through Transaction.updates.
return .pending
case .userCancelled:
// The user canceled the purchase.
return .userCancelled
@unknown default:
return .unknownError
}
}
private func handle(updatedTransaction verificationResult: VerificationResult<Transaction>) {
guard case .verified(let transaction) = verificationResult else {
// Ignore unverified transactions.
return
}
if let _ = transaction.revocationDate {
// Remove access to the product identified by transaction.productID.
// Transaction.revocationReason provides details about
// the revoked transaction.
UserDatabaseService.shared.removeGodMode()
} else if let expirationDate = transaction.expirationDate,
expirationDate < Date() {
// Do nothing, this subscription is expired.
// we need to check the date in case we need to remove godmode
userCache.fetchCurrentUser { user in
if let user = user {
if user.godmode == true {
UserDatabaseService.shared.removeGodMode()
}
}
}
} else if transaction.isUpgraded {
// Do nothing, there is an active transaction
// for a higher level of service.
return
} else {
// Provide access to the product identified by
// transaction.productID.
UserDatabaseService.shared.startGodMode()
Task.init {
await transaction.finish()
}
latestPurchaseTransaction = transaction
UIViewController.showGodModeActivatedViewController()
}
}
}
extension IAPManager {
// Function for checking if user is currently subscribed
func isUserSubscribedToGodMode() async -> Bool {
guard let productID = productIDs.first, let verificationResult = await Transaction.latest(for: productID) else {
// The user hasn't purchased this product.
return false
}
if case let .verified(transaction) = verificationResult {
if Date.daysSinceDate(transaction.purchaseDate) <= 7 {
return true
} else {
return false
}
} else {
return false
}
}
}
extension IAPManager {
// Function for restoring previous subscription
func canRestoreSubscription() async -> Bool {
guard let productID = productIDs.first, let verificationResult = await Transaction.latest(for: productID) else {
// The user hasn't purchased this product.
return false
}
if case .verified(_) = verificationResult {
return true
} else {
return false
}
}
}
Here are the image showing selecting Ask to buy
in Xcode
. It shows disable because it is already enabled