Rozmowa Kwalifikacyjna StoreKit 2: Zarządzanie Subskrypcjami i Walidacja Paragonów

Opanuj pytania na rozmowy iOS dotyczące StoreKit 2, zarządzania subskrypcjami, walidacji paragonów i implementacji zakupów w aplikacji z praktycznymi przykładami kodu Swift.

Architektura subskrypcji iOS StoreKit 2 i walidacja paragonów

StoreKit 2 stanowi fundamentalną zmianę w sposobie obsługi zakupów w aplikacji w iOS. Wprowadzony wraz z iOS 15, ten nowoczesny framework drastycznie upraszcza kod wymagany do zarządzania subskrypcjami i walidacji transakcji. Od iOS 18, StoreKit 1 jest oficjalnie przestarzały, co czyni StoreKit 2 jedyną wspieraną ścieżką. Techniczne rozmowy kwalifikacyjne iOS regularnie poruszają ten temat, aby ocenić wiedzę programistów w zakresie monetyzacji aplikacji.

Kluczowy Wgląd na Rozmowę

StoreKit 2 wykorzystuje natywne API Swift z async/await, eliminując potrzebę skomplikowanych callbacków i walidacji paragonów po stronie klienta. Rozmówcy cenią kandydatów, którzy potrafią wyartykułować podstawowe różnice w stosunku do StoreKit 1 oraz nowe funkcje iOS 18.4, takie jak appTransactionID i SubscriptionOfferView.

Przegląd Architektury StoreKit 2

StoreKit 2 jest zbudowany na nowoczesnej architekturze, która w pełni wykorzystuje Swift Concurrency. W przeciwieństwie do StoreKit 1, wszystkie operacje są asynchroniczne i używają natywnych typów Swift.

Pobieranie Dostępnych Produktów

Pierwszy krok obejmuje załadowanie produktów skonfigurowanych w App Store Connect. StoreKit 2 upraszcza tę operację za pomocą deklaratywnego API.

ProductService.swiftswift
import StoreKit

actor ProductService {

    // Product identifiers configured in App Store Connect
    private let productIdentifiers: Set<String> = [
        "com.app.subscription.monthly",
        "com.app.subscription.yearly",
        "com.app.premium.lifetime"
    ]

    // Product cache to avoid repeated network calls
    private var cachedProducts: [Product] = []

    func loadProducts() async throws -> [Product] {
        // Return cache if available
        guard cachedProducts.isEmpty else {
            return cachedProducts
        }

        // Asynchronous request to the App Store
        let products = try await Product.products(for: productIdentifiers)
        cachedProducts = products
        return products
    }

    func product(for identifier: String) async throws -> Product? {
        let products = try await loadProducts()
        return products.first { $0.id == identifier }
    }
}

Użycie actor zapewnia bezpieczeństwo wątków podczas zarządzania cache produktów.

Typy Produktów w StoreKit 2

StoreKit 2 wyraźnie rozróżnia różne typy produktów. Zrozumienie tego rozróżnienia jest kluczowe dla rozmów kwalifikacyjnych.

ProductType+Extensions.swiftswift
import StoreKit

extension Product.ProductType {

    var displayName: String {
        switch self {
        case .consumable:
            // Consumable products (credits, lives, etc.)
            return "Consumable"
        case .nonConsumable:
            // Permanent purchases (feature unlock)
            return "Non-Consumable"
        case .autoRenewable:
            // Subscriptions with automatic renewal
            return "Auto-Renewable Subscription"
        case .nonRenewable:
            // Subscriptions without renewal (temporary pass)
            return "Non-Renewable Subscription"
        @unknown default:
            return "Unknown"
        }
    }
}

Subskrypcje z automatycznym odnawianiem stanowią dominujący model biznesowy nowoczesnych aplikacji iOS.

Obsługa Transakcji Zakupowych

Proces zakupu w StoreKit 2 staje się liniowy dzięki async/await. Koniec z delegatami i zagnieżdżonymi callbackami z StoreKit 1.

Inicjowanie Zakupu

Począwszy od iOS 18.2, zakupy wymagają parametru kontekstu UI. Ta zmiana zapewnia, że arkusz zakupu pojawia się prawidłowo w hierarchii widoków aplikacji.

PurchaseManager.swiftswift
import StoreKit
import UIKit

actor PurchaseManager {

    enum PurchaseError: Error {
        case productNotFound
        case purchaseCancelled
        case purchasePending
        case verificationFailed
        case unknown
    }

    func purchase(_ product: Product, in scene: UIWindowScene) async throws -> Transaction {
        // Initiate purchase with UI context (iOS 18.2+)
        let result = try await product.purchase(confirmIn: scene)

        switch result {
        case .success(let verification):
            // Verify the transaction signature
            let transaction = try checkVerification(verification)

            // Mark the transaction as finished
            await transaction.finish()

            return transaction

        case .userCancelled:
            // User cancelled the purchase modal
            throw PurchaseError.purchaseCancelled

        case .pending:
            // Purchase pending (parental approval, etc.)
            throw PurchaseError.purchasePending

        @unknown default:
            throw PurchaseError.unknown
        }
    }

    private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            // Transaction signed and verified by the App Store
            return safe
        case .unverified(_, let error):
            // Invalid or corrupted signature
            throw PurchaseError.verificationFailed
        }
    }
}

Metoda finish() jest kluczowa: sygnalizuje App Store, że treść została dostarczona użytkownikowi.

Ostrzeżenie na Rozmowę

Nigdy nie zapominaj wywołać transaction.finish() po dostarczeniu treści. Niezakończona transakcja zostanie przywrócona przy następnym uruchomieniu aplikacji, powodując nieoczekiwane zachowanie.

Nasłuchiwanie Transakcji w Tle

StoreKit 2 wprowadza Transaction.updates, asynchroniczny strumień, który emituje transakcje zachodzące poza aplikacją (odnowienia, zakupy w ramach family sharing itp.).

TransactionObserver.swiftswift
import StoreKit

actor TransactionObserver {

    private var updateTask: Task<Void, Never>?

    func startObserving() {
        // Cancel any previous observation
        updateTask?.cancel()

        updateTask = Task(priority: .background) {
            // Infinite stream of transaction updates
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerification(result)

                    // Process the transaction based on its type
                    await self.handleTransaction(transaction)

                    // Always finish the transaction
                    await transaction.finish()
                } catch {
                    // Log the error for debugging
                    print("Transaction verification failed: (error)")
                }
            }
        }
    }

    func stopObserving() {
        updateTask?.cancel()
        updateTask = nil
    }

    private func handleTransaction(_ transaction: Transaction) async {
        switch transaction.productType {
        case .autoRenewable:
            // Update subscription status
            await updateSubscriptionStatus(transaction)
        case .nonConsumable:
            // Unlock premium feature
            await unlockFeature(transaction.productID)
        default:
            break
        }
    }

    private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            return safe
        case .unverified:
            throw PurchaseError.verificationFailed
        }
    }

    private func updateSubscriptionStatus(_ transaction: Transaction) async {
        // Implementation of status update
    }

    private func unlockFeature(_ productID: String) async {
        // Implementation of feature unlock
    }

    enum PurchaseError: Error {
        case verificationFailed
    }
}

Obserwacja transakcji powinna rozpocząć się natychmiast po uruchomieniu aplikacji, aby nie przegapić żadnych aktualizacji.

Często Zadawane Pytania na Rozmowach o Subskrypcje StoreKit 2

Rozmowy iOS często zawierają konkretne pytania dotyczące zarządzania subskrypcjami. Oto najczęstsze z nich wraz ze szczegółowymi odpowiedziami. Więcej tematów dotyczących współbieżności Swift znajdziesz w przewodniku po Swift structured concurrency.

Pytanie 1: Jak Sprawdzić Aktualny Status Subskrypcji?

StoreKit 2 zapewnia bezpośredni dostęp do aktywnych entitlementów. Należy zauważyć, że Transaction.currentEntitlement(for:) jest przestarzałe od iOS 18.4. Nowe currentEntitlements(for:) zwraca asynchroniczną sekwencję do obsługi wielu entitlementów (na przykład subskrypcja plus dostęp Family Sharing).

SubscriptionStatusChecker.swiftswift
import StoreKit

struct SubscriptionStatusChecker {

    struct SubscriptionStatus {
        let isActive: Bool
        let expirationDate: Date?
        let willRenew: Bool
        let productID: String?
        let appTransactionID: UInt64?  // iOS 18.4+
    }

    static func checkCurrentStatus(for productID: String) async -> SubscriptionStatus {
        // Use the new currentEntitlements(for:) API (iOS 18.4+)
        var latestTransaction: Transaction?

        for await result in Transaction.currentEntitlements(for: productID) {
            if case .verified(let transaction) = result {
                if transaction.productType == .autoRenewable {
                    if latestTransaction == nil ||
                       transaction.purchaseDate > latestTransaction!.purchaseDate {
                        latestTransaction = transaction
                    }
                }
            }
        }

        guard let transaction = latestTransaction else {
            return SubscriptionStatus(
                isActive: false,
                expirationDate: nil,
                willRenew: false,
                productID: nil,
                appTransactionID: nil
            )
        }

        // Check if the subscription is still active
        let isActive = transaction.expirationDate ?? Date.distantPast > Date()

        // Retrieve renewal information
        let renewalInfo = await getRenewalInfo(for: transaction.productID)

        return SubscriptionStatus(
            isActive: isActive,
            expirationDate: transaction.expirationDate,
            willRenew: renewalInfo?.willAutoRenew ?? false,
            productID: transaction.productID,
            appTransactionID: transaction.appTransactionID
        )
    }

    private static func getRenewalInfo(for productID: String) async -> Product.SubscriptionInfo.RenewalInfo? {
        guard let product = try? await Product.products(for: [productID]).first,
              let subscription = product.subscription else {
            return nil
        }

        // Retrieve renewal status
        let status = try? await subscription.status.first
        return status?.renewalInfo
    }
}

To podejście zapewnia precyzyjny stan subskrypcji bez wywołań serwerowych. Pole appTransactionID, wprowadzone w iOS 18.4, umożliwia śledzenie klientów między transakcjami bez wywołań serwer-serwer.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Pytanie 2: Jak Obsługiwać Bezpłatne Wersje Próbne i Oferty Promocyjne?

StoreKit 2 upraszcza dostęp do informacji o ofertach. Rozmówcy często testują rozumienie różnych typów ofert. Od iOS 18.4, oferty promocyjne można podpisywać za pomocą JWS przy użyciu nowego PromotionOfferV2SignatureCreator dostępnego w App Store Server Library.

OfferManager.swiftswift
import StoreKit

struct OfferManager {

    enum OfferType {
        case freeTrial(duration: String)
        case payAsYouGo(periods: Int, price: Decimal)
        case payUpFront(duration: String, price: Decimal)
        case oneTime(price: Decimal)  // iOS 17.2+: offer codes for all product types
        case none
    }

    static func getIntroductoryOffer(for product: Product) -> OfferType {
        guard let subscription = product.subscription,
              let offer = subscription.introductoryOffer else {
            return .none
        }

        switch offer.paymentMode {
        case .freeTrial:
            // Free trial period
            return .freeTrial(duration: formatPeriod(offer.period))

        case .payAsYouGo:
            // Reduced price over multiple periods
            return .payAsYouGo(
                periods: offer.periodCount,
                price: offer.price
            )

        case .payUpFront:
            // Single payment for a period
            return .payUpFront(
                duration: formatPeriod(offer.period),
                price: offer.price
            )

        case .oneTime:
            // One-time offer code (consumables, non-consumables, non-renewing)
            return .oneTime(price: offer.price)

        @unknown default:
            return .none
        }
    }

    static func checkEligibility(for product: Product) async -> Bool {
        // Check if user can benefit from the offer
        guard let subscription = product.subscription else {
            return false
        }

        // isEligibleForIntroOffer indicates if this is a new subscriber
        return await subscription.isEligibleForIntroOffer
    }

    private static func formatPeriod(_ period: Product.SubscriptionPeriod) -> String {
        let value = period.value
        switch period.unit {
        case .day:
            return "(value) day(value > 1 ? "s" : "")"
        case .week:
            return "(value) week(value > 1 ? "s" : "")"
        case .month:
            return "(value) month(value > 1 ? "s" : "")"
        case .year:
            return "(value) year(value > 1 ? "s" : "")"
        @unknown default:
            return ""
        }
    }
}

Metoda isEligibleForIntroOffer jest kluczowa, aby unikać wyświetlania niedostępnych ofert. Należy zauważyć, że kody ofert, wcześniej ograniczone do subskrypcji z automatycznym odnawianiem, teraz obsługują produkty konsumpcyjne, niekonsumpcyjne i subskrypcje bez odnawiania od iOS 16.3.

Pytanie 3: Jak Przywrócić Wcześniejsze Zakupy?

Przywracanie zakupów jest obowiązkową funkcją zgodnie z wytycznymi App Store Review Guidelines.

RestoreManager.swiftswift
import StoreKit

actor RestoreManager {

    struct RestoreResult {
        let restoredProducts: [String]
        let activeSubscriptions: [String]
    }

    func restorePurchases() async throws -> RestoreResult {
        var restoredProducts: [String] = []
        var activeSubscriptions: [String] = []

        // Synchronize with the App Store
        try await AppStore.sync()

        // Iterate through all user transactions
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }

            switch transaction.productType {
            case .nonConsumable:
                // Restore permanent purchases
                restoredProducts.append(transaction.productID)

            case .autoRenewable:
                // Check if subscription is active
                if let expiration = transaction.expirationDate,
                   expiration > Date() {
                    activeSubscriptions.append(transaction.productID)
                }

            default:
                // Consumables are not restorable
                break
            }
        }

        return RestoreResult(
            restoredProducts: restoredProducts,
            activeSubscriptions: activeSubscriptions
        )
    }
}

AppStore.sync() wymusza synchronizację z serwerami Apple, przydatne po reinstalacji.

SubscriptionOfferView: Nowoczesna Prezentacja Subskrypcji

iOS 18.4 wprowadza SubscriptionOfferView, komponent SwiftUI, który wyświetla opcje subskrypcji z automatycznym ładowaniem metadanych produktów. To częsty temat rozmów kwalifikacyjnych dla programistów tworzących aplikacje oparte na subskrypcjach.

SubscriptionOfferViewExample.swiftswift
import SwiftUI
import StoreKit

struct SubscriptionSettingsView: View {
    let subscriptionGroupID = "com.app.subscription.group"
    
    var body: some View {
        VStack(spacing: 16) {
            // Show upgrade options for current subscribers
            SubscriptionOfferView(groupID: subscriptionGroupID)
                .visibleRelationship(.upgrade)
                .prefersPromotionalIcon(true)
                .subscriptionOfferViewDetailAction { path in
                    path.append("subscriptionDetail")
                }
            
            // Show current plan details
            SubscriptionOfferView(groupID: subscriptionGroupID)
                .visibleRelationship(.current)
        }
        .padding()
    }
}

Parametr visibleRelationship kontroluje, które poziomy subskrypcji są wyświetlane: .upgrade pokazuje plany o jeden poziom wyżej, .downgrade pokazuje niższe poziomy, .crossgrade pokazuje równoważne poziomy z lepszą wartością, a .all wyświetla każdy plan w grupie.

Walidacja Paragonów po Stronie Serwera

Walidacja po stronie serwera pozostaje zalecana dla aplikacji z krytycznymi subskrypcjami. StoreKit 2 używa JWS (JSON Web Signatures) dla nowoczesnej walidacji. Przestarzały endpoint verifyReceipt został zastąpiony przez App Store Server API.

Wyodrębnianie Danych Transakcji dla Serwera

ServerValidation.swiftswift
import StoreKit

struct TransactionPayload: Codable {
    let transactionID: String
    let originalTransactionID: String
    let productID: String
    let purchaseDate: Date
    let expirationDate: Date?
    let jwsRepresentation: String
    let appTransactionID: String?  // iOS 18.4+
}

actor ServerValidationService {

    private let apiEndpoint = "https://api.example.com/verify"

    func validateWithServer(_ transaction: Transaction) async throws -> Bool {
        // Create payload for the server
        let payload = TransactionPayload(
            transactionID: String(transaction.id),
            originalTransactionID: String(transaction.originalID),
            productID: transaction.productID,
            purchaseDate: transaction.purchaseDate,
            expirationDate: transaction.expirationDate,
            jwsRepresentation: transaction.jwsRepresentation ?? "",
            appTransactionID: transaction.appTransactionID.map { String($0) }
        )

        // Encode to JSON
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let jsonData = try encoder.encode(payload)

        // Send to server
        var request = URLRequest(url: URL(string: apiEndpoint)!)
        request.httpMethod = "POST"
        request.httpBody = jsonData
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            return false
        }

        // Decode server response
        let validationResponse = try JSONDecoder().decode(
            ValidationResponse.self,
            from: data
        )

        return validationResponse.isValid
    }
}

struct ValidationResponse: Codable {
    let isValid: Bool
    let subscriptionStatus: String?
}

Pole jwsRepresentation zawiera podpis kryptograficzny weryfikowalny przez serwer. Nowe pole appTransactionID umożliwia precyzyjne śledzenie klientów bez dodatkowych wywołań serwer-serwer.

App Store Server API

App Store Server API Apple zapewnia walidację po stronie serwera z tokenami JWS, Server Notifications V2 dla zdarzeń subskrypcji w czasie rzeczywistym oraz nowe SubscriptionStatus API, które akceptuje Transaction IDs (iOS 18.4+) do bezpośredniego zapytywania o status.

Konfiguracja Powiadomień Serwerowych

Server Notifications V2 umożliwiają odbieranie zdarzeń w czasie rzeczywistym.

ServerNotificationTypes.swiftswift
enum NotificationType: String, Codable {
    case subscribed = "SUBSCRIBED"
    case didRenew = "DID_RENEW"
    case didFailToRenew = "DID_FAIL_TO_RENEW"
    case didChangeRenewalStatus = "DID_CHANGE_RENEWAL_STATUS"
    case expired = "EXPIRED"
    case gracePeriodExpired = "GRACE_PERIOD_EXPIRED"
    case offerRedeemed = "OFFER_REDEEMED"
    case priceIncrease = "PRICE_INCREASE"
    case refund = "REFUND"
    case renewalExtended = "RENEWAL_EXTENDED"
    case revoke = "REVOKE"
}

struct ServerNotification: Codable {
    let notificationType: NotificationType
    let subtype: String?
    let data: NotificationData

    struct NotificationData: Codable {
        let bundleId: String
        let environment: String
        let signedTransactionInfo: String
        let signedRenewalInfo: String?
    }
}

Te powiadomienia umożliwiają synchronizację stanu subskrypcji z backendem.

Obsługa Błędów i Przypadki Brzegowe

Rozmowy często testują zrozumienie scenariuszy błędów i ich właściwej obsługi. Więcej o wzorcach architektonicznych iOS znajdziesz w przewodniku dla senior iOS po wzorcach projektowych.

Częste Błędy w StoreKit 2

PurchaseErrorHandler.swiftswift
import StoreKit

struct PurchaseErrorHandler {

    enum UserFacingError {
        case networkError
        case paymentFailed
        case notAuthorized
        case productNotAvailable
        case unknown

        var message: String {
            switch self {
            case .networkError:
                return "Connection failed. Please check your internet connection."
            case .paymentFailed:
                return "Payment failed. Please verify your payment information."
            case .notAuthorized:
                return "In-app purchases are disabled on this device."
            case .productNotAvailable:
                return "This product is not currently available."
            case .unknown:
                return "An unexpected error occurred."
            }
        }
    }

    static func handle(_ error: Error) -> UserFacingError {
        // Check if it's a StoreKit error
        if let storeKitError = error as? StoreKitError {
            switch storeKitError {
            case .networkError:
                return .networkError
            case .userCancelled:
                // Don't show error, user cancelled
                return .unknown
            case .notAvailableInStorefront:
                return .productNotAvailable
            case .notEntitled:
                return .notAuthorized
            default:
                return .unknown
            }
        }

        // Purchase errors
        if let purchaseError = error as? Product.PurchaseError {
            switch purchaseError {
            case .invalidQuantity:
                return .unknown
            case .productUnavailable:
                return .productNotAvailable
            case .purchaseNotAllowed:
                return .notAuthorized
            default:
                return .unknown
            }
        }

        return .unknown
    }
}

Przejrzysta obsługa błędów poprawia doświadczenie użytkownika i upraszcza debugowanie.

Wsparcie dla Trybu Offline

OfflineCapabilityManager.swiftswift
import StoreKit

actor OfflineCapabilityManager {

    private let userDefaults = UserDefaults.standard
    private let entitlementsKey = "cached_entitlements"

    func cacheCurrentEntitlements() async {
        var entitlements: [String: Date] = [:]

        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                entitlements[transaction.productID] = transaction.expirationDate
            }
        }

        // Save locally
        if let data = try? JSONEncoder().encode(entitlements) {
            userDefaults.set(data, forKey: entitlementsKey)
        }
    }

    func getCachedEntitlements() -> [String: Date] {
        guard let data = userDefaults.data(forKey: entitlementsKey),
              let entitlements = try? JSONDecoder().decode(
                  [String: Date].self,
                  from: data
              ) else {
            return [:]
        }

        // Filter expired entitlements
        let now = Date()
        return entitlements.filter { _, expiration in
            expiration > now
        }
    }

    func hasValidEntitlement(for productID: String) -> Bool {
        let cached = getCachedEntitlements()
        return cached[productID] != nil
    }
}

Lokalne cachowanie zachowuje dostęp do funkcji premium nawet bez łączności.

Najlepsze Praktyki Produkcyjne

Zalecana Architektura

SubscriptionService.swiftswift
import StoreKit
import Combine

@MainActor
final class SubscriptionService: ObservableObject {

    static let shared = SubscriptionService()

    @Published private(set) var products: [Product] = []
    @Published private(set) var purchasedProductIDs: Set<String> = []
    @Published private(set) var subscriptionStatus: SubscriptionStatus = .none

    private var transactionObserver: Task<Void, Never>?

    enum SubscriptionStatus {
        case none
        case active(expirationDate: Date)
        case expired
        case inGracePeriod
    }

    private init() {
        // Start transaction observation
        startTransactionObserver()

        // Load initial state
        Task {
            await loadProducts()
            await updateSubscriptionStatus()
        }
    }

    private func startTransactionObserver() {
        transactionObserver = Task.detached(priority: .background) {
            for await result in Transaction.updates {
                if case .verified(let transaction) = result {
                    await transaction.finish()
                    await self.updateSubscriptionStatus()
                }
            }
        }
    }

    func loadProducts() async {
        do {
            let productIDs = [
                "com.app.subscription.monthly",
                "com.app.subscription.yearly"
            ]
            products = try await Product.products(for: productIDs)
        } catch {
            print("Failed to load products: (error)")
        }
    }

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

        switch result {
        case .success(let verification):
            if case .verified(let transaction) = verification {
                await transaction.finish()
                await updateSubscriptionStatus()
            }
        case .userCancelled:
            break
        case .pending:
            break
        @unknown default:
            break
        }
    }

    func updateSubscriptionStatus() async {
        var purchasedIDs: Set<String> = []
        var latestSubscription: Transaction?

        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                purchasedIDs.insert(transaction.productID)

                if transaction.productType == .autoRenewable {
                    if latestSubscription == nil ||
                       transaction.purchaseDate > latestSubscription!.purchaseDate {
                        latestSubscription = transaction
                    }
                }
            }
        }

        purchasedProductIDs = purchasedIDs

        if let subscription = latestSubscription,
           let expiration = subscription.expirationDate {
            if expiration > Date() {
                subscriptionStatus = .active(expirationDate: expiration)
            } else {
                subscriptionStatus = .expired
            }
        } else {
            subscriptionStatus = .none
        }
    }

    deinit {
        transactionObserver?.cancel()
    }
}

Ta scentralizowana architektura upraszcza zarządzanie stanem zakupów w całej aplikacji.

Źródła

Kluczowe Punkty dla Rozmów o StoreKit 2

StoreKit 2 przekształca zarządzanie zakupami w aplikacji w nowoczesne i bezpieczne doświadczenie programistyczne. Kluczowe punkty do zapamiętania na rozmowę:

  • Natywne API async/await eliminuje skomplikowane delegaty i callbacki z StoreKit 1, który jest teraz przestarzały od iOS 18
  • Automatyczna weryfikacja przez VerificationResult obsługuje kryptograficzną walidację transakcji
  • Transaction.updates zapewnia asynchroniczny strumień dla transakcji w tle, w tym odnowienia i family sharing
  • Transaction.currentEntitlements(for:) zastępuje przestarzałe currentEntitlement(for:) do obsługi wielu entitlementów
  • Pole appTransactionID (iOS 18.4+) umożliwia śledzenie klientów bez wywołań serwer-serwer
  • SubscriptionOfferView zapewnia wbudowany UI dla prezentacji subskrypcji z visibleRelationship dla opcji upgrade, downgrade i crossgrade
  • Oferty promocyjne można podpisywać za pomocą JWS z PromotionOfferV2SignatureCreator
  • Kody ofert teraz obsługują produkty konsumpcyjne, niekonsumpcyjne i subskrypcje bez odnawiania (iOS 16.3+)
  • Walidacja serwera używa App Store Server API z tokenami JWS, zastępując przestarzały endpoint verifyReceipt
  • Zawsze wywołuj transaction.finish() po dostarczeniu treści, aby zapobiec nieoczekiwanemu przywracaniu
  • Implementuj offline cache entitlementów dla płynnego doświadczenia użytkownika bez łączności

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w iOS?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 20 października 2026

Tagi

#ios
#storekit
#in-app-purchase
#swift
#interview

Udostępnij

Powiązane artykuły