# Phỏng Vấn StoreKit 2: Quản Lý Đăng Ký và Xác Thực Biên Lai > Làm chủ các câu hỏi phỏng vấn iOS về StoreKit 2, quản lý đăng ký, xác thực biên lai và triển khai mua hàng trong ứng dụng với các ví dụ mã Swift thực tế. - Published: 2026-03-09 - Updated: 2026-04-29 - Author: SharpSkill - Tags: ios, storekit, in-app-purchase, swift, interview - Reading time: 14 min --- StoreKit 2 đại diện cho một sự thay đổi cơ bản trong cách xử lý mua hàng trong ứng dụng trên iOS. Được giới thiệu cùng iOS 15, framework hiện đại này đơn giản hóa đáng kể mã cần thiết cho việc quản lý đăng ký và xác thực giao dịch. Các cuộc phỏng vấn kỹ thuật iOS thường xuyên đề cập đến chủ đề này để đánh giá chuyên môn của các nhà phát triển trong việc kiếm tiền từ ứng dụng. > **Hiểu Biết Quan Trọng cho Phỏng Vấn** > > StoreKit 2 tận dụng API Swift gốc với async/await, loại bỏ nhu cầu về các callback phức tạp và xác thực biên lai phía client. Người phỏng vấn đánh giá cao những ứng viên có thể trình bày rõ những khác biệt cơ bản so với StoreKit 1. ## Tổng Quan Kiến Trúc StoreKit 2 StoreKit 2 được xây dựng trên kiến trúc hiện đại tận dụng đầy đủ Swift Concurrency. Không giống như StoreKit 1, tất cả các thao tác đều bất đồng bộ và sử dụng các kiểu Swift gốc. ### Lấy Sản Phẩm Khả Dụng Bước đầu tiên là tải các sản phẩm được cấu hình trong App Store Connect. StoreKit 2 đơn giản hóa thao tác này với một API khai báo. ```swift // ProductService.swift import StoreKit actor ProductService { // Mã định danh sản phẩm được cấu hình trong App Store Connect private let productIdentifiers: Set = [ "com.app.subscription.monthly", "com.app.subscription.yearly", "com.app.premium.lifetime" ] // Bộ nhớ đệm sản phẩm để tránh các lệnh gọi mạng lặp lại private var cachedProducts: [Product] = [] func loadProducts() async throws -> [Product] { // Trả về bộ nhớ đệm nếu có guard cachedProducts.isEmpty else { return cachedProducts } // Yêu cầu bất đồng bộ đến 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 } } } ``` Việc sử dụng `actor` đảm bảo an toàn luồng khi quản lý bộ nhớ đệm sản phẩm. ### Các Loại Sản Phẩm StoreKit 2 StoreKit 2 phân biệt rõ ràng giữa các loại sản phẩm khác nhau. Hiểu được sự phân biệt này là điều cần thiết cho các cuộc phỏng vấn. ```swift // ProductType+Extensions.swift import StoreKit extension Product.ProductType { var displayName: String { switch self { case .consumable: // Sản phẩm tiêu hao (tín dụng, mạng sống, v.v.) return "Consumable" case .nonConsumable: // Mua hàng vĩnh viễn (mở khóa tính năng) return "Non-Consumable" case .autoRenewable: // Đăng ký với gia hạn tự động return "Auto-Renewable Subscription" case .nonRenewable: // Đăng ký không gia hạn (vé tạm thời) return "Non-Renewable Subscription" @unknown default: return "Unknown" } } } ``` Đăng ký gia hạn tự động đại diện cho mô hình kinh doanh chủ đạo cho các ứng dụng iOS hiện đại. ## Xử Lý Giao Dịch Mua Hàng Quá trình mua hàng trong StoreKit 2 trở nên tuyến tính nhờ async/await. Không còn delegate và callback lồng nhau từ StoreKit 1. ### Khởi Tạo Một Giao Dịch Mua ```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 { // Khởi tạo giao dịch mua với modal hệ thống let result = try await product.purchase() switch result { case .success(let verification): // Xác minh chữ ký giao dịch let transaction = try checkVerification(verification) // Đánh dấu giao dịch là đã hoàn thành await transaction.finish() return transaction case .userCancelled: // Người dùng hủy modal mua hàng throw PurchaseError.purchaseCancelled case .pending: // Giao dịch đang chờ (phê duyệt của phụ huynh, v.v.) throw PurchaseError.purchasePending @unknown default: throw PurchaseError.unknown } } private func checkVerification(_ result: VerificationResult) throws -> T { switch result { case .verified(let safe): // Giao dịch được ký và xác minh bởi App Store return safe case .unverified(_, let error): // Chữ ký không hợp lệ hoặc bị hỏng throw PurchaseError.verificationFailed } } } ``` Phương thức `finish()` rất quan trọng: nó báo hiệu cho App Store rằng nội dung đã được giao đến người dùng. > **Cảnh Báo Phỏng Vấn** > > Không bao giờ quên gọi `transaction.finish()` sau khi giao nội dung. Một giao dịch chưa hoàn thành sẽ được khôi phục vào lần khởi chạy ứng dụng tiếp theo, gây ra hành vi không mong muốn. ### Lắng Nghe Giao Dịch Nền StoreKit 2 giới thiệu `Transaction.updates`, một luồng bất đồng bộ phát ra các giao dịch xảy ra bên ngoài ứng dụng (gia hạn, mua family sharing, v.v.). ```swift // TransactionObserver.swift import StoreKit actor TransactionObserver { private var updateTask: Task? func startObserving() { // Hủy bất kỳ quan sát nào trước đó updateTask?.cancel() updateTask = Task(priority: .background) { // Luồng vô hạn các cập nhật giao dịch for await result in Transaction.updates { do { let transaction = try self.checkVerification(result) // Xử lý giao dịch dựa trên loại của nó await self.handleTransaction(transaction) // Luôn hoàn thành giao dịch await transaction.finish() } catch { // Ghi lại lỗi để gỡ lỗi print("Transaction verification failed: \(error)") } } } } func stopObserving() { updateTask?.cancel() updateTask = nil } private func handleTransaction(_ transaction: Transaction) async { switch transaction.productType { case .autoRenewable: // Cập nhật trạng thái đăng ký await updateSubscriptionStatus(transaction) case .nonConsumable: // Mở khóa tính năng cao cấp 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 { // Triển khai cập nhật trạng thái } private func unlockFeature(_ productID: String) async { // Triển khai mở khóa tính năng } enum PurchaseError: Error { case verificationFailed } } ``` Việc quan sát giao dịch nên bắt đầu ngay khi ứng dụng được khởi chạy để tránh bỏ lỡ bất kỳ cập nhật nào. ## Câu Hỏi Phỏng Vấn Thường Gặp về Đăng Ký StoreKit 2 Các cuộc phỏng vấn iOS thường bao gồm các câu hỏi cụ thể về quản lý đăng ký. Dưới đây là những câu hỏi phổ biến nhất với câu trả lời chi tiết. ### Câu Hỏi 1: Làm Thế Nào Để Kiểm Tra Trạng Thái Đăng Ký Hiện Tại? StoreKit 2 cung cấp quyền truy cập trực tiếp vào các quyền đang hoạt động thông qua `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 { // Truy xuất tất cả các giao dịch đang hoạt động var latestTransaction: Transaction? for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result { // Chỉ lọc các đăng ký if transaction.productType == .autoRenewable { // Giữ giao dịch gần đây nhất if latestTransaction == nil || transaction.purchaseDate > latestTransaction!.purchaseDate { latestTransaction = transaction } } } } guard let transaction = latestTransaction else { return SubscriptionStatus( isActive: false, expirationDate: nil, willRenew: false, productID: nil ) } // Kiểm tra xem đăng ký còn hoạt động không let isActive = transaction.expirationDate ?? Date.distantPast > Date() // Truy xuất thông tin gia hạn 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 } // Truy xuất trạng thái gia hạn let status = try? await subscription.status.first return status?.renewalInfo } } ``` Cách tiếp cận này cung cấp trạng thái đăng ký chính xác mà không cần lệnh gọi máy chủ. ### Câu Hỏi 2: Làm Thế Nào Để Xử Lý Dùng Thử Miễn Phí và Ưu Đãi Khuyến Mãi? StoreKit 2 đơn giản hóa quyền truy cập vào thông tin ưu đãi. Người phỏng vấn thường kiểm tra hiểu biết về các loại ưu đãi khác nhau. ```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: // Thời gian dùng thử miễn phí return .freeTrial(duration: formatPeriod(offer.period)) case .payAsYouGo: // Giá giảm trong nhiều giai đoạn return .payAsYouGo( periods: offer.periodCount, price: offer.price ) case .payUpFront: // Thanh toán một lần cho một giai đoạn return .payUpFront( duration: formatPeriod(offer.period), price: offer.price ) @unknown default: return .none } } static func checkEligibility(for product: Product) async -> Bool { // Kiểm tra xem người dùng có thể hưởng lợi từ ưu đãi không guard let subscription = product.subscription else { return false } // isEligibleForIntroOffer chỉ ra liệu đây có phải là người đăng ký mới 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 "" } } } ``` Phương thức `isEligibleForIntroOffer` rất cần thiết để tránh hiển thị các ưu đãi không khả dụng. ### Câu Hỏi 3: Làm Thế Nào Để Khôi Phục Các Giao Dịch Mua Trước Đó? Khôi phục giao dịch mua là một tính năng bắt buộc theo hướng dẫn của 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] = [] // Đồng bộ với App Store try await AppStore.sync() // Lặp qua tất cả các giao dịch của người dùng for await result in Transaction.currentEntitlements { guard case .verified(let transaction) = result else { continue } switch transaction.productType { case .nonConsumable: // Khôi phục các giao dịch mua vĩnh viễn restoredProducts.append(transaction.productID) case .autoRenewable: // Kiểm tra xem đăng ký có hoạt động không if let expiration = transaction.expirationDate, expiration > Date() { activeSubscriptions.append(transaction.productID) } default: // Sản phẩm tiêu hao không thể khôi phục break } } return RestoreResult( restoredProducts: restoredProducts, activeSubscriptions: activeSubscriptions ) } } ``` `AppStore.sync()` buộc đồng bộ với máy chủ Apple, hữu ích sau khi cài đặt lại. ## Xác Thực Biên Lai Phía Máy Chủ Xác thực phía máy chủ vẫn được khuyến nghị cho các ứng dụng có đăng ký quan trọng. StoreKit 2 giới thiệu JWS (JSON Web Signatures) cho việc xác thực hiện đại. ### Trích Xuất Dữ Liệu Giao Dịch cho Máy Chủ ```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 { // Tạo payload cho máy chủ let payload = TransactionPayload( transactionID: String(transaction.id), originalTransactionID: String(transaction.originalID), productID: transaction.productID, purchaseDate: transaction.purchaseDate, expirationDate: transaction.expirationDate, jwsRepresentation: transaction.jwsRepresentation ?? "" ) // Mã hóa thành JSON let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let jsonData = try encoder.encode(payload) // Gửi đến máy chủ 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 } // Giải mã phản hồi máy chủ let validationResponse = try JSONDecoder().decode( ValidationResponse.self, from: data ) return validationResponse.isValid } } struct ValidationResponse: Codable { let isValid: Bool let subscriptionStatus: String? } ``` `jwsRepresentation` chứa chữ ký mật mã có thể xác minh bởi máy chủ. > **App Store Server API** > > Apple hiện cung cấp App Store Server API cho việc xác thực phía máy chủ. API hiện đại này thay thế endpoint verifyReceipt đã lỗi thời và cung cấp các tính năng tiên tiến như thông báo máy chủ-đến-máy chủ. ### Cấu Hình Thông Báo Máy Chủ Server Notifications V2 cho phép nhận sự kiện theo thời gian thực. ```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? } } ``` Những thông báo này cho phép giữ trạng thái đăng ký đồng bộ với backend. ## Xử Lý Lỗi và Trường Hợp Biên Các cuộc phỏng vấn thường kiểm tra hiểu biết về các kịch bản lỗi và cách xử lý phù hợp. ### Các Lỗi Phổ Biến trong 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 { // Kiểm tra xem đó có phải là lỗi StoreKit không if let storeKitError = error as? StoreKitError { switch storeKitError { case .networkError: return .networkError case .userCancelled: // Không hiển thị lỗi, người dùng đã hủy return .unknown case .notAvailableInStorefront: return .productNotAvailable case .notEntitled: return .notAuthorized default: return .unknown } } // Lỗi mua hàng 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 } } ``` Xử lý lỗi rõ ràng cải thiện trải nghiệm người dùng và đơn giản hóa việc gỡ lỗi. ### Hỗ Trợ Chế Độ Ngoại Tuyến ```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 } } // Lưu cục bộ 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 [:] } // Lọc các quyền đã hết hạn let now = Date() return entitlements.filter { _, expiration in expiration > now } } func hasValidEntitlement(for productID: String) -> Bool { let cached = getCachedEntitlements() return cached[productID] != nil } } ``` Bộ nhớ đệm cục bộ duy trì quyền truy cập các tính năng cao cấp ngay cả khi không có kết nối. ## Thực Hành Tốt Nhất cho Sản Xuất ### Kiến Trúc Được Đề Xuất ```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() { // Bắt đầu quan sát giao dịch startTransactionObserver() // Tải trạng thái ban đầu 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() } } ``` Kiến trúc tập trung này đơn giản hóa việc quản lý trạng thái mua hàng trong toàn bộ ứng dụng. ## Kết Luận StoreKit 2 biến đổi việc quản lý mua hàng trong ứng dụng thành một trải nghiệm phát triển hiện đại và an toàn. Những điểm chính cần ghi nhớ cho một cuộc phỏng vấn: ✅ API async/await gốc: không còn delegate và callback phức tạp từ StoreKit 1 ✅ Xác minh tự động: `VerificationResult` xử lý xác thực mật mã của các giao dịch ✅ `Transaction.updates`: luồng bất đồng bộ cho các giao dịch nền ✅ `Transaction.currentEntitlements`: truy cập trực tiếp vào các quyền đang hoạt động của người dùng ✅ Ưu đãi khuyến mãi: `isEligibleForIntroOffer` để kiểm tra tính đủ điều kiện ✅ Xác thực máy chủ: JWS và App Store Server API để bảo mật nâng cao ✅ `transaction.finish()`: bắt buộc sau khi giao nội dung ✅ Xử lý ngoại tuyến: bộ nhớ đệm quyền cục bộ cho trải nghiệm liền mạch --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/ios/storekit-2-subscriptions-receipts-validation-interview