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.

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.
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.
import StoreKit
actor ProductService {
// Identifier produk yang dikonfigurasi di App Store Connect
private let productIdentifiers: Set<String> = [
"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.
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
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<T>(_ result: VerificationResult<T>) 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.
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.).
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
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<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 {
// 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.
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.
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.
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.
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
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.
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.
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
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
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
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() {
// 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<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.
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
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Swift Testing Framework Wawancara 2026: Makro #expect dan #require vs XCTest
Kuasai Swift Testing Framework baru untuk wawancara iOS: makro #expect dan #require, migrasi dari XCTest, pola lanjutan, dan jebakan umum.

Wawancara iOS Push Notifications 2026: APNs, token, dan troubleshooting
Panduan lengkap untuk mempersiapkan wawancara iOS tentang Push Notifications, APNs, manajemen token, dan troubleshooting. Pertanyaan umum dengan jawaban mendetail.

25 Pertanyaan Interview Swift Teratas untuk Developer iOS
Persiapkan diri untuk interview iOS dengan 25 pertanyaan Swift yang paling sering ditanyakan: optionals, closures, ARC, protocols, async/await, dan pola lanjutan.