# 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. - Published: 2026-03-09 - Updated: 2026-04-29 - Author: SharpSkill - Tags: ios, storekit, in-app-purchase, swift, interview - Reading time: 14 min --- 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. 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. ## 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. ```swift // ProductService.swift import StoreKit actor ProductService { // Identifier produk yang dikonfigurasi di App Store Connect private let productIdentifiers: Set = [ "com.app.subscription.monthly", "com.app.subscription.yearly", "com.app.premium.lifetime" ] // Cache produk untuk menghindari panggilan jaringan berulang private var cachedProducts: [Product] = [] func loadProducts() async throws -> [Product] { // Mengembalikan cache jika tersedia guard cachedProducts.isEmpty else { return cachedProducts } // Permintaan asinkron ke 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. ```swift // ProductType+Extensions.swift import StoreKit extension Product.ProductType { var displayName: String { switch self { case .consumable: // Produk yang dapat dikonsumsi (kredit, nyawa, dll.) return "Consumable" case .nonConsumable: // Pembelian permanen (membuka fitur) return "Non-Consumable" case .autoRenewable: // Langganan dengan perpanjangan otomatis return "Auto-Renewable Subscription" case .nonRenewable: // Langganan tanpa perpanjangan (pass sementara) 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 ```swift // PurchaseManager.swift import StoreKit actor PurchaseManager { enum PurchaseError: Error { case productNotFound case purchaseCancelled case purchasePending case verificationFailed case unknown } func purchase(_ product: Product) async throws -> Transaction { // Memulai pembelian dengan modal sistem let result = try await product.purchase() switch result { case .success(let verification): // Memverifikasi tanda tangan transaksi let transaction = try checkVerification(verification) // Menandai transaksi sebagai selesai await transaction.finish() return transaction case .userCancelled: // Pengguna membatalkan modal pembelian throw PurchaseError.purchaseCancelled case .pending: // Pembelian tertunda (persetujuan orang tua, dll.) throw PurchaseError.purchasePending @unknown default: throw PurchaseError.unknown } } private func checkVerification(_ result: VerificationResult) throws -> T { switch result { case .verified(let safe): // Transaksi ditandatangani dan diverifikasi oleh App Store return safe case .unverified(_, let error): // Tanda tangan tidak valid atau rusak 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.). ```swift // TransactionObserver.swift import StoreKit actor TransactionObserver { private var updateTask: Task? func startObserving() { // Membatalkan pengamatan sebelumnya updateTask?.cancel() updateTask = Task(priority: .background) { // Stream tak terbatas dari pembaruan transaksi for await result in Transaction.updates { do { let transaction = try self.checkVerification(result) // Memproses transaksi berdasarkan tipenya await self.handleTransaction(transaction) // Selalu menyelesaikan transaksi await transaction.finish() } catch { // Mencatat kesalahan untuk debugging print("Transaction verification failed: \(error)") } } } } func stopObserving() { updateTask?.cancel() updateTask = nil } private func handleTransaction(_ transaction: Transaction) async { switch transaction.productType { case .autoRenewable: // Memperbarui status langganan await updateSubscriptionStatus(transaction) case .nonConsumable: // Membuka fitur premium await unlockFeature(transaction.productID) default: break } } private func checkVerification(_ result: VerificationResult) throws -> T { switch result { case .verified(let safe): return safe case .unverified: throw PurchaseError.verificationFailed } } private func updateSubscriptionStatus(_ transaction: Transaction) async { // Implementasi pembaruan status } private func unlockFeature(_ productID: String) async { // Implementasi pembukaan fitur } 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. ### Pertanyaan 1: Bagaimana Cara Memeriksa Status Langganan Saat Ini? StoreKit 2 menyediakan akses langsung ke entitlement aktif melalui `Transaction.currentEntitlements`. ```swift // SubscriptionStatusChecker.swift import StoreKit struct SubscriptionStatusChecker { struct SubscriptionStatus { let isActive: Bool let expirationDate: Date? let willRenew: Bool let productID: String? } static func checkCurrentStatus() async -> SubscriptionStatus { // Mengambil semua transaksi aktif var latestTransaction: Transaction? for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result { // Memfilter hanya langganan if transaction.productType == .autoRenewable { // Menyimpan transaksi terbaru if latestTransaction == nil || transaction.purchaseDate > latestTransaction!.purchaseDate { latestTransaction = transaction } } } } guard let transaction = latestTransaction else { return SubscriptionStatus( isActive: false, expirationDate: nil, willRenew: false, productID: nil ) } // Memeriksa apakah langganan masih aktif let isActive = transaction.expirationDate ?? Date.distantPast > Date() // Mengambil informasi perpanjangan let renewalInfo = await getRenewalInfo(for: transaction.productID) return SubscriptionStatus( isActive: isActive, expirationDate: transaction.expirationDate, willRenew: renewalInfo?.willAutoRenew ?? false, productID: transaction.productID ) } 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 } // Mengambil status perpanjangan let status = try? await subscription.status.first return status?.renewalInfo } } ``` Pendekatan ini memberikan status langganan yang akurat tanpa panggilan server. ### 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. ```swift // OfferManager.swift import StoreKit struct OfferManager { enum OfferType { case freeTrial(duration: String) case payAsYouGo(periods: Int, price: Decimal) case payUpFront(duration: String, price: Decimal) 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: // Periode uji coba gratis return .freeTrial(duration: formatPeriod(offer.period)) case .payAsYouGo: // Harga lebih rendah selama beberapa periode return .payAsYouGo( periods: offer.periodCount, price: offer.price ) case .payUpFront: // Pembayaran sekali untuk satu periode return .payUpFront( duration: formatPeriod(offer.period), price: offer.price ) @unknown default: return .none } } static func checkEligibility(for product: Product) async -> Bool { // Memeriksa apakah pengguna dapat memanfaatkan penawaran guard let subscription = product.subscription else { return false } // isEligibleForIntroOffer menunjukkan apakah ini pelanggan baru 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. ### Pertanyaan 3: Bagaimana Cara Memulihkan Pembelian Sebelumnya? Pemulihan pembelian adalah fitur wajib menurut pedoman App Store. ```swift // RestoreManager.swift import StoreKit actor RestoreManager { struct RestoreResult { let restoredProducts: [String] let activeSubscriptions: [String] } func restorePurchases() async throws -> RestoreResult { var restoredProducts: [String] = [] var activeSubscriptions: [String] = [] // Sinkronisasi dengan App Store try await AppStore.sync() // Iterasi melalui semua transaksi pengguna for await result in Transaction.currentEntitlements { guard case .verified(let transaction) = result else { continue } switch transaction.productType { case .nonConsumable: // Memulihkan pembelian permanen restoredProducts.append(transaction.productID) case .autoRenewable: // Memeriksa apakah langganan aktif if let expiration = transaction.expirationDate, expiration > Date() { activeSubscriptions.append(transaction.productID) } default: // Consumable tidak dapat dipulihkan break } } return RestoreResult( restoredProducts: restoredProducts, activeSubscriptions: activeSubscriptions ) } } ``` `AppStore.sync()` memaksa sinkronisasi dengan server Apple, berguna setelah penginstalan ulang. ## Validasi Tanda Terima Sisi Server Validasi sisi server tetap direkomendasikan untuk aplikasi dengan langganan kritis. StoreKit 2 memperkenalkan JWS (JSON Web Signatures) untuk validasi modern. ### Mengekstrak Data Transaksi untuk Server ```swift // ServerValidation.swift import StoreKit struct TransactionPayload: Codable { let transactionID: String let originalTransactionID: String let productID: String let purchaseDate: Date let expirationDate: Date? let jwsRepresentation: String } actor ServerValidationService { private let apiEndpoint = "https://api.example.com/verify" func validateWithServer(_ transaction: Transaction) async throws -> Bool { // Membuat payload untuk server let payload = TransactionPayload( transactionID: String(transaction.id), originalTransactionID: String(transaction.originalID), productID: transaction.productID, purchaseDate: transaction.purchaseDate, expirationDate: transaction.expirationDate, jwsRepresentation: transaction.jwsRepresentation ?? "" ) // Mengkodekan ke JSON let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let jsonData = try encoder.encode(payload) // Mengirim ke 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 } // Mendekode respons server 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. > **App Store Server API** > > Apple sekarang menyediakan App Store Server API untuk validasi sisi server. API modern ini menggantikan endpoint verifyReceipt yang sudah usang dan menawarkan fitur-fitur canggih seperti notifikasi server-ke-server. ### Mengonfigurasi Notifikasi Server Server Notifications V2 memungkinkan penerimaan event secara real-time. ```swift // ServerNotificationTypes.swift 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. ### Kesalahan Umum StoreKit 2 ```swift // PurchaseErrorHandler.swift 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 { // Memeriksa apakah ini kesalahan StoreKit if let storeKitError = error as? StoreKitError { switch storeKitError { case .networkError: return .networkError case .userCancelled: // Tidak menampilkan kesalahan, pengguna membatalkan return .unknown case .notAvailableInStorefront: return .productNotAvailable case .notEntitled: return .notAuthorized default: return .unknown } } // Kesalahan pembelian 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 ```swift // OfflineCapabilityManager.swift 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 } } // Menyimpan secara lokal 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 [:] } // Memfilter entitlement yang kedaluwarsa 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 ```swift // SubscriptionService.swift 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 = [] @Published private(set) var subscriptionStatus: SubscriptionStatus = .none private var transactionObserver: Task? enum SubscriptionStatus { case none case active(expirationDate: Date) case expired case inGracePeriod } private init() { // Memulai pengamatan transaksi startTransactionObserver() // Memuat status awal 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 = [] 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. ## Kesimpulan StoreKit 2 mengubah manajemen pembelian dalam aplikasi menjadi pengalaman pengembangan yang modern dan aman. Poin-poin penting untuk diingat dalam wawancara: ✅ API native async/await: tidak ada lagi delegate dan callback kompleks dari StoreKit 1 ✅ Verifikasi otomatis: `VerificationResult` menangani validasi kriptografi transaksi ✅ `Transaction.updates`: stream asinkron untuk transaksi latar belakang ✅ `Transaction.currentEntitlements`: akses langsung ke entitlement aktif pengguna ✅ Penawaran promosi: `isEligibleForIntroOffer` untuk memeriksa kelayakan ✅ Validasi server: JWS dan App Store Server API untuk keamanan yang ditingkatkan ✅ `transaction.finish()`: wajib setelah pengiriman konten ✅ Penanganan offline: caching entitlement lokal untuk pengalaman yang lancar --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/ios/storekit-2-subscriptions-receipts-validation-interview