Skip to content

Ask Browser Users to Install the iOS App, or Open It When They Have It - #863

Open
Dharp02 wants to merge 5 commits into
mainfrom
feat/859-install-app-prompt
Open

Dharp02 wants to merge 5 commits into
mainfrom
feat/859-install-app-prompt

Conversation

@Dharp02

@Dharp02 Dharp02 commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #859

What changes for people

https://claude.ai/artifact/Y6Z47nsaairUrVLWxKABB5#page-dcb81aee8d83

image

The first time TimeHuddle opens in a browser tab, it points people at the iOS app. Neither moving between pages nor a reload brings the prompt back (a session-storage flag records that it was shown); a new tab or window does.

Where No app Has the iOS app
iPhone or iPad browser Bottom Sheet: Get the App / Not Now Bottom Sheet: Open in App / Not Now
Laptop or desktop browser Modal with a QR code of the App Store listing, Open App Store / Not Now Nothing
Native app, Android browser, automated browser Nothing Nothing

Open in App links to timehuddle://open plus the current path and query, e.g. timehuddle://open/app/huddle?team=…. The existing appUrlOpen handler in main.tsx then opens that same page in the app. iOS shows its own "Open in TimeHuddle?" confirmation first.

Prototype: https://claude.ai/artifact/Y6Z47nsaairUrVLWxKABB5

How "has the app" is known

A browser can't see which apps are installed, so the server keeps the record:

  • Backend: the new users.markNativeAppSeen({ platform }) method sets nativeAppLastSeenAt.<platform> on the user. It is exposed over REST in main.js.
  • Native app: SessionProvider calls the method once per launch and signed-in user, whether or not notifications are allowed. It reaches installed apps through OTA, so no App Store release is needed.
  • Web app: users.getCurrentUser returns hasIosApp, worked out when it's read: true if the iOS app was used in the last 30 days. Nothing is persisted for display. Someone who deletes the app gets asked to install it again after 30 days.
  • Pure rules: whether a use counts as recent lives in meteor-backend/server/native-app-core.js.

Code

  • src/features/app-prompt/appPromptKind.ts decides which prompt to show, if any. It's a pure function with unit tests.
  • src/features/app-prompt/AppPrompt.tsx renders it from @mieweb/ui (Sheet, AppModal/Modal, Button, buttonVariants) and qrcode.react. It's mounted once in AppLayout.
  • APP_STORE_URL moves from accountMenu.ts to src/lib/appLinks.ts, alongside openInAppUrl().
  • ddp.ts: the current-user type that was written out twice is now one DdpCurrentUser interface.
  • release-notes/1.2.0.md: 1.2.0 isn't tagged yet, so the note goes into that draft.

The e2e suite and navigator.webdriver

The prompt stays hidden when navigator.webdriver is true. Otherwise the laptop popup would cover the page on every load in every Playwright test. tests/e2e/navigation/app-prompt.spec.ts overrides that flag to test the prompt itself.

Testing

  • appPromptKind.test.ts and appLinks.test.ts, plus the full frontend Vitest run: 914 passed
  • meteor-backend/tests/native-app-core.test.ts and the backend unit suite: 611 passed
  • meteor-backend/tests/native-app.test.ts (REST integration against the test backend): 4 passed
  • tests/e2e/navigation/app-prompt.spec.ts: 5 passed. It covers the laptop popup on load, staying hidden on in-app navigation and on reload; no prompt on a laptop for someone with the app; the iPhone install sheet; the iPhone open-in-app link; and no prompt in an automated browser.
  • npm run lint, npm run typecheck, npm run format
  • Smoke-tested on the dev app at :3000, at laptop size and as an iPhone
  • Full e2e suite (npm run test:all): not run locally
  • On a real iPhone with the app installed: Open in App lands on the same page

Not in this PR

🤖 Generated with Claude Code

Each time TimeHuddle opens in a browser it now points people at the iOS
app: a QR code on a laptop, a Get the App sheet on an iPhone or iPad, and
an Open in App sheet for people who already have the app, which opens the
page they're on through the existing timehuddle://open deep link.

A browser can't see what's installed, so the native app reports itself on
every launch (users.markNativeAppSeen, stored as nativeAppLastSeenAt per
platform) and users.getCurrentUser derives hasIosApp from a use in the
last 30 days. Nothing shows inside the native app, on Android (#840), or
to automated browsers, which keeps the Playwright suite unaffected.

APP_STORE_URL moves to src/lib/appLinks.ts so the account menu and the
prompt share it.

Closes #859
Copilot AI balanced review requested due to automatic review settings November 11, 2026 14:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The prompt can conflict with the required username dialog and disrupt keyboard focus, and its hardcoded English copy does not meet the translatability requirement.

2 open issues
What changed in this PR

Adds browser prompts that help signed-in users install the iOS app or open their current page in it, using recent native-app activity recorded by the backend.

Changes:

  • Adds iPhone/iPad sheets and a desktop QR-code modal with device-specific visibility rules.
  • Records native-app use and exposes a derived hasIosApp flag based on a 30-day window.
  • Centralizes app links and adds unit, integration, and browser tests plus release notes.
File Description
tests/​e2e/​navigation/​app-prompt.spec.ts Tests browser prompts, dismissal, reloads, and app links.
src/​ui/​AppLayout.tsx Mounts the prompt in the application shell.
src/​ui/​accountMenu.ts Uses the shared App Store URL.
src/​lib/​useSession.tsx Loads app-use status and reports native launches.
src/​lib/​ddp.ts Consolidates current-user typing and adds native-use reporting.
src/​lib/​appLinks.ts Defines shared store and deep-link helpers.
src/​lib/​appLinks.test.ts Tests deep links and fallback routes.
src/​lib/​api.ts Adds hasIosApp to the user type.
src/​features/​app-prompt/​appPromptKind.ts Defines prompt selection rules.
src/​features/​app-prompt/​appPromptKind.test.ts Tests device and app-use combinations.
src/​features/​app-prompt/​AppPrompt.tsx Implements install and open-in-app dialogs.
release-notes/​1.2.0.md Documents the browser prompts.
meteor-backend/​tests/​native-app.test.ts Tests reporting through REST and authentication checks.
meteor-backend/​tests/​native-app-core.test.ts Tests platform validation and recency rules.
meteor-backend/​server/​users.js Stores native-app activity per platform.
meteor-backend/​server/​native-app-core.js Defines supported platforms and the recency window.
meteor-backend/​server/​main.js Exposes native-use reporting over REST.
meteor-backend/​server/​auth-bridge.js Returns derived iOS app-use status.

Review effort: Balanced

Comment thread src/features/app-prompt/AppPrompt.tsx Resolved
Comment thread src/features/app-prompt/AppPrompt.tsx Resolved
Copilot review on #863: social signup shows the shell under the
required UsernameClaimModal, so the prompt opened a second dialog over it.
Copilot AI balanced review requested due to automatic review settings November 11, 2026 16:58
@Dharp02

Dharp02 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the Copilot review:

  • Prompt over the username dialog: fixed in 16a0492. The prompt now waits until a new social signup has picked a username, so it no longer opens a second dialog over UsernameClaimModal.
  • Translatable copy: not in this PR. TimeHuddle has no translation library or locale files yet, so every screen's text is plain English. The prompt keeps its strings, including the QR label, in one COPY object, ready to move once the app adopts i18n. That belongs in its own app-wide ticket.

The app prompt's 5 e2e tests pass after the fix; lint, typecheck and format are clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Dismissing the prompt leaves keyboard focus on the document body because focus is not restored to the prior control or a logical fallback.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Restore keyboard focus when the prompt is dismissed

src/​features/​app-prompt/​AppPrompt.tsx:68

Dismissing the prompt unmounts the focused dialog, but the bundled Sheet/Modal focus trap does not restore focus, so Not Now or Esc leaves keyboard focus on the document body. Capture a return-focus target before opening and restore it on dismissal. Use a logical page control as a fallback when the startup prompt opens before any control is focused.

2 resolved since last review

Review effort: Balanced

Refreshing the page brought the install / open-in-app prompt back each
time. It now records in session storage that it has been shown, so a
reload or in-app navigation leaves it closed, while a new tab or window
shows it again. The flag is written only once the prompt is on screen,
so a new signup still sees it after picking a username.
Copilot AI balanced review requested due to automatic review settings November 11, 2026 20:22
…prompt

# Conflicts:
#	release-notes/1.2.0.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

This branch has not been deployed

No deployments
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.

Prompt Browser Users to Install or Open the TimeHuddle iOS App

2 participants