Skip to content

Feat/open in desktop app not installed#429

Draft
appflowy wants to merge 7 commits into
mainfrom
feat/open-in-desktop-app-not-installed
Draft

Feat/open in desktop app not installed#429
appflowy wants to merge 7 commits into
mainfrom
feat/open-in-desktop-app-not-installed

Conversation

@appflowy

@appflowy appflowy commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Description


Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Summary by Sourcery

Add a user preference and shared utilities for opening links in the AppFlowy desktop app, and apply this behavior to invitations, shared pages, and direct desktop deep links.

New Features:

  • Introduce a per-user "open in desktop app" setting stored in user metadata and exposed in the account settings UI.
  • Add a reusable desktop handoff hook and deep-link utilities to open pages and invitations in the desktop app with graceful fallback when the app is not installed.
  • Enable invitation acceptance and guest/shared page landing flows to optionally open in the desktop app based on the new preference, with web fallback.

Enhancements:

  • Unify construction of desktop deep-link URLs and add detection to avoid repeatedly attempting to open the desktop app on devices where it appears missing.
  • Extend app context handling to trigger desktop handoff for directly opened shared page URLs and explicit desktop query links.
  • Add tests for the desktop handoff hook and open-desktop utility behavior, including not-installed detection and deep-link builders.

Tests:

  • Add unit tests for the open_desktop_app utility to cover deep-link building, app-missing detection, and open flow behavior.
  • Add unit tests for the useDesktopHandoff hook to verify preference handling and first-time prompt behavior.

appflowy and others added 7 commits June 26, 2026 03:34
Adds open_desktop_app.ts: a reusable helper to open a target in the AppFlowy
desktop app and fall back gracefully when the app is not installed, mirroring
Notion's behavior.

Because browsers cannot reliably detect an installed app, it uses the
attempt-and-observe heuristic (fire appflowy-flutter:// scheme, watch for the
page losing focus/visibility within a timeout). When the app does not open:
- stay on the web (no forced download redirect),
- show a non-blocking "Couldn't open the AppFlowy desktop app" prompt with
  Download / Continue-in-browser actions,
- remember the "not detected" result per-device (localStorage, ~weekly TTL) so
  subsequent links don't stall again; the flag is cleared on any later success.

openInDesktopApp() layers the prompt + skip-when-known-missing logic; callers
(invite accept page, share-link page) pass onContinueInBrowser to proceed to the
web. Includes i18n strings and unit tests (8 cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VB3ZauXUUczqNTLFu9uoMs
Foundation for the desktop handoff: a per-user, server-synced preference
stored in user metadata, identical to how timezone/language/date-format are
persisted.

- user-metadata.ts: add MetadataKey.OpenInDesktopApp ('open_in_desktop_app'),
  its boolean MetadataValues type, a `false` default, and a
  UserMetadataBuilder.setOpenInDesktopApp() helper.
- useOpenInDesktopApp(): read currentUser.metadata[open_in_desktop_app]
  (enabled / isSet) and persist via UserService.updateProfile (server JSONB
  merge) + updateCurrentUser (local cache), mirroring AccountAppPanel.

No backend change required (the af_user.metadata JSONB merge accepts the key
as-is). The settings toggle and the handoff-gating wrapper build on this hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VB3ZauXUUczqNTLFu9uoMs
Adds the Notion-style preference toggle in AccountAppPanel, bound to
useOpenInDesktopApp() (reads/writes the synced open_in_desktop_app metadata
key). Includes i18n strings under settings.accountPage.openInDesktopApp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VB3ZauXUUczqNTLFu9uoMs
… flows

- useOpenInDesktopApp: read AFConfigContext directly so the hook is safe on
  landing/invitation pages (reports enabled=false outside the provider).
- useDesktopHandoff: preference-gated handoff — when "open in desktop app" is
  on, attempt the deep link via openInDesktopApp (with not-installed handling);
  otherwise stay on the web (onStayInBrowser).
- Flow 1 (AcceptInvitationPage): after accepting, hand off to
  invitation-callback if enabled, else navigate to /app/{ws}.
- Flow 3 (AppContextConsumer/OpenClient): on a fresh share-link load of
  /app/:ws/:view, attempt open-page once when enabled (module-scoped guard so
  internal navigation doesn't retrigger). The explicit ?is_desktop=true path is
  preserved for notification links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VB3ZauXUUczqNTLFu9uoMs
- open_desktop_app.ts: add buildOpenPageLink / buildInvitationCallbackLink as
  the single source of truth for the deep-link scheme format.
- Flow 2 (AsGuest): the "View page" button now hands off to the desktop app
  (open-page with the shared view_id from getGuestInvitation) when the
  preference is on, else opens the page on the web.
- Refactor AppContextConsumer and AcceptInvitationPage to use the shared
  builders instead of inline scheme strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VB3ZauXUUczqNTLFu9uoMs
- Extract isSpecificPagePath() so the "share-link arrival vs internal
  navigation" classification (which gates the Flow 3 handoff) is pure and
  testable.
- Tests: deep-link builders, isSpecificPagePath (share vs internal nav), and
  useDesktopHandoff gating (attempts when preference on, stays in browser when
  off) via renderHook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VB3ZauXUUczqNTLFu9uoMs
…ce (P4)

When a user who hasn't chosen the preference opens an invite/share link, show a
one-time Notion-style prompt instead of silently staying in the browser:
- "Open in app" → enables the preference and opens the desktop app
- "Stay in browser" → disables it
Either choice is remembered, so the prompt is shown once.

- open_desktop_app.ts: add promptOpenInDesktopApp (sonner toast + i18n).
- useDesktopHandoff: enabled → attempt; unset → prompt (remembers choice); off → stay.
- OpenClient: route the share-link handoff through useDesktopHandoff so the
  unset case prompts (still fire-once per document load).
- i18n strings + test for the unset/prompt path (14 tests total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VB3ZauXUUczqNTLFu9uoMs
@github-actions

Copy link
Copy Markdown

🥷 Ninja i18n – 🛎️ Translations need to be updated

Project /project.inlang

lint rule new reports level link
Missing translation 280 warning contribute (via Fink 🐦)

@sourcery-ai

sourcery-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a user preference and supporting utilities to control opening AppFlowy links in the desktop app, then wires that preference-driven handoff into guest landing, invitation acceptance, and app context navigation flows with robust handling of the desktop app not being installed.

File-Level Changes

Change Details Files
Introduce desktop handoff utilities and preference-driven open-in-desktop behavior, including app-not-installed detection and prompts.
  • Add open_desktop_app utility module to build deep links, detect desktop app presence via visibility/blur heuristics, and show not-installed/prompt toasts.
  • Persist a per-device flag in localStorage when the desktop app appears missing, with TTL-based re-probing and cleared on successful launches.
  • Expose openInDesktopApp API that wraps deep-link attempts with skip-when-known-missing logic and non-blocking fallback notifications.
src/utils/open_desktop_app.ts
src/utils/__tests__/open_desktop_app.test.ts
Add a per-user, server-synced "open in desktop app" metadata preference and hooks to read/update it, including a first-time prompt to choose behavior.
  • Extend user metadata keys, values, defaults, and builder with OpenInDesktopApp boolean flag.
  • Create useOpenInDesktopApp hook to read the preference from AFConfigContext and persist updates via UserService.updateProfile plus local user cache.
  • Create useDesktopHandoff hook that gates desktop deep-link attempts based on the preference, showing a one-time prompt when unset and respecting explicit off.
src/application/user-metadata.ts
src/components/app/hooks/useOpenInDesktopApp.ts
src/components/app/hooks/useDesktopHandoff.ts
src/components/app/hooks/__tests__/useDesktopHandoff.test.tsx
Wire desktop handoff into guest landing and invitation acceptance flows so that, when enabled, shared pages and accepted workspaces open in the desktop app with safe web fallbacks.
  • Update AsGuest landing page to derive current user, use desktop handoff, build page deep-link via buildOpenPageLink, and route primary action through openPage callback with onStayInBrowser fallback.
  • Update AcceptInvitationPage to use desktop handoff with buildInvitationCallbackLink so accepted invitations attempt desktop open while safely falling back to web workspace navigation.
  • Ensure callbacks guard against missing workspace/page IDs and keep existing error handling and loading behavior intact.
src/components/app/landing-pages/AsGuest.tsx
src/pages/AcceptInvitationPage.tsx
Integrate preference-driven desktop handoff into the main app context so share links and explicit desktop-targeted URLs attempt deep-linking appropriately without affecting internal navigation.
  • Add isSpecificPagePath and landedOnSpecificPage/didAttemptPreferenceHandoff flags to detect document-level entry via shared page URLs.
  • Refactor OpenClient effect to use buildOpenPageLink for ?is_desktop=true deep-link URLs and require workspace/view IDs before attempting.
  • Add a second effect that, when landing on a specific page and preference is set, calls handoff with buildOpenPageLink so external share links honor the desktop preference once per load.
src/components/app/components/AppContextConsumer.tsx
Expose the "Open links in desktop app" setting in the Account settings UI so users can enable/disable the behavior.
  • Import useOpenInDesktopApp and Switch into AccountAppPanel and bind them to a new row with title/description text and a test ID.
  • Wire the Switch to the preference via checked and onCheckedChange, calling setEnabled asynchronously.
  • Insert dividers to keep the new setting visually separated from existing date/time settings.
src/components/app/settings/AccountAppPanel.tsx
src/@types/translations/en.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant