From 43d01f1fc923b84113c09bccc94156c8900bbb89 Mon Sep 17 00:00:00 2001 From: Pushp Date: Sun, 25 Oct 2026 11:11:19 +0530 Subject: [PATCH 1/2] feat(app-link): single source for store links; never link to a store 404 - Store listings live only in src/config/app-store-links.ts, imported by both the web app and the CDK stack (the old duplicate in infra was unnecessary). - Listings are null until published: badges and in-app notices show 'coming soon' instead of the current 404 store URLs. - 'Get the app' goes to the store smart link only on a phone whose listing is live; otherwise to /#get-app (GetAppLink). --- infra/stacks/constants.ts | 24 ++++++++------ .../auth/FinishSetupInAppNotice.tsx | 13 ++++++-- src/components/auth/SignupInAppNotice.tsx | 14 +++++++-- src/components/common/GetAppLink.tsx | 27 ++++++++++++++++ src/components/common/SiteHeader.tsx | 16 +++------- src/components/legal/Pricing.tsx | 18 +++-------- src/components/messenger/Messenger.tsx | 10 ++---- src/config/app-store-links.ts | 18 +++++++++++ src/config/membership.config.ts | 3 -- src/config/signup.config.ts | 31 ++++++++++++++++--- src/tests/infra-ownership.test.ts | 9 +++--- src/tests/signup-surface.test.ts | 31 ++++++++++++++++--- 12 files changed, 151 insertions(+), 63 deletions(-) create mode 100644 src/components/common/GetAppLink.tsx create mode 100644 src/config/app-store-links.ts diff --git a/infra/stacks/constants.ts b/infra/stacks/constants.ts index a11c58c..71f012f 100644 --- a/infra/stacks/constants.ts +++ b/infra/stacks/constants.ts @@ -15,6 +15,8 @@ * been removed. */ +import { APP_STORE_LINKS as STORE_LISTINGS } from '../../src/config/app-store-links.ts'; + /** AWS accounts by role. */ export const ACCOUNTS = { /** Owns the production frontend stack and the onehook.club hosted zone. */ @@ -47,19 +49,21 @@ export const APP_LINK_SUBDOMAIN = 'app'; /** * Store destinations for the smart link. * - * KEEP IN SYNC with `src/config/signup.config.ts` (`APP_STORE_LINKS`), which the web UI uses for its - * own "get the app" buttons. Infra cannot import from `src` (separate tsconfig), so the values are - * duplicated here deliberately; `deployment-pipeline.test.ts` asserts they match. + * The ios/android listings come from `src/config/app-store-links.ts`, the single place they are set + * (the web UI reads the same file). Only the infra-specific fallback is defined here. */ -export const APP_STORE_LINKS = { - ios: 'https://apps.apple.com/app/onehook', - android: 'https://play.google.com/store/apps/details?id=club.onehook.android', +export const APP_STORE_LINKS: { + readonly ios: string | null; + readonly android: string | null; + readonly fallback: string; +} = { + ...STORE_LISTINGS, /** - * Desktop / unknown devices cannot install the app, so they land on the marketing page rather - * than an App Store page that would immediately reject them. + * Desktops cannot install the app, and a phone whose store listing is not live yet must not be + * sent to a store 404, so both go to the web sign-in page, where the app is usable in a browser. */ - fallback: 'https://onehook.club/app', -} as const; + fallback: 'https://onehook.club/login', +}; /** * Mr OneHook AI chatbot custom domains (Azure App Service, OneHookUXEnhancement/infra). diff --git a/src/components/auth/FinishSetupInAppNotice.tsx b/src/components/auth/FinishSetupInAppNotice.tsx index c6e8366..1798e39 100644 --- a/src/components/auth/FinishSetupInAppNotice.tsx +++ b/src/components/auth/FinishSetupInAppNotice.tsx @@ -41,15 +41,24 @@ export function FinishSetupInAppNotice({ missing, onRetry, retrying = false, che const secondaryClass = 'w-full py-3 border border-border text-xs font-bold uppercase tracking-[0.3em] rounded hover:bg-bg transition-colors flex items-center justify-center gap-2 disabled:opacity-50'; - const iosLink = ( + // A store link is null until that listing is published: show "coming soon" instead of a store 404. + const iosLink = APP_STORE_LINKS.ios ? ( Open on iPhone + ) : ( + + iPhone app coming soon + ); - const androidLink = ( + const androidLink = APP_STORE_LINKS.android ? ( Open on Android + ) : ( + + Android app coming soon + ); return ( diff --git a/src/components/auth/SignupInAppNotice.tsx b/src/components/auth/SignupInAppNotice.tsx index ac3535a..07ea7a2 100644 --- a/src/components/auth/SignupInAppNotice.tsx +++ b/src/components/auth/SignupInAppNotice.tsx @@ -37,7 +37,8 @@ export function SignupInAppNotice() { const secondaryClass = 'w-full py-3 border border-border text-xs font-bold uppercase tracking-[0.3em] rounded hover:bg-bg transition-colors flex items-center justify-center gap-2'; - const iosLink = ( + // A store link is null until that listing is published: show "coming soon" instead of a store 404. + const iosLink = APP_STORE_LINKS.ios ? ( Download for iPhone + ) : ( + + iPhone app coming soon + ); - const androidLink = ( + const androidLink = APP_STORE_LINKS.android ? ( Download for Android + ) : ( + + Android app coming soon + ); return ( @@ -69,6 +78,7 @@ export function SignupInAppNotice() { className="w-full max-w-md" >
+

Redeem your OneHook invite

New accounts are created in the OneHook app

diff --git a/src/components/common/GetAppLink.tsx b/src/components/common/GetAppLink.tsx new file mode 100644 index 0000000..3a1882a --- /dev/null +++ b/src/components/common/GetAppLink.tsx @@ -0,0 +1,27 @@ +import type { AnchorHTMLAttributes } from 'react'; +import { Link } from 'react-router-dom'; +import { getAppHref } from '../../config/signup.config'; +import { UiElement, trackProps } from '../../lib/analytics/analytics'; + +/** + * A "Get the app" call to action. A phone whose store listing is live opens the install smart link + * in a new tab; everyone else (desktop, or a platform not published yet) goes to the homepage + * get-app section via client-side navigation. See `getAppHref`. + * + * Always reported as `LANDING_OPEN_IN_APP`, whichever destination it resolves to. + */ +export function GetAppLink({ children, ...rest }: Omit, 'href'>) { + const href = getAppHref(); + if (/^https?:\/\//.test(href)) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +} diff --git a/src/components/common/SiteHeader.tsx b/src/components/common/SiteHeader.tsx index 71f9b7a..6727e88 100644 --- a/src/components/common/SiteHeader.tsx +++ b/src/components/common/SiteHeader.tsx @@ -2,13 +2,9 @@ import { useLayoutEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Smartphone, X } from 'lucide-react'; import { BrandWordmark } from './BrandWordmark'; +import { GetAppLink } from './GetAppLink'; import { UiElement, trackProps } from '../../lib/analytics/analytics'; -// The platform-aware install smart link: app.onehook.club answers with a 302 to the App Store or -// Play Store based on the visitor's device (see infra/stacks/frontend-stack.ts), so one URL works -// everywhere — including in the QR code, which is scanned by an unknown device by definition. -const APP_DOWNLOAD_URL = 'https://app.onehook.club'; - function isMobileDevice(): boolean { if (typeof navigator === 'undefined') return false; return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); @@ -110,16 +106,12 @@ export function SiteHeader({ pageTitle, onApply }: { pageTitle?: string; onApply {mobile ? (
- {/* Mobile menu: Open-in-app */} - Open in appApp - + {/* once the name has collapsed to the logo, also offer a plain way back home */} {compact && (
diff --git a/src/components/messenger/Messenger.tsx b/src/components/messenger/Messenger.tsx index 4306862..3f5504a 100644 --- a/src/components/messenger/Messenger.tsx +++ b/src/components/messenger/Messenger.tsx @@ -35,7 +35,7 @@ import { Finale, ProgressBar, useFullScreenEnding } from '../science/Science'; import { StoryTimelineProvider, bandIndex, makeBands, usePassed, useSceneClock, useSceneProgress, useTimeline, useVisualProgress } from './storyTimeline'; import { AppleIcon, GoogleIcon } from '../common/BrandIcons'; import { UiElement, trackProps } from '../../lib/analytics/analytics'; -import { APP_DOWNLOAD_URL } from '../../config/membership.config'; +import { GetAppLink } from '../common/GetAppLink'; /** * /messenger — one continuous, scroll-driven story on a light page. @@ -2782,15 +2782,11 @@ export function Messenger() { initial={false} animate={{ opacity: 1 }} > - Get the app - +