Compare commits
No commits in common. "4f553147ed7ba97432adc97a4d37e4db9648f68b" and "c82f0969a83cb0531770612af16974d6000fa5cb" have entirely different histories.
4f553147ed
...
c82f0969a8
5 changed files with 49 additions and 126 deletions
|
|
@ -22,17 +22,11 @@ Feature: Public landing page at the root
|
||||||
When I click the link "Créer un compte"
|
When I click the link "Créer un compte"
|
||||||
Then the URL should include "/signup"
|
Then the URL should include "/signup"
|
||||||
|
|
||||||
Scenario: The landing page offers the Android app download, on desktop
|
Scenario: The landing page offers the Android app download
|
||||||
Given I am not signed in
|
Given I am not signed in
|
||||||
When I visit "/"
|
When I visit "/"
|
||||||
Then I should see the Android download link
|
Then I should see the Android download link
|
||||||
|
|
||||||
Scenario: A mobile visitor is offered to open the app first, not just a download link
|
|
||||||
Given I am not signed in
|
|
||||||
When I visit "/" as an Android phone browser
|
|
||||||
Then I should see the button to open the app, with sign-up demoted to secondary
|
|
||||||
And the Android download link should not be shown
|
|
||||||
|
|
||||||
Scenario: Another guarded route still redirects a logged-out visitor to sign in
|
Scenario: Another guarded route still redirects a logged-out visitor to sign in
|
||||||
Given I am not signed in
|
Given I am not signed in
|
||||||
When I visit "/recettes"
|
When I visit "/recettes"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
import { Then } from "@badeball/cypress-cucumber-preprocessor";
|
||||||
|
|
||||||
// Landing-page-specific steps. Everything else this feature needs
|
// Landing-page-specific steps. Everything else this feature needs
|
||||||
// (signing in / not, visiting a path, URL and heading assertions) comes
|
// (signing in / not, visiting a path, URL and heading assertions) comes
|
||||||
|
|
@ -15,50 +15,19 @@ Then("I should see the landing call to action", () => {
|
||||||
.and("have.attr", "href", "/login");
|
.and("have.attr", "href", "/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Issue #85: desktop only now (see LandingPage.tsx) — routed through
|
// The Android build is distributed as a Forgejo release asset with a stable
|
||||||
// `/telecharger` (DownloadPage.tsx, issue #84's fallback page) rather than
|
// filename (issue #64 builds/attaches it). Points at this API's own
|
||||||
// linking the Android build's actual `/mobile/download` redirect directly,
|
// `/mobile/download` (a plain server-side redirect to the actual latest
|
||||||
// so "get the app" has exactly one canonical destination app-wide.
|
// `.apk`) rather than a Forgejo URL directly: Forgejo has neither a
|
||||||
|
// GitHub-style `/releases/latest/download/<file>` shortcut nor CORS headers
|
||||||
|
// on its Releases API, so resolving the real asset URL client-side isn't
|
||||||
|
// possible either way (both verified live). `VITE_API_URL` is unset in this
|
||||||
|
// environment, so `API_BASE_URL` (see `api/client.ts`) is `""` — same
|
||||||
|
// origin, hence the bare `/mobile/download` path here rather than an
|
||||||
|
// absolute URL. We only assert the link is there and points at the right
|
||||||
|
// path — the redirect target itself is external to the suite.
|
||||||
Then("I should see the Android download link", () => {
|
Then("I should see the Android download link", () => {
|
||||||
cy.get(".landing__hero .landing__mobile-link")
|
cy.get(".landing__hero .landing__mobile-link")
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.and("have.attr", "href", "/telecharger");
|
.and("have.attr", "href", "/mobile/download");
|
||||||
});
|
|
||||||
|
|
||||||
// Spoofs a real Android phone Chrome UA via `onBeforeLoad` — the one place
|
|
||||||
// `isAndroidPhoneBrowser()` (`lib/mobile-app-redirect.ts`) actually reads
|
|
||||||
// from, so this is the only way to reach the landing page's mobile-only
|
|
||||||
// branch from a real page visit (Cypress's own Electron browser always
|
|
||||||
// reports a desktop UA otherwise). Same literal UA string already used by
|
|
||||||
// `mobile-app-redirect.cy.tsx`'s own `isAndroidPhoneBrowser` unit test.
|
|
||||||
When("I visit {string} as an Android phone browser", (path: string) => {
|
|
||||||
cy.visit(path, {
|
|
||||||
onBeforeLoad(win) {
|
|
||||||
Object.defineProperty(win.navigator, "userAgent", {
|
|
||||||
value:
|
|
||||||
"Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36",
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Then("I should see the button to open the app, with sign-up demoted to secondary", () => {
|
|
||||||
// Deliberately never clicked here: its `onClick` calls
|
|
||||||
// `attemptOpenNativeApp()` with the real (production) `openAppUrl`, a
|
|
||||||
// genuine `window.location.href` to an unrecognized custom scheme — that
|
|
||||||
// hangs the Cypress/Electron runner indefinitely rather than safely
|
|
||||||
// no-op'ing (see dev-environment-gotchas.md). The function itself is
|
|
||||||
// already fully unit-tested with an injected stub in
|
|
||||||
// mobile-app-redirect.cy.tsx; this only covers LandingPage's own
|
|
||||||
// conditional rendering.
|
|
||||||
cy.get(".landing__hero .landing__cta--primary")
|
|
||||||
.should("be.visible")
|
|
||||||
.and("contain.text", "Ouvrir dans l'application");
|
|
||||||
cy.contains(".landing__hero .landing__cta--secondary", "Créer un compte").should("be.visible");
|
|
||||||
cy.contains(".landing__hero .landing__cta--secondary", "Se connecter").should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
Then("the Android download link should not be shown", () => {
|
|
||||||
cy.get(".landing__hero .landing__mobile-link").should("not.exist");
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1336,7 +1336,6 @@
|
||||||
"lede": "Planifiez vos repas, importez des recettes depuis vos sites préférés, et générez la liste de courses qui va avec — pour tout le foyer, sans y penser deux fois.",
|
"lede": "Planifiez vos repas, importez des recettes depuis vos sites préférés, et générez la liste de courses qui va avec — pour tout le foyer, sans y penser deux fois.",
|
||||||
"ctaSignup": "Créer un compte",
|
"ctaSignup": "Créer un compte",
|
||||||
"ctaLogin": "Se connecter",
|
"ctaLogin": "Se connecter",
|
||||||
"ctaOpenApp": "Ouvrir dans l'application",
|
|
||||||
"mobileDownload": "Télécharger pour Android",
|
"mobileDownload": "Télécharger pour Android",
|
||||||
"mobileHint": "Fichier .apk à installer soi-même : Android demandera d'autoriser l'installation depuis une source inconnue. Ce n'est pas une version du Play Store, et un compte reste nécessaire."
|
"mobileHint": "Fichier .apk à installer soi-même : Android demandera d'autoriser l'installation depuis une source inconnue. Ce n'est pas une version du Play Store, et un compte reste nécessaire."
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { CalendarDays, HeartPulse, Smartphone, Sparkles, Users } from "lucide-react";
|
import { CalendarDays, HeartPulse, Smartphone, Sparkles, Users } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { attemptOpenNativeApp, isAndroidPhoneBrowser } from "../../lib/mobile-app-redirect";
|
import { API_BASE_URL } from "../../api/client";
|
||||||
import "./landing-page.scss";
|
import "./landing-page.scss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -22,17 +22,6 @@ import "./landing-page.scss";
|
||||||
*/
|
*/
|
||||||
export function LandingPage() {
|
export function LandingPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
// UA-based, not `useIsMobileViewport()` — same reasoning as
|
|
||||||
// `attemptOpenNativeApp`'s own doc comment: this decides whether trying
|
|
||||||
// to open the *native app* makes sense at all (a platform question), not
|
|
||||||
// whether the viewport is narrow (a resized desktop window shouldn't
|
|
||||||
// attempt a scheme navigation just because it's a small window).
|
|
||||||
const isMobile = isAndroidPhoneBrowser();
|
|
||||||
|
|
||||||
function handleOpenApp() {
|
|
||||||
attemptOpenNativeApp({ onFallback: () => navigate("/telecharger") });
|
|
||||||
}
|
|
||||||
|
|
||||||
const features: { icon: ReactNode; key: "planning" | "import" | "health" | "household" }[] = [
|
const features: { icon: ReactNode; key: "planning" | "import" | "health" | "household" }[] = [
|
||||||
{ icon: <CalendarDays aria-hidden />, key: "planning" },
|
{ icon: <CalendarDays aria-hidden />, key: "planning" },
|
||||||
|
|
@ -55,38 +44,7 @@ export function LandingPage() {
|
||||||
<h1>{t("landing.hero.title")}</h1>
|
<h1>{t("landing.hero.title")}</h1>
|
||||||
<p className="landing__lede">{t("landing.hero.lede")}</p>
|
<p className="landing__lede">{t("landing.hero.lede")}</p>
|
||||||
<div className="landing__cta-row">
|
<div className="landing__cta-row">
|
||||||
{/*
|
<Link className="landing__cta landing__cta--primary" to="/signup">
|
||||||
* Issue #85: on a real Android phone browser, opening/getting
|
|
||||||
* the app is more useful than a web account — the web
|
|
||||||
* experience isn't optimized for mobile at all (the whole
|
|
||||||
* point of this chantier), and someone with the app already
|
|
||||||
* installed can sign up/log in from inside it just as well.
|
|
||||||
* So this becomes the *primary* action, demoting Signup/Login
|
|
||||||
* to secondary rather than hiding them — some visitors
|
|
||||||
* genuinely want the browser flow, e.g. to try it before
|
|
||||||
* installing anything. Desktop is untouched: a scheme-open
|
|
||||||
* attempt makes no sense there, so Signup stays primary and
|
|
||||||
* this button doesn't render at all (see the plain `/telecharger`
|
|
||||||
* link below instead, replacing the old direct-APK link).
|
|
||||||
*/}
|
|
||||||
{isMobile && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="landing__cta landing__cta--primary"
|
|
||||||
onClick={handleOpenApp}
|
|
||||||
>
|
|
||||||
<Smartphone aria-hidden />
|
|
||||||
{t("landing.hero.ctaOpenApp")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
className={
|
|
||||||
isMobile
|
|
||||||
? "landing__cta landing__cta--secondary"
|
|
||||||
: "landing__cta landing__cta--primary"
|
|
||||||
}
|
|
||||||
to="/signup"
|
|
||||||
>
|
|
||||||
{t("landing.hero.ctaSignup")}
|
{t("landing.hero.ctaSignup")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link className="landing__cta landing__cta--secondary" to="/login">
|
<Link className="landing__cta landing__cta--secondary" to="/login">
|
||||||
|
|
@ -94,19 +52,31 @@ export function LandingPage() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
* Desktop only — a scheme-open attempt (the mobile button above)
|
* Secondary action — an account is needed on any platform, so the
|
||||||
* has no meaning here. Routed through `/telecharger` (the same
|
* Android build is a discreet link under the CTA row, not a third
|
||||||
* fallback page a failed mobile attempt lands on) rather than
|
* button. Points at this API's own `/mobile/download` — a plain
|
||||||
* linking `${API_BASE_URL}/mobile/download` directly, so "get
|
* redirect straight to the latest release's actual `.apk`,
|
||||||
* the app" has exactly one canonical destination app-wide
|
* resolved server-side (see `mobile.service.ts`) — rather than a
|
||||||
* instead of two slightly different mechanisms to keep in sync.
|
* Forgejo URL directly: Forgejo, unlike GitHub, has no
|
||||||
|
* `/releases/latest/download/<file>` shortcut (only
|
||||||
|
* `/releases/latest` itself resolves, to the release's HTML page,
|
||||||
|
* not a file), and its Releases API sends no CORS headers, so a
|
||||||
|
* client-side fetch to resolve the direct URL ourselves isn't an
|
||||||
|
* option either — both verified live. `target="_blank"` so the
|
||||||
|
* redirect-then-download happens in a new tab, never navigating
|
||||||
|
* this one away. The hint is honest that this is a sideloaded
|
||||||
|
* .apk, not a Play Store listing.
|
||||||
*/}
|
*/}
|
||||||
{!isMobile && (
|
<a
|
||||||
<Link className="landing__mobile-link" to="/telecharger">
|
className="landing__mobile-link"
|
||||||
|
href={`${API_BASE_URL}/mobile/download`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
<Smartphone aria-hidden />
|
<Smartphone aria-hidden />
|
||||||
{t("landing.hero.mobileDownload")}
|
{t("landing.hero.mobileDownload")}
|
||||||
</Link>
|
</a>
|
||||||
)}
|
<p className="landing__mobile-hint">{t("landing.hero.mobileHint")}</p>
|
||||||
</div>
|
</div>
|
||||||
<HeroIllustration />
|
<HeroIllustration />
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -89,17 +89,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing__cta {
|
.landing__cta {
|
||||||
// Issue #85: the mobile "Ouvrir dans l'application" CTA is a real
|
|
||||||
// `<button>` (it runs `attemptOpenNativeApp`, not a navigation), unlike
|
|
||||||
// every other `.landing__cta` here, which are `<Link>`s — both need to
|
|
||||||
// look pixel-identical, so `appearance: none` strips the native button
|
|
||||||
// chrome that `--primary`/`--secondary` below don't already override
|
|
||||||
// (they redefine `background`/`border`/`color`, but not this).
|
|
||||||
appearance: none;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--space-xs);
|
|
||||||
padding: var(--space-sm) var(--space-lg);
|
padding: var(--space-sm) var(--space-lg);
|
||||||
border-radius: var(--radius-base);
|
border-radius: var(--radius-base);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
|
|
@ -108,12 +100,6 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--primary {
|
&--primary {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
@ -137,9 +123,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Android download link (issue #85: desktop only now — a real Android
|
// --- Android download (discreet, under the CTA row) --------------------
|
||||||
// phone browser gets the prominent "Ouvrir dans l'application" `.landing__
|
|
||||||
// cta` button above instead, see LandingPage.tsx) ---------------------------
|
|
||||||
.landing__mobile-link {
|
.landing__mobile-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -160,6 +144,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.landing__mobile-hint {
|
||||||
|
margin: var(--space-xs) 0 0;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
max-width: 34rem;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Features -----------------------------------------------------------
|
// --- Features -----------------------------------------------------------
|
||||||
.landing__features h2,
|
.landing__features h2,
|
||||||
.landing__preview h2 {
|
.landing__preview h2 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue