Восстановить покупку — это восстановление покупки, когда пользователь вообще не совершал покупки

Я использую Storekit для реализации покупок в приложении в своем приложении. Это сейчас в прямом эфире. Я реализовал нерасходную покупку для удаления рекламы. Теперь проблема в том, что если даже новый пользователь нажмет на кнопку «Восстановить покупку», он успешно восстановит покупку, чего не должно быть. Я не могу его отладить из-за отсутствия тестового устройства, но я попросил нескольких людей загрузить приложение и восстановить покупку, и они были успешно переведены на Премиум.

Ниже приведен код, который я использую:

enum IAPHandlerAlertType {
    case initialize
    case setProductIds
    case disabled
    case restored
    case purchased
    case failed
    case error
    case restoreFailed
    
    var message: String{
        switch self {
        case .error: return "An error occured"
        case .initialize: return ""
        case .setProductIds: return "Product ids not set, call setProductIds method!"
        case .disabled: return "Purchases are disabled in your device!"
        case .restored: return "You've successfully restored your purchase!"
        case .purchased: return "You've successfully bought this purchase!"
        case .failed: return "Failed to buy this purchase!"
        case .restoreFailed: return "Failed to restore this purchase!"
        }
    }
}


class IAPManager: NSObject {
    
    //MARK:- Shared Object
    //MARK:-
    static let shared = IAPManager()
    private override init() { }
    
    //MARK:- Properties
    //MARK:- Private
    fileprivate var productIds = ["com.identifier.appName.removeAds"]
    fileprivate var productID = ""
    fileprivate var productsRequest = SKProductsRequest()
    fileprivate var fetchProductComplition: (([SKProduct])->Void)?
    
    fileprivate var productToPurchase: SKProduct?
    var purchaseProductComplition: ((IAPHandlerAlertType, Error?)->Void)?
    
    //MARK:- Public
    var isLogEnabled: Bool = true
    
    //MARK:- Methods
    //MARK:- Public
    
    //Set Product Ids
    func setProductIds(ids: [String]) {
        self.productIds = ids
    }

    //MAKE PURCHASE OF A PRODUCT
    func canMakePurchases() -> Bool {  return SKPaymentQueue.canMakePayments()  }
    
    func purchase(product: SKProduct, completion: @escaping ((IAPHandlerAlertType, Error?) -> Void)) {
        
        self.purchaseProductComplition = completion
        self.productToPurchase = product

        if self.canMakePurchases() {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(self)
            SKPaymentQueue.default().add(payment)
            
            log("PRODUCT TO PURCHASE: \(product.productIdentifier)")
            productID = product.productIdentifier
        }
        else {
            completion(IAPHandlerAlertType.disabled, nil)
        }
    }
    
    // RESTORE PURCHASE
    func restorePurchase(){
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
    
    
    // FETCH AVAILABLE IAP PRODUCTS
    func fetchAvailableProducts(completion: @escaping (([SKProduct])->Void)){
        
        self.fetchProductComplition = completion
        // Put here your IAP Products ID's
        if self.productIds.isEmpty {
            log(IAPHandlerAlertType.setProductIds.message)
            fatalError(IAPHandlerAlertType.setProductIds.message)
        }
        else {
            productsRequest = SKProductsRequest(productIdentifiers: Set(self.productIds))
            productsRequest.delegate = self
            productsRequest.start()
        }
    }
    
    //MARK:- Private
    fileprivate func log <T> (_ object: T) {
        if isLogEnabled {
            NSLog("\(object)")
        }
    }
}

//MARK:- Product Request Delegate and Payment Transaction Methods

extension IAPManager: SKProductsRequestDelegate, SKPaymentTransactionObserver {
    
    func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
        
        if let completion = self.fetchProductComplition {
            completion(response.products)
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        if let completion = self.fetchProductComplition {
            completion([])
        }
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        if let completion = self.purchaseProductComplition {
            completion(IAPHandlerAlertType.restored, nil)
        }
    }
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        if let completion = self.purchaseProductComplition {
            completion(IAPHandlerAlertType.restoreFailed, error)
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction:AnyObject in transactions {
            if let trans = transaction as? SKPaymentTransaction {
                switch trans.transactionState {
                case .purchased:
                    log("Product purchase done")
                    SKPaymentQueue.default().finishTransaction(trans)
                    if let completion = self.purchaseProductComplition {
                        completion(IAPHandlerAlertType.purchased, nil)
                    }
                    break
                    
                case .failed:
                    log("Product purchase failed")
                    SKPaymentQueue.default().finishTransaction(trans)
                    if let completion = self.purchaseProductComplition {
                        completion(IAPHandlerAlertType.failed, trans.error)
                    }
                    break
                case .restored:
                    log("Product restored")
                    SKPaymentQueue.default().finishTransaction(trans)
                    if let completion = self.purchaseProductComplition {
                        completion(IAPHandlerAlertType.restored, nil)
                    }
                    break
                    
                default: break
                }
            }
        }
    }
}

Модель представления

func restoreAction() {
            Spinner.start()
            IAPManager.shared.fetchAvailableProducts { products in
                if products.count > 0 {
                    IAPManager.shared.restorePurchase()
                    IAPManager.shared.purchaseProductComplition = { [self] result, error in
                        self.handlePurchaseRestoreResult(result: result, error: error)
                        Spinner.stop()
                    }
                } else {
                    Spinner.stop()
                }
            }
        }
    
    
     func handlePurchaseRestoreResult(result: IAPHandlerAlertType, error: Error?) {
            if error != nil {
                showAlert = .init(id: .error)
                return
            }
            switch result {
            case .disabled:
                showAlert = .init(id: .disabled)
            case .purchased:
                Defaults.isPremiumPurchased = true
                Defaults.totalCoins += 1000
                isPremiumPurchased = 1
                break
            case .restored:
                Defaults.isPremiumPurchased = true
                isPremiumPurchased = 1
                break
            case .failed:
                showAlert = .init(id: .failed)
            default:
                break
            }
        }

Я делаю здесь что-то не так?

Оригинальный магазинный комплект устарел, вам следует дать шанс магазинному комплекту 2.

lorem ipsum 05.09.2024 00:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Кажется, вам не хватает функции validatePurchase, прежде чем предоставить доступ пользователю. Прежде чем устанавливать статус покупки для пользователя, будет лучше, если вы подтвердите, действительно ли пользователь приобрел премию в вашей базе данных. Ниже приведена минимальная реализация функции, которую вы хотите иметь перед выдачей премии пользователю:

func isPremiumUserOrNot() {
    guard let receiptURL = Bundle.main.appStoreReceiptURL else {
        // Handle no receipt case
        return
    }
    
    // API call to check if the user is in your DB for receiptURL and return the status
}

Добавление этого должно решить вашу проблему.

Ответ принят как подходящий

В вашем методе делегата paymentQueueRestoreCompletedTransactionsFinished вы вызываете свой completionHandler и передаете IAPHandlerAlertType.restored.

В вашем handlePurchaseRestoreResult этом статусе вы устанавливаете isPremiumPurchased = 1

Однако вы неправильно поняли цель метода paymentQueueRestoreCompletedTransactionsFinished — он просто указывает на то, что процесс восстановления завершен. Обычно вы используете это для обновления вашего пользовательского интерфейса; например, удаление индикатора активности.

Этот метод делегата вызывается независимо от того, были ли какие-либо покупки для восстановления, поэтому не следует устанавливать isPremiumPurchased = 1 просто потому, что этот метод был вызван.

Вам следует устанавливать isPremiumPurchased = 1 только в ответ на транзакцию, представленную в вашей очереди платежей.

Исходный API StoreKit также устарел в iOS 18. Возможно, вы захотите перейти на API StoreKit2. Это намного проще в использовании, не требует потоков восстановления покупок и даже не требует сохранения вашего собственного «купленного» состояния — вы можете просто проверить, был ли куплен продукт.

Другие вопросы по теме