StoreKit 2 Sollicitatiegesprek: Abonnementenbeheer en Bonvalidatie
Beheers iOS sollicitatievragen over StoreKit 2, abonnementenbeheer, bonvalidatie en de implementatie van in-app aankopen met praktische Swift-codevoorbeelden.

StoreKit 2 vertegenwoordigt een fundamentele verschuiving in hoe in-app aankopen op iOS worden afgehandeld. Geïntroduceerd met iOS 15 vereenvoudigt dit moderne framework de code voor abonnementenbeheer en transactievalidatie aanzienlijk. Technische iOS-sollicitatiegesprekken behandelen dit onderwerp regelmatig om de expertise van ontwikkelaars in app-monetisatie te beoordelen.
StoreKit 2 maakt gebruik van een native Swift-API met async/await, waardoor complexe callbacks en client-side bonvalidatie overbodig worden. Interviewers waarderen kandidaten die de fundamentele verschillen met StoreKit 1 helder kunnen verwoorden.
Overzicht van de StoreKit 2-Architectuur
StoreKit 2 is gebouwd op een moderne architectuur die volledig gebruikmaakt van Swift Concurrency. In tegenstelling tot StoreKit 1 zijn alle bewerkingen asynchroon en gebruiken ze native Swift-types.
Beschikbare Producten Ophalen
De eerste stap bestaat uit het laden van de in App Store Connect geconfigureerde producten. StoreKit 2 vereenvoudigt deze bewerking met een declaratieve API.
import StoreKit
actor ProductService {
// Productidentificatoren geconfigureerd in App Store Connect
private let productIdentifiers: Set<String> = [
"com.app.subscription.monthly",
"com.app.subscription.yearly",
"com.app.premium.lifetime"
]
// Productcache om herhaalde netwerkoproepen te voorkomen
private var cachedProducts: [Product] = []
func loadProducts() async throws -> [Product] {
// Geeft de cache terug indien beschikbaar
guard cachedProducts.isEmpty else {
return cachedProducts
}
// Asynchrone aanvraag bij de 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 }
}
}Het gebruik van een actor garandeert thread-safety bij het beheren van de productcache.
Producttypen in StoreKit 2
StoreKit 2 maakt duidelijk onderscheid tussen verschillende producttypen. Het begrijpen van dit onderscheid is essentieel voor sollicitatiegesprekken.
import StoreKit
extension Product.ProductType {
var displayName: String {
switch self {
case .consumable:
// Verbruiksproducten (credits, levens, etc.)
return "Consumable"
case .nonConsumable:
// Permanente aankopen (functie ontgrendelen)
return "Non-Consumable"
case .autoRenewable:
// Abonnementen met automatische verlenging
return "Auto-Renewable Subscription"
case .nonRenewable:
// Abonnementen zonder verlenging (tijdelijke pas)
return "Non-Renewable Subscription"
@unknown default:
return "Unknown"
}
}
}Automatisch verlengbare abonnementen vertegenwoordigen het dominante bedrijfsmodel voor moderne iOS-applicaties.
Aankooptransacties Afhandelen
Het aankoopproces in StoreKit 2 wordt lineair dankzij async/await. Geen genest delegates en callbacks meer zoals in StoreKit 1.
Een Aankoop Initiëren
import StoreKit
actor PurchaseManager {
enum PurchaseError: Error {
case productNotFound
case purchaseCancelled
case purchasePending
case verificationFailed
case unknown
}
func purchase(_ product: Product) async throws -> Transaction {
// Start de aankoop met de systeemmodal
let result = try await product.purchase()
switch result {
case .success(let verification):
// Verifieer de transactiehandtekening
let transaction = try checkVerification(verification)
// Markeer de transactie als voltooid
await transaction.finish()
return transaction
case .userCancelled:
// Gebruiker heeft de aankoopmodal geannuleerd
throw PurchaseError.purchaseCancelled
case .pending:
// Aankoop in afwachting (ouderlijke toestemming, etc.)
throw PurchaseError.purchasePending
@unknown default:
throw PurchaseError.unknown
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
// Transactie ondertekend en geverifieerd door de App Store
return safe
case .unverified(_, let error):
// Ongeldige of beschadigde handtekening
throw PurchaseError.verificationFailed
}
}
}De finish()-methode is cruciaal: deze geeft de App Store aan dat de inhoud is geleverd aan de gebruiker.
Vergeet nooit transaction.finish() aan te roepen na het leveren van de inhoud. Een onafgemaakte transactie wordt hersteld bij de volgende app-start, wat onverwacht gedrag veroorzaakt.
Luisteren naar Achtergrondtransacties
StoreKit 2 introduceert Transaction.updates, een asynchrone stream die transacties uitzendt die buiten de app plaatsvinden (verlengingen, family sharing-aankopen, etc.).
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
func startObserving() {
// Annuleer eventuele eerdere observatie
updateTask?.cancel()
updateTask = Task(priority: .background) {
// Oneindige stream van transactie-updates
for await result in Transaction.updates {
do {
let transaction = try self.checkVerification(result)
// Verwerk de transactie op basis van het type
await self.handleTransaction(transaction)
// Voltooi altijd de transactie
await transaction.finish()
} catch {
// Log de fout voor debuggen
print("Transaction verification failed: \(error)")
}
}
}
}
func stopObserving() {
updateTask?.cancel()
updateTask = nil
}
private func handleTransaction(_ transaction: Transaction) async {
switch transaction.productType {
case .autoRenewable:
// Werk de abonnementsstatus bij
await updateSubscriptionStatus(transaction)
case .nonConsumable:
// Ontgrendel de premium functie
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 {
// Implementatie van statusupdate
}
private func unlockFeature(_ productID: String) async {
// Implementatie van functie-ontgrendeling
}
enum PurchaseError: Error {
case verificationFailed
}
}De transactie-observatie moet starten zodra de app wordt gestart om geen updates te missen.
Veelgestelde Sollicitatievragen over StoreKit 2-Abonnementen
iOS-sollicitatiegesprekken bevatten vaak specifieke vragen over abonnementenbeheer. Hier zijn de meest voorkomende met gedetailleerde antwoorden.
Vraag 1: Hoe Controleer Je de Huidige Abonnementsstatus?
StoreKit 2 biedt directe toegang tot actieve entitlements via 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 {
// Haal alle actieve transacties op
var latestTransaction: Transaction?
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
// Filter alleen abonnementen
if transaction.productType == .autoRenewable {
// Bewaar de meest recente transactie
if latestTransaction == nil ||
transaction.purchaseDate > latestTransaction!.purchaseDate {
latestTransaction = transaction
}
}
}
}
guard let transaction = latestTransaction else {
return SubscriptionStatus(
isActive: false,
expirationDate: nil,
willRenew: false,
productID: nil
)
}
// Controleer of het abonnement nog actief is
let isActive = transaction.expirationDate ?? Date.distantPast > Date()
// Haal de verlengingsinformatie op
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
}
// Haal de verlengingsstatus op
let status = try? await subscription.status.first
return status?.renewalInfo
}
}Deze aanpak biedt de exacte abonnementsstatus zonder serveroproepen.
Klaar om je iOS gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Vraag 2: Hoe Behandel Je Gratis Proefperioden en Promotionele Aanbiedingen?
StoreKit 2 vereenvoudigt de toegang tot aanbiedingsinformatie. Interviewers testen vaak het begrip van verschillende aanbiedingstypen.
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:
// Gratis proefperiode
return .freeTrial(duration: formatPeriod(offer.period))
case .payAsYouGo:
// Verlaagde prijs over meerdere perioden
return .payAsYouGo(
periods: offer.periodCount,
price: offer.price
)
case .payUpFront:
// Eenmalige betaling voor een periode
return .payUpFront(
duration: formatPeriod(offer.period),
price: offer.price
)
@unknown default:
return .none
}
}
static func checkEligibility(for product: Product) async -> Bool {
// Controleer of de gebruiker kan profiteren van de aanbieding
guard let subscription = product.subscription else {
return false
}
// isEligibleForIntroOffer geeft aan of het een nieuwe abonnee is
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 ""
}
}
}De methode isEligibleForIntroOffer is essentieel om te voorkomen dat niet-beschikbare aanbiedingen worden weergegeven.
Vraag 3: Hoe Herstel Je Eerdere Aankopen?
Het herstellen van aankopen is een verplichte functie volgens de App Store-richtlijnen.
import StoreKit
actor RestoreManager {
struct RestoreResult {
let restoredProducts: [String]
let activeSubscriptions: [String]
}
func restorePurchases() async throws -> RestoreResult {
var restoredProducts: [String] = []
var activeSubscriptions: [String] = []
// Synchroniseer met de App Store
try await AppStore.sync()
// Itereer door alle gebruikerstransacties
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
switch transaction.productType {
case .nonConsumable:
// Herstel permanente aankopen
restoredProducts.append(transaction.productID)
case .autoRenewable:
// Controleer of het abonnement actief is
if let expiration = transaction.expirationDate,
expiration > Date() {
activeSubscriptions.append(transaction.productID)
}
default:
// Verbruiksproducten zijn niet herstelbaar
break
}
}
return RestoreResult(
restoredProducts: restoredProducts,
activeSubscriptions: activeSubscriptions
)
}
}AppStore.sync() forceert synchronisatie met de Apple-servers, nuttig na een herinstallatie.
Server-Side Bonvalidatie
Server-side validatie blijft aanbevolen voor applicaties met kritieke abonnementen. StoreKit 2 introduceert JWS (JSON Web Signatures) voor moderne validatie.
Transactiegegevens Extraheren voor de 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 {
// Maak payload voor de server
let payload = TransactionPayload(
transactionID: String(transaction.id),
originalTransactionID: String(transaction.originalID),
productID: transaction.productID,
purchaseDate: transaction.purchaseDate,
expirationDate: transaction.expirationDate,
jwsRepresentation: transaction.jwsRepresentation ?? ""
)
// Codeer naar JSON
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(payload)
// Verstuur naar 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
}
// Decodeer serverrespons
let validationResponse = try JSONDecoder().decode(
ValidationResponse.self,
from: data
)
return validationResponse.isValid
}
}
struct ValidationResponse: Codable {
let isValid: Bool
let subscriptionStatus: String?
}De jwsRepresentation bevat de cryptografische handtekening die door de server kan worden geverifieerd.
Apple biedt nu de App Store Server API voor server-side validatie. Deze moderne API vervangt het verouderde verifyReceipt-endpoint en biedt geavanceerde functies zoals server-naar-server-meldingen.
Servermeldingen Configureren
Server Notifications V2 maken realtime ontvangst van gebeurtenissen mogelijk.
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?
}
}Deze meldingen zorgen ervoor dat de abonnementsstatus gesynchroniseerd blijft met de backend.
Foutafhandeling en Randgevallen
Sollicitatiegesprekken testen vaak het begrip van foutscenario's en hun juiste afhandeling.
Veelvoorkomende StoreKit 2-Fouten
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 {
// Controleer of het een StoreKit-fout is
if let storeKitError = error as? StoreKitError {
switch storeKitError {
case .networkError:
return .networkError
case .userCancelled:
// Geen fout tonen, gebruiker heeft geannuleerd
return .unknown
case .notAvailableInStorefront:
return .productNotAvailable
case .notEntitled:
return .notAuthorized
default:
return .unknown
}
}
// Aankoopfouten
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
}
}Duidelijke foutafhandeling verbetert de gebruikerservaring en vereenvoudigt het debuggen.
Ondersteuning voor Offline-Modus
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
}
}
// Lokaal opslaan
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 verlopen entitlements
let now = Date()
return entitlements.filter { _, expiration in
expiration > now
}
}
func hasValidEntitlement(for productID: String) -> Bool {
let cached = getCachedEntitlements()
return cached[productID] != nil
}
}Lokale caching behoudt toegang tot premium functies zelfs zonder verbinding.
Best Practices voor Productie
Aanbevolen Architectuur
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 transactie-observatie
startTransactionObserver()
// Laad de initiële status
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()
}
}Deze gecentraliseerde architectuur vereenvoudigt het beheer van de aankoopstatus in de hele applicatie.
Conclusie
StoreKit 2 transformeert het beheer van in-app aankopen in een moderne en veilige ontwikkelingservaring. Belangrijke punten om te onthouden voor een sollicitatiegesprek:
✅ Native async/await-API: geen complexe delegates en callbacks meer zoals in StoreKit 1
✅ Automatische verificatie: VerificationResult regelt de cryptografische validatie van transacties
✅ Transaction.updates: asynchrone stream voor achtergrondtransacties
✅ Transaction.currentEntitlements: directe toegang tot actieve gebruikersentitlements
✅ Promotionele aanbiedingen: isEligibleForIntroOffer om in aanmerking te komen
✅ Servervalidatie: JWS en App Store Server API voor verbeterde beveiliging
✅ transaction.finish(): verplicht na contentlevering
✅ Offline afhandeling: lokale caching van entitlements voor naadloze ervaring
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

Swift Testing Framework Sollicitatiegesprek 2026: Macro's #expect en #require vs XCTest
Beheers het nieuwe Swift Testing Framework voor iOS-sollicitaties: macro's #expect en #require, migratie vanuit XCTest, geavanceerde patronen en veelgemaakte fouten.

iOS Push Notifications-sollicitatiegesprek 2026: APNs, tokens en troubleshooting
Complete gids om je voor te bereiden op iOS-sollicitatiegesprekken over Push Notifications, APNs, tokenbeheer en troubleshooting. Veelgestelde vragen met uitgebreide antwoorden.

De 25 belangrijkste Swift-sollicitatievragen voor iOS-ontwikkelaars
Voorbereiding op iOS-sollicitatiegesprekken met de 25 meest gestelde Swift-vragen: optionals, closures, ARC, protocols, async/await en geavanceerde patronen.