[iOS] [CodeHealth] Remove BraveSkusSDK wrapper type (#32431)

The `BraveSkusSDK` class simply hosted a `SkusService` and added utility methods to handle `BraveStoreProduct` types in the skus service. This change removes it in favour of simply adding these methods directly on `SkusService`

This change also:
- Removes up an empty method: `refreshAllSkusOrders`
- Deprecates the `BraveStoreSDK` singleton
- Moves the `receipt(for:)` method into the `AppStoreReceipt` type

There is no logical change with this removal, all places that used `BraveSkusSDK.shared` now grab a `SkusService` directly from the factory the same way `BraveSkusSDK` did.
This commit is contained in:
Kyle Hickinson
2025-11-21 14:39:30 -05:00
committed by GitHub
parent a7bdd6dd60
commit bee8f7b88a
11 changed files with 153 additions and 174 deletions
@@ -137,9 +137,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
await BraveVPN.hideActiveStorePromotion()
}
// Brave Store SDK - Initialization
BraveStoreSDK.shared.refreshAllSkusOrders()
// Override point for customization after application launch.
var shouldPerformAdditionalDelegateHandling = true
AdblockEngine.setDomainResolver()
+1
View File
@@ -410,6 +410,7 @@ var package = Package(
.target(
name: "BraveVPN",
dependencies: [
"BraveCore",
"BraveStore",
"BraveStrings",
"SnapKit",
@@ -645,8 +645,10 @@ public struct AIChatView: View {
// Purchased via AppStore
AIChatBusyErrorView {
Task { @MainActor in
if let orderId = Preferences.AIChat.subscriptionOrderId.value {
try? await BraveSkusSDK.shared.fetchCredentials(orderId: orderId, for: .leo)
if let skusService = Skus.SkusServiceFactory.get(privateMode: false),
let orderId = Preferences.AIChat.subscriptionOrderId.value
{
try? await skusService.fetchCredentials(orderId: orderId, for: .leo)
}
model.retryLastRequest()
@@ -60,13 +60,16 @@ public struct AIChatLeoSkusLogsView: View {
@MainActor
private func getSkusState() async -> String {
guard let skusService = Skus.SkusServiceFactory.get(privateMode: false) else {
return ""
}
var result = ""
let orderId = Preferences.AIChat.subscriptionOrderId.value ?? "None"
result += "OrderId: \(orderId)\n"
do {
let credentials = try await BraveSkusSDK.shared.credentialsSummary(for: .leo)
let credentials = try await skusService.credentialsSummary(for: .leo)
if let jsonData = try? jsonEncoder.encode(credentials),
let credentialsJSON = String(data: jsonData, encoding: .utf8)
{
@@ -3,6 +3,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
import BraveCore
import BraveStore
import Foundation
import Preferences
@@ -55,20 +56,24 @@ public class AIChatSubscriptionDetailModelView: ObservableObject {
@MainActor
func fetchCredentialSummary() async {
self.isLoading = true
defer {
self.isLoading = false
}
if storeSDK.leoSubscriptionStatus != nil {
self.isLoading = false
return
}
guard let skusService = Skus.SkusServiceFactory.get(privateMode: false) else {
return
}
do {
let credentialSummary = try await BraveSkusSDK.shared.credentialsSummary(for: .leo)
let credentialSummary = try await skusService.credentialsSummary(for: .leo)
self.credentialSummary = credentialSummary
} catch {
Logger.module.error("Error Fetching Skus Credential Summary: \(error)")
}
self.isLoading = false
}
var skuOrderExpirationDate: Date? {
@@ -90,7 +90,7 @@ class BraveSkusAccountLink {
do {
// Retrieve the LocalStorage Key and Receipt to inject
let storageKey = product.localStorageKey
let receipt = try BraveSkusSDK.receipt(for: product)
let receipt = try AppStoreReceipt.receipt(for: product)
// Inject the receipt into LocalStorage
try await tab.evaluateJavaScript(
@@ -118,7 +118,7 @@ class BraveSkusScriptHandler: TabContentScript {
let product = BraveStoreProduct(rawValue: vpnSubscriptionProductId)
{
let storageKey = product.localStorageKey
let receipt = try BraveSkusSDK.receipt(for: product)
let receipt = try AppStoreReceipt.receipt(for: product)
return ["key": storageKey, "data": receipt]
}
}
@@ -129,7 +129,7 @@ class BraveSkusScriptHandler: TabContentScript {
let product = BraveStoreProduct(rawValue: aiChatSubscriptionProductId)
{
let storageKey = product.localStorageKey
let receipt = try BraveSkusSDK.receipt(for: product)
let receipt = try AppStoreReceipt.receipt(for: product)
return ["key": storageKey, "data": receipt, "braveLeo.orderId": orderId]
}
}
@@ -37,6 +37,50 @@ public actor AppStoreReceipt {
}
}
/// Encodes a receipt for use with SkusSDK and Brave's Account Linking page
/// - Parameter product: The purchased product to create a receipt for
/// - Returns: Returns a Receipt structure encoded as Base64
public static func receipt(for product: BraveStoreProduct) throws -> String {
struct Receipt: Codable {
let type: String
let rawReceipt: String
let package: String
let subscriptionId: String
enum CodingKeys: String, CodingKey {
case type, package
case rawReceipt = "raw_receipt"
case subscriptionId = "subscription_id"
}
}
// Retrieve the AppStore receipt stored in the Application bundle
let rawReceipt = try receipt
// Fetch the Application's Bundle-ID
guard let bundleId = Bundle.main.bundleIdentifier else {
throw SkusError.invalidBundleId
}
// Create a Receipt structure for Skus-iOS
let json = Receipt(
type: "ios",
rawReceipt: rawReceipt,
package: bundleId,
subscriptionId: product.rawValue
)
do {
// Encode the Receipt as JSON and Base-64 Encode it
return try JSONEncoder().encode(json).base64EncodedString
} catch {
Logger.module.error(
"Failed to serialize AppStore Receipt for LocalStorage: \(error.localizedDescription, privacy: .public)"
)
throw SkusError.cannotEncodeReceipt
}
}
/// Forces the AppStore to add the receipt to the Application Bundle
/// When using StoreKit 2, receipts are no longer stored in the Application
/// This function forces the AppStore to place it in the bundle. Once back-end services update to use Transactions API
@@ -156,7 +156,10 @@ public enum BraveStoreProduct: String, AppStoreProduct, CaseIterable {
/// A structure for handling Brave Store transactions, products, and purchases
public class BraveStoreSDK: AppStoreSDK {
public static let shared = BraveStoreSDK()
@available(iOS, deprecated, message: "Create a new BraveStoreSDK instance instead.")
public static let shared = BraveStoreSDK(
skusService: Skus.SkusServiceFactory.get(privateMode: false)
)
// MARK: - Error
@@ -199,7 +202,11 @@ public class BraveStoreSDK: AppStoreSDK {
/// All observers this uses
private var observers = [AnyCancellable]()
private override init() {
private let skusService: (any SkusSkusService)?
public init(skusService: (any SkusSkusService)?) {
self.skusService = skusService
super.init()
// Observe Product Updates
@@ -260,11 +267,6 @@ public class BraveStoreSDK: AppStoreSDK {
return false
}
/// Refreshes all the Skus SDK orders
public func refreshAllSkusOrders() {
}
/// Restores a single purchased product
/// - Parameter product: The product whose purchase receipt to restore
/// - Returns: Returns true if receipt restoration was successful. False otherwise
@@ -335,7 +337,7 @@ public class BraveStoreSDK: AppStoreSDK {
}
}
/// Processes the product purchase transaction with the BraveSkusSDK
/// Processes the product purchase transaction with the SkusService
/// If the transaction cannot be processed (receipt is empty or null), throw an exception
/// - Parameter productId: The ID of the product that is currently being purchased
override public func processPurchase(of productId: Product.ID) async throws {
@@ -433,23 +435,24 @@ public class BraveStoreSDK: AppStoreSDK {
return
}
guard let skusService else {
throw SkusError.skusServiceUnavailable
}
Logger.module.info("[BraveStoreSDK] - Refreshing Receipt")
// Attempt to update the Application Bundle's receipt, if necessary
try await AppStoreReceipt.sync()
// Create a Skus-SDK for the specified product
let skusSDK = BraveSkusSDK.shared
// Create an order for the AppStore receipt
// If an order already exists, refreshes the order information
if let orderId = Preferences.AIChat.subscriptionOrderId.value {
try await skusSDK.refreshOrder(orderId: orderId, for: productGroup)
try await skusService.refreshOrder(orderId: orderId, for: productGroup)
return
}
Logger.module.info("[BraveStoreSDK] - No Order To Refresh")
throw BraveSkusSDK.SkusError.cannotCreateOrder
throw SkusError.cannotCreateOrder
}
/// Updates the Skus-SDK credentials and order information
@@ -463,6 +466,10 @@ public class BraveStoreSDK: AppStoreSDK {
return
}
guard let skusService else {
throw SkusError.skusServiceUnavailable
}
Preferences.AIChat.subscriptionProductId.value = product.rawValue
Logger.module.info("[BraveStoreSDK] - Syncing Receipt")
@@ -475,15 +482,13 @@ public class BraveStoreSDK: AppStoreSDK {
}
// Create a Skus-SDK for the specified product
let skusSDK = BraveSkusSDK.shared
// Create an order for the AppStore receipt
// If an order already exists, refreshes the order information
let orderId = try await skusSDK.createOrder(for: product)
let orderId = try await skusService.createOrder(for: product)
// There is an order, and an expiry date, but no credentials
// Fetch the credentials
try await skusSDK.fetchCredentials(orderId: orderId, for: product.group)
try await skusService.fetchCredentials(orderId: orderId, for: product.group)
// Store the Order-ID
Preferences.AIChat.subscriptionOrderId.value = orderId
@@ -91,69 +91,53 @@ public enum BraveSkusEnvironment {
}
}
/// A class for handling Brave Skus via SkusService
public class BraveSkusSDK {
/// An error related to Skus handling
public enum SkusError: Error {
/// The SkusService failed is unavailable for use
/// Can be thrown due to SkusServiceFactory returning null
case skusServiceUnavailable
/// Singleton instance since there can only ever be one instance of SkusService
public static let shared = BraveSkusSDK()
/// The Application's BundleID is invalid
/// Can be thrown when encoding AppStore receipts
case invalidBundleId
private init() {
self.skusService = Skus.SkusServiceFactory.get(privateMode: false)
}
/// The URL of the receipt stored in the Application Bundle is null or invalid
case invalidReceiptURL
// MARK: - Structures
/// The receipt is invalid
/// Thrown when Skus cannot validate the receipt and fetch credentials
case invalidReceiptData
/// An error related to Skus handling
public enum SkusError: Error {
/// The SkusService failed is unavailable for use
/// Can be thrown due to SkusServiceFactory returning null
case skusServiceUnavailable
/// The receipt cannot be encoded/serialized for use with Skus
case cannotEncodeReceipt
/// The Application's BundleID is invalid
/// Can be thrown when encoding AppStore receipts
case invalidBundleId
/// The SDK was unable to create a purchase order or retrieve an existing order
case cannotCreateOrder
/// The URL of the receipt stored in the Application Bundle is null or invalid
case invalidReceiptURL
/// The SDK was unable to submit a receipt
case cannotSubmitReceipt
/// The receipt is invalid
/// Thrown when Skus cannot validate the receipt and fetch credentials
case invalidReceiptData
/// The SDK was unable to refresh an order
case cannotRefreshOrder
/// The receipt cannot be encoded/serialized for use with Skus
case cannotEncodeReceipt
/// The SDK was unable to fetch the customer's purchase credential summary
case cannotFetchCredentialSummary
/// The SDK was unable to create a purchase order or retrieve an existing order
case cannotCreateOrder
/// The SDK was unable to fetch the customer's purchase credentials
case cannotFetchCredentials
/// The SDK was unable to submit a receipt
case cannotSubmitReceipt
/// The SDK was unable to prepare the customer's purchase credentials
case cannotPrepareCredentials
/// The SDK was unable to refresh an order
case cannotRefreshOrder
/// The SDK was unable to fetch the customer's purchase credential summary
case cannotFetchCredentialSummary
/// The SDK was unable to fetch the customer's purchase credentials
case cannotFetchCredentials
/// The SDK was unable to prepare the customer's purchase credentials
case cannotPrepareCredentials
/// There was an error decoding an SDK response
/// Can be thrown when the SDK fails to decode an order, order summary, credentials, etc
case decodingError
}
// MARK: - Private
/// The Skus Brave-Core Service
private let skusService: SkusSkusService?
/// There was an error decoding an SDK response
/// Can be thrown when the SDK fails to decode an order, order summary, credentials, etc
case decodingError
}
extension JSONDecoder {
/// A custom JSON Decoder that handles decoding Skus Object dates as ISO-8601
/// with optional milli-seconds
private let jsonDecoder: JSONDecoder = {
fileprivate static let skus: JSONDecoder = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [
.withYear,
@@ -181,87 +165,40 @@ public class BraveSkusSDK {
})
return decoder
}()
}
/// Encodes a receipt for use with SkusSDK and Brave's Account Linking page
/// - Parameter product: The purchased product to create a receipt for
/// - Returns: Returns a Receipt structure encoded as Base64
public static func receipt(for product: BraveStoreProduct) throws -> String {
struct Receipt: Codable {
let type: String
let rawReceipt: String
let package: String
let subscriptionId: String
enum CodingKeys: String, CodingKey {
case type, package
case rawReceipt = "raw_receipt"
case subscriptionId = "subscription_id"
}
}
// Retrieve the AppStore receipt stored in the Application bundle
let receipt = try AppStoreReceipt.receipt
// Fetch the Application's Bundle-ID
guard let bundleId = Bundle.main.bundleIdentifier else {
throw SkusError.invalidBundleId
}
// Create a Receipt structure for Skus-iOS
let json = Receipt(
type: "ios",
rawReceipt: receipt,
package: bundleId,
subscriptionId: product.rawValue
)
do {
// Encode the Receipt as JSON and Base-64 Encode it
return try JSONEncoder().encode(json).base64EncodedString
} catch {
Logger.module.error(
"[BraveSkusSDK] - Failed to serialize AppStore Receipt for LocalStorage: \(error.localizedDescription, privacy: .public)"
)
throw SkusError.cannotEncodeReceipt
}
}
// MARK: - Implementation
/// A group of helper methods built around BraveStoreProduct
extension SkusSkusService {
/// Creates an order from an AppStore Receipt
/// If an order already exists, returns the existing Order-ID
/// - Parameter product: The purchased product to create an order for
/// - Returns: The Order-ID associated with the AppStore receipt
@MainActor
public func createOrder(for product: BraveStoreProduct) async throws -> String {
guard let skusService = skusService else {
throw SkusError.skusServiceUnavailable
}
Logger.module.info("[SkusService] - Creating Order")
Logger.module.info("[BraveSkusSDK] - Creating Order")
let receipt = try AppStoreReceipt.receipt(for: product)
let receipt = try BraveSkusSDK.receipt(for: product)
Logger.module.info("[SkusService] - Fetched Receipt")
Logger.module.info("[BraveSkusSDK] - Fetched Receipt")
Logger.module.info("[BraveSkusSDK] - Creating Order From Receipt")
let skusResult = await skusService.createOrderFromReceipt(
Logger.module.info("[SkusService] - Creating Order From Receipt")
let skusResult = await createOrderFromReceipt(
domain: product.group.skusDomain,
receipt: receipt
)
if skusResult.code != Skus.SkusResultCode.ok {
Logger.module.info("[BraveSkusSDK] - No OrderID - \(skusResult.message, privacy: .public)")
Logger.module.info("[SkusService] - No OrderID - \(skusResult.message, privacy: .public)")
throw SkusError.cannotCreateOrder
}
let orderId = skusResult.message
if orderId.isEmpty {
Logger.module.info("[BraveSkusSDK] - No OrderID")
Logger.module.info("[SkusService] - No OrderID")
throw SkusError.cannotCreateOrder
}
Logger.module.info("[BraveSkusSDK] - OrderID: \(orderId, privacy: .private(mask: .hash))")
Logger.module.info("[SkusService] - OrderID: \(orderId, privacy: .private(mask: .hash))")
return orderId
}
@@ -272,12 +209,8 @@ public class BraveSkusSDK {
@MainActor
public func submitReceipt(orderId: String, for product: BraveStoreProduct) async throws -> String
{
guard let skusService = skusService else {
throw SkusError.skusServiceUnavailable
}
let receipt = try BraveSkusSDK.receipt(for: product)
let skusResult = await skusService.submitReceipt(
let receipt = try AppStoreReceipt.receipt(for: product)
let skusResult = await submitReceipt(
domain: product.group.skusDomain,
orderId: orderId,
receipt: receipt
@@ -285,7 +218,7 @@ public class BraveSkusSDK {
if skusResult.code != Skus.SkusResultCode.ok {
Logger.module.info(
"[BraveSkusSDK] - Failed to Submit Receipt - \(skusResult.message, privacy: .public)"
"[SkusService] - Failed to Submit Receipt - \(skusResult.message, privacy: .public)"
)
throw SkusError.cannotSubmitReceipt
}
@@ -313,22 +246,18 @@ public class BraveSkusSDK {
throw SkusError.decodingError
}
return try self.jsonDecoder.decode(SkusOrder.self, from: data)
return try JSONDecoder.skus.decode(SkusOrder.self, from: data)
}
Logger.module.info(
"[BraveSkusSDK] - Refreshing Order: \(orderId, privacy: .private(mask: .hash))"
"[SkusService] - Refreshing Order: \(orderId, privacy: .private(mask: .hash))"
)
guard let skusService = skusService else {
throw SkusError.skusServiceUnavailable
}
let skusResult = await skusService.refreshOrder(domain: group.skusDomain, orderId: orderId)
let skusResult = await refreshOrder(domain: group.skusDomain, orderId: orderId)
if skusResult.code != Skus.SkusResultCode.ok {
Logger.module.info(
"[BraveSkusSDK] - Failed Refreshing Order - \(skusResult.message, privacy: .public)"
"[SkusService] - Failed Refreshing Order - \(skusResult.message, privacy: .public)"
)
throw SkusError.cannotRefreshOrder
}
@@ -353,17 +282,13 @@ public class BraveSkusSDK {
throw SkusError.decodingError
}
return try self.jsonDecoder.decode(SkusCredentialSummary.self, from: data)
return try JSONDecoder.skus.decode(SkusCredentialSummary.self, from: data)
}
guard let skusService = skusService else {
throw SkusError.skusServiceUnavailable
}
let skusResult = await skusService.credentialSummary(domain: group.skusDomain)
let skusResult = await credentialSummary(domain: group.skusDomain)
if skusResult.code != Skus.SkusResultCode.ok {
Logger.module.info(
"[BraveSkusSDK] - Failed Fetching CredentialSummary - \(skusResult.message, privacy: .public)"
"[SkusService] - Failed Fetching CredentialSummary - \(skusResult.message, privacy: .public)"
)
throw SkusError.cannotFetchCredentialSummary
}
@@ -377,26 +302,22 @@ public class BraveSkusSDK {
/// - Throws: An exception if fetching credentials failed
@MainActor
public func fetchCredentials(orderId: String, for group: BraveStoreProductGroup) async throws {
guard let skusService = skusService else {
throw SkusError.skusServiceUnavailable
}
Logger.module.info("[BraveSkusSDK] - Fetching Order Credentials")
let skusResult = await skusService.fetchOrderCredentials(
Logger.module.info("[SkusService] - Fetching Order Credentials")
let skusResult = await fetchOrderCredentials(
domain: group.skusDomain,
orderId: orderId
)
if skusResult.code != Skus.SkusResultCode.ok {
Logger.module.info(
"[BraveSkusSDK] - Failed Fetching Credentials - \(skusResult.message, privacy: .public)"
"[SkusService] - Failed Fetching Credentials - \(skusResult.message, privacy: .public)"
)
throw SkusError.cannotFetchCredentials
}
if !skusResult.message.isEmpty {
Logger.module.error(
"[BraveSkusSDK] - Failed to Fetch Credentials: \(skusResult.message, privacy: .public)"
"[SkusService] - Failed to Fetch Credentials: \(skusResult.message, privacy: .public)"
)
throw SkusError.cannotFetchCredentials
}
@@ -412,18 +333,14 @@ public class BraveSkusSDK {
for group: BraveStoreProductGroup,
path: String = "*"
) async throws -> String {
guard let skusService = skusService else {
throw SkusError.skusServiceUnavailable
}
let skusResult = await skusService.prepareCredentialsPresentation(
let skusResult = await prepareCredentialsPresentation(
domain: group.skusDomain,
path: path
)
if skusResult.code != Skus.SkusResultCode.ok {
Logger.module.info(
"[BraveSkusSDK] - Failed Preparing Credentials - \(skusResult.message, privacy: .public)"
"[SkusService] - Failed Preparing Credentials - \(skusResult.message, privacy: .public)"
)
throw SkusError.cannotPrepareCredentials
}
@@ -3,6 +3,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
import BraveCore
import BraveShared
import BraveStore
import BraveUI
@@ -103,8 +104,12 @@ public class BraveVPNSettingsViewController: TableViewController {
return nil
}
guard let skusService = Skus.SkusServiceFactory.get(privateMode: false) else {
return nil
}
do {
return try await BraveSkusSDK.shared.credentialsSummary(for: .vpn)
return try await skusService.credentialsSummary(for: .vpn)
} catch {
Logger.module.error("Error Fetching VPN Skus Credential Summary: \(error)")
}