PAYATHON 2026

Android Amazon In-App Billing 如何验证永久 Licence

支付老李

结论

支持。一次性永久 licence 在 Amazon In-App Purchasing 中应配置为 ENTITLED 商品。应用启动时,通过 PurchasingService.getPurchaseUpdates(...) 查询当前 Amazon 用户的购买记录。

恢复 licence 时,处理 PurchaseUpdatesResponse.getReceipts();撤销 licence 时,处理 PurchaseUpdatesResponse.getRevokedSkus()。用户无需再次点击购买,也不能依靠“已经购买过此商品”之类的提示恢复权益。

Amazon 所说的“永久”,是指权益绑定到购买时使用的 Amazon 账号,并非绑定到设备或应用安装。重新安装后,用户必须登录原来购买 licence 的 Amazon 账号。

商品类型必须是 ENTITLED

Amazon IAP 通常将商品分为三类:

  • CONSUMABLE:可以重复购买的消耗品。
  • ENTITLED:购买一次后持续拥有的永久权益。
  • SUBSCRIPTION:按周期续费的订阅。

永久禁用广告的 licence 应创建为 ENTITLED。如果误配成 CONSUMABLE,恢复购买时的行为会与预期不同。

应用启动时恢复购买

先注册 PurchasingListener,再请求用户信息和购买更新。监听器必须在请求发出前注册。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        PurchasingService.registerListener(
                getApplicationContext(),
                new AppPurchasingListener(getApplicationContext())
        );

        PurchasingService.getUserData();

        // 初次安装或需要完整重建本地权益时,请求完整购买记录
        PurchasingService.getPurchaseUpdates(true);
    }
}

reset 参数决定从哪里开始读取购买更新:

  • true:重置同步位置,从头获取购买更新。适合重新安装后重建本地 licence。
  • false:从上次同步位置继续获取更新,也用于读取分页结果。

不同版本的 Amazon Appstore SDK 可能采用不同的同步状态保存方式。如果本地没有可信的权益数据、Amazon 用户发生变化,或者应用重新安装后进行首次同步,可以使用 true;后续增量同步则使用 false。

处理有效购买和退款撤销

下面是一个简化的监听器实现:

import android.content.Context;
import android.content.SharedPreferences;

import com.amazon.device.iap.PurchasingListener;
import com.amazon.device.iap.PurchasingService;
import com.amazon.device.iap.model.FulfillmentResult;
import com.amazon.device.iap.model.ProductDataResponse;
import com.amazon.device.iap.model.ProductType;
import com.amazon.device.iap.model.PurchaseResponse;
import com.amazon.device.iap.model.PurchaseUpdatesResponse;
import com.amazon.device.iap.model.Receipt;
import com.amazon.device.iap.model.UserDataResponse;

public final class AppPurchasingListener implements PurchasingListener {

    private static final String LICENCE_SKU = "remove_ads";

    private final SharedPreferences preferences;

    public AppPurchasingListener(Context context) {
        preferences = context.getSharedPreferences(
                "purchase_state",
                Context.MODE_PRIVATE
        );
    }

    @Override
    public void onPurchaseUpdatesResponse(
            PurchaseUpdatesResponse response
    ) {
        switch (response.getRequestStatus()) {
            case SUCCESSFUL:
                for (Receipt receipt : response.getReceipts()) {
                    if (LICENCE_SKU.equals(receipt.getSku())
                            && receipt.getProductType() == ProductType.ENTITLED) {
                        grantLicence();

                        PurchasingService.notifyFulfillment(
                                receipt.getReceiptId(),
                                FulfillmentResult.FULFILLED
                        );
                    }
                }

                for (String revokedSku : response.getRevokedSkus()) {
                    if (LICENCE_SKU.equals(revokedSku)) {
                        revokeLicence();
                    }
                }

                // Amazon 可能分批返回购买记录
                if (response.hasMore()) {
                    PurchasingService.getPurchaseUpdates(false);
                }
                break;

            case FAILED:
            case NOT_SUPPORTED:
                // 保留当前可信状态,并安排稍后重试。
                // 不应因为一次网络失败就直接撤销永久 licence。
                break;
        }
    }

    private void grantLicence() {
        preferences.edit()
                .putBoolean("remove_ads", true)
                .apply();
    }

    private void revokeLicence() {
        preferences.edit()
                .putBoolean("remove_ads", false)
                .apply();
    }

    @Override
    public void onPurchaseResponse(PurchaseResponse response) {
        if (response.getRequestStatus()
                != PurchaseResponse.RequestStatus.SUCCESSFUL) {
            return;
        }

        Receipt receipt = response.getReceipt();

        if (LICENCE_SKU.equals(receipt.getSku())
                && receipt.getProductType() == ProductType.ENTITLED) {
            grantLicence();

            PurchasingService.notifyFulfillment(
                    receipt.getReceiptId(),
                    FulfillmentResult.FULFILLED
            );
        }
    }

    @Override
    public void onUserDataResponse(UserDataResponse response) {
        // 应保存并检查 response.getUserData().getUserId(),
        // 避免把一个 Amazon 用户的 licence 交给另一个用户。
    }

    @Override
    public void onProductDataResponse(ProductDataResponse response) {
        // 本场景中没有必须处理的逻辑。
    }
}

枚举值和方法签名应以项目实际使用的 Amazon Appstore SDK 版本为准,整体流程如下:

  1. 注册监听器。
  2. 获取当前 Amazon 用户。
  3. 请求购买更新。
  4. 从 getReceipts() 中恢复有效的 ENTITLED 商品。
  5. 根据 getRevokedSkus() 撤销已退款或取消的权益。
  6. 如果 hasMore() 为 true,继续读取下一批更新。

不要忽略分页

getPurchaseUpdates() 返回的结果可能不是完整列表。如果只处理第一次回调,较早的购买记录可能会被漏掉。

if (response.hasMore()) {
    PurchasingService.getPurchaseUpdates(false);
}

第一次请求可以传入 true 来重置同步位置。读取后续分页时必须传入 false,否则可能一直从头开始读取。

licence 必须按 Amazon 用户隔离

购买权益属于 Amazon 用户,不属于 Android 设备。应用至少应记录:

Amazon userId -> licence state

如果 onUserDataResponse() 返回的 userId 与本地保存的用户不一致,应用不应继续使用旧用户的 licence。此时需要清除或切换本地权益缓存,并为新用户重新调用:

PurchasingService.getPurchaseUpdates(true);

否则,共享设备或切换 Amazon 账号后,应用可能错误地保留前一个用户的无广告权益。

网络失败时不要立即撤销 licence

FAILED 或 NOT_SUPPORTED 不代表用户没有购买,只能说明本次查询没有得到可信结果。应用启动时可能断网,Amazon 服务也可能暂时不可用,设备环境同样可能出现异常。此时直接关闭 licence 会影响正常用户。

可以采用以下策略:

  • 保留上一次成功验证的结果。
  • 网络恢复后重新同步。
  • 只有收到明确的 revokedSku,或者服务端验证明确显示收据已经取消时,才撤销 licence。
  • 对高价值权益设置合理的重新验证周期,不要让每次离线启动都直接验证失败。

是否需要服务端验证

只在客户端调用 getPurchaseUpdates(),通常就能处理重新安装后的权益恢复和退款撤销。如果 licence 价值较高,或者需要防止本地数据被修改、客户端回调被伪造,还应将 userId 和 receiptId 发送到自己的服务器,再通过 Amazon Receipt Verification Service 验证。

典型流程如下:

应用取得 receipt
        ↓
发送 userId、receiptId、SKU 到业务服务器
        ↓
业务服务器调用 Amazon Receipt Verification Service
        ↓
服务器确认商品、购买状态和取消状态
        ↓
服务器向应用返回最终 licence 状态

Receipt Verification Service 使用的开发者密钥不能放进 APK。APK 可以被反编译,因此密钥必须保存在服务端。

服务端还应检查:

  • 收据中的 SKU 是否为预期商品。
  • 商品类型是否为永久权益。
  • Amazon userId 是否与当前业务用户一致。
  • 收据是否包含取消或退款信息。
  • 同一个 receiptId 是否被异常绑定到多个账户。

具体响应字段可能随 Amazon 接口版本变化,应以项目使用的 SDK 和 Receipt Verification Service 文档为准。

实现时需要注意什么

  • 不要把再次调用 purchase() 当作恢复购买的主要方式。
  • 不要通过购买按钮显示的提示文字判断用户是否拥有商品。
  • 不要只保存一个没有用户归属的全局布尔值。
  • 必须处理 getRevokedSkus()。
  • 必须处理 hasMore() 分页。
  • 不要把一次网络失败当作退款。
  • 成功交付权益后再调用 notifyFulfillment(),并确保重复回调不会产生副作用。
  • licence 的授予和撤销逻辑应具备幂等性。同一收据即使被多次处理,也应得到相同结果。

这个场景不需要用户手动“再购买一次”。将商品配置为 ENTITLED,通过 getPurchaseUpdates() 自动恢复权益,再根据撤销 SKU 或服务端收据验证结果处理退款即可。

备注:内容仅供参考。