From 1d3dfd395f1b2e48389c42080d4c72824dae9815 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 4 Jul 2026 11:05:15 +0900 Subject: [PATCH] fix(ios): stop share extension hijacking shared .txt files (#4917) The iOS share extension is a web-article URL clipper, but its activation rule enabled NSExtensionActivationSupportsText. A .txt file is public.plain-text (conforms to public.text), so that key made the URL-only extension activate for plain-text files it cannot handle: the share sheet hung instead of the file taking the main app's CFBundleDocumentTypes "Copy to Readest" import path, which handles txt fine (like EPUB and PDF, which never matched the extension). Drop NSExtensionActivationSupportsText so the extension activates only for web URLs. Shared .txt files now route to the working document-open import path; sharing a web page URL from Safari or Chrome still works. Add a regression guard asserting project.yml (the xcodegen source of truth for the generated, skip-worktree Info.plist) never re-enables text activation. Co-authored-by: Claude Opus 4.8 (1M context) --- .../src-tauri/gen/apple/project.yml | 6 ++- .../share-extension-activation-rule.test.ts | 43 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 apps/readest-app/src/__tests__/ios/share-extension-activation-rule.test.ts diff --git a/apps/readest-app/src-tauri/gen/apple/project.yml b/apps/readest-app/src-tauri/gen/apple/project.yml index b230b5a7..5c0bda45 100644 --- a/apps/readest-app/src-tauri/gen/apple/project.yml +++ b/apps/readest-app/src-tauri/gen/apple/project.yml @@ -146,9 +146,13 @@ targets: NSExtensionPointIdentifier: com.apple.share-services NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController NSExtensionAttributes: + # Web URLs only. Do NOT add NSExtensionActivationSupportsText: a + # .txt file is public.plain-text (conforms to public.text), so that + # key makes this article-URL clipper activate for plain-text FILES + # it can't handle (the share sheet hangs). Book files must fall + # through to the main app's CFBundleDocumentTypes import path. NSExtensionActivationRule: NSExtensionActivationSupportsWebURLWithMaxCount: 1 - NSExtensionActivationSupportsText: true # Entitlements referenced via CODE_SIGN_ENTITLEMENTS build setting # rather than xcodegen's `entitlements:` block — otherwise xcodegen # may overwrite the file (even with no properties declared) during diff --git a/apps/readest-app/src/__tests__/ios/share-extension-activation-rule.test.ts b/apps/readest-app/src/__tests__/ios/share-extension-activation-rule.test.ts new file mode 100644 index 00000000..f5020526 --- /dev/null +++ b/apps/readest-app/src/__tests__/ios/share-extension-activation-rule.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +/** + * Regression guard: sharing a `.txt` file to Readest on iOS got stuck. + * + * The Share Extension (added in #4256 / #4267) is an article-URL clipper — its + * `ShareViewController` only ever extracts an http(s) URL from the shared item. + * Its activation rule enabled `NSExtensionActivationSupportsText`, but a `.txt` + * file is UTI `public.plain-text` (which conforms to `public.text`), so that key + * made the extension activate for plain-text FILES it cannot handle: the share + * sheet hung instead of the file taking the main app's CFBundleDocumentTypes + * "Copy to Readest" open-in-place path (which imports txt fine, exactly like the + * EPUB/PDF that never matched the extension). + * + * Fix: the extension activates only for web URLs, never for text — so `.txt` + * (and any plain-text file) routes to the working document-open import path. + * + * We assert on `project.yml` only. It is the xcodegen source of truth: Tauri's + * iOS CLI runs `xcodegen` at build time, which regenerates each target's + * `Info.plist` from this file. The committed `ShareExtension/Info.plist` is a + * generated artifact (marked `skip-worktree`) and must not be relied on. + */ + +// `NSExtensionActivationSupportsText` is legitimately named in a `#` comment +// warning contributors not to re-add it. Strip comments so the check targets +// active config only. +const stripYamlComments = (text: string): string => text.replace(/#.*$/gm, ''); + +const projectYml = stripYamlComments( + readFileSync(resolve(process.cwd(), 'src-tauri/gen/apple/project.yml'), 'utf-8'), +); + +describe('iOS Share Extension activation rule (txt share stuck)', () => { + it('does not enable NSExtensionActivationSupportsText (would capture .txt files)', () => { + expect(projectYml).not.toContain('NSExtensionActivationSupportsText'); + }); + + it('still activates for web URLs (article clipping preserved)', () => { + expect(projectYml).toContain('NSExtensionActivationSupportsWebURLWithMaxCount'); + }); +});