Folo 移动端 v0.5.7 发版解析:订阅计费链路、错误体验与渲染样式修复
2026/9/9 14:10:50 网站建设 项目流程

Folo 移动端 v0.5.7 发版解析:订阅计费链路、错误体验与渲染样式修复

【免费下载链接】follow🧡 Folo is the AI RSS Reader项目地址: https://gitcode.com/GitHub_Trending/fol/follow

本文以 Folo 移动端版本记录 apps/mobile/changelog/0.5.7.md 为线索,逐条拆解 v0.5.7 的改进与修复项:RSSHub 订阅上限的本地化升级引导、苹果内购(IAP)产品与交易标识混淆导致的购买/恢复故障、Stripe 活跃与逾期订阅者的升级流程、深色模式文本可读性,以及 Web 渲染内容因共享 CSS token 冲突产生的边框样式问题。文章结合apps/mobilelocales下的源码与测试用例,帮助读者理解每项变更背后的错误处理模型、购买校验链路与样式作用域,掌握这类发版级问题定位与验证的通用方法。

一、版本概览与变更主线

v0.5.7 是一次典型的「体验修复型」版本,变更集中在移动端两条核心链路上:

  1. 付费与订阅:RSSHub 订阅数量超限的错误提示、App Store 内购购买/恢复、Stripe 订阅者升级共 3 项;其中内购修复直接关系到交易能否落账,属于计费正确性问题;
  2. 渲染与视觉:深色模式文本可读性与 Web 渲染内容边框样式共 2 项,属于界面一致性问题。

从 apps/mobile/changelog/0.5.7.md 的目录结构看,该仓库的移动端版本记录采用固定骨架(Improvements/No longer broken/Thanks),与 apps/mobile/changelog/next.md 模板保持一致。下文按版本记录原文顺序展开源码级解读。

二、Improvements:RSSHub 订阅上限错误的本地化升级引导

Improved RSSHub subscription-limit errors with localized upgrade guidance and without internal request details

这一项同时解决了三个问题:错误信息是否本地化、是否给出升级引导、是否会泄露内部请求细节

2.1 错误码 2012 与多语言翻译

Folo 将服务端错误码映射到 i18n keyerrors:{code}。RSSHub 订阅超限对应错误码2012,在 locales/errors/en.json 及各语言文件中均存在对应文案,例如zh-CN为「RSSHub 订阅源数量限制已超出」。这一点在 apps/mobile/src/lib/error-parser.test.ts 的 mock 中也有体现:测试用t("errors:2012")返回"RSSHub feed subscription limit exceeded",验证的正是本地化取词路径。

2.2 从错误对象到升级弹窗的解析链路

错误解析入口位于 apps/mobile/src/lib/error-parser.ts 的getFetchErrorInfo:它对ofetchFetchError与自定义的FollowAPIError分别处理,先按code尝试命中errors:{code}翻译,若翻译命中则优先展示翻译文案。

真正决定「展示升级引导还是仅 Toast 提示」的是 toastFetchError,关键分支逻辑如下:

const isPaymentFeatureEnabled = getIsPaymentEnabled() // 支付功能关闭时,402 统一落到通用付费提示 if (status === 402 && !isPaymentFeatureEnabled) { return toast.error(t("errors:1004")) } // 支付功能开启时,402 认为是需要升级,弹出升级引导对话框 const needUpgradeError = status === 402 && isPaymentFeatureEnabled if (needUpgradeError) { showUpgradeRequiredDialog({ title: message || t("settings:subscription.actions.upgrade"), message: t("settings:subscription.summary.free_description"), }) return }

因此 RSSHub 订阅超限(HTTP 402 + 错误码 2012)在有付费能力的版本上会弹出UpgradeRequiredDialog,标题使用本地化后的错误文案,正文使用settings:subscription.summary.free_description这类套餐说明文案,引导用户前往订阅页升级。对应测试 apps/mobile/src/lib/error-parser.test.ts 明确断言:此时应调用showUpgradeRequiredDialog不再弹出普通错误 Toast。

值得一提的是MAX_RSSHUB_SUBSCRIPTIONS也出现在订阅页的特性清单 Plan.tsx 中,说明「RSSHub 订阅数量」本身即是一项按套餐区分的限额资源,这解释了为什么超限时最合理的动作是引导升级而非仅提示错误。

2.3 剥离内部请求细节:sanitizeErrorMessage

「不暴露内部请求细节」由独立的 sanitizeErrorMessage 实现:

const FOLLOW_API_REQUEST_CONTEXT_PATTERN = /\r?\nRequest:[\s\S]*$/u export const sanitizeErrorMessage = (message: string) => message.replace(FOLLOW_API_REQUEST_CONTEXT_PATTERN, "").trim()

该函数把从Request: ...起(含可选的\r\n换行、兼容 CRLF)到消息结尾的全部上下文裁掉。服务端返回的错误信息通常形如:

RSSHub feed subscription limit exceeded Request: POST /subscriptions (original: /subscriptions) Args: { "headers": { "cookie": "session=secret" } }

其中Args可能携带 cookie 等敏感内容。经裁剪后仅保留首行可读文案。在 apps/mobile/src/lib/error-parser.ts 与toastFetchErrorfallbackMessage(第 62 行)、最终兜底处(第 117 行)都会调用该函数。配套测试 apps/mobile/src/lib/error-message.test.ts 覆盖了普通消息保留、多行请求上下文剔除、CRLF 兼容三种场景。error-parser.test.ts中则进一步断言:当 API 错误码没有翻译时,仅展示剥离上下文后的首行,避免把Request: POST /subscriptions这类内部细节透传给用户。

三、修复:苹果内购购买与恢复时「产品 ID 与交易 ID 混淆」

Fixed Apple subscription purchases and restores failing when product and transaction identifiers were confused

App Store 内购涉及多套标识符:产品标识(productId)单笔交易标识(transactionId)原始交易标识(originalTransactionIdentifier)、以及带签名信息的signedTransactionInfo。若向服务端校验时把它们张冠李戴,服务端将无法把交易对账到正确的订阅,导致购买或恢复失败。

3.1 标识建模与请求组装

标识关系的清晰化体现在 apple-iap-purchase.ts 的类型定义中:

export type ApplePurchaseIdentity = { id: string originalTransactionIdentifierIOS?: string | null productId: string purchaseToken?: string | null transactionId?: string | null } export type AppleVerificationRequest = { originalTransactionId?: string signedTransactionInfo?: string transactionId?: string }

组装服务端校验请求的 buildAppleVerificationRequest 严格按字段语义取值:originalTransactionIdoriginalTransactionIdentifierIOStransactionIdtransactionId || id,而signedTransactionInfo只接受经isCompactJws校验的 JWS 串(第 17-27 行,标准三段式header.payload.signature)。从源码结构看,v0.5.7 的修复点就在于把此前可能混用的「购买事件里携带的产品标识」与「服务端真正需要的交易/原始交易标识」做了语义分离。

3.2 只有「已知订阅产品」才进入自动核验

iOS 购买完成后会回调currentPurchase。在 AppleIAPProvider.tsx 的处理useEffect中,先通过isKnownAppleSubscriptionPurchase(实现见 apple-iap-purchase.ts)判断该 purchase 的productId是否属于已知订阅套餐:合法 ID 集合由服务端配置PAYMENT_PLAN_LIST中每个套餐的appleProductIdentifier/appleProductIdentifierAnnual汇总而来(AppleIAPProvider.tsx),与订阅页 Plan.tsx 按计费周期取产品 ID 的逻辑一致。只有命中集合的交易才继续自动核验,否则直接跳过,从入口处规避了非订阅事件被误当成订阅处理的问题。

3.3 防重复处理的交易去重

processedTransactionsReftransactionId/originalTransactionIdentifierIOS/id:transactionDate兜底组合作为 key 对交易去重(AppleIAPProvider.tsx):同一笔交易在核验成功前若再次回调会被忽略,核验失败时则从集合中删除以便重试(第 196 行)。这套去重逻辑配合「先verifyPurchase成功,再finishTransaction收尾、最后refreshBillingState刷新用户与账单状态」的顺序(第 190-203 行),确保购买与恢复都不会因重复提交产生重复核验或丢失交易。

3.4 恢复购买按到期时间排序逐一核验

restoreSubscriptionPurchases 首先调用restorePurchases()拉取 Apple 侧的历史购买,随后把「非已知订阅产品」过滤掉;剩余候选项按expirationDateIOS ?? transactionDate倒序(到期更晚的排前面)依次尝试verifyPurchase,第一个核验成功的即作为恢复结果并刷新账单状态。按到期时间排序可优先命中当前仍有效的订阅,避免用已过期订阅覆盖活跃状态。

四、修复:Stripe 活跃或逾期订阅者升级时打开账单管理

Fixed upgrades for active or past-due Stripe subscribers by opening billing management

当用户已有 Stripe 订阅(处于 active 或 past-due 状态)却再次发起升级时,合理的做法不是再走一遍 Stripe Checkout 新建订阅,而是引导用户进入账单管理门户修改既有订阅。对应错误码常量定义在 Plan.tsx:

const ACTIVE_STRIPE_SUBSCRIPTION_EXISTS_ERROR_CODE = "ACTIVE_STRIPE_SUBSCRIPTION_EXISTS"

升级逻辑 upgradeMutation 中,非 iOS(或 iOS 但当前订阅源为 Stripe)时走authClient.subscription.upgrade发起 Checkout,并在服务端返回ACTIVE_STRIPE_SUBSCRIPTION_EXISTS错误后调用 openStripeBillingPortal:该函数请求POST /billing/portal换取url,再通过openURL打开 Stripe 托管门户。这样一来:

  • Stripe 渠道用户:不会因「已有订阅却再次下单」而重复扣费或冲突,而是进入门户自行调整套餐/付款方式;
  • iOS + Apple 渠道用户:仍走 App Store 内购(requestSubscriptionPurchase),见 Plan.tsx 的渠道分流。

订阅卡片上的管理入口(onManageSubscription/canManageSubscription判断、取消/试用/续费状态的到期日期展示)位于 PlanAction,配合billingSubscription查询(第 370-379 行)返回的source/status/productId/canManage字段,保证「当前订阅是 Apple 时提示去系统设置管理、是 Stripe 时提供门户按钮」的体验分叉正确。

五、修复:深色模式文本颜色可读性

Restored readable text colors in dark mode

深色模式文案发灰、对比度不足是移动端常见回归。从源码布局看,移动端界面大量使用语义化 Tailwind 类(如text-label/text-secondary-label)而非硬编码色值,主题由 theme/colors 相关模块统一提供,渲染入口见 Plan.tsx 的useColor使用方式。可以推断,v0.5.7 对该项的修复方式是恢复语义色 token 在dark变体下的取值,使依赖这些 token 的文本(如设置页的说明文字、订阅页的text-secondary-label摘要)重新满足对比度要求。由于该项属于全局视觉修复,覆盖面广而单个文件改动小,若读者需要自行排查类似问题,建议从「是否硬编码颜色」「是否有dark:前缀缺失」「语义色 token 定义」三个方向入手。

六、修复:Web 渲染内容边框样式受共享 CSS token 冲突影响

Fixed border styling in web-rendered content affected by a shared CSS token collision

Folo 的文章正文等富内容通过独立的 HTML 渲染包渲染,位于 apps/mobile/web-app/html-renderer,其组件同时被多个入口复用(移动端 WebView 与桌面端共享同一套渲染实现)。这类「共享渲染包」容易出现全局 CSS token 被一处样式改动污染、进而影响他处的隐性耦合——v0.5.7 修复的正是边框样式(border)被共享 token 覆盖的问题。从 HTML.tsx 可见正文排版同时依赖dark:prose-invert等 Tailwind 工具类切换明暗主题,而shiki代码高亮组件也维护独立的明暗主题 token(shiki/hooks.ts),可见该渲染包中存在多套明暗样式体系并存。可以推断,修复方案是为边框样式的 token 增加更精确的作用域(或独立命名),避免与其它共享 token 互相覆盖。

此类问题在共享前端包中尤为典型,排查时可以:先定位渲染样式入口(此处为html-renderer),再对比明暗模式下相同元素的 border 样式差异,最后检查是否存在同名 CSS 变量在不同模块中被重复定义。桌面端与移动端共用同一渲染实现的架构,详见仓库中 apps/desktop 与 apps/mobile/web-app 两个应用目录下的实现对照。

七、小结:版本修复背后的工程模式

从 v0.5.7 的五个条目可以提炼出 Folo 移动端在计费与渲染两个领域的工程质量实践:

变更领域关键落点仓库证据
错误信息本地化 + 升级引导按错误码取 i18n 文案、402 分流到升级弹窗error-parser.ts、locales/errors
敏感信息不落地正则裁掉Request:之后的请求上下文error-message.ts 及配套测试
IAP 标识语义分离productId/transactionId/originalTransactionId分字段建模apple-iap-purchase.ts、AppleIAPProvider.tsx
Stripe 升级冲突服务端返回已有订阅错误时转门户管理Plan.tsx
渲染样式隔离明暗 token 与共享边框样式作用域治理web-app/html-renderer

对普通用户而言,升级到 v0.5.7 后即可在订阅超限、内购恢复与深色模式阅读中获得更稳定的体验;对开发者而言,本版本的测试用例(error-parser.test.ts、error-message.test.ts)是理解错误处理约定最直接的入口,值得作为回归验证的基线保留。

【免费下载链接】follow🧡 Folo is the AI RSS Reader项目地址: https://gitcode.com/GitHub_Trending/fol/follow

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

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

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

立即咨询