Irbisa · Шпаргалка

StoreKit и релиз в App Store

StoreKit 2, подписки и офферы, права доступа, серверные уведомления, возвраты, тестирование покупок, App Review, поэтапный релиз, принудительное обновление

Middle-разработчик · 19 вопросов

  1. 01

    Какие покупки обязаны идти через встроенные покупки Apple, а какие можно провести через Apple Pay или свою оплату?

    Всё цифровое, чем пользуются внутри приложения (премиум-функции, подписка на контент, монеты, отключение рекламы), продаётся только через встроенные покупки (in-app purchase).

  2. 02

    Какие четыре типа встроенных покупок существуют и что каждый из них обязывает поддерживать в приложении?

    Расходуемые покупки (монеты, кредиты) покупают снова и снова, нерасходуемые (вечная разблокировка) один раз и навсегда, автовозобновляемые подписки продлеваются до отмены, а невозобновляемые подписки (сезонный абонемент) просто заканчиваются.

  3. 03

    Опишите покупку в StoreKit 2: какие вызовы происходят между загрузкой продукта и открытием контента?

    Загрузите продукты через Product.products(for:), вызовите purchase() у выбранного, убедитесь, что результат равен .success(.verified(transaction)), выдайте контент и затем вызовите transaction.finish().

    import StoreKit @MainActorfinal class PaywallModel {  private(set) var products: [Product] = []  var unlock: (String) -> Void = { _ in }…
  4. 04

    Чем отличаются CFBundleShortVersionString и CFBundleVersion и что из них должно меняться при каждой загрузке?

    CFBundleShortVersionString задаёт версию, которую пользователь видит в App Store (2.4.0); CFBundleVersion задаёт номер сборки (1873).

  5. 05

    Как попросить оценку в App Store через requestReview и почему приложение не может узнать, показался ли диалог?

    Вызовите действие requestReview из окружения SwiftUI (в UIKit AppStore.requestReview(in:)) в удачный момент, а показывать ли диалог, решит система.

    import StoreKitimport SwiftUI struct OrderPlacedView: View {  @Environment(\.requestReview) private var requestReview  let completedOrders: Int…
  6. 06

    Почему приложение должно начинать слушать Transaction.updates при запуске и какие покупки теряются без этого?

    Многие транзакции вообще не проходят через ваш вызов purchase(): продления, одобрения Ask to Buy, покупки на другом устройстве, возвраты денег, offer codes, активированные в App Store, и всё, что осталось незавершённым после краша.

    import StoreKit @MainActorfinal class Store {  static let shared = Store()  private(set) var ownedProductIDs: Set<String> = []…
  7. 07

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

    func handle(_ result: VerificationResult<Transaction>) async {  guard case .verified(let transaction) = result else { return }  await transaction.finish()  do {    // The backend verifies the JWS and adds the coins to the account    try await api.creditCoins(signedTransaction: result.jwsRepresentation)    await wallet.reload()  } catch {    logger.error("credit failed: \(error)")    showToast("Something went wrong. Please try again.")  }}// Called for purchase() results and for every Transaction.updates element

    finish() вызывается до того, как монеты начислены.

  8. 08

    Как определить, чем владеет пользователь, через Transaction.currentEntitlements и почему флаг isPremium в UserDefaults не годится как источник правды?

    Пересчитывайте права доступа из Transaction.currentEntitlements при запуске и после каждого обновления: там лежит последняя подписанная транзакция по каждой активной подписке и нерасходуемой покупке.

    import StoreKit enum Tier: Comparable { case free, plus, pro } @MainActorfinal class Entitlements {…
  9. 09

    Что делает Restore Purchases в StoreKit 2, почему App Review всё ещё ждёт эту кнопку и когда вызывать AppStore.sync()?

    В StoreKit 2 восстановление в основном происходит само: транзакции синхронизируются на все устройства с тем же Apple Account и появляются в currentEntitlements.

    import StoreKitimport SwiftUI struct RestoreButton: View {  let refresh: () async -> Bool   // recomputes entitlements; true if anything is owned  @State private var note: String?…
  10. 10

    Как работают группы и уровни подписок и что происходит при апгрейде, даунгрейде и кроссгрейде?

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

  11. 11

    Как показать бесплатный пробный период только тем, кому он положен, и чем отличаются introductory offers, promotional offers и offer codes?

    Показывайте пробный период, только когда isEligibleForIntroOffer отвечает «да»: одно вводное предложение на группу подписок на один Apple Account.

    import StoreKit // The line under a plan on the paywallfunc offerLine(for product: Product) async -> String {  guard let subscription = product.subscription,        let intro = subscription.introductoryOffer,…
  12. 12

    У подписчика не прошла оплата при продлении. Что такое billing retry и billing grace period и что приложение должно показывать в каждом состоянии?

    Если продление не прошло, Apple повторяет списание до 60 дней (billing retry).

    import StoreKit enum BillingBanner {  case none  case fixPayment(accessUntil: Date?)   // grace period: still premium  case paymentFailed                    // billing retry: access is off, Apple keeps retrying…
  13. 13

    Пользователь получил возврат денег от Apple. Как об этом узнают приложение и ваш сервер и что позволяет предложить в приложении beginRefundRequest?

    Решение о возврате принимает Apple, а не вы.

    import StoreKitimport SwiftUI struct PurchaseRow: View {  let transactionID: StoreKit.Transaction.ID   // SwiftUI has its own Transaction type  @State private var showRefund = false…
  14. 14

    Как тестировать встроенные покупки с помощью файла конфигурации StoreKit, песочницы и TestFlight и чего не ловит каждый из способов?

    Используйте три слоя: файл конфигурации .storekit для быстрых локальных запусков и юнит-тестов, песочницу (sandbox) для настоящих продуктов из App Store Connect и серверных уведомлений и TestFlight для релизной сборки в руках тестировщиков.

    import StoreKitimport StoreKitTestimport XCTest final class EntitlementTests: XCTestCase {  func testRefundRemovesTheEntitlement() async throws {…
  15. 15

    App Review отклонил вашу сборку. Как ответить в App Store Connect, когда подавать апелляцию и когда просить ускоренное ревью?

    Прочитайте указанный пункт guidelines и заметки ревьюера, затем исправьте и отправьте снова или ответьте в App Store Connect, чтобы объяснить или спросить, что именно нужно.

  16. 16

    В App Store нет принудительного обновления. Как перевести пользователей на минимальную поддерживаемую версию и что должно продолжать работать в старой сборке?

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

    import Foundation // Served by your backend or remote config, cached on disk, fetched at launchstruct VersionPolicy: Decodable {  let minimum: String       // below it: a blocking screen  let recommended: String   // below it: a banner the user can dismiss…
  17. 17

    Как ваш бэкенд узнаёт о продлениях, возвратах и отменах и почему доверять подписанной Apple JWS-транзакции, а не слову приложения?

    Apple отправляет App Store Server Notifications V2 на зарегистрированный вами HTTPS-эндпоинт, а App Store Server API отвечает на вопросы по запросу.

    {  "notificationType": "DID_FAIL_TO_RENEW",  "subtype": "GRACE_PERIOD",  "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20",  "version": "2.0",  "signedDate": 1761292800000,…
  18. 18

    Пользователь оформил подписку на iPhone, а потом вошёл на Android и на сайте. Где живёт право доступа и как appAccountToken связывает покупку с вашим аккаунтом?

    На вашем сервере, по вашему ID пользователя: каждый магазин знает только о своих покупках.

    import StoreKit struct Account { let id: String; let storeToken: UUID }   // storeToken is created by your server func buy(_ product: Product, for account: Account) async throws -> Product.PurchaseResult {  // The UUID comes back in the signed transaction and in every server notification…
  19. 19

    Релиз падает при запуске у трети пользователей, а отката в App Store нет. Что можно сделать в первый час и что должно было существовать заранее?

    Остановите распространение, затем катите вперёд: поставьте поэтапный релиз на паузу, по возможности отключите падающий путь с сервера и выпустите исправление (или код предыдущей версии под более высоким номером версии) с запросом ускоренного ревью.