实施应用内购买 (IAP)


实施应用内购买 (IAP)

本页介绍如何使用基于任务的方法在应用中实施 IAP API,提供了使用案例和代码段来对 IAP v2.0 API 参考文档进行补充。请注意,本文档中的许多代码段都可以在消费品 IAP 示例应用中找到。

关于程序包 com.amazon.device.iap

com.amazon.device.iap 程序包提供了将应用内购买 API 和 Amazon Appstore 结合使用所需的类和接口。

此程序包包含以下接口和类:

  • ResponseReceiver:​ 从 Amazon Appstore 接收广播意图的类。
  • PurchasingService:​ 通过 Amazon Appstore 发出请求的类。
  • PurchasingListener​: 接收 PurchasingService 发出的请求的异步响应的接口。

下表显示了 PurchasingService 的请求方法和相关联的 PurchasingListener 响应回调。这些是您在实施 IAP API 时最常用的方法、回调和响应对象:

PurchasingService 方法 PurchasingListener 回调 响应对象
getUserData() onUserDataResponse() UserDataResponse
getPurchaseUpdates() onPurchaseUpdatesResponse() PurchaseUpdatesResponse
getProductData() onProductDataResponse() ProductDataResponse
purchase() onPurchaseResponse() PurchaseResponse
notifyFulfillment()

有关这些类和方法的更多信息,请参阅 IAP v2.0 API 参考文档

ResponseReceiver

应用内购买 API 以异步方式执行其所有活动。您的应用需要通过 ResponseReceiver 类从 Amazon Appstore 接收广播意图。此类从不在您的应用中直接使用,但要使您的应用接收意图,您必须将 ResponseReceiver 条目添加到 AndroidManifest.xml 文件中。以下代码段显示了如何将 ResponseReceiver 添加到 IAP v2.0 的 AndroidManifest.xml 文件中:

 <application>
  ...
  <receiver android:name = "com.amazon.device.iap.ResponseReceiver" >
    <intent-filter>
      <action android:name = "com.amazon.inapp.purchasing.NOTIFY"
              android:permission = "com.amazon.inapp.purchasing.Permission.NOTIFY" />
    </intent-filter>
  </receiver>
  ...
 </application>

PurchasingService

使用 PurchasingService 类来检索各类信息,执行购买,并将购买的履行情况通知亚马逊。PurchasingService 实现以下方法:

  • registerListener(PurchasingListener purchasingListener):​ 在 PurchasingService 类中调用其他方法之前先调用此方法。
  • getUserData():​ 调用此方法来检索当前登录的用户的特定于应用的 ID 和市场。例如,如果用户切换账户或者多个用户在同一设备上访问您的应用,则此调用将帮助您确保您检索的收据针对当前用户账户。
  • getPurchaseUpdates(boolean reset):​ GetPurchaseUpdates 跨所有设备检索所有订阅和权利购买。只能从在其中购买消费品的设备检索消费品购买。GetPurchaseUpdates 只检索未履行的和已取消的消费品购买。亚马逊建议您保留返回的 PurchaseUpdatesResponse 数据并仅向系统查询更新。响应会分页。
  • getProductData(java.util.Set skus):​ 调用此方法来检索一组 SKU 的项目数据以显示在您的应用中。
  • purchase(java.lang.String sku):​ 调用此方法以发起特定 SKU 的购买。
  • notifyFulfillment(java.lang.String receiptId, FulfillmentResult fulfillmentResult): 调用此方法以发送指定 receiptIdFulfillmentResultFulfillmentResult 的可能值为 FULFILLED 或 UNAVAILABLE。

PurchasingListener

实现 PurchasingListener 接口来处理异步回调。因为您的 UI 线程调用这些回调,所以不要在 UI 线程中处理长时间运行的任务。您的 PurchasingListener 实例应实现以下方法:

  • onUserDataResponse(UserDataResponse userDataResponse):​ 在调用 getUserData() 后调用。确定当前登录用户的 UserIdmarketplace
  • onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse): 在调用 getPurchaseUpdates(boolean reset) 后调用。检索购买历史记录。亚马逊建议您保留返回的 PurchaseUpdatesResponse 数据并仅向系统查询更新。
  • onProductDataResponse(ProductDataResponse productDataResponse):​ 在调用 getProductDataRequest(java.util.Set skus) 后调用。检索有关要从您的应用销售的 SKU 的信息。在 onPurchaseResponse() 中使用有效的 SKU。
  • onPurchaseResponse(PurchaseResponse purchaseResponse):​ 在调用 purchase(String sku) 后调用。用于确定购买的状态。

ResponseObjects

您通过 PurchasingService 发出的每个调用都会使 PurchasingListener 收到相应响应。其中每个响应都使用响应对象:

  • UserDataResponse 提供当前登录用户的特定于应用的 UserIdmarketplace
  • PurchaseUpdatesResponse:​ 提供收据的分页列表。收据是无序的。
  • ProductDataResponse:​ 提供以 SKU 为键的项目数据。getUnavailableSkus() 方法列出不可用的任何 SKU。
  • PurchaseResponse:​ 提供在您的应用内发起的购买的状态。请注意,PurchaseResponse.RequestStatus 结果为 FAILED 可能只表示用户在完成之前取消了购买。

将 IAP API 与应用集成

现在您已更为了解将用于实施 IAP 的类,您可以开始编写代码以在应用中实施 IAP 了。

此过程中的代码段都可以在软件开发工具包附带的消费品 IAP 示例应用中找到。

  1. 组织您的代码。使用占位符或无存根代码段,将您的应用设置为在以下位置调用以下方法:
    • 在您的 onCreate() 方法中调用 registerListener()。
    • 在您的 onResume() 方法中调用 getUserData()
    • 在您的 onResume() 方法中调用 getPurchaseUpdates()
    • 在您的 onResume() 方法中调用 getProductData()

      这四个调用属于 PurchasingService 类的一部分,用于为执行应用内购买提供基础。后面的步骤将更详细地介绍如何实施这些调用并提供可用于为您的代码建模的示例代码段。

  2. 在您的代码中,实施并注册 PurchasingListener,以便您的应用可以侦听并处理由 ResponseReceiver 触发的回调。以下是您在上一步中设置的调用:

    private SampleIapManager sampleIapManager;
    protected void onCreate(final Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setupApplicationSpecificOnCreate();
      setupIAPOnCreate();
    }
    
    private void setupIAPOnCreate() {
      sampleIapManager = new SampleIapManager(this);
    
      final SamplePurchasingListener purchasingListener = new SamplePurchasingListener(sampleIapManager);
      Log.d(TAG, "onCreate: registering PurchasingListener");
    
      PurchasingService.registerListener(this.getApplicationContext(), purchasingListener);
    
      Log.d(TAG, "IS_SANDBOX_MODE:" + PurchasingService.IS_SANDBOX_MODE);
    }
    

    除了注册 PurchasingListener 外,此示例代码还执行以下两个可选任务:

    • 创建新的 SampleIapManager 实例来存储与购买收据相关的数据。这是一个可选步骤;但是,您的应用应将购买收据数据存储在您可以在其中访问这些数据的某个位置。是选择使用数据库还是将这些数据存储在内存中由您决定。
    • 通过检查 PurchasingService.IS_SANDBOX_MODE 来检查应用是否在沙盒模式下运行。当应用正在开发中时,此标志很有用,并且您将使用 App Tester 在本地测试您的应用。
  3. 通过在 onResume() 中实施 getUserData() 来确定当前用户是谁(用户 ID 和市场):

    
    // ...
    
    private String currentUserId =  null ;
    private String currentMarketplace =  null ;
    
    // ...
    
    public void onUserDataResponse( final UserDataResponse response) {
    
     final UserDataResponse.RequestStatus status = response.getRequestStatus();
    
     switch (status) {
       case SUCCESSFUL:
    	 currentUserId = response.getUserData().getUserId();
    	 currentMarketplace = response.getUserData().getMarketplace();
    	 break ;
    
       case FAILED:
       case NOT_SUPPORTED:
    	 // Fail gracefully.
    	 break ;
     }
    }
    

    请注意,此示例还将用户 ID 和市场保存到内存中以供应用可能以后使用。

  4. onResume() 调用中,实施 getPurchaseUpdates() 调用,您可以调用它来检索自上次调用此方法之后用户进行的所有购买交易。
    • reset 设置为 false 可返回自上次调用 getPurchaseUpdates() 之后的购买历史记录的分页响应。此组合检索用户的待定消费品、权利和订阅购买的收据。(亚马逊建议在几乎所有情况下使用此方法。)
    • 仅当您希望检索用户的全部购买历史记录并将其存储在某个位置(如服务器端数据缓存中)或将一切都保存在内存中时,才将 reset 设置为 true

      无论 reset 标志的值是什么,请注意 getPurchaseUpdates() 的以下行为:

      • getPurchaseUpdates() 调用始终返回所有未履行的消费品购买。
      • getPurchaseUpdates() 调用仅在极少情况下返回已履行的购买。例如,如果应用在履行之后但在通知亚马逊之前崩溃,或在履行之后亚马逊端出现问题,则 getPurchaseUpdates() 会返回已履行的消费品购买。在这些情况下,您需要删除重复的收据以免过度履行项目。在您交付项目时,记录您已交付的地方,不要再次交付,即使您收到第二个收据也是如此。

        getPurchaseUpdates() 返回的响应将触发 PurchasingListener.onPurchaseUpdatesResponse() 回调。

        @Override
        protected void onResume() {
          super.onResume();
        
        //...
        
          PurchasingService.getUserData();
        
        //...
        
          PurchasingService.getPurchaseUpdates(false);
        }
        

        现在,处理响应:

        1. 当触发 PurchasingListener.onPurchaseUpdatesResponse() 回调时,检查 PurchaseUpdatesResponse.getPurchaseUpdatesRequestStatus() 返回的请求状态。如果 requestStatus 为 SUCCESSFUL,则处理每个收据
        2. 要处理分页,请获取 PurchaseUpdatesResponse.hasMore() 的值。

          如果 PurchaseUpdatesResponse.hasMore() 返回 true,则对 getPurchaseUpdates() 进行递归调用,如以下示例代码所示:

          public class MyPurchasingListener  implements PurchasingListener {
             boolean reset =  false ;
             //...
          
             public void onPurchaseUpdatesResponse( final PurchaseUpdatesResponse response) {
               //...
          
               // Process receipts
               switch (response.getPurchaseUpdatesRequestStatus()) {
                 case SUCCESSFUL:
                   for ( final Receipt receipt : response.getReceipts()) {
                     // Process receipts
                   }
          
                   if (response.hasMore()) {
                     PurchasingService.getPurchaseUpdates(reset);
                   }
                   break ;
                 case FAILED:
                   break ;
               }
             }
          
             //...
          }
          
  5. 还在 onResume() 方法中,调用 getProductData() 来验证您的 SKU,以便用户的购买不会因 SKU 无效而意外失败。

    以下示例代码向 亚马逊验证应用的消费品、权利和订阅项目的 SKU:

    protected void onResume() {
       super.onResume();
    
       // ...
    
       final Set <string>productSkus =  new HashSet<string>();
       productSkus.add( "com.amazon.example.iap.consumable" );
       productSkus.add( "com.amazon.example.iap.entitlement" );
       productSkus.add( "com.amazon.example.iap.subscription" );
       PurchasingService.getProductData(productSkus);
    
       Log.v(TAG,  "Validating SKUs with Amazon" );
       }
    

    调用 PurchasingService.getProductData() 方法会触发 PurchasingListener.onProductDataResponse() 回调。检查 ProductDataResponse.getRequestStatus() 中返回的请求状态,并且仅销售经过此调用验证的项目或 SKU。

    • 如果 requestStatusSUCCESSFUL,则检索以应用中显示的 SKU 为键的产品数据图。产品数据图包含以下值:
      • 产品类型
      • 图标 URL
      • 本地化价格
      • 名称
      • 描述
      • SKU

        请注意,如果您希望在应用中显示 IAP 图标,则需要编辑 AndroidManifest.xml 文件以包括 android.permission.INTERNET 权限。

        此外,如果 requestStatusSUCCESSFUL,但具有不可用的 SKU,则调用 PurchaseUpdatesResponse.getUnavailableSkus() 来检索无效 SKU 的产品数据并阻止应用的用户购买这些产品。

    • 如果 requestStatusFAILED,则禁用应用中的 IAP 功能,如以下示例代码所示:

      public class MyPurchasingListener  implements PurchasingListener {
         //  ...
      
         public void onProductDataResponse( final ProductDataResponse response) {
           switch (response.getRequestStatus()) {
             case SUCCESSFUL:
               for ( final String s : response.getUnavailableSkus()) {
                 Log.v(TAG,  "Unavailable SKU:" + s);
               }
      
               final Map <string,>products = response.getProductData();
               for ( final String key : products.keySet()) {
                 Product product = products.get(key);
                 Log.v(TAG, String.format( "Product: %s\n Type: %s\n SKU: %s\n Price: %s\n Description: %s\n" , product.getTitle(), product.getProductType(), product.getSku(), product.getPrice(), product.getDescription()));
               }
               break ;
      
             case FAILED:
               Log.v(TAG,  "ProductDataRequestStatus: FAILED" );
               break ;
           }
         }
      
         //  ...
      }
      
  6. 编写代码来执行购买。虽然此特定示例执行消费品的购买,您应该还能够使用类似代码购买订阅和权利。

    来自 MainActivity 的以下代码段调用 PurchasingService.purchase() 来初始化购买。在消费品示例应用中,在应用用户点击 Buy Orange 按钮时执行此方法:

      public void onBuyOrangeClick(final View view) {
         final RequestId requestId = PurchasingService.purchase(MySku.ORANGE.getSku());
         Log.d(TAG, "onBuyOrangeClick: requestId (" + requestId + ")");
      }
    

    接下来,实施 SamplePurchasingListener.onPurchaseResponse() 回调。在此代码段中,SampleIapManager.handleReceipt() 处理实际购买:

    public void onPurchaseResponse(final PurchaseResponse response) {
    	switch (status) {
    		// ...
    		case SUCCESSFUL:
    			final Receipt receipt = response.getReceipt();
    			iapManager.setAmazonUserId(response.getUserData().getUserId(), response.getUserData().getMarketplace());
    			Log.d(TAG, "onPurchaseResponse: receipt json:" + receipt.toJSON());
    			iapManager.handleReceipt(receipt, response.getUserData());
    			iapManager.refreshOranges();
    			break;
    	}
    }
    
  7. 履行购买并处理购买收据。在设计您自己的应用时,请记住,您很可能将实施某种履行引擎来在一个地方处理所有这些步骤。
    • 如果可购买项目是订阅,请牢记以下有关 receiptId 的值的指南。

      • 如果订阅是连续的且从未在任何时候取消,则应用将仅收到该订阅/客户的一个收据。

      • 如果订阅不是连续的,例如客户未自动续订,让订阅终止,然后一个月后再次订阅,则应用将收到多个收据。

    • 在履行项目之前,让您的后端服务器使用亚马逊的 Receipt Verification Service (RVS) 验证 receiptId 来验证购买收据。亚马逊提供了 RVS 沙盒环境和 RVS 生产环境。请参阅 Receipt Verification Service (RVS) 文档来了解如何设置 RVS 沙盒和您的服务器以使用 RVS。
      • 在开发过程中,使用 RVS 沙盒环境来验证 App Tester 生成的收据。
      • 在生产中,使用 RVS 生产终端节点。
    • 在此示例中,在 SampleIapManager 中,handleConsumablePurchase() 方法检查是否已取消收据。

      • 如果已取消收据,并且以前已履行项目,则调用 revokeConsumablePurchase() 方法来撤销购买。
      • 如果未取消收据,则使用 RVS 从您的服务器验证收据,然后调用 grantConsumablePurchase() 来履行购买。

        public void handleConsumablePurchase(final Receipt receipt, final UserData userData) {
            try {
                if (receipt.isCanceled()) {
                    revokeConsumablePurchase(receipt, userData);
                } else {
                    // We strongly recommend that you verify the receipt server-side
                    if (!verifyReceiptFromYourService(receipt.getReceiptId(), userData)) {
                        // if the purchase cannot be verified,
                        // show relevant error message to the customer.
                        mainActivity.showMessage("Purchase cannot be verified, please retry later.");
                        return;
                    }
                    if (receiptAlreadyFulfilled(receipt.getReceiptId(), userData)) {
                        // if the receipt was fulfilled before, just notify Amazon
                        // Appstore it's Fulfilled again.
                        PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
                        return;
                    }
        
                    grantConsumablePurchase(receipt, userData);
                }
                return;
            } catch (final Throwable e) {
                mainActivity.showMessage("Purchase cannot be completed, please retry");
            }
        }
        
  8. 通过执行以下任务将项目授予用户:

    1. 为购买创建购买记录并将该记录存储在某个位置。
    2. 验证 SKU:

      • 如果 SKU 可用,则履行项目并调用具有状态 FULFILLED 的 notifyFulfillment。完成此步骤后,Amazon Appstore 将不再尝试向应用发送购买收据。
      • 如果 SKU 不可用,则调用具有状态 UNAVAILABLE 的 notifyFulfillment

        private void grantConsumablePurchase(final Receipt receipt, final UserData userData) {
            try {
                // following sample code is a simple implementation, please
                // implement your own granting logic thread-safe, transactional and
                // robust
        
                // create the purchase information in your app/your server,
                // And grant the purchase to customer - give one orange to customer
                // in this case
                createPurchase(receipt.getReceiptId(), userData.getUserId());
                final MySku mySku = MySku.fromSku(receipt.getSku(), userIapData.getAmazonMarketplace());
                // Verify that the SKU is still applicable.
                if (mySku == null) {
                    Log.w(TAG, "The SKU [" + receipt.getSku() + "] in the receipt is not valid anymore ");
                    // if the sku is not applicable anymore, call
                    // PurchasingService.notifyFulfillment with status "UNAVAILABLE"
                    updatePurchaseStatus(receipt.getReceiptId(), null, PurchaseStatus.UNAVAILABLE);
                    PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.UNAVAILABLE);
                    return;
                }
        
                if (updatePurchaseStatus(receipt.getReceiptId(), PurchaseStatus.PAID, PurchaseStatus.FULFILLED)) {
                    // Update purchase status in SQLite database success
                    userIapData.setRemainingOranges(userIapData.getRemainingOranges() + 1);
                    saveUserIapData();
                    Log.i(TAG, "Successfuly update purchase from PAID->FULFILLED for receipt id " + receipt.getReceiptId());
                    // update the status to Amazon Appstore.Once receive Fulfilled
                    // status for the purchase, Amazon will not try to send the
                    // purchase receipt to application any more
                    PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
                } else {
                    // Update purchase status in SQLite database failed - Status
                    // already changed.
                    // Usually means the same receipt was updated by another
                    // onPurchaseResponse or onPurchaseUpdatesResponse callback.
                    // simply swallow the error and log it in this sample code
                    Log.w(TAG, "Failed to update purchase from PAID->FULFILLED for receipt id " + receipt.getReceiptId()
                               + ", Status already changed.");
                }
        
            } catch (final Throwable e) {
                // If for any reason the app is not able to fulfill the purchase,
                // add your own error handling code here.
                // Amazon will try to send the consumable purchase receipt again
                // next time you call PurchasingService.getPurchaseUpdates api
                Log.e(TAG, "Failed to grant consumable purchase, with error " + e.getMessage());
            }
        }
        
  9. 处理收据。请注意,此步骤是 getPurchaseUpdates() 的结果,不是调用 purchase(SKU) 的结果。调用 SampleIapManager.handleReceipt 方法来处理作为 PurchaseUpdatesResponse 的一部分返回的所有收据。

    public void onPurchaseUpdatesResponse(final PurchaseUpdatesResponse response) {
    // ....
    switch (status) {
    case SUCCESSFUL:
    	iapManager.setAmazonUserId(response.getUserData().getUserId(), response.getUserData().getMarketplace());
    	for (final Receipt receipt : response.getReceipts()) {
    		iapManager.handleReceipt(receipt, response.getUserData());
    	}
    	if (response.hasMore()) {
    		PurchasingService.getPurchaseUpdates(false);
    	}
    	iapManager.refreshOranges();
    	break;
    // ...