メインコンテンツまでスキップ

应用内购买集成(Swift SDK)

iOS Billing SDK 是实现 Aptoide Connect 计费的简单解决方案。它由一个 Billing 客户端组成,使您能够从 Aptoide Connect 获取产品并处理这些项目的购买。

该 SDK 会自动处理向 Apple 报告交易以用于 Core Technology Commission(CTC)计算,从而免除开发者的这一负担。它包含用于报告购买、退款和其他交易事件的智能逻辑,并采用区分哪些地区需要 CTC 报告、哪些不需要的地区感知处理。

概要

使用 SDK 的应用程序计费流程如下:

  1. 设置 AppCoins SDK Swift Package;
  2. 查询您的应用内产品;
  3. 用户想要购买产品;
  4. 应用程序发起购买,SDK 进行处理,并在完成时返回购买状态和验证数据;
  5. 应用程序将产品交付给用户。

分步指南

设置

  1. 添加 AppCoins SDK Swift Package
    在 XCode 中,从仓库 https://github.com/Catappult/appcoins-sdk-ios.git 添加 Swift Package。

  2. 添加 AppCoins SDK Keychain Access 权限
    为了使 AppCoins SDK 能够将用户的 Aptoide Wallet 信息保存在 keychain 中,应用程序需要向 SDK 授予 Keychain Access 权限。为此,请按照以下步骤操作:

    1. 在项目导航器(左侧边栏)中选择您的项目;
    2. 在“TARGETS”下选择您的 target;
    3. 转到“Signing & Capabilities”选项卡;
    4. 点击“+”按钮添加新的 capability;
    5. 搜索“Keychain Sharing”并选择它;
    6. 双击以启用“Keychain Sharing”capability;
    7. 这将自动在“Keychain Groups”文本框中写入您应用的标识符,您应将其替换为“com.aptoide.appcoins-wallet”;
    8. Xcode 将自动生成一个权限文件(例如 YourAppName.entitlements)并将其添加到您的项目中;
  3. 添加 AppCoins SDK URL Type
    为了管理特定支付方式集成的重定向深度链接,您的应用程序必须在 info.plist 文件中包含一个 URL Type。为此,请按照以下步骤操作:

    1. 在项目导航器(左侧边栏)中选择您的项目。
    2. 在“TARGETS”下选择您的 target。
    3. 导航到“Info”选项卡。
    4. 向下滚动到“URL Types”部分。
    5. 点击“+”按钮添加新的 URL Type。
    6. 将 URL Scheme 设置为“$(PRODUCT_BUNDLE_IDENTIFIER).iap”,并将 role 设置为“Editor”。
  4. 配置数字商品设置
    为了启用 SDK 的自动交易报告以用于 CTC(Core Technology Commission)计算,您必须配置您的 target 以表明其销售数字商品。请按照以下步骤操作:

    1. 在项目导航器(左侧边栏)中选择您的项目。
    2. 在“TARGETS”下选择您的 target。
    3. 导航到“Info”选项卡。
    4. 向您的 Target Properties 添加一个新的“MKSellsDigitalGoods”键。
    5. 将值设置为“YES”以启用数字商品交易报告。

Objective-C 支持

Swift 和 Unity 插件 是推荐的集成方式,但也支持 Objective-C。要在 Objective-C 项目中使用 SDK:

  1. 仓库发布页面下载最新的 SDK 版本(.zip)。
  2. 将随附的 .xcframework 添加到您的项目中。
  3. 创建一个 bridging header,以将 Swift API 暴露给 Objective-C。

bridging header 就位后,其实现将遵循下文针对 Swift 所述的相同步骤。

实现

现在您已经设置好 SDK 和必要的权限,可以开始使用其功能了。为此,您必须在任何想要使用它的文件中导入 SDK 模块,方法是调用以下语句:import AppCoinsSDK

  1. 初始化 AppCoins SDK

    ⚠️

    关键: 在使用任何其他 SDK 功能之前,您必须在每个应用程序入口点调用 AppcSDK.initialize()。此方法设置内部 SDK 进程,是 SDK 正常运行所必需的。

    SDK 必须在应用程序的入口点方法中初始化。根据您应用的设置,这将位于 SceneDelegate.swift(适用于 iOS 13+)或 AppDelegate.swift 中。

    SceneDelegate.swift:

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    AppcSDK.initialize() // REQUIRED
    // ... rest of your code
    }

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    AppcSDK.initialize() // REQUIRED
    // ... rest of your code
    }

    AppDelegate.swift:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    AppcSDK.initialize() // REQUIRED
    // ... rest of your code
    }

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    AppcSDK.initialize() // REQUIRED
    // ... rest of your code
    }
  2. 处理重定向
    SDK 需要集成到应用程序的入口点中,以正确处理深度链接。这可确保支付重定向和其他深度链接功能无缝运行。
    根据您应用的设置,您应在 SceneDelegate.swift(适用于 iOS 13+)或 AppDelegate.swift(适用于旧版本以及仍在使用它的应用)中处理深度链接。

    1. SceneDelegate.swift

      如果您的应用使用 SceneDelegate.swift,请实现以下方法:

      func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
      AppcSDK.initialize()

      if AppcSDK.handle(redirectURL: URLContexts.first?.url) { return }

      // Your application initialization
      initialize()
      }

      func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
      // Create the SwiftUI view that provides the window contents.
      let contexts = connectionOptions.urlContexts

      AppcSDK.initialize()

      // Your application initialization
      initialize()

      if AppcSDK.handle(redirectURL: contexts.first?.url) { return }
      }

      为何采用此逻辑?

      1. willConnectTo 中首先初始化
        • 当应用启动或恢复时,必须首先设置 UI 和依赖项。
        • 如果 SDK 或服务尚未就绪,在此之前处理深度链接可能会导致问题。
      2. openURLContexts 中优先处理深度链接
        • 当应用运行时有深度链接到达,立即处理它,如果已处理则 return。
        • 这可防止不必要的重新初始化,并确保应用快速响应。
    2. AppDelegate.swift
      如果您的应用不使用 SceneDelegate.swift,请在 AppDelegate.swift 中实现深度链接处理。

      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      AppcSDK.initialize()

      // Your application initialization
      initialize()

      if let url = launchOptions?[.url] as? URL {
      if AppcSDK.handle(redirectURL: url) { return true }
      }
      return true
      }

      func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
      AppcSDK.initialize()

      if AppcSDK.handle(redirectURL: url) { return true }

      // Your application initialization
      initialize()
      return true
      }

      为何采用此逻辑?

      1. didFinishLaunchingWithOptions 中首先初始化
        • 在处理深度链接之前,确保 UI 和依赖项已就绪。
        • 过早处理深度链接可能会在服务未初始化时导致问题。
      2. open url 中优先处理深度链接
        • 当应用运行时收到深度链接,立即处理它。
        • 如果 AppcSDK.handle(redirectURL:) 处理了该链接,则提前 return。
  3. 检查 AppCoins SDK 可用性(支付分支)
    默认情况下,AppCoins SDK 仅在 iOS 版本等于或高于 17.4 的设备上可用,且仅当应用程序不是通过 Apple App Store 安装时才可用。因此,在尝试购买之前,您应通过调用 AppcSDK.isAvailable 来检查 SDK 是否可用。

    isAvailable() 仅在两个条件都满足时返回 true:应用是从替代应用市场安装的,并且设备运行 iOS 17.4 或更高版本。对于通过 App Store 安装以及低于 iOS 17.4 的设备,它会返回 false。请使用它来分支您的计费逻辑:当它返回 true 时,使用 AppCoins SDK;否则,回退到 Apple In-App Purchase。

    if await AppcSDK.isAvailable() {
    // 在 iOS 17.4+ 上从替代应用市场安装:使用 AppCoins SDK
    } else {
    // 通过 App Store 安装或 iOS 低于 17.4:使用 Apple In-App Purchase
    }
  4. 查询应用内产品
    您应首先获取要向用户提供的应用内产品。您可以通过调用 Product.products 来执行此操作。

    此方法既可以返回您所有的 Aptoide 应用内产品,也可以返回特定列表。

    1. Product.products()

      返回应用程序所有的 Aptoide 应用内产品:

      let products = try await Product.products()
    2. Product.products(for: [String])

      返回 Aptoide 应用内产品的特定列表:

      let products = try await Product.products(for: ["gas"])
      ⚠️

      警告: 只有在您的应用程序在 Aptoide Connect 上经过审核并获得批准后,您才能查询您的应用内产品。

  5. 购买应用内产品
    要购买应用内产品,您必须在 Product 对象上调用 purchase() 函数。SDK 将为您处理所有购买逻辑,并在完成时返回购买结果。此结果可以是 .success(let verificationResult).pending.userCancelled.failed(let error)

    在成功的情况下,应用程序将在本地验证交易的签名。验证后,您应处理其结果:
    – 如果购买已验证,您应消耗该项目并将其交付给用户;
    – 如果未验证,您需要根据您的业务逻辑做出决定,您可以仍然消耗该项目并将其交付给用户,否则该购买将不会被确认,我们将在 24 小时内向用户退款。

    在失败的情况下,您可以在 switch 语句中处理不同类型的错误。SDK 返回的每个错误都是 AppCoinsSDKError 类型,本文档后面将对其进行说明。

    您还可以向 purchase 方法传递一个 Payload,以便将某种信息与特定购买关联。例如,您可以使用它将特定用户与某次购买关联起来:gas.purchase(payload: "User123")。\

    let result = await products?.first?.purchase()

    switch result {
    case .success(let verificationResult):
    switch verificationResult {
    case .verified(let purchase):
    // consume the item and give it to the user
    try await purchase.finish()
    case .unverified(let purchase, let verificationError):
    // deal with unverified transactions
    }
    case .pending: // transaction is not finished
    case .userCancelled: // user cancelled the transaction
    case .failed(let error): // deal with any possible errors
    }
  6. 在应用启动时处理未完成的购买(关键)

    ⚠️

    关键: 每次应用程序启动时,您都必须查询并消耗未完成的购买。否则会导致用户无法收到他们已经付费的项目,并且未在 24 小时内消耗的购买将被自动退款。

    未完成的购买是指已付费但尚未被您的应用程序消耗的交易。这可能在以下情况下发生:

    • 应用在购买过程中被关闭或崩溃
    • 用户在购买被处理之前强制退出应用
    • 购买完成期间发生网络错误

    为何这至关重要:

    • 用户已经为这些项目付费
    • 如果未在 24 小时内消耗,购买将被自动退款
    • 用户期望在重新打开应用后立即收到他们购买的项目

    最佳实践:在应用的初始化流程中调用 Purchase.unfinished(),最好在检查 SDK 可用性之后。

    // Example: In your app initialization (e.g., ViewModel or app startup)
    func initializeApp() async {
    if await AppcSDK.isAvailable() {
    do {
    // Query all unfinished purchases
    let unfinishedPurchases = try await Purchase.unfinished()

    // Consume each purchase and give the user their items
    for purchase in unfinishedPurchases {
    // Give the item to the user based on the SKU
    giveItemToUser(sku: purchase.sku)

    // Mark the purchase as finished
    try await purchase.finish()
    }
    } catch {
    // Handle error - log it and potentially retry later
    print("Failed to process unfinished purchases: \(error)")
    }
    }
    }
  7. 处理购买意图

    除标准的应用内购买外,AppCoins SDK 还支持应用内购买意图——并非由用户操作直接触发的购买(例如,在应用内点击“购买”按钮)。常见用例包括:

    1. 直接从 Aptoide Store 的应用内产品目录中购买项目。
    2. 通过网页链接购买项目。

    购买意图可以通过以下 URL 格式发起:

    {domain}.iap://wallet.appcoins.io/purchase?product={sku}&oemid={oemid}&discount_policy={discount_policy}
    1. domain – 您应用程序的 Bundle ID。
    2. oemid – 与您在 Aptoide Connect 上的开发者账户关联的 OEM ID。
    3. discount_policy – 要应用的折扣策略(例如 D2C)。

    SDK 允许开发者通过 Purchase.updates 方法管理这些购买并向用户交付消耗型商品。此方法返回一个 Task 对象,用于流式传输实时购买更新,从而实现无缝的交易处理。

    该流会发出一个 PurchaseIntent 对象,您可以根据您的应用程序逻辑对其进行管理。PurchaseIntent 类提供两个方法:

    1. confirm(payload: String?, orderID: String?):确认并处理购买。等同于调用 .purchase()
    2. reject():拒绝该意图,使其在将来无法使用。

    如果您不希望立即处理该意图——例如,等待用户登录以便将购买与其账户关联——您可以先忽略该意图。之后,当您的逻辑允许时,您可以调用 Purchase.intent,它会返回当前待处理的意图。然后您可以根据需要确认或拒绝它。

    以下是处理应用内购买意图的骨架实现。\

    import AppCoinsSDK

    actor PurchaseManager {
    static let shared = PurchaseManager() // Singleton instance

    private init() {
    Task { await observePurchases() }
    }

    private func observePurchases() async {
    for await intent in Purchase.updates {
    if User.isSignedIn {
    let result = await intent.confirm()
    await handle(purchaseResult: result)
    }
    }
    }

    // HINT: You can use the same handle method for both regular and intent IAP
    private func handle(result: PurchaseResult) async {
    switch result {
    case .success(let verificationResult):
    switch verificationResult {
    case .verified(let purchase):
    // consume the item and give it to the user
    try await purchase.finish()
    case .unverified(let purchase, let verificationError):
    // deal with unverified transactions
    }
    case .pending: // transaction is not finished
    case .userCancelled: // user cancelled the transaction
    case .failed(let error): // deal with any possible errors
    }
    }
    }

  8. 查询购买
    您可以使用以下方法之一来查询用户的购买:

    1. Purchase.all

      此方法返回用户在您的应用程序中执行的所有购买。

      let purchases = try await Purchase.all()
    2. Purchase.latest(sku: String)

      此方法返回特定应用内产品的用户最新购买。

      let purchase = try await Purchase.latest(sku: "gas")
    3. Purchase.unfinished

      此方法返回用户在应用程序中所有未完成的购买。未完成的购买是指既未被确认(由 SDK 验证)也未被消耗的任何购买。

      ⚠️

      关键: 您必须在应用初始化期间调用此方法,以确保用户能收到因购买被中断而获得的项目。有关详细实现,请参阅步骤 6“在应用启动时处理未完成的购买”。

      let purchases = try await Purchase.unfinished()

测试

📘
以下 Xcode 设置仅用于测试

以下 Xcode 设置仅用于在开发期间模拟安装来源。在生产环境中,SDK 会通过 Apple 的 API 自动检测真实的安装来源,这些测试设置将被忽略。它们对生产构建没有任何影响。

要在开发期间测试 SDK 集成,您需要为开发构建设置安装来源,模拟应用是通过 Aptoide 分发的。此操作将启用 SDK 的 isAvailable 方法。

请按照以下步骤操作:

  1. 在您的 target 构建设置中,搜索“Marketplaces”。

  2. 在“Deployment”下,将“Marketplaces”或“Alternative Distribution - Marketplaces”键设置为“com.aptoide.ios.store”。

    Marketplaces 设置为 com.aptoide.ios.store 的 Xcode 构建设置
  3. 在您的 scheme 中,转到“Run”选项卡,然后导航到“Options”选项卡。在“Distribution”下拉菜单中,选择“com.aptoide.ios.store”。

    Distribution 设置为 com.aptoide.ios.store 的 Xcode scheme Run 选项

有关更多信息,请参阅 Apple 的官方文档:https://developer.apple.com/documentation/appdistribution/distributing-your-app-on-an-alternative-marketplace#Test-your-app-during-development

在单个构建中测试两种计费系统

为了便于在单个构建中测试 Apple BillingAptoide Billing——无需生成应用程序的单独版本——AppCoins SDK 包含一个深度链接机制,可在 truefalse 之间切换 SDK 的 isAvailable 方法。这使您能够在测试 AppCoins SDK(可用时)和 Apple Billing(不可用时)之间无缝切换。

要启用或禁用 AppCoins SDK,请打开您设备的浏览器并输入以下 URL:

{domain}.iap://wallet.appcoins.io/default?value={value}

其中:

  • domain – 您应用程序的 Bundle ID。
  • value
    • true → 启用 AppCoins SDK 以进行测试。
    • false → 禁用 AppCoins SDK,从而允许改为测试 Apple Billing。

沙盒

为了验证您的计费集成是否设置成功,我们提供了一个沙盒环境,您可以在其中模拟购买并确保您的客户能够顺利购买您的产品。有关如何使用此环境的文档可在以下位置找到:沙盒

类定义和属性

SDK 集成基于处理其逻辑的四个主要对象类:

Product

Product 表示一个应用内产品。您可以使用它来静态查询产品,或使用特定实例来执行购买。

属性:

  • sku: String - 唯一产品标识符。示例:gas
  • title: String - 产品显示标题。示例:Best Gas
  • description: String? - 产品描述。示例:Buy gas to fill the tank.
  • priceCurrency: String - 用户的地理定位货币。示例:EUR
  • priceValue: String - 产品在指定货币中的价格值。示例:0.93
  • priceLabel: String - 向用户显示的价格标签。示例:€0.93
  • priceSymbol: String - 地理定位货币的符号。示例:€

Purchase

Purchase 表示一次应用内购买。您可以使用它来静态查询用户的购买,或使用特定实例来消耗相应的购买。

属性:

  • uid: String - 唯一购买标识符。示例:catappult.inapp.purchase.ABCDEFGHIJ1234
  • sku: String - 所购买产品的唯一标识符。示例:gas
  • state: String - 购买状态可以是以下三种之一:PENDING、ACKNOWLEDGED 和 CONSUMED。Pending 购买是指既未被 SDK 验证也未被应用程序消耗的购买。Acknowledged 购买是指已被 SDK 验证但尚未被消耗的购买。示例:CONSUMED
  • orderUid: String - 与购买关联的 orderUid。示例:ZWYXGYZCPWHZDZUK4H
  • payload: String - 开发者 Payload。示例:707048467.998992
  • created: String - 购买的创建日期。示例:2023-01-01T10:21:29.014456Z
  • verification: PurchaseVerification - 与购买关联的验证数据。

PurchaseVerification

PurchaseVerification 表示应用内购买的验证数据。

属性:

  • type: String - 所进行验证的类型。示例:GOOGLE
  • signature: String - 购买签名。示例:C4x6cr0HJk0KkRqJXUrRAhdANespHEsyx6ajRjbG5G/v3uBzlthkUe8BO7NXH/1Yi/UhS5sk7huA+hB8EbaQK9bwaiV/Z3dISl5jgYqzSEz1c/PFPwVEHZTMrdU07i/q4FD33x0LZIxrv2XYbAcyNVRY3GLJpgzAB8NvKtumbWrbV6XG4gBmYl9w4oUgJLnedii02beKlvmR7suQcqIqlSKA9WEH2s7sCxB5+kYwjQ5oHttmOQENnJXlFRBQrhW89bl18rccF05ur71wNOU6KgMcwppUccvIfXUpDFKhXQs4Ut6c492/GX1+KzbhotDmxSLQb6aw6/l/kzaSxNyjHg==
  • data: PurchaseVerificationData - 与购买验证关联的数据。

PurchaseVerificationData

PurchaseVerificationData 表示应用内购买验证数据的主体。

属性:

  • orderId: String - 与购买关联的 orderUid。示例:372EXWQFTVMKS6HI
  • packageName: String - 产品应用程序的 Bundle ID。示例:com.appcoins.trivialdrivesample
  • productId: String - 所购买产品的唯一标识符。示例:gas
  • purchaseTime: Integer - 产品的购买时间。示例:1583058465823
  • purchaseToken: String - 购买产品时提供给用户设备的令牌。示例:catappult.inapp.purchase.SZYJ5ZRWUATW5YU2
  • purchaseState: Integer - 订单的购买状态。可能的值为:0(已购买)和 1(已取消)
  • developerPayload: String - 开发者指定的字符串,包含有关订单的补充信息。示例:myOrderId:12345678

PurchaseIntent

PurchaseIntent 表示用户进行应用内购买的意图。它通常用于确认或拒绝在应用程序外部发起的购买。

属性:

  • id: String - 购买意图的唯一标识符。
  • timestamp: Date - 意图创建的日期和时间。
  • product: Product - 用户打算购买的产品。

AppcSDK

此类负责处理通用方法,例如处理重定向或检查 SDK 是否可用。

方法:

  • initialize(): 必需 - 设置内部 SDK 进程。必须在每个应用程序入口点(SceneDelegate 或 AppDelegate 方法)中调用。如果没有此调用,SDK 将无法正常运行。
  • isAvailable() async -> Bool: 检查 AppCoins SDK 在当前设备上是否可用。如果设备运行 iOS 17.4+,且应用不是通过 Apple App Store 安装的,则返回 true。
  • handle(redirectURL: URL?) -> Bool: 处理支付重定向和购买意图的深度链接。如果 SDK 处理了该 URL,则返回 true。

AppCoinsSDKError

SDK 在执行任何操作时可能返回的错误枚举。

可能的错误:

  • networkError: 与网络相关的错误;
  • systemError: 内部 APPC 系统错误;
  • notEntitled: 宿主应用未配置正确的权限;
  • productUnavailable: 产品不可用;
  • purchaseNotAllowed: 不允许用户执行该购买;
  • unknown: 其他错误。

常见问题

如果我已经使用 StoreKit,还需要 Aptoide Billing SDK 吗?

简短回答: 需要。Apple In-App Purchase 在替代应用市场中不可用。

详细说明: 要在通过替代应用市场分发的应用中提供应用内购买,集成 Aptoide iOS Billing SDK(AppCoins SDK)是强制性的。当使用替代计费系统时,您还需要向 Apple 报告外部购买;请在适用的地区完成相应的外部应用内购买报告配置。

每个平台上有哪些 SDK 功能可用?

简短回答: iOS 提供应用内购买;Android 还额外提供 App Updates 和 Referral Deeplinks。

详细说明:

两个平台均不提供应用内评价弹窗(没有与 Google Play In-App Review API 相当的功能)。