
Electron inAppPurchase 中 ProductDiscount 结构体详解字段语义、StoreKit 映射与源码实现【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electronElectron 的ProductDiscount对象是 Mac App Store 应用内购买inAppPurchaseAPI 中的核心数据结构用于描述一个商品的折扣信息包括促销价格、支付方式按量付费、预付、免费试用以及订阅周期。掌握它的每个字段就能正确渲染首周半价之类的促销 UI、计算用户实际付费金额并在inAppPurchase.getProducts()返回结果中定位introductoryPrice与discounts两个折扣入口。本文基于仓库中的 ProductDiscount 结构文档、Product 结构文档、inAppPurchase API 文档 以及 C/Objective-C 源码in_app_purchase_product.h、in_app_purchase_product.mm、electron_api_in_app_purchase.cc逐字段说明该结构的语义、取值来源和边界行为。ProductDiscount 在 API 体系中的位置inAppPurchase是 Electron 提供的主进程模块仅用于 Mac App StoreMAS场景。调用inAppPurchase.getProducts(productIDs)后返回PromiseProduct[]而每个 Product 对象通过两个字段携带ProductDiscountintroductoryPriceProductDiscount可选—— 商品的首次引入价格introductory price即首单优惠价discountsProductDiscount[] —— 该商品所有在售折扣优惠的数组。从源码结构看这一映射关系在 Objective-C 层由 skProductToStruct: 完成当SKProduct.introductoryPrice非 nil 时转换并填入introductoryPrice当product.discounts非 nil 时逐个转换所有SKProductDiscount并 push 进discounts向量。字段逐一说明ProductDiscount共 7 个字段其中 6 个必选、1 个可选字段类型必选说明identifierstring是用于唯一标识一个折扣优惠的字符串typenumber是折扣类型pricenumber是折扣价以本地货币计priceLocalestring是用于格式化折扣价的语言环境paymentModestring是支付模式payAsYouGo/payUpFront/freeTrialnumberOfPeriodsnumber是折扣可用的期数整数subscriptionPeriodProductSubscriptionPeriod否折扣适用的订阅周期对象identifier与type折扣身份标识identifier是 App Store 为该折扣分配的唯一标识符可用于在折扣数组中区分不同优惠例如同一个季度订阅下可能同时挂有2 周试用和首月半价两个折扣。type表示折扣类型。从 skProductDiscountToStruct: 的实现可以看到这两个字段都是对 StoreKitSKProductDiscount对应属性的直接透传identifier通过base::SysNSStringToUTF8转成 UTF-8 字符串type则直接以(int)productDiscount.type强转为整数。也就是说 Electron 层不解析type的枚举含义原始枚举值会原样到达 JS 侧开发者若需区分类型可结合paymentMode一起判断。price与priceLocale价格数值与格式化price是折扣的本地货币数值double。转换源码中通过[productDiscount.price doubleValue]从NSDecimalNumber取出双精度数值因此 JS 侧拿到的是一个可直接参与算术运算的 number。priceLocale字段的实际填充方式值得特别注意在 in_app_purchase_product.mm 中当productDiscount.priceLocale非 nil 时Electron 会调用内部的formatPrice:withLocal:方法基于NSNumberFormatter的CurrencyStyle货币格式化把按该语言环境格式化后的价格字符串写入priceLocale。这与同文件内Product结构formattedPrice字段的处理逻辑一致——文档将其描述为用于格式化价格的语言环境而源码中该位置携带的是格式化结果字符串实际使用时应以其展示用价格文本的角色来对待而不是拿它做数值解析。paymentMode三种支付模式paymentMode是开发者最常使用的字段之一取值只有三种payAsYouGo—— 按量付费按订阅周期持续扣费payUpFront—— 预付一次性支付整个折扣期的费用freeTrial—— 免费试用。这一枚举映射在 skProductDiscountToStruct: 中通过比较SKProductDiscountPaymentModePayAsYouGo、SKProductDiscountPaymentModePayUpFront、SKProductDiscountPaymentModeFreeTrial三个 StoreKit 常量完成。一个典型的产品定价策略组合是订阅商品同时提供一个freeTrial折扣numberOfPeriods为试用次数加一个payAsYouGo折扣试用转正后的按周期扣费价JS 侧通常通过遍历discounts并过滤paymentMode来分别渲染。numberOfPeriods折扣可用期数numberOfPeriods指示该折扣可用多少期例如首 3 个月半价对应numberOfPeriods: 3。源码中以(int)productDiscount.numberOfPeriods转为整型存储。subscriptionPeriod可选的订阅周期subscriptionPeriod是唯一的可选字段类型为 ProductSubscriptionPeriod包含两个子字段numberOfUnitsnumber —— 订阅周期中的单位数量unitstring —— 时间增量单位可取day、week、month、year。从源码看只有当SKProductDiscount.subscriptionPeriod非 nil 时才会填充该字段见 in_app_purchase_product.mm且单位字符串由SKProductPeriodUnitDay/Week/Month/Year四个枚举逐一映射而来。对于非订阅类折扣如一次性购买的商品促销JS 侧应做undefined防御。C 数据结构与 JS 暴露链路ProductDiscount在 Electron 内部经历两层转换理解这条链路有助于排查字段为什么是这样。第一层是 C 结构体定义位于 in_app_purchase_product.hstruct ProductDiscount { std::string identifier; int type; double price 0.0; std::string priceLocale; std::string paymentMode; int numberOfPeriods; std::optionalProductSubscriptionPeriod subscriptionPeriod; };可以推断几个默认行为price默认初始化为0.0subscriptionPeriod使用std::optional表达可选性ProductDiscount的拷贝构造与默认构造均为default便于在std::vectorProductDiscount中按值传递。第二层是 gin 的 V8 转换器位于 electron_api_in_app_purchase.cc。Converterin_app_purchase::ProductDiscount::ToV8将上述 7 个字段逐个dict.Set写入 JS 对象其中subscriptionPeriod仅在has_value()为真时才写入——这保证了可选字段在 JS 侧表现为属性不存在而非null。外层Product的转换器electron_api_in_app_purchase.cc以同样方式处理introductoryPrice可选有值才 Set与discounts数组整体 Set。实战获取并解析商品的折扣信息以下示例整合了 In-App Purchases 教程 的完整工作流并展示了如何解析introductoryPrice与discounts。开发前需注意测试 IAP 必须修改node_modules/electron/dist/Electron.app/Contents/Info.plist中的CFBundleIdentifier替换为你在 iTunes Connect 中创建的 bundle identifier教程原文给出的示例keyCFBundleIdentifier/key stringcom.example.app/string// Main process const { inAppPurchase } require(electron) const PRODUCT_IDS [com.example.app.product1] // 尽早监听交易事件教程强调务必在调用 purchaseProduct 之前 inAppPurchase.on(transactions-updated, (event, transactions) { if (!Array.isArray(transactions)) return for (const transaction of transactions) { const payment transaction.payment switch (transaction.transactionState) { case purchasing: console.log(Purchasing ${payment.productIdentifier}...) break case purchased: { const receiptURL inAppPurchase.getReceiptURL() // 将 receiptURL 提交服务端校验校验通过后交付商品 inAppPurchase.finishTransactionByDate(transaction.transactionDate) break } case failed: console.log(Failed to purchase ${payment.productIdentifier}.) inAppPurchase.finishTransactionByDate(transaction.transactionDate) break // restored / deferred ... } } }) if (!inAppPurchase.canMakePayments()) { console.log(The user is not allowed to make in-app purchase.) } // 获取商品信息并解析 ProductDiscount 结构 inAppPurchase.getProducts(PRODUCT_IDS).then((products) { if (!Array.isArray(products) || products.length 0) { console.log(Unable to retrieve the product information.) return } for (const product of products) { console.log(${product.localizedTitle} 标准价: ${product.formattedPrice}) // 1. 引入价可选字段可能不存在 if (product.introductoryPrice) { const intro product.introductoryPrice console.log( 引入价 ${intro.priceLocale}${intro.paymentMode} 共 ${intro.numberOfPeriods} 期) if (intro.subscriptionPeriod) { console.log( 引入价周期: ${intro.subscriptionPeriod.numberOfUnits} ${intro.subscriptionPeriod.unit}) } } // 2. 全部在售折扣 for (const discount of product.discounts) { console.log( 折扣[${discount.identifier}] ${discount.priceLocale}) // 用 paymentMode 区分展示策略 // freeTrial - 免费试用 N 期 // payAsYouGo - N 期内每期优惠价 // payUpFront - 一次付清 if (discount.paymentMode freeTrial) { console.log( - 免费试用 ${discount.numberOfPeriods} 期) } } } // 购买教程示例用法 const selectedProduct products[0] inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, 1) .then((isProductValid) { if (!isProductValid) { console.log(The product is not valid.) return } console.log(The payment has been added to the payment queue.) }) })关于行为的边界验证仓库测试 api-in-app-purchase-spec.ts 确认了getProducts()对无效商品 id 会解析为空数组——因此解析discounts前应先对products本身做非空判断示例中已包含。适用前提与限制平台限制inAppPurchase模块的创建逻辑被#if BUILDFLAG(IS_MAC)编译保护见 electron_api_in_app_purchase.ccinAppPurchase属性仅在 macOS 构建中注入且该模块面向 Mac App Store 分发签名/公证/沙箱要求见 code-signing 教程。Windows 与 Linux 上require(electron).inAppPurchase为undefined。数据源依赖ProductDiscount的所有字段均来自 App Store Connect 后台配置的 StoreKit 产品本地代码无法伪造或修改折扣字段缺失如subscriptionPeriod不存在是配置决定的正常现象必须做可选性判断。字段语义以当前仓库源码为准本文涉及的映射关系如paymentMode三值、priceLocale的填充方式均基于当前版本的 in_app_purchase_product.mm 实现升级 Electron 大版本后建议重新核对转换器代码。相关文档inAppPurchase API 完整方法列表Product 结构 / ProductSubscriptionPeriod 结构 / ProductDiscount 原始文档In-App Purchases 开发教程源码ProductDiscount 结构体定义 / StoreKit 转换实现 / gin V8 转换器【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考