Commit Graph
1381 Commits
Author SHA1 Message Date
DIYgodandClaude Opus 5.5 a993acb754 fix(mobile): make streaming TTS start, stop and hand over reliably
Long articles often failed to start: the stream had to produce decodable
audio within a fixed 10 s, while tts.folo.is takes several seconds to send
the first bytes of a long text. The timeout didn't stop the WebView, which
kept downloading and later played audio the app no longer tracked, and
playEntryTts fell back to downloading the whole file, which could start
playing over a newer stream minutes later.

- The start timeout is a 15 s stall timer that WebView progress events
  (response headers, then chunks) restart, and it stops the WebView.
- Bridge events carry the request id, so events of a replaced or stopped
  request are ignored, and a play command always starts over instead of
  toggling the entry that is playing.
- Each WebView playback is isolated, so work left over from a replaced one
  can't stop or report on the one that replaced it.
- A superseded or stopped request never falls back and its fallback
  download is aborted. The player's stop button cancels a loading request.
- Segments start after the audio already queued (desktop got this in
  4ab06f42b), a paused stream stays paused while more audio decodes, and
  the last decode of a stream is no longer dropped.
- TTS and native playback no longer overlap: a starting stream pauses the
  native player, and native playback (a podcast, the lock screen) stops
  TTS unless that TTS request is the newer one.
- A stream whose download stalls after it started reports an error once
  its queued audio ran out, and a WebView whose content process the system
  killed is recreated instead of swallowing every later request.
- Cached TTS files are pruned once the player is set up and before each
  fallback download, keeping the file the player still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:19:26 +08:00
DIYgodandClaude Opus 5.5 0f12dc2bfb fix(mobile): keep forwarding the caller's abort while a fetch body downloads
trackFetch removed its abort listener as soon as the headers arrived, so
aborting the caller's signal could no longer cancel a body that was still
downloading. Every fetch in the app goes through it (the global fetch, the
API client and the TTS service), so all of them ignored aborts after the
headers, unlike a plain expo fetch. The listener now stays until the
response is finalized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:19:10 +08:00
DIYgodandClaude Opus 5.5 7d4b23bb4a fix(mobile): show the player on pushed screens with the JS tab bar
On iPad, Android and iOS before 26 the player bar lives in the JS tab bar,
which screens pushed onto the root stack cover. Starting TTS from an
article or a folder timeline gave no feedback and no way to pause or stop
it until returning to a tab root.

Render a floating player bar through the root portal while a pushed screen
is on top. It hides behind modals and the lightbox, which the root portal
host is drawn above on Android, and the article and feed screens keep
their content clear of it. iPhone on iOS 26+ keeps the native tab bar and
its glass player bar unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:55:43 +08:00
DIYgodandClaude Opus 5.5 9603533ed4 fix(i18n): translate hard-coded UI strings and complete every locale
Move the hard-coded English left in the desktop renderer, the Electron
main process, the mobile app and the shared packages to i18next, with
en, zh-CN, zh-TW, ja and fr-FR translations. SSR share pages are
unchanged.

- Fill every namespace in every language, delete keys no code reads
  (server error codes stay) and update translations left behind by
  later English changes.
- Stop HTML-escaping interpolated values in the desktop renderer.
  React already escapes text, so names with & or quotes showed entities.
- Render error boundary fallbacks as components so they can use hooks.
- Localize zod validation messages with the app language.
- Throw ClipboardError from the clipboard helpers and show translated
  toasts. formatEstimatedMins takes a formatter so callers localize the
  units.
- Use one glossary term per concept in ja, fr-FR and zh-TW.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:50:23 +08:00
DIYgodandClaude Opus 5.5 0eb2fd46fa fix(settings): sync every setting again except the device-local ones
Until 1.6.0 the settings sync sent every appearance, general and AI setting
except the device-local keys each domain lists: interface language, voice,
launch at startup, anonymous data, fonts and the like. 93e4a3b48 read those
lists as the keys to send and swapped omit for pick, on desktop and mobile,
so since then only the device-local keys synced across devices and nothing
else did, the action language and the AI settings included. Its e2e test
checked that the interface language syncs.

The lists name the device-local keys again and are renamed to say so. The
appearance domain also leaves the spotlight keys in its tab to the spotlight
domain, which still syncs only those. actionLanguage and personalizePrompt,
which b0f555266 added to the lists, are out of them again, so they sync.

The server kept what the clients sent before 1.6.0, so once per account
each device first queues the settings it changed from their defaults, before
it takes any from the server. While a local change to a tab waits,
applyRemoteSettings now leaves that tab alone, as the change log already
did; settings still at their defaults take the account's values afterwards.

The e2e test now syncs the action language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:17:23 +08:00
DIYgodandClaude Opus 5.5 b0f5552661 fix(settings): sync the action language and AI personalize prompt
The server adds the general actionLanguage and the AI personalizePrompt
settings to the AI chat system prompt, but neither was in the server sync
whitelist, so both stayed on the device and AI chat ignored them: a French
action language still got English answers. Desktop now syncs both, and
mobile syncs actionLanguage, the only one of the two it has.

A value set before this change reaches the server the next time it is
changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:20:49 +08:00
DIYgodandClaude Opus 5.5 b7df9ad01e fix(i18n): stop title-casing non-English titles and labels
Entry titles and settings labels went through title-case unconditionally,
but it applies English capitalization rules, so with a French UI and French
feeds titles rendered as "Pourquoi Votre Café Est Une Arme De Défense" and
settings labels as "Afficher Le Nombre De Non Lus". CJK titles were also
touched ("iOS 27 beta" became "IOS 27 Beta").

titleCaseIfEnglish in @follow/utils only applies title case when the
language is English (en, en-US, en_GB, or franc's eng). On desktop, entry
titles use the entry language and translated titles the translation
language, falling back to the UI language when unknown; settings labels
and section titles use the UI language. Mobile grouped list cells use the
UI language too. AI chat tool names keep title case since they are English
identifiers.

Mobile no longer imports title-case directly, so the dependency moves to
@follow/utils.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:46:44 +08:00
DIYgodandClaude Opus 5.5 0f35b68dea chore(mobile): tag the RNFB disableSPM line in the committed Podfile
The line was added by hand without the markers that the
@react-native-firebase/app config plugin writes, so every
`expo prebuild --no-clean` inserted a second, tagged copy of it. Wrap it
in the plugin's generated markers so prebuild leaves the committed
Podfile unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:09:50 +08:00
DIYgodandClaude Opus 5.5 1bd65fbae7 fix(mobile): keep the RNFB Podfile block inside post_install
The follow-rnfb-ios-compat block was inserted a fixed 7 lines after
`post_install do |installer|`. In the Expo SDK 57 template that hook is
only the react_native_post_install call and its `end`, so a regenerated
Podfile got the block after the `end`, referenced `installer` outside
the hook and failed `pod install`.

Anchor on the `end` that closes post_install instead, so the block is
always the last thing in the hook and runs after
react_native_post_install. mergeContents now accepts an anchor function
for this. Podfiles that already contain the tagged block are still
updated in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:05:28 +08:00
DIYgodandClaude Opus 5.5 2312fe4965 fix(mobile): stop hidden tabs from clearing the tab bar background
All tab screens stay mounted in release builds, and each
SafeNavigationScrollView wrote the shared tab bar background opacity on
layout, content size changes and scroll. A hidden tab whose content fits the
screen, such as Settings, reported that it was scrolled to the end and made
the React Native tab bar transparent over the visible tab, so its icons were
drawn straight over the content. This affects the tab bar used on iPad and on
systems before iOS 26.

Only the focused tab screen now updates the background. useTabScreenIsFocused
returns false outside a tab screen instead of throwing, which replaces
useInTabScreen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:54:38 +08:00
DIYgodandClaude Opus 5.5 181327be31 fix(mobile): keep the native tab bar visible with the iOS 27 SDK
When the app is linked against the iOS 27 SDK, UITabBarController hosts its
tab bar in a new container where setting isTabBarHidden to false no longer
clears tabBar.isHidden. CustomTabbarController hid the bar unconditionally
and only unhid it through isTabBarHidden, so the Liquid Glass tab bar never
appeared on iOS 27. Builds linked against the iOS 26 SDK still use the old
container and were not affected.

Only hide the native bar where React Native renders its own tab bar
(iPad and systems before iOS 26).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:54:36 +08:00
DIYgodandClaude Opus 5.5 de7ac25ca9 fix(mobile): adopt the UIScene life cycle for iOS 27 SDK builds
Apps linked against the iOS 27 SDK (Xcode 27) stop at launch on iOS 27 in
_UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption unless they use
the scene-based life cycle. Enable expo-build-properties'
ios.enableSceneSupport (Expo 57.0.23+) and apply the same changes to the
committed native project, so a prebuild reproduces it:

- AppDelegate conforms to ExpoReactNativeFactoryProvider and no longer
  creates the window or starts React Native.
- Info.plist declares a scene manifest that points at Expo's
  EXExpoAppSceneDelegate, which creates the window and starts React Native.

Builds made with Xcode 26 keep working with the scene life cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:54:20 +08:00
DIYgodandClaude Opus 5.5 3a10c5a16d fix(mobile): return failed social sign-ins to the app
When the OAuth callback failed, the auth session followed Better Auth's
default error page, which production redirects to the folo.is homepage,
so the login sheet never reached folo:// and the app kept waiting with
no session. On real iPhones this happened to every Google sign-in that
had to go through the Google login form.

Run the Expo authorization proxy session from the app with
errorCallbackURL set to the app scheme: a rejected callback now closes
the sheet and shows a toast, and a denied consent screen counts as a
cancellation. The flow is otherwise the Expo plugin's: same proxy URL
and cookie storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:44:10 +08:00
DIYgodandClaude Opus 5.5 9cd769f2f3 fix(mobile): retry Android entry images through the image proxy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:33 +08:00
DIYgodandClaude Opus 5.5 356ac09a7d fix(mobile): show custom subscription titles for entries
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:04:32 +08:00
DIYgod 0336138fc1 Merge pull request #5095 from RSSNext/mobile-main
chore(sync): merge mobile-main into dev
2026-09-26 21:04:52 +08:00
DIYgod 2f392594d5 chore(deps): refresh dependencies and verify affected app flows 2026-09-22 16:20:40 +08:00
DIYgodandClaude Fable 5.1 141567e928 fix(store): pair unread snapshots with the sync id they reflect
A `/reads` recount taken while new-entry hints were still hidden behind a
stale KV head already counted those entries; when the hints arrived with
the next pull their increments were added on top, and the reads that
followed left the hint values behind as phantom unread counts.

The engine now keeps the sync id an unread snapshot reflects
(`unreadSnapshotSyncId`, from the `/reads` answer or `/sync/state` read
just before) and applies the counter part of `N`, `U` and `inbox_entry`
`D` actions only above it. A refresh the user asks for recounts through
the engine, and the list-versus-counter check also fires when a fully
loaded unread-only list shows fewer unread entries than the counter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 10:29:35 +08:00
DIYgodandClaude Fable 5.1 4dd9d46d48 chore(mobile): keep the build number from the release bump
A local simulator build raised CFBundleVersion in Info.plist and the change
slipped into the previous commit; the bump had set it to 13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:39:12 +08:00
DIYgod 9a213b8a83 fix(mobile): drain expo fetch requests before reloading the runtime
Signing out (and the other runtime reloads) crashed the app with SIGSEGV on
the expo.modules.fetch.RequestQueue thread. Expo's native fetch keeps a
closure that owns the JS promise until the response reaches a terminal state;
reloading while a request was pending destroyed that closure against the
torn-down runtime. Sign-out always had requests pending: clearing the cookie
refetches the session queries and popping to the root loads the guest timeline.

Expo also installs its fetch as the global `fetch`, so every request in the app
goes through it. `network-activity.ts` wraps the global fetch (from the
polyfill entry) and the explicit `expo/fetch` imports, tracks each request
until the native response reports `readyForJSFinalization`, and can abort them
all. `reloadApp` cancels the queries, aborts the in-flight requests, waits for
the network to go idle (three seconds at most) and only then reloads. Every
`reloadAppAsync` call site goes through it.

Verified with the Maestro iOS auth journey against production: register, sign
out and log in pass with no crash report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit a662c0872710ff128880b97f0795dbeaf43310b1)
2026-09-18 19:38:47 +08:00
DIYgodandClaude Fable 5.1 7ecbc018e4 fix(mobile): drain expo fetch requests before reloading the runtime
Signing out (and the other runtime reloads) crashed the app with SIGSEGV on
the expo.modules.fetch.RequestQueue thread. Expo's native fetch keeps a
closure that owns the JS promise until the response reaches a terminal state;
reloading while a request was pending destroyed that closure against the
torn-down runtime. Sign-out always had requests pending: clearing the cookie
refetches the session queries and popping to the root loads the guest timeline.

Expo also installs its fetch as the global `fetch`, so every request in the app
goes through it. `network-activity.ts` wraps the global fetch (from the
polyfill entry) and the explicit `expo/fetch` imports, tracks each request
until the native response reports `readyForJSFinalization`, and can abort them
all. `reloadApp` cancels the queries, aborts the in-flight requests, waits for
the network to go idle (three seconds at most) and only then reloads. Every
`reloadAppAsync` call site goes through it.

Verified with the Maestro iOS auth journey against production: register, sign
out and log in pass with no crash report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:37:27 +08:00
DIYgod 1ded9fd2b0 release(mobile): release v0.5.10 2026-09-18 19:14:53 +08:00
DIYgodandClaude Fable 5.1 a2e8c1b97b docs(mobile): prepare release metadata
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:14:45 +08:00
DIYgodandClaude Fable 5.1 259c283b06 test(mobile): center the sign-out row before tapping it in the auth flow
The settings list grew, so the last row now stops underneath the floating tab
bar when Maestro scrolls until it is visible, and the tap landed on the tab bar
instead of the sign-out row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:39:23 +08:00
DIYgodandClaude Fable 5.1 bedc3bc6d0 fix(mobile): raise pod deployment targets to the app floor for Xcode 27
Xcode 27 refuses the iOS 9.0 and 12.0 deployment targets that older pods still
declare on their resource bundle targets, and its Swift compiler rejects
react-native-ios-context-menu, which uses UIAction.subtitle behind an iOS 15
availability check. The app requires iOS 16.4, so the Podfile now raises every
pod target to at least ios.deploymentTarget in post_install; the config plugin
generates the block and the checked-in Podfile carries it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:22:56 +08:00
DIYgodandClaude Fable 5.1 c75533cbd2 fix: report the API as unreachable only when a probe confirms it
One request failing without a response proved little: an endpoint may be slow or
briefly broken while the API itself is fine, and the prompt appeared on every such
hiccup.

- a failed request now only schedules a probe, a cheap `/status/configs` request two
  seconds later, repeated once; the API counts as unreachable when every probe fails
  and no other request got an answer in the meantime
- while unreachable the probe repeats every fifteen seconds so the prompt goes as soon
  as the API answers; any ordinary answer lifts it as well
- the desktop probe travels the same path as its requests, through the main process;
  the mobile probe uses the app's fetch
- without a probe, three failures in a row are needed instead of one

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 16:18:30 +08:00
DIYgodandClaude Fable 5.1 b6c50296a1 feat(store): stop requesting the session, actions, settings, wallet and push token on every launch
A launch of the web app made twelve API requests. Half of them fetched data the
sync engine can keep current, or fetched the same thing more than once.

- session: start-up fetches it through the query cache and `useSession` reuses
  it for a minute, instead of three separate requests
- action rules and settings become sync models: they are loaded in full once per
  account, stored locally, and afterwards follow the change log; unsaved rule
  edits and queued setting changes are never replaced
- sync models outside the engine register through `sync/model-registry`, with a
  one-time bootstrap and an apply step; a model whose actions went by unhandled
  is bootstrapped again
- wallet: requested when the profile menu opens, which is the only place that
  uses the answer
- web push: the token is registered again only when it changed or the change
  log shows another browser took over the channel
- the new-user guide remembers accounts that already have settings, so the
  entry list no longer requests `/settings`

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 21:32:04 +08:00
DIYgodandClaude Fable 5.1 97e92a8ac2 feat: show a prompt when the API cannot be reached
Requests that fail without any response (DNS failure, refused or reset
connection, offline device, timeout) previously failed silently. Track
them through the SDK interceptors and surface a single prompt per outage:
a persistent toast on desktop that is dismissed once the API answers
again, and a transient toast on mobile.

The connection state lives in an atom because the first requests can fail
before the toaster is mounted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 21:08:05 +08:00
DIYgodandClaude Fable 5.1 b09af5f5cd feat(store): rely on the delta feed instead of full snapshots once a sync cursor exists
The sync engine added an incremental channel, but the app kept fetching full
snapshots next to it: both prefetch hooks, a recount after every pull, every
loaded page of the entry lists, and whole-list refetches after unsubscribing or
importing.

- unread counters keep confirmed counts under the local transactions; the change
  log moves them by the flips and new unread rows the server reports, and an
  acknowledged transaction drops its prediction once the engine reached its
  sync id
- usePrefetchSubscription, usePrefetchUnread, pull to refresh, the refresh
  button, OPML import, unsubscribe and the mobile background task go through the
  engine and only fall back to the full requests when it cannot help
- snapshots remain as calibration: unread hourly, subscriptions daily, plus a
  throttled recount when the list on screen disagrees after a delta pull
- new entries fetch only the first page of the lists on screen and merge it in
  front of the loaded pages; a refresh trims the infinite query to one page
- mobile refreshes subscriptions through the delta feed after an OPML import

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 19:53:09 +08:00
DIYgodandClaude Fable 5.1 9b890144e8 chore(deps): upgrade client sdk to 0.3.96 and use its sync module
Bump `@follow-app/client-sdk` to 0.3.96 (and `@folo-services/drizzle` to
0.1.48) and hand `followApi.sync` to the store instead of requesting the
`/sync` endpoints through `followClient.request`.

The new SDK no longer declares the frozen wallet and RSS3 writes. Keep the
existing screens and their runtime behaviour unchanged through a small
compatibility module: legacy withdraw fields and the `IS_RSS3_TESTNET`
flag get local types, and the removed `wallets.post()` is requested at the
same path with `followClient.request`. Drop an over-wide tuple annotation
in the mobile plan screen now that `PaymentFeature` keys are precise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 14:36:00 +08:00
DIYgodandClaude Fable 5.1 247a9294ea feat(store): add an incremental sync engine driven by server sync ids
Add `sync/sync-engine.ts`, which keeps the last applied sync id in a new
`sync_meta` SQLite table (migration 0039), bootstraps from `/sync/state`
plus the existing snapshot endpoints, and then pages through `/sync/delta`
on launch, foreground, reconnect, a 60 s interval and after the transaction
queue receives an acknowledgement. Subscription, collection and timeline
actions are applied to the stores and SQLite, pending local marks keep
precedence through the transaction queue overlays, and unread counts are
refreshed once per pull. A 404 from the sync endpoints keeps today's full
refetch behaviour for the session.

Both apps expose the endpoints through `followClient.request` until the
client SDK release with the `sync` module lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 13:53:47 +08:00
DIYgodandClaude Fable 5.1 93b83f95eb feat(mobile): add reading settings, manual theme and immersive header
- add a Reading settings screen (content font, line height, text color,
  inline style, code theme) reachable from Appearance and from the entry
  "more" menu
- add a System / Light / Dark theme setting applied through
  Appearance.setColorScheme so the reader WebView follows it as well
- optionally slide the entry header away while scrolling down and keep a
  blurred status bar strip so the clock stays readable
- forward reader typography to the html renderer through a new
  setReaderStyle bridge call

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 12:50:51 +08:00
DIYgodandClaude Fable 5.1 b606f0c432 feat(store): add a persisted transaction queue with rebase
Introduce a Linear-style transaction queue for user mutations. Read marks,
unread marks, mark-all and star/unstar are applied optimistically, written to
a new `sync_transactions` outbox table, batched and sent in order, retried
while offline and rolled back only when the server rejects them. Pending and
just-acknowledged transactions expose overlays so server snapshots are rebased
on top of the local intent, which replaces the 30 s read-protection window.

Also add the sync engine design spec that covers the server change log and
delta endpoints planned for the next phase.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:13:42 +08:00
DIYgod 5476270687 fix(mobile): repair player and search layouts 2026-09-12 17:48:57 +08:00
DIYgod 8b097abc52 fix(reader): show missing cover images above entry content 2026-09-09 13:13:09 +08:00
DIYgod dbab1121fa fix(mobile): add subscription legal links 2026-09-03 18:54:32 +08:00
DIYgod 465b997e89 fix(mobile): upgrade audio player to avoid background service crashes 2026-09-02 16:18:34 +08:00
DIYgod 22a9bcd4e4 release(mobile): release v0.5.9 2026-09-02 10:00:44 +08:00
DIYgod d7a3c1e6d9 docs(mobile): prepare release metadata 2026-09-02 09:59:53 +08:00
DIYgod 9787707ca3 feat(timeline): support oldest-first unread sorting 2026-09-01 20:26:14 +08:00
DIYgod a2ad2a8922 fix(mobile): preserve timeline position across switches 2026-09-01 18:28:26 +08:00
DIYgod 78f6bd1b74 fix(mobile): fix settings header safe area and contrast 2026-08-26 11:07:51 +08:00
DIYgod f6777ee559 release(mobile): release v0.5.8 2026-08-01 17:45:07 +08:00
DIYgod 6a8c07c000 docs(mobile): prepare release metadata 2026-08-01 17:44:47 +08:00
DIYgod e291c3d1da fix(mobile): comply with Android media permissions policy 2026-08-01 17:41:46 +08:00
DIYgod eb782e0389 release(mobile): release v0.5.7 2026-07-31 16:31:13 +08:00
DIYgod 8a2babfd4f docs(mobile): prepare release metadata 2026-07-31 16:27:59 +08:00
DIYgod a27db571b3 fix(mobile): use product IDs for Apple IAP verification 2026-07-30 19:27:59 +08:00
DIYgod 2350884eae fix(mobile): handle RSSHub subscription limit errors 2026-07-26 07:29:14 +08:00
DIYgod 3d65478fb6 fix(subscription): recover past-due Stripe checkout 2026-07-25 20:22:55 +08:00