Пошаговое руководство по созданию рабочего примера приложения с подписками на SwiftUI.

Этот туториал сопровождается примерами кода и образцом приложения, которые можно найти на https://github.com/RevenueCat/storekit2-demo-app

Вступление

Встроенные покупки и подписки - один из лучших способов для приложений зарабатывать деньги в App Store. StoreKit 2 - это недавно обновленный фреймворк от Apple для встроенных покупок, который позволяет разработчикам добавлять IAP (in-app purchases) в свои приложения для iOS, macOS, watchOS и tvOS. Документация Apple отлично справляется со своей задачей, давая великолепное объяснение того, как использовать StoreKit, но не вдается в тонкости и не предоставляет полный рабочий пример.

Этот туториал охватит основные понятия, настройку App Store Connect, добавление StoreKit 2, отображение, завершение и проверку покупки, а также обработку изменений, происходящих вне приложения (продление, отмена, выставление счета и т. д.). Также будет рассказано про преимущества и компромиссы наличия или отсутствия сервера. Будут приведены примеры кода на языке Swift и загружаемый, запускаемый пример приложения.

Оглавление

  • Вступление

  • Терминология

  • Настройка App Store Connect

  • Настройка файла конфигурации StoreKit

  • Аппаратная реализация подписок с помощью StoreKit 2 в Swift

    • Шаг 1: Список продуктов

    • Шаг 2: Покупка продуктов

    • Шаг 3: Подготовка к разблокировке встроенных опций

    • Шаг 4: Разблокировка встроенных опций

    • Шаг 5: Обработка приобретенных продуктов в офлайне

    • Шаг 6: Восстановление покупок

    • Шаг 7: Совместное использование активных покупок с расширениями

    • Шаг 8: Обработка продлений, отмен, проблем с выставлением счетов и т.д.

    • Шаг 9: Проверка поступлений

    • Шаг 10: Поддержка встроенный покупок из App Store

    • Завершение данного туториала по "аппаратной" подписке

  • Реализация StoreKit 2 с помощью сервера

    • Сервер App Store

    • Проектирование системы

    • Преимущества и компромиссы

  • Заключительные заметки

Терминология

In-app purchases (IAPs) (Встроенные покупки) - это цифровые продукты, которые приобретаются для использования в приложении. Чаще всего покупка происходит непосредственно в приложении, но иногда может быть инициирована из App Store.

У Apple есть четыре типа IAP:

  • Расходуемые - это продукты, которые можно приобрести один раз, несколько раз и в большом количестве. Примерами расходуемых покупок могут быть "жизни" или драгоценные камни в играх, бонусы в приложении для знакомств для повышения “видимости” или подсказки для разработчиков или творцов.

  • Нерасходуемые - это продукты, которые можно приобрести только один раз и срок годности которых не истекает. Примерами нерасходуемых покупок могут быть разблокировка премиум-функций в приложении, получение дополнительных кистей в приложении для рисования или получение косметических предметов в игре.

  • Подписки с автопродлением - эти продукты предоставляют постоянный доступ к контенту, услугам или премиум-функциям приложения. Плата с клиентов взимается на регулярной основе до тех пор, пока они не отменят подписку или пока не возникнут проблемы с выставлением счетов. Примерами автопродления могут быть доступ к программным услугам или контенту (например, уроки в образовательном приложении).

  • Подписки без автопродления - это продукты, которые предоставляют доступ к контенту, услугам и премиум-функциям приложения на ограниченное время без автоматического продления. Этот тип требует от пользователя ручной подписки для возобновления доступа к услуге или контенту. Примером подписки без автопродления может быть сезонный абонемент на внутриигровой контент.

Настройка App Store Connect

Первым шагом к добавлению встроенных покупок в приложение является создание продуктов в App Store Connect, - панели инструментов разработчика, используемой для размещения приложений в App Store. Разработчики могут использовать App Store Connect для создания листинга приложений, написания характеристик приложения, загрузки скриншотов, создания встроенных продуктов, управления тестировщиками и выпуска новых версий приложения в App Store.

Есть два разных раздела для создания и управления встроенными покупками. Первый раздел — «In-App Purchases», а второй — «Subscriptions». «In-App Purchases» предназначен для расходуемых и нерасходуемых покупок, а «Subscriptions» — для подписок с автопродлением и без автопродления. Подписки были разделены, поскольку их конфигурация сложнее, чем расходуемых и нерасходуемых покупок.

Панель инструментов App Store Connect указывает на расположение "In-App Purchases" и “Subscriptions".
Панель инструментов App Store Connect указывает на расположение "In-App Purchases" и “Subscriptions".

Требования App Store Connect

Существует несколько административных требований, которые необходимо выполнить, прежде чем ваше приложение сможет продавать IAP:

  1. Настроить банковскую информацию

  2. Подписать соглашение о Платном Приложении

Создание подписок (с автопродлением и без)

  1. Перейдите в раздел “Подписки".

  2. Создайте "Группу Подписок”

  3. Добавьте локализацию для "Группы Подписок”.

  4. Создайте новую подписку (имя ссылки и id продукта)

  5. Заполните все метаданные (продолжительность, цена, локализация, информация об отзывах)

Экран App Store Connect для добавления подписок с автопродлением и без
Экран App Store Connect для добавления подписок с автопродлением и без

Создание расходуемых и нерасходуемых покупок

  1. Перейдите в раздел "In-app Purchases”.

  2. Создайте новую встроенную покупку

  3. Выберите тип (расходуемая или нерасходуемая) и задайте название ссылки и ID продукта

  4. Заполните все метаданные (цена, локализация, информация об отзывах)

Экран App Store Connect для добавления расходуемых и нерасходуемых встроенных покупок
Экран App Store Connect для добавления расходуемых и нерасходуемых встроенных покупок

Настройка файла конфигурации StoreKit

Настройка продуктов в App Store Connect может потребовать много работы: первый шаг в начале пути к встроенным покупкам и подпискам может показаться огромным. Однако, несмотря на то, что эти шаги необходимы для выпуска приложения с IAP, для локальной разработки вам не нужен App Store! Начиная с Xcode 13, весь рабочий процесс со встроенными покупками может быть выполнен с помощью файла конфигурации StoreKit.

Использование конфигурации StoreKit имеет гораздо больше преимуществ, чем просто отсрочка входа в App Store Connect. Она также может быть использована для:

  • Тестирования потоков покупок в симуляторе

  • Тестирования потоков покупок в unit и UI-тестах

  • Локальное тестирование при отсутствии сетевого подключения

  • Отладка крайних случаев, которые сложно настроить или воспроизвести в среде песочницы

  • Тестирование сквозных транзакций с отказами, продлениями, проблемами с выставлением счетов, рекламными и ознакомительными предложениями.

На самом деле, пример кода и образец приложения в паре с этим туториалом используют файл конфигурации StoreKit. Исходный код можно загрузить и запустить без необходимости настраивать что-либо в App Store Connect.

Есть несколько замечательных видео WWDC, которые демонстрируют всю мощь файла конфигурации StoreKit:

У нас также есть статья в блоге об Улучшениях Тестирования StoreKit в iOS 14, которая отлично обобщает эти видео WWDC.

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

Создайте файл

  • Запустите Xcode, затем выберите File > New > File.

  • В поле поиска Filter введите "storekit".

  • Выберите "StoreKit Configuration File”.

  • Назовите его, установите флажок в “Sync this file with an app in App Store Connect”(“Синхронизировать этот файл с приложением в App Store Connect”) и сохраните его.

Добавьте продукты (необязательно)

В Xcode 14 добавлена возможность синхронизировать данный файл с приложением в App Store Connect. Это избавит вас от необходимости вручную добавлять продукты в файл конфигурации StoreKit, что полезно, если у вас уже есть продукты, определенные в App Store Connect, которые вы хотели бы отразить в локальном тестировании.

Однако, если вы еще используете Xcode 13 или хотите провести тестирование с другими типами продуктов или длительностью, вы все равно сможете это сделать.

  1. Нажмите «+» в левом нижнем углу в редакторе файлов конфигурации StoreKit в Xcode.

  2. Выберите тип встроенной покупки.

  3. Заполните обязательные поля:

  • Имя ссылки

  • ID продукта

  • Цена

  • Хотя бы одна локализация

Активируем Файл Конфигурации StoreKit

Чтобы использовать файл конфигурации StoreKit, недостаточно просто создать его. Файл конфигурации StoreKit должен быть выбран в схеме Xcode.

  1. Нажмите scheme и выберите «Edit scheme».

  2. Перейдите в «Run» > «Options».

  3. Выберите значение для «StoreKit Configuration».

Несмотря на то, что использование файла конфигурации StoreKit - самый простой способ тестирования IAP, бывают случаи, когда необходимо протестировать среду песочницы на устройстве. Постоянно менять схему между этими двумя файлами для изменения тестирования может быть хлопотно, поэтому специально для файла конфигурации StoreKit мы рекомендуем продублировать схему и назвать её как-нибудь вроде "YourApp (SK Config)”.

Аппаратная реализация подписок с помощью StoreKit 2 в Swift

Гарантия наличия продуктов, с которыми приложение будет приносить прибыль, - является первым шагом перед попыткой взаимодействия с любым из API StoreKit с помощью Swift, независимо от того, созданы они с помощью App Store Connect или файла конфигурации StoreKit.

Этот раздел представляет собой пошаговое руководство по использованию StoreKit 2 для написания кода, чтобы:

  • Составить список продуктов

  • Покупать продукты

  • Разблокировать опции для активных подписок и пожизненных покупок

  • Обрабатывать продления, отмены и ошибки выставления счетов

  • Проверять поступления (*доходы)

Эти шаги будут реализованы таким образом, чтобы не требовался внешний сервер. В StoreKit 1 выполнение логики покупок в приложении без бэкенда было сложным и небезопасным, но в StoreKit 2 Apple внесла ряд значительных улучшений, чтобы сделать его возможным и безопасным. Обработка покупок в бэкенде требует больше работы, но дает много преимуществ, которые мы рассмотрим позже.

Приступим!

Шаг 1: Список продуктов

Мы начинаем с того, что показываем пользователю платный доступ ко всем встроенным продуктам, которые можно купить в приложении, отображаемым в виде кнопок, позволяющих пользователю приобретать продукты при нажатии. В этом примере наше приложение покажет ежемесячную подписку, годовую подписку и покупку пожизненной разблокировки. Вот что будет реализовано в конце первого шага:

Пример платного доступа
Пример платного доступа

Для получения товаров из StoreKit 2 требуется всего несколько строк кода:

import StoreKit

let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]
let products = try await Product.products(for: productIds)

Приведенный выше фрагмент кода импортирует StoreKit, определяет массив строк идентификаторов продуктов для отображения в платном доступе, а затем извлекает эти продукты. Идентификаторы продуктов должны соответствовать продуктам, определенным в файле конфигурации StoreKit или в App Store Connect. Конечным результатом является массив объектов Product. Объект Product содержит всю информацию, необходимую для отображения на кнопках платного доступа. Эти Product-объекты в конечном итоге будут использоваться для совершения покупки при нажатии кнопки.

Идентификаторы продуктов жестко закодированы в этом примере, но они не должны быть жестко закодированы в прод-приложении (продакшине). Жестко закодированные идентификаторы затрудняют замену разных продуктов, требуя обновления приложения. Не у всех пользователей включены автоматические обновления, а это означает, что некоторые идентификаторы продуктов могут быть куплены на более длительный срок, чем предполагалось. Лучше всего предоставить список доступных продуктов для покупки из удаленного источника.

Теперь пришло время показать эти продукты во вью. Ниже представлено вью SwiftUI, которое будет хранить продукты в state-переменной.

import SwiftUI
import StoreKit

struct ContentView: View {
    let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]

    @State
    private var products: [Product] = []

    var body: some View {
        VStack(spacing: 20) {
           Text("Products")
            ForEach(self.products) { product in
                Button {
                    // Don't do anything yet
                } label: {
                    Text("\(product.displayPrice) - \(product.displayName)")
                }
            }
        }.task {
            do {
                try await self.loadProducts()
            } catch {
                print(error)
            }
        }
    }

    private func loadProducts() async throws {
        self.products = try await Product.products(for: productIds)
    }
}

В этом фрагменте метод StoreKit 2 для получения продуктов перемещен в новую функцию loadProducts(). Затем эта функция вызывается, когда наше вью появляется с помощью .task(). Продукты повторяются внутри цикла ForEach, который отображает Button для каждого продукта. На данный момент эта кнопка ничего не делает при нажатии.

Полную реализацию до этого шага можно найти здесь.

Шаг 2: Покупка продуктов

Теперь, когда пользователь может видеть все продукты в платном доступе, следующим шагом будет разрешить покупку продукта при нажатии кнопки.

Инициировать покупку с Product так же просто, как вызвать функцию purchase() для продукта.

private func purchase(_ product: Product) async throws {
   let result = try await product.purchase()
}

Если этот метод выдает ошибку (Product.PurchaseError или StoreKitError), покупка не прошла. Однако отсутствие ошибки еще не говорит о том, что покупка прошла успешно.

Результатом purchase() является перечисление Product.PurchaseResult. Результат покупки выглядит следующим образом:

public enum PurchaseResult {
    case success(VerificationResult<Transaction>)
    case userCancelled
    case pending
}

public enum VerificationResult<SignedType> {
    case unverified(SignedType, VerificationResult<SignedType>.VerificationError)
    case verified(SignedType)
}

Фрагмент ниже показывает, как обновить функцию purchase в приложении, чтобы проверить все возможные результаты покупки продукта.

private func purchase(_ product: Product) async throws {
    let result = try await product.purchase()

    switch result {
    case let .success(.verified(transaction)):
        // Successful purhcase
        await transaction.finish()
    case let .success(.unverified(_, error)):
        // Successful purchase but transaction/receipt can't be verified
        // Could be a jailbroken phone
        break
    case .pending:
        // Transaction waiting on SCA (Strong Customer Authentication) or
        // approval from Ask to Buy
        break
    case .userCancelled:
        // ^^^
        break
    @unknown default:
        break
    }
}

Эта таблица объясняет все различные типы результатов и их значение:

Последняя необходимая часть для совершения покупки — это вызов функции Purchase( product: Product) при нажатии кнопки.

ForEach(self.products) { (product) in
    Button {
       Task {
            do {
                try await self.purchase(product)
            } catch {
                print(error)
            }
        }
    } label: {
        Text("\(product.displayPrice) - \(product.displayName)")
    }
}

Полную реализацию вплоть до этого шага можно увидеть здесь.

Шаг 3: Подготовка к разблокировке встроенных опций

Теперь пользователь может увидеть все доступные встроенные продукты ( в данном примере две подписки и пожизненная покупка) и приобрести продукт, нажав кнопку. Однако реализация все еще не завершена: может потребоваться разблокировка опций, загрузка контента при совершении покупки или активная подписка.

Разблокировка контента и опций для пользователей после покупки — вот где все становится сложнее. Было бы идеально, если бы нам нужно было добавить логику разблокировки опций только после успешной покупки, но этого недостаточно. Помимо успешной покупки, есть еще несколько развитий процесса покупки, и на предыдущем шаге упоминались два из них: со Строгой Аутентификацией Клиента и Запросом на Покупку. Встроенные покупки также можно совершать вне приложения, приобретая их напрямую через App Store. Успешные покупки могут произойти в любое время, и наше приложение должно быть готово к любой ситуации.

Прежде чем начать обрабатывать все эти кейсы, нам нужно очистить существующую реализацию. В настоящее время вся логика покупок в приложении находится во вью SwiftUI. Это было здорово, когда мы пытались получить сквозной поток покупок, но плохо масштабировалось по мере роста приложения. Вся логика IAP должна быть перемещена из вью в переиспользуемый компонент. Это можно сделать разными способами, поэтому каждое приложение будет делать это по-своему, но на этом этапе мы перенесем логику IAP в новый объект PurchaseManager. Первоначально объект PurchaseManager будет отвечать за загрузку встроенных продуктов и покупку продукта, но вскоре будут добавлены дополнительные функции.

import Foundation
import StoreKit

@MainActor
class PurchaseManager: ObservableObject {

    private let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]

    @Published
    private(set) var products: [Product] = []
    private var productsLoaded = false

    func loadProducts() async throws {
        guard !self.productsLoaded else { return }
        self.products = try await Product.products(for: productIds)
        self.productsLoaded = true
    }

    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()

        switch result {
        case let .success(.verified(transaction)):
            // Successful purhcase
            await transaction.finish()
        case let .success(.unverified(_, error)):
            // Successful purchase but transaction/receipt can't be verified
            // Could be a jailbroken phone
            break
        case .pending:
            // Transaction waiting on SCA (Strong Customer Authentication) or
            // approval from Ask to Buy
            break
        case .userCancelled:
            // ^^^
            break
        @unknown default:
            break
        }
    }
}

Функции loadProducts() и Purchase() были перемещены в PurchaseManager, и ContentView будет использовать этот объект PurchaseManager. Однако ContentView не будет владельцем PurchaseManager. Он будет создан в App и передан в ContentView как объект среды. Такой подход позволяет другим вью SwiftUI легко получать доступ к тому же объекту PurchaseManager.

struct YourApp: App {
    @StateObject
    private var purchaseManager = PurchaseManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(purchaseManager)
        }
    }
}

Поскольку объект PurchaseManager является ObservableObject, вью SwiftUI будет автоматически обновляться при изменении его свойств. Прямо сейчас единственным свойством, которое имеет PurchaseManager, являются products.

struct ContentView: View {
    @EnvironmentObject
    private var purchaseManager: PurchaseManager

    var body: some View {
        VStack(spacing: 20) {
            Text("Products")
            ForEach(purchaseManager.products) { product in
                Button {
                    Task {
                        do {
                            try await purchaseManager.purchase(product)
                        } catch {
                            print(error)
                        }
                    }
                } label: {
                    Text("\(product.displayPrice) - \(product.displayName)")
                        .foregroundColor(.white)
                        .padding()
                        .background(.blue)
                        .clipShape(Capsule())
                }
            }
        }.task {
           Task {
                do {
                    try await purchaseManager.loadProducts()
                } catch {
                    print(error)
                }
            }
        }
    }
}

Приложение будет работать точно так же, как на Шаге 2, но теперь код стал более управляемым при добавлении поддержки дополнительных кейсов встроенных покупок.

Полную реализацию вплоть до этого шага можно увидеть здесь.

Шаг 4: Разблокировка встроенных опций

PurchaseManager теперь является идеальным местом для обработки логики определения того, следует ли разблокировать встроенные опции. Все это будет построено вокруг новой функции Transaction.currentEntitlements из StoreKit 2.

for await result in Transaction.currentEntitlements {
    // Do something with transaction
}

Имя currentEntitlements может звучать несколько странно по сравнению с остальными названиями StoreKit 2, но Transaction.currentEntitlements просто возвращает массив активных транзакций. Документация для currentEntitlements объясняет это следующим образом:

  • Транзакция для каждой нерасходуемой покупки в приложении

  • Последняя транзакция для каждой активной подписки с автопродлением

  • Последняя транзакция для каждой подписки без автопродления

  • Транзакция для каждой незавершенной расходуемой встроенной покупки.

PurchaseManager будет перебирать эти транзакции и сохранять идентификаторы продуктов в Set (массив, который не может содержать повторяющиеся элементы) с именем PurchasedProductID. Эти активные идентификаторы продуктов теперь можно использовать для разблокировки опций и контента в приложении для пользователей, которые их приобрели.

class PurchaseManager: ObservableObject {

    ...

    @Published
    private(set) var purchasedProductIDs = Set<String>()

    var hasUnlockedPro: Bool {
       return !self.purchasedProductIDs.isEmpty
    }

    func updatePurchasedProducts() async {
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }

            if transaction.revocationDate == nil {
                self.purchasedProductIDs.insert(transaction.productID)
            } else {
                self.purchasedProductIDs.remove(transaction.productID)
            }
        }
    }
}

Эту новую функцию updatePurchasedProducts необходимо вызывать при запуске приложения, после совершения покупки и при обновлении транзакций, чтобы гарантировать, что идентификаторы PurchasedProductID (и hasUnlockedPro) возвращают правильное значение, ожидаемое пользователем.

Во-первых, App необходимо обновить, чтобы оно вызывало updatePurchasedProducts() при запуске приложения. Это инициализирует массив приобретенных продуктов с текущими правами.

struct YourApp: App {
    @StateObject
    private var purchaseManager = PurchaseManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(purchaseManager)
                .task {
                    await purchaseManager.updatePurchasedProducts()
                }
        }
    }
}

Затем функция Purchase() должна вызвать updatePurchasedProducts() после успешной покупки. Это обновит массив приобретенных продуктов новым приобретенным продуктом.

func purchase(_ product: Product) async throws {
    let result = try await product.purchase()

    switch result {
    case let .success(.verified(transaction)):
        await transaction.finish()
        await self.updatePurchasedProducts()
    case let .success(.unverified(_, error)):
        break
    case .pending:
        break
    case .userCancelled:
        break
    @unknown default:
        break
    }
}

Последняя недостающая часть прослушивает новые транзакции, созданные вне приложения. Эти транзакции могут быть подписками, которые были отменены, продлены или отозваны из-за проблем с выставлением счетов, но они также могут быть новыми покупками, совершенными на другом устройстве, которые должны разблокировать контент на этом. Этот мониторинг можно выполнить, прослушивая изменения в асинхронной последовательности Transaction.updates:

@MainActor
 class PurchaseManager: ObservableObject {

    ...

    private var updates: Task<Void, Never>? = nil

    init() {
        updates = observeTransactionUpdates()
    }

    deinit {
        updates?.cancel()
    }

    ...

    private func observeTransactionUpdates() -> Task<Void, Never> {
        Task(priority: .background) { [unowned self] in
            for await verificationResult in Transaction.updates {
                // Using verificationResult directly would be better
                // but this way works for this tutorial
                await self.updatePurchasedProducts()
            }
        }
    }
}

Со всеми изменениями в PurchaseManager вью SwiftUI требуется всего несколько новых строк кода, чтобы удалить платный доступ после покупки продукта. Это делается путем добавления оператора if, проверяющего PurchaseManager.hasUnlockedPro. Поскольку PurchaseManager являетсяObservableObject, вью SwiftUI будет автоматически обновляться при изменении его свойств.

var body: some View {
    VStack(spacing: 20) {
        if purchaseManager.hasUnlockedPro {
            Text("Thank you for purchasing pro!")
        } else {
            Text("Products")
            ForEach(purchaseManager.products) { (product) in
                Button {
                    Task {
                        do {
                            try await purchaseManager.purchase(product)
                        } catch {
                            print(error)
                        }
                    }
                } label: {
                    Text("\(product.displayPrice) - \(product.displayName)")
                        .foregroundColor(.white)
                        .padding()
                        .background(.blue)
                        .clipShape(Capsule())
                }
            }
        }
    }
}

Полную реализацию вплоть до этого шага можно увидеть здесь.

Шаг 5: Обработка приобретенных продуктов в офлайне

В предыдущем шаге был представлен Transaction.currentEntitlements, который использовался для перебора последовательности приобретенных пользователем продуктов (нерасходуемые продукты, активные подписки или незавершенные расходуемые продукты). Чтобы гарантировать, что пользователь получает ожидаемое поведение приложения для каждого статуса подписки, эта функция будет получать последние транзакции, если есть доступ в Интернет. Если у пользователя нет подключения к Интернету — Wi-Fi не работает, включен режим полета и т. д. — Transaction.currentEntitlements вернет данные из локального кэша. Транзакции также будут отправлены на устройство, когда оно подключится к сети, что может позволить приложению иметь самые актуальные транзакции, когда пользователь время от времени выходит из сети.

StoreKit 2 отлично справился с этой реализацией. Разработчикам не нужно беспокоиться о создании какой-либо логики или пользовательского кэширования, чтобы их приложения с поддержкой StoreKit 2 работали в автономном режиме. Получение приобретенных продуктов с помощью Transaction.currentEntitlements работает одинаково как в сети, так и в автономном режиме.

Шаг 6: Восстановление покупок

Как и на предыдущем шаге, для восстановления покупок ничего делать не нужно: StoreKit будет автоматически обновлять статус подписки в приложении и историю транзакций с помощью Transaction.currentEntitlements и Transaction.all. При использовании этих функций у пользователя нет технических причин пытаться вручную восстановить транзакции. Тем не менее, в приложении по-прежнему рекомендуется иметь кнопку «Восстановить покупки»:

  1. Раздел 3.1.1 Руководства по проверке App Store требует, чтобы в приложении присутствовал механизм восстановления для любых восстанавливаемых встроенных покупок. Несмотря на то, что здесь прямо не указано, что кнопка «Восстановить покупки» необходима, и можно было бы обсудить, что поведение StoreKit по умолчанию поддерживает синхронизацию всех транзакций, наличие кнопки «Восстановить покупки» легко устраняет любую неопределенность в этом руководстве.

  2. Несмотря на то, что функционал восстановления покупки может не понадобиться, идеальный вариант - это дать пользователям ощущение контроля. Пользователи могут задаться вопросом, актуальны ли покупки или нет, и эта кнопка позволяет им убедиться в этом.

Добавление функции «Восстановление покупок» стало тривиальным благодаря AppStore.sync(). Согласно его документации, AppStore.sync() должен быть использован в кнопке «Восстановить покупки» и редко бывает нужен; но его наличие комфортно для пользователя, который подозревает, что что-то не так с транзакциями или статусом подписки. Существует сессия WWDC22 по внедрению профилактического восстановления встроенных покупок, которая более подробно рассматривает эту тему.

В редких случаях, когда пользователь подозревает, что приложение показывает не все транзакции, вызывается AppStore.sync(), что заставляет приложение получать информацию о транзакциях и статусе подписки из App Store.

Each(purchaseManager.products) { product in
    Button {
        Task {
            do {
                try await purchaseManager.purchase(product)
            } catch {
                print(error)
            }
        }
    } label: {
        Text("\(product.displayPrice) - \(product.displayName)")
    }
}

Button {
    Task {
        do {
            try await AppStore.sync()
        } catch {
            print(error)
        }
    }
} label: {
    Text("Restore Purchases")
}

Кнопка, вызывающая AppStore.sync(), будет размещена в платном доступе под списком продуктов, доступных для покупки. В исключительном случае, когда пользователь приобрел продукт, но платный доступ все еще отображается, AppStore.sync() обновит транзакции, платный доступ исчезнет, а купленный встроенный контент будет доступен для использования.

Полную реализацию до этого шага можно увидеть здесь.

Шаг 7: Совместное использование активных покупок с расширениями

Приложения iOS часто включают больше, чем просто основное приложение. Приложения могут состоять из Расширений Виджетов, Intent* Расширений, приложений для Watch и многого другого. Эти расширения, скорее всего, работают в отдельных контекстах от основного приложения, но есть большая вероятность, что статусы подписки могут разблокировать функциональность в этих расширениях. К счастью, StoreKit 2 упрощает эту задачу для большинства расширений.

Transaction.currentEntitlements можно использовать в расширениях так же, как на предыдущих шагах. Это работает для таких расширений, как Widgets и Intents. Однако приложение iOS с сопутствующим приложением watchOS не будет работать, даже если в нем можно выполнить Transaction.currentEntitlements. Сопутствующее приложение для часов не обновляется с той же историей транзакций, что и его приложение для iOS, поскольку они являются отдельными платформами.

* Фреймворк App Intents предлагает программный способ сделать контент и функции вашего приложения доступными для системных служб, таких как Siri и Shortcuts.

Расширения

Как упоминалось ранее, Transaction.currentEntitlements будет извлекать локально кэшированные транзакции, но также может получать последние транзакции, если устройство подключено к сети. Поскольку расширения приложений имеют очень ограниченное время выполнения или ограниченные возможности, поведение Transaction.currentEntitlements может оказаться слишком трудоемким, чтобы многократно обращаться к этим расширениям.

Лучше всего, чтобы расширения приложений смотрели на логическую составляющую, если определенные продукты были приобретены. Это можно сделать, сохранив общую информацию в экземпляре UserDefaults, который совместно используется приложениями и расширениями приложений с группами приложений.

Появится новый класс EntitlementsManager, который будет нести исключительную ответственность за сохранение состояния разблокированной функции, которое происходит при покупке продукта. PurchaseManager обновит EntitlementsManager после вызова Transaction.currentEntitlements из уже существующей функции updatePurchasedProducts().

import SwiftUI

class EntitlementManager: ObservableObject {
    static let userDefaults = UserDefaults(suiteName: "group.your.app")!

    @AppStorage("hasPro", store: userDefaults)
    var hasPro: Bool = false
}

EntitlementManager — это еще один ObservableObject, поэтому вью SwiftUI может наблюдать за изменениями и обновляться всякий раз, когда в нем что-то меняется. SwiftUI поставляется с очень хорошей оберткой свойств под названием @AppStorage, которая сохраняет свое значение в UserDefaults(suiteName:«group.your.app»), а также будет действовать как переменная @Published, которая будет перерисовывать ваши вью SwiftUI при обновлении.

Следующим шагом является предоставление PurchaseManager-у экземпляра EntitlementManager для обновления в updatePurchasedProducts().

class PurchaseManager: ObservableObject {

    ...

    private let entitlementManager: EntitlementManager

    init(entitlementManager: EntitlementManager) {
        self.entitlementManager = entitlementManager
    }

    ...

    func updatePurchasedProducts() async {
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }

            if transaction.revocationDate == nil {
                self.purchasedProductIDs.insert(transaction.productID)
            } else {
                self.purchasedProductIDs.remove(transaction.productID)
            }
        }

        self.entitlementManager.hasPro = !self.purchasedProductIDs.isEmpty
    }
}

Последняя переработка связана с инициализацией PurchaseManager в App. Раньше PurchaseManager можно было создать как StateObject с помощью @StateObject var PurchaseManager = PurchaseManager(). Теперь у нас будет две переменные StateObject, одна из которых зависит от другой, поэтому код инициализации должен быть более подробным.

struct YourApp: App {
    @StateObject
    private var entitlementManager: EntitlementManager

    @StateObject
    private var purchaseManager: PurchaseManager

    init() {
        let entitlementManager = EntitlementManager()
        let purchaseManager = PurchaseManager(entitlementManager: entitlementManager)

        self._entitlementManager = StateObject(wrappedValue: entitlementManager)
        self._purchaseManager = StateObject(wrappedValue: purchaseManager)
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(entitlementManager)
                .environmentObject(purchaseManager)
                .task {
                    await purchaseManager.updatePurchasedProducts()
                }
        }
    }
}
struct ContentView: View {
    @EnvironmentObject
    private var entitlementManager: EntitlementManager

    @EnvironmentObject
    private var purchaseManager: PurchaseManager

    var body: some View {
        VStack(spacing: 20) {
            if entitlementManager.hasPro {
                Text("Thank you for purchasing pro!")
            } else {
                Text("Products")
                ForEach(purchaseManager.products) { product in

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

Чтобы расширения могли проверять статус разблокировки, EntitlementManager просто необходимо предоставить целевым объектам расширений, что дает расширению возможность вызывать entitlementManager.hasPro. См. фрагмент ниже для точного кода, используемого в расширениях:

let entitlementManager = EntitlementManager()
if entitlementManager.hasPro {
    // Do something
} else {
    // Don't do something
}

Приложение в Watch

Как упоминалось выше, приложения-компаньоны ведут себя иначе, чем расширения, потому что они не получают тех же данных от приложения iOS с Transaction.currentEntitlements. Приложения на Watch также не могут использовать UserDefaults, которые совместно используются через группу приложений, а это означает, что EntitlementManager нельзя использовать непосредственно в приложении watchOS. Однако приложение для часов может использовать WatchConnectivity для связи с основным приложением iOS, которое может возвращать статусы прав из EntitlementManager. Возможно, это не та функция, которая будет нужна большинству разработчиков, но полезно знать, чем приложение для watchOS может отличаться от других расширений приложений для iOS.

Полную реализацию до этого шага можно увидеть здесь.

Шаг 8: Обработка продлений, отмен, проблем с выставлением счетов и т. д.

Это еще одна область, в которой блистает Transaction.currentEntitlements. Как упоминалось ранее, Transaction.currentEntitlements вернет последнюю транзакцию для каждой активной подписки с автопродлением и последнюю транзакцию для каждой подписки без автопродления.

Это означает, что всякий раз, когда приложение открывается, Transaction.currentEntitlements будет иметь обновленные транзакции, отражающие любые продления, отмены или проблемы с выставлением счетов. Пользователи сохранят свои права при продлении подписки и потеряют их при отмене. Пользователи также потеряют свои подписки, если возникнут какие-либо нерешенные проблемы с выставлением счетов.

Несмотря на то, что приложение будет следить за тем, чтобы пользователь получал правильный доступ к текущему состоянию своих подписок, это может привести к не лучшему взаимодействию с пользователем. Продление и отмена — это события, о которых пользователей не нужно уведомлять, но пользователь захочет получать уведомления о проблеме с выставлением счетов. Проблемы с выставлением счетов - это, как правило, незапланированные события, о которых пользователи захотят позаботиться, чтобы подписка не прерывалась.

Аппаратная обработка подписки не может хорошо информировать пользователя о проблемах с выставлением счетов и льготных периодах. Статус подписки всегда может быть актуальным, но в этом случае будет полезно обрабатывать подписку на стороне сервера. Обработка подписки на стороне сервера позволяет быстрее обнаруживать проблемы с выставлением счетов и льготными периодами, а также позволяет приложениям информировать пользователей.

Шаг 9: Проверка поступлений

Проверка поступлений исторически была очень важной задачей при интеграции StoreKit. В StoreKit 1 проверка и анализ квитанции были единственным способом определения покупок и того, что нужно разблокировать для пользователей. У Apple есть целая страница документации по выбору наилучшего способа проверки квитанций об оплате, в которой упоминается, что существует два способа проверки подлинности квитанции:

  • Локальная, аппаратная проверка квитанции, которая лучше всего подходит для проверки подписи квитанции для приложений со встроенными покупками.

  • Проверка квитанций на стороне сервера с помощью App Store, которая лучше всего подходит для сохранения встроенных покупок для ведения записей о покупках и управления ими.

До этого момента этот туториал был сосредоточен на полной локальной реализации StoreKit без каких-либо серверных компонентов. Таким образом, локальная аппаратная проверка остается единственным способом проверки подлинности квитанции. Существует также множество сообщений в блогах о том, как проверять и анализировать квитанцию, но, к счастью, любые мысли о поступлении можно игнорировать из-за улучшений, внесенных в StoreKit 2.

Причина, по которой квитанция до сих пор не упоминалась, заключается в том, что StoreKit 2 инкапсулирует весь синтаксический анализ и проверку внутри Transaction.currentEntitlements и Transaction.all, поэтому разработчикам не нужно беспокоиться ни о чем из этого.

Шаг 10: Поддержка встроенных покупок из App Store

Покупки в приложении не всегда инициируются из самого приложения: существуют потоки, в которых пользователь может приобретать встроенные продукты непосредственно из App Store. Разработчики могут устанавливать продвинутые встроенные покупки, которые отображаются на странице продукта этого приложения. Когда пользователь нажимает на встроенные продукты на странице App Store, приложение открывается и предлагает пользователю завершить покупку. Однако iOS не продолжает автоматически действие пользователя по покупке продукта. Разработчику необходимо добавить наблюдателя, чтобы определить, когда происходит такое поведение, но начиная с iOS 16, добавление подобного наблюдателя с помощью StoreKit 2 невозможно.

Продолжить транзакцию из App Store можно только с API StoreKit 1:

  1. Настраиваем протокол класса SKPaymentTransactionObserver (должен наследоваться от NSObject).

  2. Реализуем функцию paymentQueue(_:shouldAddStorePayment:for:).

  3. Добавляем SKPaymentTransactionObserver в SKPaymentQueue.

@MainActor
class PurchaseManager: NSObject, ObservableObject {

    ...

    init(entitlementManager: EntitlementManager) {
        self.entitlementManager = entitlementManager
        super.init()
        SKPaymentQueue.default().add(self)
    }

    ...
}

extension PurchaseManager: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {

    }

    func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
        return true
    }
}

Эта реализация (где paymentQueue(:shouldAddStorePayment:for:) возвращает true) продолжит транзакцию, как только пользователь войдет в приложение. Это самый простой способ заставить этот поток работать, но бывают случаи, когда немедленное продолжение транзакции может быть не лучшим поведением. Если это так, транзакции можно отложить на более позднее (и лучшее) время, вернув false и вручную добавив SKPayment в SKPaymentQueue, когда это лучше всего подходит для приложения.

Полную реализацию этого шага можно увидеть здесь.

Завершение данного туториала по аппаратной подписке

На этом туториал по внедрению встроенных покупок исключительно на устройстве завершен. Пользователь может просмотреть список продуктов на выбор, приобрести продукт, разблокировать контент или опции в приложении, восстановить покупки, использовать разблокировку в расширениях приложения, а также приобрести продвинутые продукты в App Store. Все это было реализовано в приложении с помощью Swift с использованием преимущественно StoreKit 2, но частично StoreKit 1.

Такой тип реализации StoreKit может очень хорошо работать для некоторых типов приложений. Например, он может хорошо работать, если приложение предназначено только для платформ Apple и использует Universal Purchases. Пользователи смогут приобрести IAP и получить ожидаемый контент или разблокировку опций в приложении.

Однако, одна лишь аппаратная реализация StoreKit может быть не идеальной для некоторых типов приложений. Статус подписки не может передаваться между платформами в веб или другие нативные платформы. Этот метод не дает разработчикам информации о поведении пользователей. Пользователи не могут быть уведомлены напрямую вне приложения (по электронной почте или другими способами) об ошибках выставления счетов или о кампаниях по возврату средств при отмене подписки. Во всей магии StoreKit 2 заключено множество данных и скрытых возможностей.

В следующем разделе этого туториала мы расскажем немного о том, что нужно для реализации StoreKit 2 с помощью бэкенда пользовательского сервера. Продолжайте читать, чтобы узнать, станет ли это лучшим решением для вас.

Реализация StoreKit 2 с помощью сервера

В приведенных выше шагах мы реализовали приложение с полной подпиской без необходимости писать какой-либо код вне приложения, но это не единственный вариант. Помимо собственных API StoreKit 2, Apple предлагает API сервера App Store для получения истории транзакций и статуса подписки.

В этой части туториала будет обсуждаться, как спроектировать веб-сервер для взаимодействия с сервером App Store и как связать приложение iOS с веб-сервером.

Образец бэкенда будет написан на Vapor (веб-сервере Swift) с заглушенными функциями и маршрутами. Пользовательский бэкенд можно найти здесь.

Сервер App Store

Это сервис с соответствующим названием для связи с App Store по поводу встроенных покупок и подписок. С ним можно взаимодействовать двумя способами: вызывая API Сервера App Store и используя Уведомления Сервера App Store.

API Сервера App Store (App Store Server API)

App Store Server API — это REST API для запроса информации о встроенных покупках клиентов. Этот API можно использовать для получения истории транзакций клиента, просмотра статуса всех подписок с автопродлением, обновления статуса расходуемых IAP, поиска заказов, получения истории возврата средств и продление даты обновления подписки. Для большинства этих конечных точек требуется исходный идентификатор транзакции, который получается из объекта Transaction после совершения покупки через собственный API StoreKit 2.Запросы к App Store Server API авторизуются путем генерации Web-Tокена JSON (JWT). Для генерации JWT необходимы Секретные ключи, которые могут быть созданы через App Store Connect.

Уведомления Сервера App Store

Уведомления сервера App Store предоставляют возможность отслеживать события покупки в приложении в режиме реального времени с помощью прямых уведомлений из App Store. URL-адрес HTTPS веб-сервера должен быть задан в App Store Connect для того, чтобы App Store Server Notifications, чтобы знать, куда отправлять запрос. Для каждой продакшн-среды и среды песочницы можно задать разные URL-адреса.

Существует 15 различных типов уведомлений, которые будут отправлены из версии 2 уведомлений сервера App Store:

  • CONSUMPTION_REQUEST

  • DID_CHANGE_RENEWAL_PREF

  • DID_CHANGE_RENEWAL_STATUS

  • DID_FAIL_TO_RENEW

  • DID_RENEW

  • EXPIRED

  • GRACE_PERIOD_EXPIRED

  • OFFER_REDEEMED

  • PRICE_INCREASE

  • REFUND

  • REFUND_DECLINED

  • RENEWAL_EXTENDED

  • REVOKE

  • SUBSCRIBED

  • TEST

Запросы из App Store Server Notifications отправляются как HTTP POST, где полезная нагрузка подписывается App Store в формате веб-подписи JSON (JSON Web Signature - JWS). JWS используется для повышения безопасности и уверенности в том, что полезная нагрузка была отправлена из App Store.

Полезная нагрузка уведомлений содержит:

Чтобы проверить соединение между App Store и бэкендом, наблюдающим за App Store Server Notifications, можно запросить тестовое событие в App Store Server API. Это самый простой способ выполнить сквозное тестирование соединения. До появления этого метода выполнить тест можно было, купив продукт либо в песочнице, либо в рабочем приложении.

Проектирование системы

Держа в голове App Store Server API и App Store Server Notifications, пришло время спроектировать систему.

Покупка продуктов

Покупка продукта по-прежнему должна осуществляться с помощью Products.purchase(product)  через собственный API StoreKit 2, но после этого все быстро меняется. Приложение больше не будет полагаться на Transaction.currentEntitlements StoreKit 2 для определения того, какие продукты активны в данный момент; вместо этого он отправит информацию о транзакции в бэкенд. Бэкенд запрашивает исходный идентификатор транзакции, чтобы иметь возможность получать историю транзакций из App Store Server API и сопоставлять обновленные транзакции из App Store Server Notifications для пользователя. Это дает преимущество возможности независимо проверить покупку, а также дает бэкенду концепцию подписки.

Наряду с публикацией транзакции после успешной покупки приложение также должно публиковать обновления транзакций, которые оно обнаруживает, наблюдая за Transaction.updates. Это необходимо для покупок .pending, которые ожидали Строгой Аутентификации Клиента или Запроса на Покупку, у которых не было идентификаторов транзакций на момент покупки.

Ниже приведен пример того, как могут выглядеть изменения в приложении для iOS.

func purchase(_ product: Product) async throws {
    let result = try await product.purchase()

    switch result {
    case let .success(.verified(transaction)):6        await transaction.finish()
         await postTransaction(transaction)
    case let .success(.unverified(_, error)):9        break
    case .pending:
        break
    case .userCancelled:
        break
    @unknown default:
        break
    }
}

private func observeTransactionUpdates() -> Task<Void, Never> {
    Task(priority: .background) {
        for await result in Transaction.updates {
            guard case .verified(let transaction) = result else {
                continue
            }

            guard let product = products.first(where: { product in
                product.id == transaction.productID
            }) else { continue }
            await postTransaction(transaction, product: product)
        }
    }
}

func postTransaction(_ transaction: Transaction, product: Product) async {
    let originalTransactionID = transaction.originalID
    let body: [String: Any] = [
        "original_transaction_id": originalTransactionID,
        "price": product.price,
        "display_price": product.displayPrice
   ]
    await httpPost(body: body)
}

Образец бэкенда Vapor получит новую конечную точку для публикации транзакции. Конечная точка должна выполнять все следующие действия:

  • Убедиться, что пользователь приложения вошел в систему

  • Отправить запрос на сервер App Store для проверки и получения информации о транзакции

  • Хранить важную информацию о транзакциях в базе данных

  • Определять и обновлять разблокированные права или опции для вошедшего в систему пользователя

Ниже показано начало того, как будет выглядеть пример бэкенда Vapor.

app.post("apple/transactions") { req async -> Response in
    do {
        let newTransaction = try req.content.decode(PostTransaction.self)
        let transaction = processTransaction(
            originalTransactionID: newTransaction.originalTransactionId)
        return try await transaction.encodeResponse(for: req)
    } catch {
        return .init(status: .badRequest)
    }
}

Обратите внимание, что все обновления транзакций (продление, отмена и т. д.) также будут отслеживаться с помощью этой функции. Это означает, что бэкенд может получать повторяющиеся транзакции из приложения и в режиме реального времени из App Store Server Notifications. Бэкенд должен быть в состоянии обрабатывать эту модель поведения.

Обработка цен на продукцию

Сохранение цены вместе с транзакцией важно для понимания стоимости пожизненного (LTV) обслуживания клиента и принятия решений о расходах на привлечение пользователей. Лучший способ сделать это — хранить общую сумму расходов пользователя в истории транзакций.

Возможно вас удивит тот факт, что App Store Server не включает в транзакции цену или валюту продукта. Это означает, что приложению для iOS необходимо передавать цену продукта. Объект Product в StoreKit 2 имеет свойства price (числовое значение) и displayPrice (строковое значение), которые можно использовать для этой цели. StoreKit 2 (начиная с iOS 16) не имеет возможности напрямую получить валюту, используемую для покупки продукта. Отправка displayPrice дает бэкенду возможность вручную проанализировать валюту на основе символа в строке. Если ручного анализа валюты недостаточно, можно использовать StoreKit 1 для получения валюты из объекта SKProduct. Это муторно и требует больше работы, но это будет более надежно.

Обработка продлений, отмен, проблем с выставлением счетов и т. д.

Приложение теперь зависит от пользовательского бэкенда, чтобы поддерживать актуальность всех подписок и транзакций. Бэкенд должен как можно скорее быть уведомлен о продлениях, отменах, проблемах с выставлением счетов и возмещениях, чтобы пользователь приложения не получал функции или услуги, за которые он не платит. Это можно сделать двумя способами:

  • Наблюдая в реальном времени за уведомлениями из App Store Server Notifications

  • Опрашивая App Store Server API

Хотя нет необходимости реализовывать и то, и другое, это позволит создать гораздо более прочный, надежный и заслуживающим доверия источник.

Наблюдая за App Store Server Notifications

Активация App Store Server Notifications - это лучшая практика и наиболее эффективный способ поддерживать актуальность статуса подписки и возмещенных транзакций. Apple отправляет HTTP POST на URL-адрес, определенный разработчиком в App Store Connect, с информацией об обновленной транзакции, как только она была обновлена. Это гарантирует, что любая система, наблюдающая за App Store Server Notifications, будет соответствовать информации о транзакциях, которую Apple имеет в файле.

Фрагмент кода ниже показывает, как начать обработку уведомлений с сервера App Store:

  1. Запрос отправляется на https://.com/apple/notifications.

  2. Полезная нагрузка запроса декодируется в структуру Swift SignedPayload, которая представляет responseBodyV2. Структура содержит подписанную полезную нагрузку (JWS).

  3. Подписанная полезная нагрузка декодируется с использованием корневого сертификата Apple в структуру Swift NotificationPayload, которая представляет responseBodyV2DecodedPayload.

  4. Затем NotificationPayload отправляется в рабочую очередь для обработки транзакции вне основного процесса веб-сервера. Именно здесь будут выполняться обновления базы данных транзакций и прав доступа пользователей, а также здесь можно отправлять электронные письма или другие уведомления пользователям, чтобы сообщить им о любых проблемах с выставлением счетов, истечении срока действия или возможных предложениях по возврату средств.

struct SignedPayload: Decodable {
    let signedPayload: String
}

struct NotificationPayload: JWTPayload {
    let notificationType: NotificationType
    let notificatonSubtype: NotificationSubtype?
    let notificationUUID: String
    let data: NotificationData
    let version: String
    let signedDate: Int

    func verify(using signer: JWTSigner) throws {
    }
}

app.post("apple/notifications") { req async -> HTTPStatus in
    do {
        let notification = try req.content.decode(SignedPayload.self)

        let payload = try req.application.jwt.signers.verifyJWSWithX5C(
            notification.signedPayload,
            as: NotificationPayload.self,
            rootCert: appleRootCert)

        // Add job to process and update transaction
        // Updates user's entitlements (unlocked products or features)
        try await req.queue.dispatch(AppleNotificationJob.self, payload)

        return .ok
    } catch {
        return .badRequest
    }
 }

Полную модель SignedPayload можно найти здесь.

Одна из самых рискованных частей использования App Store Server Notifications — пропустить уведомления. Если сервер App Store не получает ответ или обнаруживает неверный HTTP-ответ на веб-запрос, он повторяет попытку отправить уведомление еще пять раз. Эти повторные попытки будут происходить через 1, 12, 24, 48 и 72 часа после предыдущей попытки. Наиболее вероятно, что это произойдет из-за перебоев в работе сервера или службы. У Apple есть другое решение для этого — получение истории уведомлений через App Store Server API. Если какая-либо из этих попыток будет пропущена, App Store Server API можно использовать для воспроизведения этих уведомлений, чтобы получить резервную копию.

Существует более обширная документация о том, как включать, получать и отвечать на уведомления, которые выходят за рамки этого туториала.

Опрос App Store Server API

Даже при использовании уведомлений в режиме реального времени, для надежной системы необходим периодический опрос App Store Server API на наличие обновлений транзакций. Обработка транзакций очень похожа на уведомления в реальном времени, но сложная часть этого подхода заключается в том, чтобы определить, как часто планировать эти вызовы API к серверу App Store.

Наиболее очевидным подходом было бы планирование подписок с автопродлением сразу после истечения срока их действия или продления. Статусы подписки также должны быть проверены перед их продлением, чтобы увидеть, не были ли они отменены, что позволяет предупредить пользователя о скором окончании обслуживания или попытаться вернуть пользователя с помощью какого-либо предложения. Идеального момента для этого не существует, поскольку пользователь может отменить услугу в любое время. В начале этот опрос может быть нечастым и становиться более частым по мере приближения срока действия или времени обновления.

Опрос также необходимо будет перенести после периода продления, если возникнут проблема с выставлением счетов из-за льготного периода. Это позволяет пользователю сохранить обслуживание на несколько дней, если он сможет исправить свою платежную информацию.

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

Поначалу опрос кажется технически простым, но может быстро превратиться в сложный танец планирования, поэтому лучше всего использовать уведомления сервера App Store: Apple будет уведомлять только о тех вещах, которые меняются, и точном времени их изменения.

Список продуктов

Доступные для покупки продукты будут возвращены API. Эти идентификаторы продуктов в идеале должны храниться в базе данных, чтобы их можно было легко настроить при изменении платного доступа к приложению. Идентификаторы продуктов, предоставляемые через API, позволяют даже проводить A/B-тестирование платных сетей с пользователями, чтобы определить, какой список продуктов работает лучше всего. Любое решение здесь лучше, чем жестко запрограммировать идентификатор продукта в приложении, которое можно изменить только через обновление приложения.

Преимущества и компромиссы

Собственный API StoreKit 2 скрывает множество тонкостей покупок и подписок в приложении, когда дело доходит до определения текущих прав. Есть веские причины использовать только собственный API, но есть и другие причины использовать серверную часть для управления подписками, в том числе следующие:

  • Получайте уведомления в режиме реального времени об изменении статуса подписки и возврате средств

  • Проверка транзакций и квитанций

  • Управляйте клиентами и предлагайте возмещение и продление подписки

  • Анализируйте статистику доходов и оттока вне App Store Connect

  • Привязать статус подписки к функциям серверной службы

  • Делитесь покупками в Интернете и на других мобильных платформах

Разработка серверной реализации для обработки IAP с помощью StoreKit 2 становится экспоненциально более сложной по сравнению с чистой реализацией на устройстве. Из-за этого эта часть руководства не будет полным пошаговым руководством по созданию собственного бэкенда подписки.

Заключительные заметки

Наша главная цель в RevenueCat — помочь разработчикам заработать больше денег. Мы делаем это, предлагая целую платформу мобильной подписки с собственными и кросс-платформенными SDK, чтобы максимально упростить интеграцию покупок в любом приложении. Нашим клиентам не нужно беспокоиться о различных собственных API-интерфейсах подписки или сложностях синхронизации статуса подписки с продлением, отменой и проблемами выставления счетов.

Несмотря на то, что мы хотели бы иметь всех в качестве клиентов, мы знаем, что иногда лучше создавать свои собственные, и в конце концов мы хотим, чтобы все разработчики преуспели.

Комментарии (0)