Wawancara StoreKit 2: Manajemen Langganan dan Validasi Tanda Terima

Kuasai pertanyaan wawancara iOS tentang StoreKit 2, manajemen langganan, validasi tanda terima, dan implementasi pembelian dalam aplikasi dengan contoh kode Swift praktis.

Arsitektur langganan iOS StoreKit 2 dan validasi tanda terima

StoreKit 2 mewakili pergeseran fundamental dalam cara pembelian dalam aplikasi ditangani di iOS. Diperkenalkan dengan iOS 15, framework modern ini secara dramatis menyederhanakan kode yang diperlukan untuk manajemen langganan dan validasi transaksi. Dengan iOS 18, StoreKit 1 sekarang resmi deprecated, menjadikan StoreKit 2 satu-satunya jalur yang didukung ke depan. Wawancara teknis iOS secara teratur membahas topik ini untuk mengevaluasi keahlian developer dalam monetisasi aplikasi.

Wawasan Wawancara Utama

StoreKit 2 memanfaatkan API Swift native dengan async/await, menghilangkan kebutuhan akan callback yang kompleks dan validasi tanda terima sisi klien. Pewawancara menghargai kandidat yang dapat mengartikulasikan perbedaan fundamental dari StoreKit 1, dan fitur iOS 18.4 baru seperti appTransactionID dan SubscriptionOfferView.

Tinjauan Arsitektur StoreKit 2

StoreKit 2 dibangun di atas arsitektur modern yang sepenuhnya memanfaatkan Swift Concurrency. Tidak seperti StoreKit 1, semua operasi bersifat asinkron dan menggunakan tipe Swift native.

Mengambil Produk yang Tersedia

Langkah pertama melibatkan pemuatan produk yang dikonfigurasi di App Store Connect. StoreKit 2 menyederhanakan operasi ini dengan API deklaratif.

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 }
    }
}

Menggunakan actor memastikan keamanan thread saat mengelola cache produk.

Tipe Produk StoreKit 2

StoreKit 2 dengan jelas membedakan antara berbagai tipe produk. Memahami perbedaan ini sangat penting untuk wawancara.

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"
        }
    }
}

Langganan perpanjangan otomatis mewakili model bisnis dominan untuk aplikasi iOS modern.

Menangani Transaksi Pembelian

Proses pembelian di StoreKit 2 menjadi linier berkat async/await. Tidak ada lagi delegate dan callback bersarang dari StoreKit 1.

Memulai Pembelian

Mulai iOS 18.2, pembelian memerlukan parameter konteks UI. Perubahan ini memastikan sheet pembelian muncul dengan benar dalam hierarki view aplikasi.

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
        }
    }
}

Metode finish() sangat penting: memberi sinyal kepada App Store bahwa konten telah dikirimkan kepada pengguna.

Peringatan Wawancara

Jangan pernah lupa memanggil transaction.finish() setelah mengirimkan konten. Transaksi yang belum selesai akan dipulihkan pada peluncuran aplikasi berikutnya, menyebabkan perilaku yang tidak terduga.

Mendengarkan Transaksi Latar Belakang

StoreKit 2 memperkenalkan Transaction.updates, stream asinkron yang memancarkan transaksi yang terjadi di luar aplikasi (perpanjangan, pembelian family sharing, dll.).

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
    }
}

Pengamatan transaksi harus dimulai segera setelah aplikasi diluncurkan untuk menghindari kehilangan pembaruan.

Pertanyaan Wawancara Umum tentang Langganan StoreKit 2

Wawancara iOS sering kali mencakup pertanyaan spesifik tentang manajemen langganan. Berikut adalah yang paling umum dengan jawaban terperinci. Untuk topik wawancara Swift lainnya, lihat panduan Swift structured concurrency.

Pertanyaan 1: Bagaimana Cara Memeriksa Status Langganan Saat Ini?

StoreKit 2 menyediakan akses langsung ke entitlement aktif. Perlu dicatat bahwa Transaction.currentEntitlement(for:) sudah deprecated pada iOS 18.4. API baru currentEntitlements(for:) mengembalikan sequence asinkron untuk menangani beberapa entitlement (misalnya, langganan plus akses 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
    }
}

Pendekatan ini memberikan status langganan yang akurat tanpa panggilan server. Field appTransactionID, diperkenalkan di iOS 18.4, memungkinkan pelacakan pelanggan lintas transaksi tanpa panggilan server-to-server.

Siap menguasai wawancara iOS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pertanyaan 2: Bagaimana Cara Menangani Uji Coba Gratis dan Penawaran Promosi?

StoreKit 2 menyederhanakan akses ke informasi penawaran. Pewawancara sering menguji pemahaman tentang berbagai jenis penawaran. Mulai iOS 18.4, penawaran promosi dapat ditandatangani menggunakan JWS dengan PromotionOfferV2SignatureCreator baru yang tersedia di 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 ""
        }
    }
}

Metode isEligibleForIntroOffer sangat penting untuk menghindari menampilkan penawaran yang tidak tersedia. Perlu dicatat bahwa offer codes, yang sebelumnya terbatas pada langganan auto-renewable, sekarang mendukung consumables, non-consumables, dan langganan non-renewing mulai iOS 16.3.

Pertanyaan 3: Bagaimana Cara Memulihkan Pembelian Sebelumnya?

Pemulihan pembelian adalah fitur wajib menurut 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() memaksa sinkronisasi dengan server Apple, berguna setelah penginstalan ulang.

SubscriptionOfferView: Merchandising Langganan Modern

iOS 18.4 memperkenalkan SubscriptionOfferView, komponen SwiftUI yang menampilkan opsi langganan dengan pemuatan metadata produk otomatis. Ini adalah topik wawancara umum untuk developer yang membangun aplikasi berbasis langganan.

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()
    }
}

Parameter visibleRelationship mengontrol tier langganan mana yang muncul: .upgrade menampilkan paket satu tier lebih tinggi, .downgrade menampilkan tier lebih rendah, .crossgrade menampilkan tier setara dengan nilai lebih baik, dan .all menampilkan setiap paket dalam grup.

Validasi Tanda Terima Sisi Server

Validasi sisi server tetap direkomendasikan untuk aplikasi dengan langganan kritis. StoreKit 2 menggunakan JWS (JSON Web Signatures) untuk validasi modern. Endpoint verifyReceipt yang deprecated telah digantikan oleh App Store Server API.

Mengekstrak Data Transaksi untuk Server

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?
}

jwsRepresentation berisi tanda tangan kriptografi yang dapat diverifikasi oleh server. Field appTransactionID baru memungkinkan pelacakan pelanggan yang tepat tanpa round-trip server tambahan.

App Store Server API

App Store Server API Apple menyediakan validasi sisi server dengan token JWS, Server Notifications V2 untuk event langganan real-time, dan SubscriptionStatus API baru yang menerima Transaction IDs (iOS 18.4+) untuk melakukan query status secara langsung.

Mengonfigurasi Notifikasi Server

Server Notifications V2 memungkinkan penerimaan event secara real-time.

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?
    }
}

Notifikasi ini memungkinkan menjaga status langganan tetap tersinkronisasi dengan backend.

Penanganan Kesalahan dan Kasus Tepi

Wawancara sering menguji pemahaman tentang skenario kesalahan dan penanganannya yang tepat. Untuk lebih lanjut tentang pola arsitektur iOS, lihat panduan wawancara iOS senior tentang design patterns.

Kesalahan Umum 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
    }
}

Penanganan kesalahan yang jelas meningkatkan pengalaman pengguna dan menyederhanakan debugging.

Dukungan Mode 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
    }
}

Caching lokal mempertahankan akses ke fitur premium bahkan tanpa konektivitas.

Praktik Terbaik Produksi

Arsitektur yang Direkomendasikan

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()
    }
}

Arsitektur terpusat ini menyederhanakan manajemen status pembelian di seluruh aplikasi.

Sumber

Poin Penting untuk Wawancara StoreKit 2

StoreKit 2 mengubah manajemen pembelian dalam aplikasi menjadi pengalaman pengembangan yang modern dan aman. Poin-poin penting untuk diingat dalam wawancara:

  • API async/await native menghilangkan delegate dan callback kompleks dari StoreKit 1, yang sekarang deprecated pada iOS 18
  • Verifikasi otomatis melalui VerificationResult menangani validasi kriptografi transaksi
  • Transaction.updates menyediakan stream asinkron untuk transaksi latar belakang termasuk perpanjangan dan family sharing
  • Transaction.currentEntitlements(for:) menggantikan currentEntitlement(for:) yang deprecated untuk menangani beberapa entitlement
  • Field appTransactionID (iOS 18.4+) memungkinkan pelacakan pelanggan tanpa panggilan server-to-server
  • SubscriptionOfferView menyediakan UI built-in untuk merchandising langganan dengan visibleRelationship untuk opsi upgrade, downgrade, dan crossgrade
  • Penawaran promosi dapat ditandatangani menggunakan JWS dengan PromotionOfferV2SignatureCreator
  • Offer codes sekarang mendukung consumables, non-consumables, dan langganan non-renewing (iOS 16.3+)
  • Validasi server menggunakan App Store Server API dengan token JWS, menggantikan endpoint verifyReceipt yang deprecated
  • Selalu panggil transaction.finish() setelah pengiriman konten untuk mencegah perilaku restorasi yang tidak terduga
  • Implementasikan caching entitlement offline untuk pengalaman pengguna yang mulus tanpa konektivitas

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tantangan harian

Bisakah kamu menemukan bug di iOS?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 20 Oktober 2026

Tag

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

Bagikan

Artikel terkait