Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ struct InAppPurchasesDebugView: View {
@State var entitledProductIDs: Set<String> = []
@State var inAppPurchasesAreSupported = true
@State var isPurchasing = false
@State private var purchaseError: PurchaseError? {
didSet {
presentAlert = purchaseError != nil
}
}
@State var presentAlert = false

var body: some View {
List {
Expand All @@ -30,11 +36,16 @@ struct InAppPurchasesDebugView: View {
Button(entitledProductIDs.contains(product.id) ? "Entitled: \(product.description)" : product.description) {
Task {
isPurchasing = true
try? await inAppPurchasesForWPComPlansManager.purchaseProduct(with: product.id, for: siteID)
do {
try await inAppPurchasesForWPComPlansManager.purchaseProduct(with: product.id, for: siteID)
} catch {
purchaseError = PurchaseError(error: error)
}
await loadUserEntitlements()
isPurchasing = false
}
}
.alert(isPresented: $presentAlert, error: purchaseError, actions: {})
}
}
}
Expand Down Expand Up @@ -102,6 +113,20 @@ struct InAppPurchasesDebugView: View {
}
}

/// Just a silly little wrapper because SwiftUI's `alert(isPresented:error:actions:)` wants a `LocalizedError`
/// but we only have an `Error` coming from `purchaseProduct`.
private struct PurchaseError: LocalizedError {
let error: Error

var errorDescription: String? {
if let error = error as? LocalizedError {
return error.errorDescription
} else {
return error.localizedDescription
}
}
}

struct InAppPurchasesDebugView_Previews: PreviewProvider {
static var previews: some View {
InAppPurchasesDebugView(siteID: 0)
Expand Down
78 changes: 72 additions & 6 deletions Yosemite/Yosemite/Stores/InAppPurchaseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,23 @@ private extension InAppPurchaseStore {

logInfo("Purchasing product \(product.id) for site \(siteID) with options \(purchaseOptions)")
let purchaseResult = try await product.purchase(options: purchaseOptions)
if case .success(let result) = purchaseResult {
logInfo("Purchased product \(product.id) for site \(siteID): \(result)")
try await handleCompletedTransaction(result)
} else {
logError("Ignorning unsuccessful purchase: \(purchaseResult)")
switch purchaseResult {
case .success(let result):
guard case .verified(let transaction) = result else {
// Ignore unverified transactions.
logError("Transaction unverified: \(result)")
throw Errors.unverifiedTransaction
}
logInfo("Purchased product \(product.id) for site \(siteID): \(transaction)")

try await submitTransaction(transaction)
await transaction.finish()
case .userCancelled:
logInfo("User cancelled the purchase flow")
case .pending:
logError("Purchase returned in a pending state, it might succeed in the future")
@unknown default:
logError("Unknown result for purchase: \(purchaseResult)")
}
completion(.success(purchaseResult))
} catch {
Expand Down Expand Up @@ -265,13 +277,67 @@ private extension InAppPurchaseStore {
}

public extension InAppPurchaseStore {
enum Errors: Error {
enum Errors: Error, LocalizedError {
/// The purchase was successful but the transaction was unverified
///
case unverifiedTransaction

/// The purchase was successful but it's not associated to an account
///
case transactionMissingAppAccountToken

/// The transaction has an associated account but it can't be translated to a site
///
case appAccountTokenMissingSiteIdentifier

/// The transaction is associated with an unknown product
///
case transactionProductUnknown

/// The storefront for the user is unknown, and so we can't know their country code
///
case storefrontUnknown

/// App receipt was missing, even after a refresh
///
case missingAppReceipt

/// In-app purchases are not supported for this user
///
case inAppPurchasesNotSupported

public var errorDescription: String? {
switch self {
case .unverifiedTransaction:
return NSLocalizedString(
"The purchase transaction couldn't be verified",
comment: "Error message used when a purchase was successful but its transaction was unverified")
case .transactionMissingAppAccountToken:
return NSLocalizedString(
"Purchase transaction missing account information",
comment: "Error message used when the purchase transaction doesn't have the right metadata to associate to a specific site")
case .appAccountTokenMissingSiteIdentifier:
return NSLocalizedString(
"Purchase transaction can't be associated to a site",
comment: "Error message used when the purchase transaction doesn't have the right metadata to associate to a specific site")
case .transactionProductUnknown:
return NSLocalizedString(
"Purchase transaction received for an unknown product",
comment: "Error message used when we received a transaction for an unknown product")
case .storefrontUnknown:
return NSLocalizedString(
"Couldn't determine App Stoure country",
comment: "Error message used when we can't determine the user's App Store country")
case .missingAppReceipt:
return NSLocalizedString(
"Couldn't retrieve app receipt",
comment: "Error message used when we can't read the app receipt")
case .inAppPurchasesNotSupported:
return NSLocalizedString(
"In-app purchases are not supported for this user yet",
comment: "Error message used when In-app purchases are not supported for this user/site")
}
}
}

enum Constants {
Expand Down