FRE-1670: publish /terms and the support address on the Nessa site

nessa.freno.me/terms was a live 404 although AppSettings.termsOfServiceURL
links it from Settings > Legal and the paywall. The document existed only on
an unmerged branch and had no host-dispatch route, so it could not serve on
the subdomain. Carry the Terms of Use page, add the /terms dispatcher, and
publish nessa-support@freno.me on terms, privacy, contact and deletion.

- src/routes/terms.tsx: host-aware dispatch for nessa; other sites 404.
- src/routes/nessa/terms.tsx: the Terms page; its contact section names the
  support address.
- privacy: add the controller clause and the support address.
- contact/deletion: show the support address; deletion also accepts a
  request by email.
- subdomain-dispatch.test.ts: list directories before filtering to .tsx
  files. The old predicate only ever received file names, so it asserted
  over an empty list and missed the missing /terms dispatch.
- support-contact.test.ts: guard that every contact route renders the one
  address the app shows.

App side unchanged: termsOfServiceURL stays https://nessa.freno.me/terms.
This commit is contained in:
2026-11-09 16:14:14 +00:00
committed by fixture
parent 97755042cb
commit 71bb9ec5df
8 changed files with 161 additions and 21 deletions

View File

@@ -1,6 +1,7 @@
import { ContactForm } from "~/components/ContactForm";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader";
import { SUPPORT_EMAIL } from "./content";
/**
* Nessa contact page (`nessa.freno.me/contact`).
@@ -16,14 +17,27 @@ import SubdomainHeader from "~/components/SubdomainHeader";
*
* Acceptance: `nessa.localhost:3000/contact` renders the contact form with
* Nessa branding; submissions email `michael@freno.me` with subject
* `[Nessa] Contact Request`.
* `[Nessa] Contact Request`. The support address shown by the app is also
* published here, so the contact route does not depend on the form alone
* (FRE-1670).
*/
export default function NessaContactPage() {
return (
<>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<SubdomainHeader />
<ContactForm />
<ContactForm>
<p class="mt-8 text-center text-sm opacity-80">
You can also email us directly at{" "}
<a
href={`mailto:${SUPPORT_EMAIL}`}
class="text-blue hover-underline-animation"
>
{SUPPORT_EMAIL}
</a>
.
</p>
</ContactForm>
</>
);
}

View File

@@ -14,6 +14,15 @@ export const TAGLINE = "The fitness app that puts you first." as const;
export const SUBTITLE =
"Track, train, and connect — without the paywall." as const;
/**
* The support address the Nessa app shows (`SubscriptionView` /
* `HelpSupportView`). Every published Nessa document that offers a contact
* route names this same address, so a user who leaves the app to read the
* terms, privacy policy, contact page, or deletion page can still reach us
* (FRE-1670). Keep in step with `nessa-support@freno.me` in the app.
*/
export const SUPPORT_EMAIL = "nessa-support@freno.me" as const;
export const ICON_DEFAULT =
"/Nessa Exports/Nessa-iOS-Default-1024x1024.png" as const;
export const ICON_DARK = "/Nessa Exports/Nessa-iOS-Dark-1024x1024.png" as const;

View File

@@ -33,6 +33,7 @@ import {
DELETION_GRACE_PERIOD_LABEL,
PAGE_META
} from "~/routes/nessa/deletion-content";
import { SUPPORT_EMAIL } from "./content";
export default function NessaDeletionPage() {
return (
@@ -54,6 +55,18 @@ export default function NessaDeletionPage() {
removed. No data related to the account is retained in any way.
</div>
<div class="text-text mt-4 w-full justify-center">
Prefer email? Send your deletion request from the address on your
Nessa account to{" "}
<a
href={`mailto:${SUPPORT_EMAIL}?subject=Account%20Deletion%20Request`}
class="text-blue hover-underline-animation"
>
{SUPPORT_EMAIL}
</a>
; the same {DELETION_GRACE_PERIOD_LABEL} grace period applies.
</div>
<DeletionForm
product={DELETION_PRODUCT_KEY}
cookieName={DELETION_COOKIE_NAME}

View File

@@ -30,6 +30,7 @@ import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader";
import { SUPPORT_EMAIL } from "./content";
export default function NessaPrivacyPolicy() {
return (
@@ -174,13 +175,27 @@ export default function NessaPrivacyPolicy() {
<span class="-ml-4 pr-2">5.</span> Contact Us
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Reaching Out:</div> If there are any
questions or comments regarding this privacy policy, you can
contact us{" "}
<A href="/contact" class="text-blue hover-underline-animation">
here
</A>
.
<div class="-ml-6">(a) Data Controller:</div> Nessa is operated by
FrenoCorp. For the purposes of applicable data-protection law,
FrenoCorp is the controller of the personal information described
in this policy.
<div class="pt-2 pb-2">
<div class="-ml-6">(b) Reaching Out:</div> If there are any
questions or comments regarding this privacy policy, or you want
to access, correct, or delete your personal information, you can
contact us{" "}
<A href="/contact" class="text-blue hover-underline-animation">
through the contact form
</A>{" "}
or email{" "}
<a
href={`mailto:${SUPPORT_EMAIL}`}
class="text-blue hover-underline-animation"
>
{SUPPORT_EMAIL}
</a>
.
</div>
</div>
</div>
</ol>

View File

@@ -0,0 +1,36 @@
/**
* Regression guard for the published Nessa support address (FRE-1670).
*
* The app tells users to mail `nessa-support@freno.me` from the subscription
* screen and Help & Support. Before this guard, no published Nessa page named
* that address: a user who left the app to read the terms, privacy policy,
* contact page, or deletion page found no way to reach support. That is a
* privacy-policy contact gap (GDPR/CCPA-style) and an App Review question
* waiting to happen.
*
* The address lives in one place (`SUPPORT_EMAIL`), and every page that
* offers a contact route must render it. Reading the route sources is
* deliberate: the defect is a page that exists but does not mention the
* address, which no runtime assertion over a single page can observe.
*/
import { describe, it, expect } from "bun:test";
import { readFileSync } from "fs";
import { SUPPORT_EMAIL } from "./content";
const CONTACT_ROUTE_FILES = [
"src/routes/nessa/terms.tsx",
"src/routes/nessa/privacy.tsx",
"src/routes/nessa/contact.tsx",
"src/routes/nessa/deletion.tsx"
];
describe("Nessa published support address", () => {
it("is the address the app shows", () => {
expect(SUPPORT_EMAIL).toBe("nessa-support@freno.me");
});
it.each(CONTACT_ROUTE_FILES)("%s renders it", (file) => {
const source = readFileSync(file, "utf8");
expect(source).toContain("SUPPORT_EMAIL");
});
});

View File

@@ -26,6 +26,7 @@ import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader";
import { SUPPORT_EMAIL } from "./content";
export default function NessaTermsOfUse() {
return (
@@ -38,7 +39,7 @@ export default function NessaTermsOfUse() {
<SubdomainHeader />
<div class="min-h-screen px-[8vw] py-[10vh]">
<div class="py-4 text-xl">Nessa&apos;s Terms of Use</div>
<div class="py-2">Last Updated: November 7, 2026</div>
<div class="py-2">Last Updated: November 9, 2026</div>
<div class="py-2">
Welcome to Nessa (&apos;We&apos;, &apos;Us&apos;, &apos;Our&apos;).
These terms of use govern your access to and use of the Nessa app and
@@ -434,8 +435,15 @@ export default function NessaTermsOfUse() {
questions or comments about these terms, want to report content,
or need help with a subscription, you can contact us{" "}
<A href="/contact" class="text-blue hover-underline-animation">
here
</A>
through the contact form
</A>{" "}
or email{" "}
<a
href={`mailto:${SUPPORT_EMAIL}`}
class="text-blue hover-underline-animation"
>
{SUPPORT_EMAIL}
</a>
.
</div>
</div>