Electron 应用内购买接入指南:使用 inAppPurchase 为 Mac App Store(MAS)应用实现 IAP
2026/9/7 1:57:31 网站建设 项目流程

Electron 应用内购买接入指南:使用 inAppPurchase 为 Mac App Store(MAS)应用实现 IAP

【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron

本文围绕 Electron 官方教程 In-App Purchases 展开,面向面向 Mac App Store(MAS)发布场景,系统讲解从 App Store Connect 后台配置、CFBundleIdentifier 调试改造,到主进程中使用inAppPurchase模块完成商品查询、下单购买、交易状态处理与收据校验的完整链路。读完本文,你将掌握在 Electron 桌面应用中接入苹果应用内购买的全部可落地步骤,并通过仓库源码理解其底层 StoreKit 桥接原理,避免事务漏处理、监听时机错误等典型坑点。

一、In-App Purchases 在 Electron 中的定位与适用范围

在 Electron 生态中,inAppPurchase是一个专门服务于macOS 上 Mac App Store(MAS)应用的主进程模块,其职责是把 Electron 应用与苹果 StoreKit 框架连接起来,让开发者能够在桌面应用中实现诸如数字商品、订阅、解锁高级功能等应用内购买能力。

从仓库文档定位看,完整的端到端说明记录在 docs/api/in-app-purchase.md,而本文主体所对应的实战教程则是 docs/tutorial/in-app-purchases.md,它与 docs/tutorial/mac-app-store-submission-guide.md(MAS 提审指南)共同构成 Electron 上架 Mac App Store 的完整知识闭环。用户需求中指定关联文档即为前者,文章严格以其骨架展开。

几点前提需要明确:

  • 进程限制inAppPurchase只能在主进程中访问(docs/api/in-app-purchase.md 明确标注 Process: Main)。
  • 平台限制:模块仅在 macOS(darwin)上具备真实能力。从 lib/browser/api/in-app-purchase.ts 可以看出:非 macOS 平台上它会被替换为一个空壳EventEmitter,其中purchaseProduct()直接抛出"The inAppPurchase module can only be used on macOS"canMakePayments()恒返回falsegetReceiptURL()恒返回空字符串。因此业务代码中调用前应做好平台判断。
  • 生态闭环:应用内购买只对通过 Mac App Store 分发的应用有意义;如果是普通 web 下载分发(dmg/zip),应改用 Electron 之外的自有支付方案。这一点也正是该模块未出现在 Linux/Windows 场景的原因。

二、接入前准备(Preparing)

教程将正式写代码之前的准备工作分为三步,任何一步缺失都会导致开发调试或上架审核失败。

2.1 签署付费应用协议(Paid Applications Agreement)

如果你的账号尚未签署过付费协议,需要先完成 Apple Developer 侧的操作:签署Paid Applications Agreement(付费应用协议),并在 iTunes Connect(现称 App Store Connect)中配置银行(banking)与税务(tax)信息。这是 App 能够上架销售并收到分成的前提,也是创建任何内购商品之前的硬性门槛。

从 Electron 角度这一步没有额外代码工作,但它直接决定了后续能否在 App Store Connect 中正常提交内购商品审核。若你尚未上架过 MAS 应用,建议同步阅读 docs/tutorial/mac-app-store-submission-guide.md 了解整体上架流程。

2.2 在 App Store Connect 中创建内购产品(Create Your In-App Purchases)

随后需要在 iTunes Connect / App Store Connect 后台逐个配置你的内购产品(In-App Purchase),配置内容通常包括:

  • 名称(name):面向用户展示的产品名;
  • 定价(pricing):该商品的价格档次;
  • 描述(description):用于突出内购项特性与功能的说明文案;
  • 产品类型:消耗型、非消耗型、自动续期订阅、非续期订阅等(类型直接决定生命周期处理策略,例如消耗型需要每次购买后重新付费,非消耗型可被restoreCompletedTransactions恢复)。

注意每个产品在后台都会被分配一个产品标识符(Product ID)。官方教程特别提醒:com.example.app.product1这类标识符里真正与 StoreKit 交互、并在代码中使用的部分,是最后一段product1。因此你的PRODUCT_IDS数组应填产品标识符的最后一段标识,而不是完整 bundle id 前缀。

2.3 修改 CFBundleIdentifier 以支持开发期测试

教程中一个极易被忽略、却又决定开发期能否打通的关键步骤是修改CFBundleIdentifier

要使用 Electron 在开发阶段(未打包的Electron.app)测试内购,必须修改如下文件中的标识符:

node_modules/electron/dist/Electron.app/Contents/Info.plist

将其中的默认值com.github.electron替换为你在 App Store Connect 创建应用时使用的 bundle identifier

<key>CFBundleIdentifier</key> <string>com.example.app</string>

为什么要改它?从源码可以印证其原理:StoreKit 的收据(receipt)是与应用 Bundle 强绑定的。getReceiptURL()的原生实现(shell/browser/mac/in_app_purchase.mm)调用的是:

NSURL* receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];

[NSBundle mainBundle]读取的正是当前可执行 Bundle 的标识与收据路径。若 Info.plist 里的 bundle id 与应用商店侧的记录不一致,沙盒(Sandbox)环境无法把购买事务归属到你的应用名下,内购流程将无法正常走通。因此,把开发期 Bundle id 与应用商店侧保持一致是开发调通的必要条件。

开发期还需要一个真实的沙盒测试账号(可在 App Store Connect 创建),并在系统设置中登录该账号后,StoreKit 才会以沙盒模式响应购买请求。

三、Electron 侧 inAppPurchase API 全景

在进入完整示例前,先建立对模块 API 的整体认知。以下方法与事件均来自 docs/api/in-app-purchase.md,并可在原生绑定(shell/browser/api/electron_api_in_app_purchase.cc)中找到一一对应的注册代码。

3.1 事件(Events)

事件触发时机回调参数
transactions-updated一个或多个交易状态被更新时触发(event, transactions),其中transactionsTransaction对象数组

创建模块实例(即 require 该模块)时原生侧即开始观察交易队列:InAppPurchase::Create中调用StartObserving(...),并在回调OnTransactionsUpdated中执行Emit("transactions-updated", transactions)(见 shell/browser/api/electron_api_in_app_purchase.cc)。

3.2 方法(Methods)

方法返回值作用
purchaseProduct(productID[, opts])Promise<boolean>将购买加入支付队列;商品有效并成功入队返回trueopts可为整数指定数量,也可传对象:{ quantity, username }
getProducts(productIDs)Promise<Product[]>根据产品标识符数组拉取商品描述信息
canMakePayments()boolean查询当前用户是否被允许发起购买(如家长控制/受限账户返回false
restoreCompletedTransactions()恢复历史已完成交易(换机重装、多设备同步场景)
getReceiptURL()string返回本应用收据文件的绝对路径
finishAllTransactions()完成(终结)所有待处理交易
finishTransactionByDate(date)按 ISO 格式的日期完成对应待处理交易

其中purchaseProduct在 TS 封装层(lib/browser/api/in-app-purchase.ts)统一了“整数 or 对象”两种调用形态:当opts为对象时取opts.quantityopts.username,否则直接把opts视为quantityusername会透传到原生applicationUsername,用于把某笔交易与你的服务端账号体系关联(对应 StoreKit 的applicationUsername,由 PR 35902 引入)。

3.3 相关结构体速览

交易与商品对象的具体字段分别定义在 docs/api/structures/transaction.md 与 docs/api/structures/product.md:

Transaction(交易)

  • transactionIdentifier/originalTransactionIdentifier:本次交易 / 被恢复交易的原始标识;
  • transactionDate:交易加入 App Store 支付队列的时间(ISO 格式字符串);
  • transactionStatepurchasing|purchased|failed|restored|deferred
  • errorCode/errorMessage:交易处理出错时的错误码与信息;
  • payment:支付对象,含productIdentifierquantityapplicationUsername,以及可选的paymentDiscount

Product(商品)

  • productIdentifierlocalizedTitlelocalizedDescription:标识与本地化文案;
  • price/formattedPrice/currencyCode:价格数值、本地化格式化价格串、ISO 4217 三位货币码;
  • introductoryPrice/discounts:优惠定价与折扣列表;
  • subscriptionGroupIdentifier/subscriptionPeriod:订阅组标识与订阅周期(订阅类商品);
  • isDownloadable/downloadContentVersion/downloadContentLengths:Apple 托管内容的下载信息。

这些字段在原生→JS 的序列化器中有完整对应(见 shell/browser/api/electron_api_in_app_purchase.cc)。

四、完整代码示例与逐段解析

教程主体给出了一段可直接运行于主进程的完整示例。下面先完整保留原代码,再逐段给出解析与增强说明。

// Main process const { inAppPurchase } = require('electron') const PRODUCT_IDS = ['id1', 'id2'] // Listen for transactions as soon as possible. inAppPurchase.on('transactions-updated', (event, transactions) => { if (!Array.isArray(transactions)) { return } // Check each transaction. for (const transaction of transactions) { const payment = transaction.payment switch (transaction.transactionState) { case 'purchasing': console.log(`Purchasing ${payment.productIdentifier}...`) break case 'purchased': { console.log(`${payment.productIdentifier} purchased.`) // Get the receipt url. const receiptURL = inAppPurchase.getReceiptURL() console.log(`Receipt URL: ${receiptURL}`) // Submit the receipt file to the server and check if it is valid. // @see https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html // ... // If the receipt is valid, the product is purchased // ... // Finish the transaction. inAppPurchase.finishTransactionByDate(transaction.transactionDate) break } case 'failed': console.log(`Failed to purchase ${payment.productIdentifier}.`) // Finish the transaction. inAppPurchase.finishTransactionByDate(transaction.transactionDate) break case 'restored': console.log(`The purchase of ${payment.productIdentifier} has been restored.`) break case 'deferred': console.log(`The purchase of ${payment.productIdentifier} has been deferred.`) break default: break } } }) // Check if the user is allowed to make in-app purchase. if (!inAppPurchase.canMakePayments()) { console.log('The user is not allowed to make in-app purchase.') } // Retrieve and display the product descriptions. inAppPurchase.getProducts(PRODUCT_IDS).then(products => { // Check the parameters. if (!Array.isArray(products) || products.length <= 0) { console.log('Unable to retrieve the product information.') return } // Display the name and price of each product. for (const product of products) { console.log(`The price of ${product.localizedTitle} is ${product.formattedPrice}.`) } // Ask the user which product they want to purchase. const selectedProduct = products[0] const selectedQuantity = 1 // Purchase the selected product. inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, selectedQuantity).then(isProductValid => { if (!isProductValid) { console.log('The product is not valid.') return } console.log('The payment has been added to the payment queue.') }) })

4.1 尽早注册 transactions-updated 监听

代码第一处强调:“在应用启动后尽可能早地注册transactions-updated监听”。原因在于 StoreKit 的支付队列是持久化的:

  • 用户上一轮购买后,若进程崩溃或网络中断,交易未及时finish,该交易会保留在SKPaymentQueue中,并在应用下次启动后再次通过transactions-updated送达;
  • 如果监听注册太晚,甚至注册前交易已以purchased状态送达并丢失处理窗口,就可能出现“用户已付款但应用未发货”的事故。

因此教程代码把inAppPurchase.on('transactions-updated', ...)放在模块加载后的最前面。API 文档亦明确:"You should listen for thetransactions-updatedevent as soon as possible and certainly before you callpurchaseProduct."

4.2 用状态机正确处理每笔交易

事件回调中拿到的是Transaction[]数组,需遍历并按transaction.transactionState分发。五种状态分别对应:

  • purchasing:购买进行中,仅记录日志,等待后续更新;
  • purchased:购买成功——这是需要完成发货校验的分支:读取收据 → 服务端校验 → 发放权益;
  • failed:购买失败(如用户取消、余额不足、沙盒问题),此时可用transaction.errorCode/transaction.errorMessage定位原因;
  • restored:通过restoreCompletedTransactions()恢复的历史购买;transaction.originalTransactionIdentifier可用于关联原始交易;
  • deferred:交易被延期(常见于家长批准(Ask to Buy)流程),等待后续更新。

4.3 purchased 分支中的收据校验与发货闭环

purchased分支中,官方建议的闭环是:

  1. 调用inAppPurchase.getReceiptURL()拿到本机收据文件路径;
  2. 收据文件内容发送到你的服务器
  3. 服务器端调用 Apple 提供的App Store 收据验证接口(远程验证,/verifyReceipt,确认收据真实有效;
  4. 校验通过后,再向用户发放对应payment.productIdentifier的商品权益;
  5. 最后调用inAppPurchase.finishTransactionByDate(transaction.transactionDate)终结该交易

顺序很重要:finishTransactionByDate只在收到 StoreKit 的purchased更新且完成本地发货/服务端对账后执行。提前终结可能导致无法可靠对账,遗漏则会让交易在下一次启动时重复送达。

purchased分支中同样会终结交易(failed状态也必须 finish,否则该笔失败交易可能阻塞队列)。

4.4 购买前置能力检查 canMakePayments

在发起getProducts/purchaseProduct之前,先调用:

if (!inAppPurchase.canMakePayments()) { console.log('The user is not allowed to make in-app purchase.') }

canMakePayments()返回boolean。它映射到 StoreKit 的SKPaymentQueue.canMakePayments(),用于判断当前系统/账户是否允许购买(例如开启了家长控制、受 Apple ID 限制的设备会返回false)。返回false时不应展示支付入口,而应引导用户调整系统设置。

4.5 拉取商品信息并展示定价

inAppPurchase.getProducts(PRODUCT_IDS).then(products => { ... })

getProducts(productIDs)接收产品标识符数组,返回Promise<Product[]>。拿到Product[]后,localizedTitleformattedPrice已按用户当前系统区域做本地化处理,可以直接用于界面展示(示例里用console.log输出“The price of X is $xx.xx”)。注意容错:返回空数组或非数组时应提示“Unable to retrieve the product information.”

4.6 发起购买 purchaseProduct

inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, selectedQuantity) .then(isProductValid => { if (!isProductValid) { console.log('The product is not valid.') return } console.log('The payment has been added to the payment queue.') })
  • purchaseProduct返回Promise<boolean>:仅表示该商品有效并已成功加入支付队列,不等于支付成功;真正的成败由后续transactions-updated事件中的状态决定。
  • 第二参数selectedQuantity即购买数量,省略时默认1(原生默认值见 electron_api_in_app_purchase.cc)。
  • 更完整的用法是传对象:purchaseProduct('product1', { quantity: 1, username: 'user-uuid' }),其中username用于把交易与你的服务端账号关联,便于后期对账。

五、围绕交易完成的辅助方法:恢复购买与批量终结

除示例展示的方法外,API 还提供三个服务于“交易收尾”的方法:

restoreCompletedTransactions()—— 面向非消耗型商品与订阅。用于以下两类场景(见 docs/api/in-app-purchase.md):

  • 用户在另外一台设备上安装你的应用,需要把已购内容带过去;
  • 用户删除了应用又重装,需要找回此前的购买。

调用后,StoreKit 支付队列会对每一笔可恢复的历史交易投递一条新的transactions-updated,其中transactionStaterestored,并携带原始交易副本(originalTransactionIdentifier)。

finishTransactionByDate(date)—— 接收ISO 格式化的交易日期字符串,终结与该日期匹配的待处理交易。源码层面(shell/browser/mac/in_app_purchase.mm)的实现逻辑是:遍历SKPaymentQueue.defaultQueue.transactions,用yyyy-MM-dd'T'HH:mm:ssZZZZZ格式逐一比对transaction.transactionDate,命中后调用finishTransaction:。因此传入的必须是示例中transaction.transactionDate那样的 ISO 字符串。

finishAllTransactions()—— 一次终结所有待处理交易。适合某些业务场景(如直接认可全部历史交易有效),日常流程不推荐替代精准的finishTransactionByDate

六、从 TS 封装到原生 StoreKit:调用链源码走读

为了让你对这套桥接有确定性认知,以下是实际调用链(均有仓库源码可查):

  1. JS/TS 入口:主进程require('electron')拿到inAppPurchase。非 macOS 平台使用桩实现(lib/browser/api/in-app-purchase.ts);macOS 平台则通过process._linkedBinding('electron_browser_in_app_purchase')取得原生模块,并在其上再包一层,用来兼容“整数/对象”两种opts调用形态。

  2. 原生绑定层NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_in_app_purchase, Initialize)将 C++ 侧的InAppPurchase实例导出(shell/browser/api/electron_api_in_app_purchase.cc)。实例创建时(InAppPurchase::Create)立刻执行StartObserving(...),向 StoreKit 交易队列注册观察者;一旦交易变化,OnTransactionsUpdated即以Emit("transactions-updated", transactions)方式把 C++ 侧序列化好的Transaction数组推给 JS 监听器。

  3. 方法映射purchaseProductgetProductscanMakePaymentsrestoreCompletedTransactionsgetReceiptURLfinishAllTransactionsfinishTransactionByDate均通过gin_helper::EventEmitterMixinSetMethod绑定到 JS 对象(electron_api_in_app_purchase.cc)。

  4. StoreKit 原生实现:真正与苹果StoreKit交互的代码在 shell/browser/mac/in_app_purchase.mm 与 shell/browser/mac/in_app_purchase_observer.h:例如GetReceiptURL()直接调用NSBundle.mainBundle.appStoreReceiptURLPurchaseProduct()创建一个携带quantityusername的购买请求对象后加入支付队列;交易观察者实现SKPaymentTransactionObserver协议并转发更新回调。

  5. 测试佐证:仓库在 spec/api-in-app-purchase-spec.ts 提供了该模块的行为测试,可佐证上文的 API 语义,包括:canMakePayments()返回布尔值;restoreCompletedTransactions()/finishAllTransactions()/finishTransactionByDate(ISO字符串)不会抛异常;getReceiptURL()的返回值匹配_MASReceipt/receipt$(印证收据路径位于应用包Contents/_MASReceipt/下);购买不存在的商品purchaseProduct('non-exist')返回false;且purchaseProduct的第二个参数既支持裸整数也支持{ quantity, username }对象。测试还显式限制仅在darwin平台运行(if (process.platform !== 'darwin') return;),与模块的平台属性一致。

该测试套件有个值得注意的工程细节:没有 App Store 会话时restoreCompletedTransactions()会触发系统级的 Apple ID 登录对话框且无法被代码关闭,因此测试在 before/after 钩子中主动清理被唤起的系统 UI(见 spec/api-in-app-purchase-spec.ts)——这从侧面提醒开发者:涉及恢复购买的调用会拉起系统级界面,要在 UI 层做好预期管理。

七、收据路径与离线校验的现实意义

getReceiptURL()返回的是.../<YourApp>.app/Contents/_MASReceipt/receipt这样一个收据文件路径(macOS 12+ 后该文件也可能位于沙盒容器内,具体以返回值与文件系统实测为准)。官方在purchased分支的示例注释中给出了两种校验思路:

  1. 服务端远程验证:把 receipt 文件内容 POST 给自家服务器,由服务器向 Apple 收据验证服务发起校验,返回状态码0表示有效。这是官方推荐的安全做法——客户端拿到任何校验结果都不应被信任,真正的验证必须在服务端完成。
  2. 本地/离线校验:在应用内使用系统库解析收据并校验签名。

这里要特别提示一个与 Electron 打包相关的关键点:收据文件只有从 MAS 下载(或经沙盒测试流程安装)的应用才会存在。若你直接运行未打包的开发版 Electron、或从 dmg 拷贝运行,getReceiptURL()返回空串属于正常现象。这也是教程要求修改node_modules/electron/dist/Electron.app/Contents/Info.plist中 bundle id、并配合沙盒账号测试的原因——只有让 StoreKit 认为“这就是那款上架应用”,收据与事务才会落位。

八、典型坑点与最佳实践清单

结合教程与源码,将最容易踩的坑与推荐做法整理如下:

阶段坑点正确做法
配置忘记签署付费协议 / 未配银行税务先在 App Store Connect 完成协议签署与税务信息,否则无法创建内购产品
配置商品 ID 填错(填成完整com.example.app.product1代码中只使用product1这样的末段标识,与后台 Product ID 对齐
调试开发期 bundle id 仍为com.github.electron按教程修改 Electron.app 的 Info.plist 并保持与应用商店侧一致
调试未登录沙盒账号使用 App Store Connect 创建的 Sandbox 测试账号在系统设置登录
代码transactions-updated监听注册太晚在启动早期、任何purchaseProduct调用前完成注册
代码purchased后不校验收据直接发货服务端远程校验收据有效后再发放权益
代码忘记 finish 交易purchased(校验成功后)与failed状态都必须调用finishTransactionByDate收尾
代码在非 macOS 平台直接使用模块调用前判平台,或捕获其抛出的 "only be used on macOS" 错误
UI用户被限制购买时仍展示购买入口先调canMakePayments(),为false时隐藏或禁用购买按钮

一处需要与时俱进修正原示例的小提醒:原教程示例中的PRODUCT_IDS = ['id1', 'id2']是占位符,务必替换为你真实创建的产品标识符;同时新版 API 已支持在purchaseProduct中携带username用于把交易绑定到自家账号体系,建议在对账需求存在的场景下显式传入。

结语

Electron 的inAppPurchase模块把苹果 StoreKit 的完整能力以 Promise + 事件的形式收敛进了主进程 API:开发侧只需在 App Store Connect 完成商品配置、保持 Bundle 标识一致,然后遵循“尽早监听事件 → 查询商品 → 入队购买 → 按状态机处理 → 服务端校验收据 → 及时 finish”这条主线,就能在 MAS 分发模式下提供可靠的内购体验。需要进一步深挖的读者,可以直接研读 docs/api/in-app-purchase.md 的完整方法签名、docs/api/structures/transaction.md 与 docs/api/structures/product.md 的字段语义,以及 shell/browser/mac/in_app_purchase.mm 的 StoreKit 桥接实现。

【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询