在React Native和Node.js中验证iOS订阅收据

洪胜涝
2023-12-01

交易收据:订阅管理的关键要素 (Transaction receipts: the key element of subscription management)

Transaction receipts are a key element in managing subscriptions through in-app purchases, as well as for automatically renewing and cancelling subscriptions based on the receipt status and validity. They should be handled with care at the initial purchase stage, ensuring that they are persisted in your backend database and stored in a secure manner.

交易收据是通过应用内购买管理订阅以及根据收据状态和有效性自动续订和取消订阅的关键要素。 在初始购买阶段应谨慎处理它们,以确保将它们持久保存在您的后端数据库中并以安全的方式存储。

This article walks through the process of generating a transaction receipt in React Native, before sending it to your server with user identifiers in order to persist a transaction to a user account. Note that even if the reader is using native iOS APIs to manage in-app purchases within their app, that receipt can still be sent to the JavaScript side via an Express route and persisted in their backend database.

本文介绍了在React Native中生成事务收据的过程,然后将其与用户标识符一起发送到服务器,以便将事务持久化到用户帐户。 请注意,即使阅读器使用本机iOS API在其应用内管理应用内购买,该收据仍可以通过Express路由发送到JavaScript端,并保存在其后端数据库中。

If the reader has not yet set up in-app purchases within their apps and would like to do so before following this piece, check out my article on setting up iOS subscriptions in React Native: React Native: Subscriptions with In-App Purchases. This piece will act as a natural follow-on to the setup discussed.

如果读者尚未在其应用程序内设置应用程序内购买,并且想要在进行此操作之前进行设置,请查看我有关在React Native中设置iOS订阅的文章: React Native:带有应用程序内购买的订阅 。 这一部分将作为所讨论设置的自然后续。

本文的结构 (How this article is structured)

There are three key pieces to validating transaction receipts and persisting them with user identifiable metadata:

验证交易收据并将其与用户可识别的元数据持久化有三个关键部分:

  • Sending transaction receipts to your server upon the initial in-app purchase of a subscription. Importantly, this initial purchase is also the only opportunity to associate a receipt to a user ID or unique account identifier: Apple do not allow ad-hoc data that could be tied into a user’s identity, and is therefore not supplied in their webhook service. Instead, we must associate this info when that initial receipt is generated. Receipts are also used later in the subscription lifecycle when it comes to renewing or cancelling, and should not be discarded under any circumstances.

    最初在应用内购买订阅后,将交易收据发送到您的服务器 。 重要的是,这次首次购买也是将收据与用户ID或唯一帐户标识符相关联的唯一机会:Apple不允许将可能与用户身份绑定的临时数据,因此不会在其Webhook服务中提供。 相反,我们必须在生成初始收据时关联此信息。 在续订或取消订阅时,收据也将在订阅生命周期的后期使用,在任何情况下都不应丢弃。

  • Validating the receipt server-side. There is a security risk inherent with web services that could entail users spoofing transaction receipts in an attempt to obtain a free subscription to your app. This can be prevented by validating the receipt server-side before persisting it in a backend database. We will be using a package called node-apple-receipt-verify to carry out the validation, that provides a simple API for contacting Apple servers for the validation.

    验证收据服务器端 。 网络服务存在固有的安全风险,可能会导致用户欺骗交易收据,以试图免费获得您的应用程序订阅。 可以通过在将收据服务器端保留在后端数据库中之前对其进行验证来防止这种情况。 我们将使用一个名为node-apple-receipt-verify的程序包进行验证,该程序包提供了一个简单的API,用于联系Apple服务器进行验证。

  • Persisting the receipt with account identifiers. A receipt should be linked to some user or account so your infrastructure knows which user or account initiated a transaction. This becomes very important when automatically renewing or cancelling a subscription with your own NodeJS runtime (that will be the subject of another article). This section will therefore document how to persist receipt and UIDs in a MongoDB collection on an Express server.

    保留带有帐户标识符的收据。 收据应链接到某些用户或帐户,以便您的基础架构知道哪个用户或帐户发起了交易。 当使用自己的NodeJS运行时自动续订或取消订阅时(这将是另一篇文章的主题),这变得非常重要。 因此,本部分将说明如何在Express服务器上的MongoDB集合中持久保存收据和UID。

Let’s start with the central piece of data of an in-app purchase — the transaction receipt. Let’s first discuss how packages like react-native-iap generate the receipt upon an in-app purchase, and how to send a transaction receipt to your backend database.

让我们从应用内购买的中心数据开始-交易收据。 让我们首先讨论如何在应用程序内购买时像react-native-iap这样的软件包生成收据,以及如何将交易收据发送到后端数据库。

在应用内购买时验证收据 (Validating Receipts upon In-App Purchase)

A transaction receipt is generated upon an in-app purchase (IAP) being made, and therefore are generated and sent directly to the device that initiated the IAP. These receipts are very important for multiple reasons:

在进行应用内购买(IAP)时会生成交易收据,因此会生成交易收据并将其直接发送到发起IAP的设备。 这些收据非常重要,原因如下:

  • The initial transaction receipt (the original receipt generated when purchasing a subscription) can be used for validation at any stage of the subscription cycle. Validating the initial receipt with Apple’s servers will return the current subscription status even if further payments have been made for the subscription in question.

    初始交易收据(购买订阅时生成的原始收据)可以在订阅周期的任何阶段用于验证。 使用Apple的服务器验证初始收据将返回当前的订阅状态,即使已为该订阅进行了进一步付款。

The same can be said with receipts for payments further down the subscription lifecycle, whereby any receipt of a successful transaction can fetch the entire purchase history of that subscription.

可以在订购生命周期的下一个阶段将付款收据说成是相同的,因此,任何成功交易的收据都可以获取该订阅的整个购买历史。

  • This initial receipt is the only opportunity a developer has to link an unique account identifier to the receipt itself. Receipts are somewhat anonymous; they do not contain an Apple ID or anything to link the transaction to a particular user of your app — this is undoubtedly for privacy reasons.

    此初始收据是开发人员必须将唯一帐户标识符链接到收据本身的唯一机会。 收据有点匿名; 它们不包含Apple ID或用于将交易链接到您应用的特定用户的任何内容-无疑出于隐私原因。

These digital receipts really do live up to the receipt definition, in that they are proof of a particular purchase that can be used at any time to validate that purchase. Unlike a grocery receipt that we tend to immediately discard, developers are going to want to securely persist these digital receipts in their backend database.

这些数字收据确实符合收据定义因为它们是特定购买的证明,可随时用于验证该购买。 与我们倾向于立即丢弃的杂货收据不同,开发人员将希望将这些数字收据安全地保存在其后端数据库中。

The issue with point 2 lies with the fact that you must know which user account to update in the event a particular subscription is cancelled or updated to another tier. Subscription webhook data does include a receipt (the entire response body is documented here), but with no means to link that receipt to an account or user.

第2点的问题在于,在取消特定订阅或将其更新到另一层的情况下,您必须知道要更新哪个用户帐户。 订阅Webhook数据确实包含收据(完整的响应正文在此处记录 ),但是无法将该收据链接到帐户或用户。

将用户ID链接到初始收据 (Linking a user ID to the initial receipt)

To resolve this issue, a simple solution is to send any unique identifiers (an user email address, unique object ID, etc) you require to get your subscription service working alongside the generated transaction receipt when the initial purchase is being made.

要解决此问题,一种简单的解决方案是发送所有必需的唯一标识符(用户电子邮件地址,唯一对象ID等),以便在首次购买时使订阅服务与生成的交易收据一起工作。

The initial purchase of a subscription acts as the only opportunity to link a receipt to a user. Intuitively, a user has to be signed in to their account, or authenticated in some way, in order to make an in-app purchase. Unique identifiers or authentication tokens will therefore be available within the app at the time of purchase.

最初购买订阅是将收据链接到用户的唯一机会。 直观上,用户必须登录其帐户或以某种方式进行身份验证才能进行应用内购买。 因此,购买时在应用程序中将可以使用唯一标识符或身份验证令牌。

For demonstration purposes, let’s turn our attention to when a transaction is generated with react-native-iap.

出于演示目的,让我们将注意力转移到使用react-native-iap生成事务时。

The APIs of react-native-iap have been visited in detail in the article mentioned above: React Native: Subscriptions with In-App Purchases.

在上面提到的文章中已详细访问 react-native-iap 的API React Native:带有应用内购买的订阅

The package conveniently supplies an event listener named purchaseUpdatedListener that is executed whenever a new in-app purchase is initiated (a subscription or one-time purchase). We typically wrap this event listener within useEffect, as to remove the listener when the containing component unmounts.

该软件包方便地提供了一个名为purchaseUpdatedListener的事件侦听器,该事件侦听器在每次发起新的应用内购买(订阅或一次性购买)时都会执行。 我们通常将此事件侦听器包装在useEffect ,以在包含组件卸载时删除侦听器。

It is within the purchaseUpdatedListener that custom functions can be embedded for handling ad-hoc tasks for managing a new purchase — such as sending vital transaction info to your backend server.

purchaseUpdatedListener ,可以嵌入自定义功能,以处理用于管理新购买的临时任务,例如将重要的交易信息发送到后端服务器。

Without delving into the react-native-iap workflow in detail (I have done so in my In-App Purchase article), note that the receipt of the in-app purchase is automatically generated and is supplied by the event listener. We are then free to handle the receipt as we please in the space labelled with Handle in-app purchase in the following example:

在没有详细研究react-native-iap工作流程的情况下(我在“应用内购买”文章中做了此操作),请注意,应用内购买的收据是自动生成的,并由事件监听器提供。 然后,在以下示例中,我们可以在标有“ Handle in-app purchase的空间中随意处理收据:

// responding to in-app purchases with `react-native-iap`useEffect(() => {
purchaseUpdateSubscription = purchaseUpdatedListener( async (purchase) => { const receipt = purchase.transactionReceipt; if (receipt) {
try {
if (Platform.OS === 'ios') {
finishTransactionIOS(purchase.transactionId);
}
await finishTransaction(purchase); /* Handle in-app purchase */
await processNewSubscription(purchase);
/*************************************/
} catch (ackErr) {
// process error
}
}
},
);
... return (() => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
purchaseUpdateSubscription = null;
}
...
})
}, []);
const processNewSubscription = async (purchase) => {
...
}

It is in this labelled space that we can either embed more logic, or call other component functions (or even imported functions) to execute as an in-app purchase is being made. The above example calls the processNewSubscription method in this space, a separate component function defined outside of the event listener itself.

在这个标记空间中,我们可以嵌入更多逻辑,也可以调用其他组件函数(甚至是导入的函数)以在进行应用内购买时执行。 上面的示例在此空间中调用processNewSubscription方法,这是在事件侦听器本身外部定义的单独的组件函数。

This space also gives you the opportunity to persist subscription data into your Redux store (such as the subscribed product), that will then trigger a re-render of the components that depend on that state. This should automatically update your UI to a “subscribed” state.

该空间还使您有机会将订阅数据持久保存到Redux存储(例如,订阅的产品)中,然后将触发依赖该状态的组件的重新呈现。 这将自动将您的用户界面更新为“已订阅”状态。

Because these listeners are wrapped in a containing component (that is subject to props and state) you are free to pass any data identifying the user directly into the component’s contained functions, and then send that data off to your backend server along with the transaction receipt for further processing. My preferred method of linking user identifiers in this way is to store account data in a Redux store, where any component can then fetch those identifiers with Redux’s useSelector hook.

由于这些侦听器被包装在一个包含组件(取决于道具和状态)中,因此您可以自由地将标识用户的任何数据直接传递到该组件的包含的函数中,然后将该数据与交易收据一起发送到后端服务器进行进一步处理。 我以这种方式链接用户标识符的首选方法是将帐户数据存储在Redux存储中,然后任何组件都可以使用Redux的useSelector挂钩获取这些标识符。

To read up how to link a Redux store to your React components, check out my article dedicated to that subject: Redux Hooks in React: An Introduction.

要阅读如何将Redux存储链接到您的React组件,请查看我关于该主题的文章: React中的Redux Hooks:简介

Sending the receipt along with user identifies, such as an authentication token, to one of your endpoints can be done with a simple fetch request. I have further abstracted the fetch function in the following example as to minimise the boilerplate, but the general idea should be clearly conveyed:

可以通过简单的fetch请求将收据和用户标识(例如身份验证令牌)一起发送到您的一个端点。 为了进一步简化样例,我在下面的示例中进一步提取了fetch函数,但是应该清楚地传达总体思想:

// sending a transaction receipt and identifiers to your serverconst processNewSubscription = async (purchase) => {  const { productId, transactionReceipt } = purchase;
const { authToken } = props; const { ack, response } = await authFetch({
url: 'iap/receipt-validate',
token: authToken,
body: {
receipt: transactionReceipt,
productId: productId,
}

});
}

authFetch is an imported utility function I have defined to abstract fetch requests, which simply pertains to a fetch request being wrapped in an asynchronous function. This results in a simplified boilerplate to make a simple fetch request. ack is the status code returned from the server (success, failure) whereas response is the JSON response object.

authFetch 是我定义的导入实用程序函数,用于抽象获取请求,该请求仅与包装在异步函数中的获取请求有关。 这样就简化了样板,可以进行简单的提取请求。 ack 是从服务器返回的状态代码( success failure ),而 response 是JSON响应对象。

The main takeaway here is that authToken is being sent along with transactionReceipt and productId to the backend server (an Express endpoint in this example) Where this data can be persisted.

这里的主要收获是authTokentransactionReceiptproductId一起发送到后端服务器(在此示例中为Express终结点),可以在其中保留这些数据。

重复交易尝试 (Repeated transaction attempts)

It's worth noting that react-native-iap adopts a queue system whereby transactions are lined up for processing and persist in the queue until they are successfully processed. Continued attempts will be made to successfully complete a transaction if the device experiences connectivity problems, that are usually retried when the app is re-opened. For this reason, do not request that a user make another purchase if there is no connectivity.

值得注意的是, react-native-iap采用了一个队列系统,该系统将事务排成一行以进行处理,并保留在队列中,直到成功处理为止。 如果设备遇到连接问题(通常在重新打开应用程序时会重试),将继续尝试成功完成交易。 因此,如果没有连接,请勿请求用户再次购买。

With a transaction receipt obtained with the vital metadata needed to manage a transaction, lets now turn our attention to the server-side for receipt validation and persistence.

通过获得具有管理交易所需的重要元数据的交易收据,现在让我们将注意力转移到服务器端以进行收据验证和持久化。

服务器端处理交易收据 (Handling Transaction Receipts Server-Side)

This section will explore receipt validation and persistence on an Express server that saves the data of interest to a MongoDB collection. Here are some useful dependencies this section has adopted to get this validation and persistence working:

本节将探讨Express服务器上的收据验证和持久性,该服务器将感兴趣的数据保存到MongoDB集合中。 以下是本节为使此验证和持久性工作而采用的一些有用的依赖项:

  • moment: Timestamps are easily generated with moment simply by calling moment().unix(). This requires a lot less boilerplate than achieving the same result with vanilla JavaScript

    moment :只需调用moment().unix()即可很容易地通过timestamp生成时间戳。 与使用原始JavaScript达到相同的结果相比,这需要更少的样板

  • mongodb: The official NodeJS Mongo drivers, that provide a streamlined asynchronous set of APIs to interact with MongoDB instances, either locally or remotely. The document based collections are a well suited match for storing the JSON-based objects of transaction data, and API / webhook data in general.

    mongodb :官方的NodeJS Mongo驱动程序,提供了简化的异步API集,可与本地或远程MongoDB实例进行交互。 基于文档的集合非常适合用于存储基于JSON的事务数据对象和一般的API / webhook数据。

  • node-apple-receipt-verify: This package provides a simple API for contacting Apple servers for receipt validation.

    node-apple-receipt-verify :此软件包提供了一个简单的API,用于联系Apple服务器以进行收据验证。

Here is a handy command for installing all these dependencies:

这是安装所有这些依赖项的便捷命令:

yarn add moment mongdb node-apple-receipt-verify

Once installed, initialise the Node Apple Receipt Verify service like so:

安装完成后,如下初始化Node Apple Receipt Verification服务:

// initialise node-apple-receipt-verifyvar appleReceiptVerify = require('node-apple-receipt-verify')appleReceiptVerify.config({
secret: process.env.APPLE_SHARED_SECRET,
environment: [process.env.APPLE_APP_STORE_ENV],
excludeOldTransactions: true,
});

Initialise this object outside of your endpoints — we do not want a build up of initialisations on every request to an endpoint. Also, it is good practice to use environment variables where-ever possible for sensitive data such as secret keys.

在您的端点之外初始化此对象-我们不希望在对端点的每个请求上都进行初始化。 同样,优良作法是在可能的情况下对敏感数据(例如密钥)使用环境变量。

Now, let’s firstly assume that we’ve delivered the receipt and user info to our Express route, ready for validation:

现在,让我们首先假设我们已将收据和用户信息传递到我们的Express路线,准备进行验证:

// receipt validation endpointrouter.post('/receipt-validation', async function (req, res, next) {const { body } = req;
const { authToken, receipt } = body; ...
}

Validating a receipt returns a list of purchased products throughout the lifetime of the subscription. The list of products will be empty if none have been purchased. Furthermore, an exception will be raised if the receipt is invalid:

验证收据会返回整个订阅期内的购买产品清单。 如果尚未购买,产品列表将为空。 此外,如果收据无效,将引发异常:

// receipt validation workflowtry {  // attempt to verify receipt
const products = await appleReceiptVerify.validate({

receipt: receipt
}); // check if products exist
if (Array.isArray(products)) {

// get the latest purchased product (subscription tier)
let { expirationDate } = products[0]; // convert ms to secs
let expirationUnix = Math.round(expirationDate / 1000); // persist in database
/* coming up next */
}} catch(e) {
// transaction receipt is invalid
}

The excludeOldTransactions field of validate() can be set to true to ignore the entire purchase history and only fetch the latest purchase made. The full extent of the returned product object is documented here on NPMJS.

可以将 validate() excludeOldTransactions 字段 设置为 true 以忽略整个购买历史记录,而仅获取最新购买的商品。 返回的 product 对象 的完整范围在 此处记录 在NPMJS上。

The above snippet firstly attempts to verify the receipt, and handles an exception in the event the receipt cannot be verified. If this is the case, return a message to your app stating there was an error. A returned failed status could be used to make further state updates such as rolling back the post-subscription based updates mentioned earlier.

以上代码段首先尝试验证收据,并在无法验证收据的情况下处理异常。 如果是这种情况,请向您的应用返回一条消息,指出存在错误。 返回的failed状态可用于进行进一步的状态更新,例如回滚前面提到的基于订阅后的更新。

Upon a successful validation, we firstly check whether purchased products exist, before referencing the latest purchase and corresponding expiry timestamp. This expiry timestamp represents the time of next renewal that is dependant on the subscription period, whether it be weekly, monthly or a larger renewal period.

验证成功后,我们会先检查购买的产品是否存在,然后再参考最新的购买日期和相应的到期时间戳记。 该到期时间戳代表下一次续订的时间,该时间取决于订阅期,无论是每周,每月还是更长的续订期。

将数据持久保存到数据库 (Persisting data to the database)

Since we are working with collection based documents, you may wish to extract more fields from the receipt validation stage and update other collections with the current state of the subscription:

由于我们正在使用基于集合的文档,因此您可能希望从收据验证阶段提取更多字段,并使用订阅的当前状态更新其他集合

// aggregating transaction data throughout collectionsusers:
plan:
autoRenew
nextRenewal
planIdiap-receipts:
latest_receipt
latest_receipt_info
verified
timestamp
productsiap-logs
receipt_id
times_verified...

This sorting of data in a range of collections is a common practice for optimisation purposes. For example, you may have metadata existing in an accounts collection with the current state of a user’s subscription:

出于最佳目的,在一系列集合中对数据进行排序是一种常见做法。 例如,您的帐户集合中可能存在具有用户订阅当前状态的元数据:

// example `user` record with subscription metadata{
_id: ObjectId(...),
email: ross@jkrbinvestments.com,
name: 'Ross',
plan: {
freeTrialEligible': false,
planId': 1,
autoRenew': true,
nextRenewal': 1594014143,
}

};

I tend to use the plan term more-so than subscription for data persistence purposes. The definitions of both are the same in relation to in-app purchases.

对于数据持久性目的, 我倾向于更多地使用 plan 术语,而不是使用 subscription 对于应用内购买,两者的定义相同。

Storing duplicated data is purely for convenience and optimisation purposes that will make a difference at scale — with the user’s plan data present in a users collection, there is no need to make a second query to an iap-receipts collection to find that same state. This is a common practice for document-based collections, and larger apps will have a number of services for the sole purpose of data aggregation (not to be confused with MongoDBs built-in aggregation feature).

存储重复的数据纯粹是出于方便和优化的目的,这将在规模上有所不同- users集合中存在用户的计划数据,因此无需再次查询iap-receipts集合即可找到相同的状态。 这是基于文档的集合的常见做法,大型应用程序将具有大量服务,这些服务仅出于数据聚合的目的(不要与MongoDB的内置聚合功能相混淆)。

If the reader is interested in data aggregation pipelines, I have posted an article showcasing a use case of MongoDBs Aggregation feature and how it can be integrated within React Native: React Native: MongoDB Aggregation with Stack Navigation.

如果读者对数据聚合管道感兴趣,那么我会发表一篇文章,展示MongoDB聚合功能的用例以及如何将其集成到React Native中: React Native:使用Stack Navigation的MongoDB Aggregation

The following is an example query of inserting a transaction receipt in a standalone iap-receipts MongoDB collection, along with vital metadata pertaining to the user and time the transaction was inserted, as well as whether the receipt was successfully verified:

以下是查询查询示例的示例:在独立的iap-receipts MongoDB集合中插入交易收据,以及与用户和插入交易的时间有关的重要元数据,以及收据是否已成功验证:

// insert receipt and products recordawait db
.collection('iap-receipts')
.insertOne({
user_id: user._id,
receipt: receipt,
verified: true,
products: products,
timestamp: moment().unix(),
environment: process.env.APPLE_APP_STORE_ENVIRONMENT,
});

Storing the environment (sandbox or production) may also be useful. Subscription webhooks do the same thing to differentiate what notifications pertain to what environment.

存储环境( sandbox production 环境 )也可能有用。 订阅Webhook会做同样的事情,以区分哪些通知与什么环境有关。

And with this, we have successfully persisted a transaction receipt with a particular app user. This record will be critical in your subscription management going forward, such as updating a user account if the user in question updates or cancels their subscription.

至此,我们已经成功地与特定的应用程序用户保持了交易收据。 此记录对于以后的订阅管理至关重要,例如,如果有问题的用户更新或取消其订阅,则更新用户帐户。

Here are the important points of this setup to keep in mind:

以下是此设置要记住的重要点:

  • Apple will require a transaction receipt of a subscription to identify the in-app purchase and its corresponding subscription history, and will never present user identifiable data.

    苹果将​​要求订阅的交易收据来识别应用内购买及其相应的订阅历史,并且永远不会提供用户可识别的数据。
  • Your app requires a user identifier linked to a transaction receipt in order to update the user’s privileges based on the state of the subscription. It is the developer’s responsibility to ensure this is setup with the initial transaction receipt.

    您的应用需要链接到交易收据的用户标识符,以便根据订阅状态更新用户的特权。 开发者有责任确保使用初始交易收据进行设置。

综上所述 (In Summary)

This article paves the necessary steps developers should take when storing an in-app purchase transaction receipt. These practices will make automatic renewal or cancellation possible, as well as more advanced Subscription webhook integration.

本文为开发人员存储应用内购买交易收据时应采取的必要步骤铺平了道路。 这些做法将使自动续订或取消以及更高级的Subscription Webhook集成成为可能。

I have also published a solution to automatically sync subscription renewals and cancellations to your app database using a NodeJS based runtime to do so — this article provides a continuation of what has been discussed here, as a means to leverage how receipts have been persisted with a particular user. Read the full article here:

我还发布了一种解决方案,该解决方案使用基于NodeJS的运行时自动将订阅续订和取消同步到您的应用数据库,这样做是本文的续篇,是一种利用收据保持收入的方式。特定用户。 在此处阅读全文:

Here are some other materials that may be of interest that relate to in-app purchase management:

以下是与应用内购买管理相关的其他一些有趣的材料:

  • Apple’s receipt verification documentation can be found here.

    可在此处找到Apple的收据验证文档。

  • The full response body of a receipt can be found here, documenting all the metadata associated with a transaction receipt.

    收据的完整响应正文可在此处找到,记录与交易收据关联的所有元数据。

  • Apple’s “What’s new with in-app purchases” presentation from WWDC 2020 can be watched here, where they present the most recent updates to in-app purchases and their surrounding APIs and webhook support.

    可以在此处观看WWDC 2020上Apple的“应用内购买有什么新功能”演示,其中介绍了应用内购买及其周围API和Webhook支持的最新更新。

翻译自: https://medium.com/@rossbulat/validating-ios-subscription-receipts-in-react-native-node-js-70775d0fb3a

 类似资料: