Electron 应用内购买接入指南:使用 inAppPurchase 为 Mac App Store(MAS)应用实现 IAP
Electron 应用内购买接入指南使用 inAppPurchase 为 Mac App StoreMAS应用实现 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 StoreMAS发布场景系统讲解从 App Store Connect 后台配置、CFBundleIdentifier 调试改造到主进程中使用inAppPurchase模块完成商品查询、下单购买、交易状态处理与收据校验的完整链路。读完本文你将掌握在 Electron 桌面应用中接入苹果应用内购买的全部可落地步骤并通过仓库源码理解其底层 StoreKit 桥接原理避免事务漏处理、监听时机错误等典型坑点。一、In-App Purchases 在 Electron 中的定位与适用范围在 Electron 生态中inAppPurchase是一个专门服务于macOS 上 Mac App StoreMAS应用的主进程模块其职责是把 Electron 应用与苹果 StoreKit 框架连接起来让开发者能够在桌面应用中实现诸如数字商品、订阅、解锁高级功能等应用内购买能力。从仓库文档定位看完整的端到端说明记录在 docs/api/in-app-purchase.md而本文主体所对应的实战教程则是 docs/tutorial/in-app-purchases.md它与 docs/tutorial/mac-app-store-submission-guide.mdMAS 提审指南共同构成 Electron 上架 Mac App Store 的完整知识闭环。用户需求中指定关联文档即为前者文章严格以其骨架展开。几点前提需要明确进程限制inAppPurchase只能在主进程中访问docs/api/in-app-purchase.md 明确标注 Process: Main。平台限制模块仅在 macOSdarwin上具备真实能力。从 lib/browser/api/in-app-purchase.ts 可以看出非 macOS 平台上它会被替换为一个空壳EventEmitter其中purchaseProduct()直接抛出The inAppPurchase module can only be used on macOScanMakePayments()恒返回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 identifierkeyCFBundleIdentifier/key stringcom.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)其中transactions为Transaction对象数组创建模块实例即 require 该模块时原生侧即开始观察交易队列InAppPurchase::Create中调用StartObserving(...)并在回调OnTransactionsUpdated中执行Emit(transactions-updated, transactions)见 shell/browser/api/electron_api_in_app_purchase.cc。3.2 方法Methods方法返回值作用purchaseProduct(productID[, opts])Promiseboolean将购买加入支付队列商品有效并成功入队返回true。opts可为整数指定数量也可传对象{ quantity, username }getProducts(productIDs)PromiseProduct[]根据产品标识符数组拉取商品描述信息canMakePayments()boolean查询当前用户是否被允许发起购买如家长控制/受限账户返回falserestoreCompletedTransactions()无恢复历史已完成交易换机重装、多设备同步场景getReceiptURL()string返回本应用收据文件的绝对路径finishAllTransactions()无完成终结所有待处理交易finishTransactionByDate(date)无按 ISO 格式的日期完成对应待处理交易其中purchaseProduct在 TS 封装层lib/browser/api/in-app-purchase.ts统一了“整数 or 对象”两种调用形态当opts为对象时取opts.quantity与opts.username否则直接把opts视为quantity。username会透传到原生applicationUsername用于把某笔交易与你的服务端账号体系关联对应 StoreKit 的applicationUsername由 PR 35902 引入。3.3 相关结构体速览交易与商品对象的具体字段分别定义在 docs/api/structures/transaction.md 与 docs/api/structures/product.mdTransaction交易transactionIdentifier/originalTransactionIdentifier本次交易 / 被恢复交易的原始标识transactionDate交易加入 App Store 支付队列的时间ISO 格式字符串transactionStatepurchasing|purchased|failed|restored|deferrederrorCode/errorMessage交易处理出错时的错误码与信息payment支付对象含productIdentifier、quantity、applicationUsername以及可选的paymentDiscount。Product商品productIdentifier、localizedTitle、localizedDescription标识与本地化文案price/formattedPrice/currencyCode价格数值、本地化格式化价格串、ISO 4217 三位货币码introductoryPrice/discounts优惠定价与折扣列表subscriptionGroupIdentifier/subscriptionPeriod订阅组标识与订阅周期订阅类商品isDownloadable/downloadContentVersion/downloadContentLengthsApple 托管内容的下载信息。这些字段在原生→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分支中官方建议的闭环是调用inAppPurchase.getReceiptURL()拿到本机收据文件路径将收据文件内容发送到你的服务器服务器端调用 Apple 提供的App Store 收据验证接口远程验证/verifyReceipt确认收据真实有效校验通过后再向用户发放对应payment.productIdentifier的商品权益最后调用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)接收产品标识符数组返回PromiseProduct[]。拿到Product[]后localizedTitle与formattedPrice已按用户当前系统区域做本地化处理可以直接用于界面展示示例里用console.log输出“The price of X is $xx.xx”。注意容错返回空数组或非数组时应提示“Unable to retrieve the product information.”4.6 发起购买 purchaseProductinAppPurchase.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返回Promiseboolean仅表示该商品有效并已成功加入支付队列不等于支付成功真正的成败由后续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其中transactionState为restored并携带原始交易副本originalTransactionIdentifier。finishTransactionByDate(date)—— 接收ISO 格式化的交易日期字符串终结与该日期匹配的待处理交易。源码层面shell/browser/mac/in_app_purchase.mm的实现逻辑是遍历SKPaymentQueue.defaultQueue.transactions用yyyy-MM-ddTHH:mm:ssZZZZZ格式逐一比对transaction.transactionDate命中后调用finishTransaction:。因此传入的必须是示例中transaction.transactionDate那样的 ISO 字符串。finishAllTransactions()—— 一次终结所有待处理交易。适合某些业务场景如直接认可全部历史交易有效日常流程不推荐替代精准的finishTransactionByDate。六、从 TS 封装到原生 StoreKit调用链源码走读为了让你对这套桥接有确定性认知以下是实际调用链均有仓库源码可查JS/TS 入口主进程require(electron)拿到inAppPurchase。非 macOS 平台使用桩实现lib/browser/api/in-app-purchase.tsmacOS 平台则通过process._linkedBinding(electron_browser_in_app_purchase)取得原生模块并在其上再包一层用来兼容“整数/对象”两种opts调用形态。原生绑定层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 监听器。方法映射purchaseProduct、getProducts、canMakePayments、restoreCompletedTransactions、getReceiptURL、finishAllTransactions、finishTransactionByDate均通过gin_helper::EventEmitterMixin的SetMethod绑定到 JS 对象electron_api_in_app_purchase.cc。StoreKit 原生实现真正与苹果StoreKit交互的代码在 shell/browser/mac/in_app_purchase.mm 与 shell/browser/mac/in_app_purchase_observer.h例如GetReceiptURL()直接调用NSBundle.mainBundle.appStoreReceiptURLPurchaseProduct()创建一个携带quantity、username的购买请求对象后加入支付队列交易观察者实现SKPaymentTransactionObserver协议并转发更新回调。测试佐证仓库在 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分支的示例注释中给出了两种校验思路服务端远程验证把 receipt 文件内容 POST 给自家服务器由服务器向 Apple 收据验证服务发起校验返回状态码0表示有效。这是官方推荐的安全做法——客户端拿到任何校验结果都不应被信任真正的验证必须在服务端完成。本地/离线校验在应用内使用系统库解析收据并校验签名。这里要特别提示一个与 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),仅供参考

相关新闻

最新新闻

日新闻

周新闻

月新闻