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 版本为准,整体流程如下:
- 注册监听器。
- 获取当前 Amazon 用户。
- 请求购买更新。
- 从
getReceipts()中恢复有效的ENTITLED商品。 - 根据
getRevokedSkus()撤销已退款或取消的权益。 - 如果
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 或服务端收据验证结果处理退款即可。
备注:内容仅供参考。