In-App Purchase 服务端接入实用技术

In-App Purchase 服务端接入实用技术

以下内容主要包含,服务端接入 App Store、Google Play 两个平台的 In-App Purchase 技术与大量数据实例。


1. 什么是 In-App Purchase

In-App Purchase, 简称 IAP,即 App 内购买项目,是平台为 App 内购买虚拟商品或服务提供的一套交易系统。

众所周知在 App Store 或者 Google Play 上架的 App 受平台严格的管理,想要在 App 内提供额外的付费内容,如数字商品、订阅和增值内容等,就必须使用平台的交易系统。不像国内的 Android 市场,可以自由使用微信、支付宝等三方支付服务。虽然本质都是 In-App Purchase,但是 App store 的影响力更大,In-App Purchase 常常会特指 App Store 的 App 内购买项目,Google Play 的 In-App Purchase 更官方一点的叫法是 Google Play 结算系统

2. App Store 的 App 内购买项目

2.1. 项目类型

App Strore 包括 4种 App 内购买项目类型,分别是:消耗型项目、非消耗型项目、自动续期订阅、非续期订阅。

  • 消耗型项目:消耗型的 App 内购买项目在使用之后即失效,并可再次购买。例如游戏中用来推动进程的生命或宝石。
  • 非消耗型项目:这些功能只需购买一次,并且不会过期。例如,照片 App 中的额外滤镜、插图 App 中的额外画笔或游戏中的皮肤。
  • 自动续期订阅:此类订阅会自动续期,除非用户选择取消。常见用例如连续包月会员、月度订阅服务。
  • 非续期订阅:这种类型的订阅不会自动续期,如果想要继续访问,用户需要在订阅结束时购买新的订阅。例如月会员,年会员。

从服务端对接的角度来看,“是否会自动续期”是它们之间最大的区别。不会自动续期的项目,也可以称之为一次性商品,一次性商品是指用户可以通过一次性的非定期付费购买的商品。以此来重新划分 App Store 里的项目:一次性商品(消耗型项目、非消耗型项目、非续期订阅),自动续期商品(自动续期订阅)。

2.2. 交易流程图

一次性商品只包含有用户主动发起的一次购买自动续期商品不仅包含用户主动发起的一次购买,如果用户未解约还会有平台发起的周期性自动续订。主要流程如下:

appstore 一次购买交易流程图

appstore 自动续订交易流程图

2.3 支付凭证 receipt

App Store 的支付凭证包含一个非标准的 base64 encode json 字符串,下面是一个 receipt 的例子:

// receipt example
"ewoJInNpZ25hdHVyZSIgPSAiQTRNVnhaQ20vcjBhakxPL0dPQUZrUy9XRVBkZ3p1cTR4eXRIV0kvSFE4d2R5UEgwb0JVV04rdlhxVlBqT04zd..."


//  base64 decode receipt
{
    "signature" = "A4MVxZCm/r0ajLO/GOAFkS/WEPdgzuq4xytHWI/HQ8wdyPH0oBUWN+vXqVPjON3tpQrFAumwH0F3b3accxHRBh6PvY+...";
    "purchase-info" = "ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDIyLTExLTA2IDA4OjI5OjQzIEFtZXJpY2EvTG9...";
    "pod" = "14";
    "signing-status" = "0";
}


// base64 decode purchase-info
{
    "original-purchase-date-pst" = "2022-11-06 08:29:43 America/Los_Angeles";
    "quantity" = "1";
    "subscription-group-identifier" = "20525862";
    "unique-vendor-identifier" = "27EFBF6A-F8EA-4668-85FF-45DFD805C88C";
    "original-purchase-date-ms" = "1667752183305";
    "expires-date-formatted" = "2022-11-09 16:29:42 Etc/GMT";
    "is-in-intro-offer-period" = "false";
    "purchase-date-ms" = "1667752182000";
    "expires-date-formatted-pst" = "2022-11-09 08:29:42 America/Los_Angeles";
    "is-trial-period" = "true";
    "item-id" = "1641036113";
    "unique-identifier" = "00008030-001A414C3AD0402E";
    "original-transaction-id" = "140001519414850";
    "expires-date" = "1668011382000";
    "app-item-id" = "426340811";
    "transaction-id" = "140001519414850";
    "in-app-ownership-type" = "PURCHASED";
    "bvrs" = "8.1.01.8";
    "web-order-line-item-id" = "140000684287054";
    "version-external-identifier" = "852766435";
    "bid" = "com.myapp.app";
    "product-id" = "com.myapp.app.product.xxxx";
    "purchase-date" = "2022-11-06 16:29:42 Etc/GMT";
    "purchase-date-pst" = "2022-11-06 08:29:42 America/Los_Angeles";
    "original-purchase-date" = "2022-11-06 16:29:43 Etc/GMT";
}

receipt 重要字段说明

  • environment: 沙盒环境会返回 Sandbox,正式环境不返回该字段
  • purchase-info.bid: 包 ID
  • purchase-info.product-id: 商品 ID
  • purchase-info.transaction-id: 苹果交易 ID
  • purchase-info.original-transaction-id: 原始苹果交易 ID,自动续期商品可以用来关联原始订单,初次购买时与 transaction-id 相同,后续无论是解约,升级降级都不会改变。
  • purchase-info.purchase-date-ms: 包 ID
  • purchase-info.bid: 订单支付时间 ms
  • purchase-info.is-trial-period: 自动续期商品是否处于免费试用(促销)
  • purchase-info.is-in-intro-offer-period: 自动续期商品是否属于首次优惠(促销)
  • purchase-info.expires-date: 订阅商品特有的商品失效时间
  • purchase-info.purchase-date-ms: 交易时间

2.4 校验 receipt

// Verify Receipt API

// request
https://buy.itunes.apple.com/verifyReceipt
// sandbox request
https://sandbox.itunes.apple.com/verifyReceipt

// paramter 
receipt-data  // Base64-encoded receipt
password      // 共享密钥

// response example
{
    "receipt": {
        "original_purchase_date_pst": "2022-11-08 04:58:39 America/Los_Angeles",
        "quantity": "1",
        "unique_vendor_identifier": "F398F2D0-006B-410D-A81B-65E8E09327D3",
        "bvrs": "8.1.11.7",
        "expires_date_formatted": "2022-12-11 13:34:30 Etc/GMT",
        "is_in_intro_offer_period": "true",
        "purchase_date_ms": "1668173670000",
        "expires_date_formatted_pst": "2022-12-11 05:34:30 America/Los_Angeles",
        "is_trial_period": "false",
        "item_id": "1465690826",
        "unique_identifier": "00008110-001228411E63801E",
        "original_transaction_id": "300001268957076",
        "subscription_group_identifier": "20525862",
        "app_item_id": "426340811",
        "transaction_id": "300001271600390",
        "in_app_ownership_type": "PURCHASED",
        "web_order_line_item_id": "300000580673445",
        "version_external_identifier": "853269835",
        "purchase_date": "2022-11-11 13:34:30 Etc/GMT",
        "product_id": "com.myapp.app.product.xxxx",
        "expires_date": "1670765670000",
        "original_purchase_date": "2022-11-08 12:58:39 Etc/GMT",
        "purchase_date_pst": "2022-11-11 05:34:30 America/Los_Angeles",
        "bid": "com.myapp.app",
        "original_purchase_date_ms": "1667912319000"
    },
    "auto_renew_product_id": "com.myapp.app.product.xxxx",
    "auto_renew_status": 0,
    "latest_receipt_info": {
        "original_purchase_date_pst": "2022-11-08 04:58:39 America/Los_Angeles",
        "quantity": "1",
        "unique_vendor_identifier": "F398F2D0-006B-410D-A81B-65E8E09327D3",
        "bvrs": "8.1.11.7",
        "expires_date_formatted": "2022-12-11 13:34:30 Etc/GMT",
        "is_in_intro_offer_period": "false",
        "purchase_date_ms": "1668173670000",
        "expires_date_formatted_pst": "2022-12-11 05:34:30 America/Los_Angeles",
        "is_trial_period": "false",
        "item_id": "1465690826",
        "unique_identifier": "00008110-001228411E63801E",
        "original_transaction_id": "300001268957076",
        "subscription_group_identifier": "20525862",
        "app_item_id": "426340811",
        "transaction_id": "300001271600390",
        "in_app_ownership_type": "PURCHASED",
        "web_order_line_item_id": "300000580673445",
        "version_external_identifier": "853269835",
        "purchase_date": "2022-11-11 13:34:30 Etc/GMT",
        "product_id": "com.myapp.app.product.xxxx",
        "expires_date": "1670765670000",
        "original_purchase_date": "2022-11-08 12:58:39 Etc/GMT",
        "purchase_date_pst": "2022-11-11 05:34:30 America/Los_Angeles",
        "bid": "com.myapp.app",
        "original_purchase_date_ms": "1667912319000"
    },
    "latest_receipt": "ewoJInNpZ25hdHVyZSIgPSAiQTJTVVlHMk0reVZSU3VyeE01dDBNbjlUL1JubWFXNCQVlUQWxWVE1JSUJJakFOQ...",
    "status": 0
}

校验返回值重要字段说明

  • status: 校验状态码
  • receipt: 当前交易数据
  • auto_renew_status: 自动续费状态
  • latest_receipt_info: 用户最新的交易数据
  • latest_receipt: 用户最新支付凭证 receipt

校验返回值 status 说明

  • 0: 成功
  • 21000: App Store无法读取你提供的JSON数据
  • 21002: 收据数据不符合格式
  • 21003: 收据无法被验证
  • 21004: 你提供的共享密钥和账户的共享密钥不一致
  • 21005: 收据服务器当前不可用
  • 21006: 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
  • 21007: 收据信息是测试用(sandbox),但却被发送到产品环境中验证
  • 21008: 收据信息是产品环境中使用,但却被发送到测试环境中验证

2.5 服务端通知 v2

App Store 在 WWDC21 推出了很多实用的 API,如 Server Notifications v2、StoreKit2. Server Notifications v2 允许通过 App Store 的服务器通知实时监控应用内购买事件。包括自动续期商品续订、退款、解约等等。 服务端通知处理成功,需要返回 HTTP 200 给 App Store,如果返回 40x 或者 50x,App Store 会发起重试。 由于新的 App Store API 都需要使用 JWT 验证,那么 JWT 又是什么呢。

2.5.1 JWT

JWT 即 JSON Web Token,一种用以产生访问令牌的开源规范。这个规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息。

JWT 并不等于JWS(JSON Web Signature),JWS只是 JWT 的一种实现,JWS 的主要目的是保证了数据在传输过程中不被修改,验证数据的完整性。但由于仅采用Base64对消息内容编码,因此不保证数据的不可泄露性。不适合用于传输敏感数据。标准规定 JWT 由3部分组成 :头部(Header)、载荷(PayLoad)、签名(signature)

/ JWS transaction info 
Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))

// header
{
  "alg": "ES256",
  "typ": "JWT"
}

// payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}
2.5.2 数据合法性验证

App Store 服务端通知,没有提供传统的公钥,用来验证数据,那么应该如何保证数据安全呢?而且 App Store 服务端通知的数据格式,与 JWT 标准格式稍有不同,header 中没有 typ,而是多了一个 x5c 数组. x5c 声明了X.509证书链

证书链,也叫信任链,或称数字证书链是一连串的数字证书,由根证书为起点,透过层层信任,使终端实体证书的持有者可以获得转授的信任,以证明身份。通常根证书是可信任的自签名证书,并且有如下关系:

  • 在证书链上除最后一个证书外,证书颁发者等于其后一个证书的主题。
  • 除了最后一个证书,每个证书都是由其后的一个证书签名的。
  • 最后的证书是信任主题,由于是通过可信过程得到的,你可以信任它。

那么这里“最后的证书”指的是 AppleRootCA_G3.cer,因为 Apple 的根证书是值得信任的,那么数据也就是安全的。

// Server Notifications v2 
{
   // JWT Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))
  "signedPayload": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0J..."
}

//decode header
{
    "alg": "ES256",
    "x5c": [ //x5c 声明中数组中的证书链
          "MIIEMDCCA7agAwIBAgIQaPoPldvpSoEH0lBrjDPv9jAKBggqhkjOPQQDAzB1MUQwQg...",
          "MIIDFjCCApygAwIBAgIUIsGhRwp0c2nvU4YSycafPTjzbNcwCgYIKoZIzj0EAwMwZz...", 
          "MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXA..." 
         ]
}

// decode payload
{
    "notificationType": "DID_RENEW",
    "notificationUUID": "554788eb-54d7-430d-a2a2-31acf2e9d242",
    "data": {
        "appAppleId": 426340811,
        "bundleId": "com.myapp.app",
        "bundleVersion": "8.0.71.7",
        "environment": "Production",
        "transactionInfo": {
            "transactionId": "120001590403154",
            "originalTransactionId": "120001493901705",
            "webOrderLineItemId": "120000703291824",
            "bundleId": "com.myapp.app",
            "productId": "com.myapp.app.product.xxxx",
            "subscriptionGroupIdentifier": "20525862",
            "purchaseDate": 1668880754000,
            "originalPurchaseDate": 1660928357000,
            "expiresDate": 1671472754000,
            "quantity": 1,
            "type": "Auto-Renewable Subscription",
            "inAppOwnershipType": "PURCHASED",
            "signedDate": 1668851979585,
            "environment": "Production"
        },
        "renewalInfo": {
            "originalTransactionId": "120001493901705",
            "autoRenewProductId": "com.myapp.app.product.xxxx",
            "productId": "com.myapp.app.product.xxxx",
            "autoRenewStatus": 1,
            "signedDate": 1668851979472,
            "environment": "Production",
            "recentSubscriptionStartDate": 1660928354000
        }
    },
    "version": "2.0",
    "signedDate": 1668851979573
}

部分 notification_type 返回值说明

  • CANCEL: 表示苹果客户支持取消了该自动续期订阅或者用户升级了他们的自动续期订阅
  • DID_RENEW: 表示客户的订阅已经成功自动续订了一个新的交易周期。
  • REFUND: 表示AppStore成功为一个消耗型或非消耗型IAP退款了一笔交易。cancellation_date_ms包含退款交易的时间戳。original_transaction_id和product_id标识原始交易和产品。cancellation_reason包含该原因。
  • DID_CHANGE_RENEWAL_STATUS: 表示订阅续期状态的一个更改。在JSON响应中,检查auto_renew_status_change_date_ms字段来了解最新状态更新的日期和时间。检查auto_renew_status字段来了解当前续订的状态。

2.6 StoreKit2

2021 年 WWDC,在 iOS 15 系统上推出了一个新的 StoreKit 2 库,该库采用了完全新的 API 来解决应用内购买问题。对于 StoreKit 2,苹果已经废弃了用 receipt 收据验证逻辑,只需要提供交易的 originalTransactionId 即可获取到完整的交易信息。StoreKit2 请求 App Store 服务器时需要用 JWT 生成一个 token.

2.6.1 生成 API 请求的 token
// Generating Tokens for API Requests

// JWT header example
{
    "alg": "ES256",
    "kid": "2X9R4HXF34",
    "typ": "JWT"
}

// JWT payload example
{
  "iss": "57246542-96fe-1a63e053-0824d011072a",
  "iat": 1623085200,
  "exp": 1623086400,
  "aud": "appstoreconnect-v1",
  "bid": "com.myapp.app"
}

// token 
Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))

2.6.2 订单号查询

Invoice Lookup API 用于识别用户的购买项目。当用户扣款了,但是没有收到商品时,用户会过来返回问题并提供了苹果邮箱里的扣款信息截图。那么服务器端可以使用截图里订单号,请求这个接口以获取 transactionId 和 originalTransactionId.

lookup order

// Invoice Lookup API

// request
https://api.storekit.itunes.apple.com/inApps/v1/lookup/{customer_order_id}

// header
 Authorization: Bearer {token}

// paramter


// response example
{
    "status": 0,
    "signedTransactions": [
        "ewogICAgInRyW5z.eyJhbGci0iJIUZI1NiIsIn5cIkpXVC.ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHj...",
        "ewogICAgInRyW5z.eyJhbGci0iJIUZI1NiIsInCI6IkpXJ9.ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHj...",
        ....
    ]
}

// decode signedTransactions
[
    {
        "transactionId": "270001182487559",
        "originalTransactionId": "270001083323576",
        "webOrderLineItemId": "270000490742744",
        "bundleId": "com.myapp.app",
        "productId": "com.myapp.app.product.xxxx",
        "subscriptionGroupIdentifier": "20525862",
        "purchaseDate": 1662218329000,
        "originalPurchaseDate": 1649814541000,
        "expiresDate": 1664810329000,
        "quantity": 1,
        "type": "Auto-Renewable Subscription",
        "inAppOwnershipType": "PURCHASED",
        "signedDate": 1668356110911,
        "environment": "Production"
    },
    ...
]
2.6.3 用户交易历史查询
// Transaction History API

// request
https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}

// header
 Authorization: Bearer {token}

// paramter
sort    // DESCENDING 降序
startDate
endDate


// response example
{
    "revision": "1644496541000_730000816272392",
    "bundleId": "com.myapp.app",
    "appAppleId": 426340811,
    "environment": "Production",
    "hasMore": false,
    "signedTransactions": [
        "ewogICAgInRyW5z.eyJhbGci0iJIUZI1NiIsIn5cIkpXVC.ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHj...",
        "ewogICAgInRyW5z.eyJhbGci0iJIUZI1NiIsInCI6IkpXJ9.ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHj...",
        ....
    ]
}

// decode signedTransactions
[
    {
        "transactionId": "730001009725667",
        "originalTransactionId": "730001009725667",
        "bundleId": "com.myapp.app",
        "productId": "com.myapp.app.product.xxxx",
        "purchaseDate": 1669269872000,
        "originalPurchaseDate": 1669269872000,
        "quantity": 1,
        "type": "Non-Renewing Subscription",
        "inAppOwnershipType": "PURCHASED",
        "signedDate": 1670470329405,
        "environment": "Production"
      },
      {
         "transactionId": "730000940439624",
         "originalTransactionId": "730000816272392",
         "webOrderLineItemId": "730000406672997",
         "bundleId": "com.myapp.app",
         "productId": "com.myapp.app.product.xxxx",
         "subscriptionGroupIdentifier": "20525862",
         "purchaseDate": 1660695094000,
         "originalPurchaseDate": 1644496530000,
         "expiresDate": 1663373494000,
         "quantity": 1,
         "type": "Auto-Renewable Subscription",
         "inAppOwnershipType": "PURCHASED",
         "signedDate": 1670470329405,
         "revocationReason": 1,
         "revocationDate": 1660822723000,
         "isUpgraded": true,
         "environment": "Production"
       },
       ...
]
2.6.4 订阅项目状态查询
// Subscriptions Status API


// request
https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}

// header
 Authorization: Bearer {token}

// paramter


// response example
{
    "environment": "Production",
    "bundleId": "com.myapp.app",
    "appAppleId": 426340811,
    "data": [{
        "subscriptionGroupIdentifier": "20525862",
        "lastTransactions": [{
            "originalTransactionId": "730000816272392",
            "status": 2,
            "signedTransactionInfo": "ewogICAgInRyW5z.eyJhbGci0iJIUZI1NiIsIn5cIkpXVC.ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHj...",
            "signedRenewalInfo": "ewogICAgInRyW5z.eyJhbGci0iJIUZI1NiIsInCI6IkpXJ9.ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHj...",
        }]
    }]
}

// decode signedTransactionInfo
{
    "transactionId": "730000940555795",
    "originalTransactionId": "730000816272392",
    "webOrderLineItemId": "730000419822735",
    "bundleId": "com.myapp.app",
    "productId": "com.myapp.app.product.xxxx",
    "subscriptionGroupIdentifier": "20525862",
    "purchaseDate": 1660710391000,
    "originalPurchaseDate": 1644496530000,
    "expiresDate": 1663388791000,
    "quantity": 1,
    "type": "Auto-Renewable Subscription",
    "inAppOwnershipType": "PURCHASED",
    "signedDate": 1670473977774,
    "environment": "Production"
}


// decode signedRenewalInfo
{
    "expirationIntent": 1,
    "originalTransactionId": "730000816272392",
    "autoRenewProductId": "com.myapp.app.product.xxxx",
    "productId": "com.myapp.app.product.xxxx",
    "autoRenewStatus": 0,
    "isInBillingRetryPeriod": false,
    "signedDate": 1670473977774,
    "environment": "Production",
    "recentSubscriptionStartDate": 1644496530000
}

2.7 测试环境(沙盒环境)

App Store 提供了方便的沙盒测试环境,简单的创建一个沙盒测试员,无需真正的付款,即可完成测试。对于自动续费商品,还可以将订阅项目续期速率调整为 每 3、5、15、30、60分钟按月续期。

sandbox

2.8. 常见问题

2.8.1 通过苹果发票查询 App 订单

用户苹果邮箱里的扣款信息截图里的订单号,既苹果发票号,合法的发票号只有10位,用户有时会提供 12位发票号,如 MQ1NB1NL9Xa0,这种通常是三方支付平台获取的,舍弃掉 a0 即可。 然后利用 StoreKit 2 提供的 Invoice Lookup API ,可以查到 transactionId 和 originalTransactionId,便可以关联到用户。

2.8.2 App Store 什么时间给自动续费用户扣款

根据经验,App Store 会在用户权订阅权益到期前8小时,为用户扣款。

2.8.3 同一笔交易 receipt 是否会唯一

同一笔交易 transactionId 是唯一的,receipt 并不能保证唯一。

3. Google Play 结算系统

3.1. 商品类型

Google Play 包括2种项目类型:分别是一次性商品(消耗型商品、非消耗型商品)、订阅。

  • 一次性商品:一次性商品是指用户可以通过一次性的非定期付费(通过用户的付款方式扣款)购买的商品。一次性商品的示例包括额外的游戏关卡、高级战利品箱和媒体文件等等。一次性商品要么是消耗型商品,要么是非消耗型商品。
  • 订阅:订阅是指用户在声明的时间段内可以享受的一系列权益。您可以在同一个应用中提供多项订阅;这些订阅可以代表完全不同的权益(例如,一款在线播放视频应用可分别提供“新闻”订阅和“体育”订阅),也可以代表一组权益的不同层级(例如,一款云端存储空间应用可分别提供 100 GB、1 TB 和 10 TB 的订阅)。

订阅续订的订单号包含一个额外的整数,它表示具体是第几次续订。例如,初始订阅的订单 ID 可能是 GPA.1234-5678-9012-34567,后续订单 ID 是 GPA.1234-5678-9012-34567..0(第一次续订)、GPA.1234-5678-9012-34567..1(第二次续订),依此类推。

Google Play 允许在下单的时候透传一个开发者载荷(DeveloperPayload),开发者载荷向来被用于各种不同用途,包括防欺诈以及将购买交易归因于正确的用户。在获取订单信息的时候,开发者载荷会原样返回,一般可以传一些订单信息或者用户信息来做订单归因。

与 App Store App 内购买项目类似,Google Play 中的商品也可以从“是否会自动续期”的特征,来重新划分为:一次性商品(一次性商品)、自动续期商品(订阅)。

3.2 交易流程图

一次性商品只包含有用户主动发起的一次购买自动续期商品不仅包含用户主动发起的一次购买,如果用户未解约还会有平台发起的周期性自动续订。主要流程如下:

google play 一次购买交易流程图

google play 自动续订交易流程图

3.3 支付凭证 Purchase

// Android Purchase example
getOriginalJson()      returns eg "{"orderId":"GPA.3349-1083-3058-95892","packageName":"com.myapp.app","productId":"com.myapp.app.product.xxxx","purchaseTime":1574857425054,"purchaseState":0,"purchaseToken":"bonnpepcldjaacncibidihai.AO-J1Oxv4bl6....","acknowledged":true}"
getSignature()         returns eg "lVAZ7IRZcqJ23+b+EQBp4meMF73iN51W07TctVi8MxRtY2WaWlBULWbKkteW3WkIlhJVJuQX4QhHYAWbnIQ7OgRX3kf7XkY54XW..."
getDeveloperPayload()  returns eg "you payload string"


// json decode Purchase
{
    "orderId": "GPA.3349-1083-3058-95892",
    "packageName": "com.myapp.app",
    "productId": "com.myapp.app.product.xxxx",
    "purchaseTime": 1574857425054,
    "purchaseState": 0,
    "purchaseToken": "bonnpepcldjaacncibidihai.AO-J1Oxv4bl6....",
    "acknowledged": true
}

3.4 校验 Purchase

Google play 订单可以使用公钥验证数据安全

// Verify Purchase
openssl_verify({purchaseData}, base64_decode({signature}), {yourGooglePlayPublicKey}, OPENSSL_ALGO_SHA1);

3.5 服务端通知

// 实时开发者通知字段
{
    "version": string,
    "notificationType": int,
    "purchaseToken": string,
    "subscriptionId": string
}

返回重要字段说明

  • version: 此通知的版本。最初,此值为“1.0”。此版本与其他版本字段不同。
  • notificationType: 订阅的 notificationType。
  • purchaseToken: 购买订阅时向用户设备提供的令牌。
  • subscriptionId: 所购买订阅的商品 ID(例如“monthly001”)。

3.6 商品核销

授予权利并确认购买交易的流程取决于购买的是非消耗型商品、消耗型商品,还是订阅。 对于消耗型商品,consumeAsync() 方法满足确认要求,并且表明您的应用已授予用户权利。此外,通过此方法,您的应用可让一次性商品可供再次购买。 如需确认非消耗型商品的购买交易,请使用 Google Play 结算库中的 BillingClient.acknowledgePurchase() 或 Google Play Developer API 中的 Product.Purchases.Acknowledge。在确认购买交易之前,您的应用应使用 Google Play 结算库中的 isAcknowledged() 方法或 Google Play Developer API 中的 acknowledgementState 字段检查该购买交易是否已经过确认。

注意:如果您在三天内未确认购买交易,则用户会自动收到退款,并且 Google Play 会撤消该购买交易。

// acknnowledge API

// request
https://www.googleapis.com/androidpublisher/v3/applications/packageName/purchases/products/{productId}/tokens/{token}:acknowledge

// paramter

// response example
// 从 Google Play Developer API 返回的订阅资源中提供的新 expiryTime 来更新订阅状态
{
  "kind": "androidpublisher#subscriptionPurchaseV2",
  "startTime": "2022-04-22T18:39:58.270Z",
  "regionCode": "US",
  "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
  "latestOrderId": "GPA.3333-4137-0319-36762",
  "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
  "lineItems": [
    {
      "productId": "com.myapp.app.product.xxxx",
      "expiryTime": next_renewal_date,
      "autoRenewingPlan": {
        "autoRenewEnabled": true
      }
    }
  ]
}

3.7 测试环境

Google Play 允许使用应用许可来测试应用内购买结算功能,即在 Play 管理中心内添加测试人员的 Gmail 地址列表,也无需付费即可完成购买。

4. 开发实用工具

  1. 反向代理工具 ngrok https://ngrok.com/

做开发的时候总遇到,需要给三方提供一个 https 回调地址,但是配置 https 很麻烦,以及开发环境外网根本无法访问,就可以用这个工具。无需配置 https,也无需公网 ip,ngrok 可以把公网的请求打到本地,非常方便调试。

5. 参考链接