You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Ask Browser Users to Install the iOS App, or Open It When They Have It - #863
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.
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
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
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.
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.
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.
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
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.
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.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #859
What changes for people
https://claude.ai/artifact/Y6Z47nsaairUrVLWxKABB5#page-dcb81aee8d83
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.
Sheet: Get the App / Not NowSheet: Open in App / Not NowModalwith a QR code of the App Store listing, Open App Store / Not NowOpen in App links to
timehuddle://openplus the current path and query, e.g.timehuddle://open/app/huddle?team=…. The existingappUrlOpenhandler inmain.tsxthen 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:
users.markNativeAppSeen({ platform })method setsnativeAppLastSeenAt.<platform>on the user. It is exposed over REST inmain.js.SessionProvidercalls 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.users.getCurrentUserreturnshasIosApp, 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.meteor-backend/server/native-app-core.js.Code
src/features/app-prompt/appPromptKind.tsdecides which prompt to show, if any. It's a pure function with unit tests.src/features/app-prompt/AppPrompt.tsxrenders it from@mieweb/ui(Sheet,AppModal/Modal,Button,buttonVariants) andqrcode.react. It's mounted once inAppLayout.APP_STORE_URLmoves fromaccountMenu.tstosrc/lib/appLinks.ts, alongsideopenInAppUrl().ddp.ts: the current-user type that was written out twice is now oneDdpCurrentUserinterface.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.webdriverThe prompt stays hidden when
navigator.webdriveris true. Otherwise the laptop popup would cover the page on every load in every Playwright test.tests/e2e/navigation/app-prompt.spec.tsoverrides that flag to test the prompt itself.Testing
appPromptKind.test.tsandappLinks.test.ts, plus the full frontend Vitest run: 914 passedmeteor-backend/tests/native-app-core.test.tsand the backend unit suite: 611 passedmeteor-backend/tests/native-app.test.ts(REST integration against the test backend): 4 passedtests/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 formatnpm run test:all): not run locallyNot in this PR
🤖 Generated with Claude Code