Compare commits
2 Commits
a0e6ff1044
...
freno-dev/
| Author | SHA1 | Date | |
|---|---|---|---|
| 71bb9ec5df | |||
| 97755042cb |
@@ -67,7 +67,8 @@ describe("generateSitemap", () => {
|
|||||||
expect(locs).toContain("https://nessa.freno.me/");
|
expect(locs).toContain("https://nessa.freno.me/");
|
||||||
expect(locs).toContain("https://nessa.freno.me/contact");
|
expect(locs).toContain("https://nessa.freno.me/contact");
|
||||||
expect(locs).toContain("https://nessa.freno.me/privacy");
|
expect(locs).toContain("https://nessa.freno.me/privacy");
|
||||||
expect(locs.length).toBe(3);
|
expect(locs).toContain("https://nessa.freno.me/terms");
|
||||||
|
expect(locs.length).toBe(4);
|
||||||
|
|
||||||
for (const loc of locs) {
|
for (const loc of locs) {
|
||||||
expect(loc).not.toContain("://freno.me/");
|
expect(loc).not.toContain("://freno.me/");
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export const SITEMAP_ROUTES: Record<SiteId, SitemapEntry[]> = {
|
|||||||
nessa: [
|
nessa: [
|
||||||
{ path: "/", changefreq: "weekly", priority: 1.0 },
|
{ path: "/", changefreq: "weekly", priority: 1.0 },
|
||||||
{ path: "/contact", changefreq: "monthly", priority: 0.6 },
|
{ path: "/contact", changefreq: "monthly", priority: 0.6 },
|
||||||
{ path: "/privacy", changefreq: "yearly", priority: 0.4 }
|
{ path: "/privacy", changefreq: "yearly", priority: 0.4 },
|
||||||
|
{ path: "/terms", changefreq: "yearly", priority: 0.4 }
|
||||||
],
|
],
|
||||||
|
|
||||||
lineage: [
|
lineage: [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ContactForm } from "~/components/ContactForm";
|
import { ContactForm } from "~/components/ContactForm";
|
||||||
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
|
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
|
||||||
import SubdomainHeader from "~/components/SubdomainHeader";
|
import SubdomainHeader from "~/components/SubdomainHeader";
|
||||||
|
import { SUPPORT_EMAIL } from "./content";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nessa contact page (`nessa.freno.me/contact`).
|
* 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
|
* Acceptance: `nessa.localhost:3000/contact` renders the contact form with
|
||||||
* Nessa branding; submissions email `michael@freno.me` with subject
|
* 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() {
|
export default function NessaContactPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
|
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
|
||||||
<SubdomainHeader />
|
<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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ export const TAGLINE = "The fitness app that puts you first." as const;
|
|||||||
export const SUBTITLE =
|
export const SUBTITLE =
|
||||||
"Track, train, and connect — without the paywall." as const;
|
"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 =
|
export const ICON_DEFAULT =
|
||||||
"/Nessa Exports/Nessa-iOS-Default-1024x1024.png" as const;
|
"/Nessa Exports/Nessa-iOS-Default-1024x1024.png" as const;
|
||||||
export const ICON_DARK = "/Nessa Exports/Nessa-iOS-Dark-1024x1024.png" as const;
|
export const ICON_DARK = "/Nessa Exports/Nessa-iOS-Dark-1024x1024.png" as const;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
DELETION_GRACE_PERIOD_LABEL,
|
DELETION_GRACE_PERIOD_LABEL,
|
||||||
PAGE_META
|
PAGE_META
|
||||||
} from "~/routes/nessa/deletion-content";
|
} from "~/routes/nessa/deletion-content";
|
||||||
|
import { SUPPORT_EMAIL } from "./content";
|
||||||
|
|
||||||
export default function NessaDeletionPage() {
|
export default function NessaDeletionPage() {
|
||||||
return (
|
return (
|
||||||
@@ -54,6 +55,18 @@ export default function NessaDeletionPage() {
|
|||||||
removed. No data related to the account is retained in any way.
|
removed. No data related to the account is retained in any way.
|
||||||
</div>
|
</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
|
<DeletionForm
|
||||||
product={DELETION_PRODUCT_KEY}
|
product={DELETION_PRODUCT_KEY}
|
||||||
cookieName={DELETION_COOKIE_NAME}
|
cookieName={DELETION_COOKIE_NAME}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { A } from "@solidjs/router";
|
|||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
|
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
|
||||||
import SubdomainHeader from "~/components/SubdomainHeader";
|
import SubdomainHeader from "~/components/SubdomainHeader";
|
||||||
|
import { SUPPORT_EMAIL } from "./content";
|
||||||
|
|
||||||
export default function NessaPrivacyPolicy() {
|
export default function NessaPrivacyPolicy() {
|
||||||
return (
|
return (
|
||||||
@@ -174,13 +175,27 @@ export default function NessaPrivacyPolicy() {
|
|||||||
<span class="-ml-4 pr-2">5.</span> Contact Us
|
<span class="-ml-4 pr-2">5.</span> Contact Us
|
||||||
</div>
|
</div>
|
||||||
<div class="pb-2 pl-4">
|
<div class="pb-2 pl-4">
|
||||||
<div class="-ml-6">(a) Reaching Out:</div> If there are any
|
<div class="-ml-6">(a) Data Controller:</div> Nessa is operated by
|
||||||
questions or comments regarding this privacy policy, you can
|
FrenoCorp. For the purposes of applicable data-protection law,
|
||||||
contact us{" "}
|
FrenoCorp is the controller of the personal information described
|
||||||
<A href="/contact" class="text-blue hover-underline-animation">
|
in this policy.
|
||||||
here
|
<div class="pt-2 pb-2">
|
||||||
</A>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
36
src/routes/nessa/support-contact.test.ts
Normal file
36
src/routes/nessa/support-contact.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
454
src/routes/nessa/terms.tsx
Normal file
454
src/routes/nessa/terms.tsx
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
/**
|
||||||
|
* Nessa terms of use — `nessa.freno.me/terms`.
|
||||||
|
*
|
||||||
|
* The document behind the `Terms of Service` row in the Nessa app
|
||||||
|
* (`Nessa/Models/AppSettings.swift`, Settings › Legal). Before this page the
|
||||||
|
* row had no Nessa-owned document to resolve to: the app linked another
|
||||||
|
* product's terms, and `nessa.freno.me/terms` was a 404 (FRE-194).
|
||||||
|
*
|
||||||
|
* Nessa sells an auto-renewing subscription through the App Store, so App
|
||||||
|
* Review 3.1.2 requires the app to link a privacy policy **and** terms of
|
||||||
|
* use. Section 5 (subscriptions) and section 6 (Apple acknowledgements) are
|
||||||
|
* the clauses that requirement is read against, so keep them when editing.
|
||||||
|
*
|
||||||
|
* House voice follows `privacy.tsx`: "we" / "Nessa", no legal entity name,
|
||||||
|
* and no governing-law clause — the Nessa pages have never stated either, and
|
||||||
|
* naming an entity or a jurisdiction is a decision to make deliberately, not
|
||||||
|
* to introduce as a side effect of a link fix.
|
||||||
|
*
|
||||||
|
* PageHead is site-aware: only the base title is supplied; the ` | Nessa`
|
||||||
|
* suffix, the `https://nessa.freno.me/terms` canonical and the OG image are
|
||||||
|
* derived automatically. Internal links use public subdomain-relative paths
|
||||||
|
* (`/privacy`, `/deletion`, `/contact`) per nav-config.ts and
|
||||||
|
* page-head-meta.ts.
|
||||||
|
*/
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<EdgeCacheHeaders maxAge={300} />
|
||||||
|
<PageHead
|
||||||
|
title="Terms of Use"
|
||||||
|
description="Terms of use for Nessa, a fitness app for tracking workouts and training with clubs, challenges, and friends."
|
||||||
|
/>
|
||||||
|
<SubdomainHeader />
|
||||||
|
<div class="min-h-screen px-[8vw] py-[10vh]">
|
||||||
|
<div class="py-4 text-xl">Nessa's Terms of Use</div>
|
||||||
|
<div class="py-2">Last Updated: November 9, 2026</div>
|
||||||
|
<div class="py-2">
|
||||||
|
Welcome to Nessa ('We', 'Us', 'Our').
|
||||||
|
These terms of use govern your access to and use of the Nessa app and
|
||||||
|
its related services. By using Nessa you agree to these terms. Our{" "}
|
||||||
|
<A href="/privacy" class="text-blue hover-underline-animation">
|
||||||
|
privacy policy
|
||||||
|
</A>{" "}
|
||||||
|
explains how we handle your data, and it forms part of these terms.
|
||||||
|
</div>
|
||||||
|
<ol>
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">1.</span> Acceptance of These Terms
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Agreement:</div> By creating an account,
|
||||||
|
downloading, or otherwise using Nessa you agree to these terms.
|
||||||
|
If you do not agree to them, do not use Nessa.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Eligibility:</div> You must be at least
|
||||||
|
the age of digital consent in your country to use Nessa. If you
|
||||||
|
are using Nessa with a parent's or guardian's
|
||||||
|
account, they are responsible for your compliance with these
|
||||||
|
terms.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Where these terms apply:</div> These
|
||||||
|
terms are between you and us. Where you obtained Nessa through
|
||||||
|
a third party such as the Apple App Store, that
|
||||||
|
party's own terms apply to your relationship with them as
|
||||||
|
well.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">2.</span> The Service
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) What Nessa is:</div> Nessa is a fitness
|
||||||
|
application for iOS, with an Apple Watch companion, that lets
|
||||||
|
you record workouts and routes, review your training history,
|
||||||
|
and take part in community features — clubs, club challenges,
|
||||||
|
posts, comments, and leaderboards. Some features are free and
|
||||||
|
some require a paid subscription (see section 5).
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Changes and availability:</div> We are
|
||||||
|
continually improving Nessa, so features may change, move
|
||||||
|
between tiers, or be withdrawn. We do not guarantee that Nessa
|
||||||
|
will be available at all times, and we may suspend or discontinue
|
||||||
|
part or all of the service. Where a change materially affects a
|
||||||
|
paid feature you have subscribed to, we will tell you on the
|
||||||
|
subscription management page or by contact.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Not medical advice:</div> Nessa is not a
|
||||||
|
medical device and is not intended to diagnose, treat, cure, or
|
||||||
|
prevent any condition. Workout data, heart-rate and other
|
||||||
|
health readings, training plans, and estimates you see in Nessa
|
||||||
|
are for general information and motivation only. Consult a
|
||||||
|
qualified professional before starting or changing a training
|
||||||
|
programme, especially if you have a health condition. Never
|
||||||
|
ignore professional advice, or delay seeking it, because of
|
||||||
|
something you read in Nessa.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(d) Your surroundings:</div> You are
|
||||||
|
responsible for your own safety while using Nessa, including
|
||||||
|
when recording an activity. Follow local laws and do not use
|
||||||
|
Nessa in a way that distracts you from driving or other
|
||||||
|
activities that require your full attention.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">3.</span> Your Account
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Sign-in:</div> Nessa accounts are
|
||||||
|
authenticated through Clerk, our identity provider. You are
|
||||||
|
responsible for keeping the credentials you use to sign in
|
||||||
|
secure, and you are responsible for the activity that happens
|
||||||
|
under your account.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Notify us:</div> Tell us promptly, using
|
||||||
|
the contact details in section 11, if you believe someone else
|
||||||
|
has used or may use your account without your permission.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) One account:</div> Keep to one account,
|
||||||
|
and give us accurate information so we can keep your training
|
||||||
|
history and your place in the community correct. Do not create
|
||||||
|
accounts to evade a moderation decision or to gain a fair
|
||||||
|
advantage in challenges or leaderboards.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">4.</span> Community Content and Acceptable
|
||||||
|
Use
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) You keep your content:</div> You own what
|
||||||
|
you post. By posting to a club, challenge, or other shared area
|
||||||
|
of Nessa you grant us a limited, non-exclusive licence to store,
|
||||||
|
reproduce, and display that content inside Nessa so that the
|
||||||
|
feature you used can work — for example, showing your post to
|
||||||
|
the members of your club. That licence ends when your content is
|
||||||
|
removed from our systems. We handle your content as described in
|
||||||
|
our{" "}
|
||||||
|
<A href="/privacy" class="text-blue hover-underline-animation">
|
||||||
|
privacy policy
|
||||||
|
</A>
|
||||||
|
; we do not sell it.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) What you are responsible for:</div> You
|
||||||
|
are responsible for the content you post and the data you record
|
||||||
|
in Nessa, and for who you share it with. Use the privacy
|
||||||
|
settings on your profile and the privacy rules of the clubs you
|
||||||
|
join before sharing anything you would not want others to see.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Do not:</div> (i) post anything unlawful,
|
||||||
|
harassing, hateful, threatening, or sexually explicit involving
|
||||||
|
minors; (ii) post content you do not have the right to share, or
|
||||||
|
that infringes someone else's intellectual property;
|
||||||
|
(iii) falsify activity data, spoof a location, or otherwise
|
||||||
|
cheat a challenge, leaderboard, or competition; (iv) scrape,
|
||||||
|
bulk-download, or access Nessa through automated means we have
|
||||||
|
not authorised; (v) interfere with, overload, or probe Nessa for
|
||||||
|
vulnerabilities, or attempt to access another user's
|
||||||
|
account or data; (vi) resell, sublicense, or offer Nessa to
|
||||||
|
others as your own service.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(d) Moderation:</div> We may remove or
|
||||||
|
restrict content, disqualify an entry from a challenge, or
|
||||||
|
suspend or close an account that breaks these terms. We are not
|
||||||
|
obliged to pre-monitor content. You can report content or a user
|
||||||
|
to us at any time using the contact details in section 11.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">5.</span> Subscriptions and Payments
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Paid tiers:</div> Nessa offers paid
|
||||||
|
subscription tiers, Nessa Plus and Nessa Pro, which unlock
|
||||||
|
features described in the app. Subscriptions are sold as
|
||||||
|
auto-renewing subscriptions through the Apple App Store and are
|
||||||
|
billed to your Apple Account.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Payment:</div> Payment is charged to your
|
||||||
|
Apple Account at confirmation of purchase. The price of your
|
||||||
|
plan is shown in the app before you buy, and it is the price on
|
||||||
|
your App Store purchase receipt.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Auto-renewal:</div> Unless you cancel at
|
||||||
|
least 24 hours before the end of the current period, your
|
||||||
|
subscription renews automatically. Your account is charged for
|
||||||
|
renewal within 24 hours before the end of the current period, at
|
||||||
|
the price of the plan you selected.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(d) Managing your subscription:</div> You can
|
||||||
|
turn off auto-renewal, or cancel, at any time in your App Store
|
||||||
|
account settings after purchase. Changes made there take effect
|
||||||
|
at the end of the current period, and you keep access to the
|
||||||
|
paid features you have already paid for until then.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(e) Trials:</div> If we offer a free trial,
|
||||||
|
any unused portion of it is forfeited when you purchase a
|
||||||
|
subscription, and the trial converts to a paid auto-renewing
|
||||||
|
subscription unless you cancel it before it ends.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(f) Refunds:</div> Purchases made through the
|
||||||
|
App Store are refunded according to Apple's refund policy
|
||||||
|
and, where applicable law gives you a right to a refund,
|
||||||
|
according to that law. Tell us using the contact details in
|
||||||
|
section 11 and we will do what we can.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(g) Price changes:</div> We may change our
|
||||||
|
prices. A price change does not apply to a period you have
|
||||||
|
already paid for; it applies from your next renewal, and we give
|
||||||
|
notice where the law requires it.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(h) Downgrade:</div> When a subscription
|
||||||
|
lapses or is cancelled you keep the free features, and anything
|
||||||
|
you created that belongs to a paid feature (such as a custom
|
||||||
|
route) is kept but is not editable or usable until you
|
||||||
|
subscribe again.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">6.</span> Apple and Other Third Parties
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Not Apple's terms:</div> These terms
|
||||||
|
are between you and us, not with Apple Inc. Apple is not
|
||||||
|
responsible for Nessa or its content, and your use of Nessa must
|
||||||
|
comply with the Apple App Store Terms of Service.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) No Apple support:</div> Apple has no
|
||||||
|
obligation to furnish maintenance and support services for
|
||||||
|
Nessa. If Nessa fails, you may notify Apple, but Apple has no
|
||||||
|
further support obligation, and support and any warranties
|
||||||
|
relating to Nessa are ours to provide as described in these
|
||||||
|
terms.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Apple is not responsible for claims:</div>{" "}
|
||||||
|
Apple is not responsible for product liability claims,
|
||||||
|
intellectual property infringement claims, or any other claims
|
||||||
|
relating to Nessa or your possession and use of it. We, not
|
||||||
|
Apple, are responsible for addressing any such claims and for
|
||||||
|
any claims that you may have relating to Nessa.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(d) Compliance:</div> You confirm that you
|
||||||
|
are not located in a country subject to an embargo of the United
|
||||||
|
States or listed by the United States government as a
|
||||||
|
terrorist-supporting country, and that you are not listed on any
|
||||||
|
United States government list of prohibited or restricted
|
||||||
|
parties.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(e) Third-party services:</div> Nessa depends
|
||||||
|
on third-party services — Clerk for authentication, Apple
|
||||||
|
HealthKit for health and workout data, Apple's In-App
|
||||||
|
Purchase system for subscriptions, and our own hosting — each
|
||||||
|
governed by its own terms. Where those terms affect how you may
|
||||||
|
use the feature they power, they apply alongside these terms.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">7.</span> Intellectual Property
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Our rights:</div> The Nessa software, its
|
||||||
|
design, text, icons, and branding are owned by us or our
|
||||||
|
licensors and are protected by copyright and other laws. We
|
||||||
|
grant you a personal, non-exclusive, non-transferable, revocable
|
||||||
|
licence to use Nessa for your own personal, non-commercial
|
||||||
|
purposes.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Restrictions:</div> You may not copy,
|
||||||
|
modify, translate, distribute, sell, lease, sublicense, or
|
||||||
|
create derivative works from Nessa, and you may not reverse
|
||||||
|
engineer, decompile, or attempt to obtain its source code,
|
||||||
|
except to the extent applicable law allows this despite a
|
||||||
|
restriction.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">8.</span> Disclaimers and Limitation of
|
||||||
|
Liability
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) As is:</div> Nessa is provided "as
|
||||||
|
is" and "as available" to the fullest extent
|
||||||
|
permitted by applicable law. We do not warrant that Nessa will
|
||||||
|
be uninterrupted, secure, error-free, or that recorded data will
|
||||||
|
be accurate — GPS-derived distance, pace, elevation, and
|
||||||
|
estimates can be wrong, and device and sensor limits apply.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Your data:</div> You are responsible for
|
||||||
|
backing up the data you store on your device. We keep
|
||||||
|
Nessa's service data as described in the{" "}
|
||||||
|
<A href="/privacy" class="text-blue hover-underline-animation">
|
||||||
|
privacy policy
|
||||||
|
</A>{" "}
|
||||||
|
and{" "}
|
||||||
|
<A href="/deletion" class="text-blue hover-underline-animation">
|
||||||
|
deletion page
|
||||||
|
</A>
|
||||||
|
, but no service is a substitute for your own backup.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Liability:</div> To the fullest extent
|
||||||
|
permitted by law, we are not liable for indirect, incidental,
|
||||||
|
special, consequential, or punitive damages, or for lost data,
|
||||||
|
lost profits, or loss of training records, arising out of your
|
||||||
|
use of, or inability to use, Nessa. Where a court would not
|
||||||
|
allow that exclusion, our liability for claims arising from your
|
||||||
|
use of Nessa is limited to the greater of the subscription fees
|
||||||
|
you paid us in the twelve months before the event giving rise to
|
||||||
|
the claim, or the minimum the applicable law requires. Nothing
|
||||||
|
in these terms limits liability that cannot lawfully be limited,
|
||||||
|
including for fraud or for death or personal injury caused by
|
||||||
|
our negligence.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">9.</span> Ending Your Use
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) You can leave:</div> You can stop using
|
||||||
|
Nessa at any time. You can delete your account and the content
|
||||||
|
associated with it through our{" "}
|
||||||
|
<A href="/deletion" class="text-blue hover-underline-animation">
|
||||||
|
account deletion page
|
||||||
|
</A>
|
||||||
|
. Deleting your account does not cancel a subscription that is
|
||||||
|
still in its paid period; cancel that in your App Store account
|
||||||
|
settings as described in section 5.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) We may end access:</div> We may suspend
|
||||||
|
or close your access to Nessa if you break these terms, if we
|
||||||
|
are required to by law, or if we reasonably believe your account
|
||||||
|
has been compromised. Where we can, we will tell you why.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) On termination:</div> Your licence to use
|
||||||
|
Nessa ends, and sections 4(a), 6, 7, 8, 10, and 11 continue to
|
||||||
|
apply.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">10.</span> Changes to These Terms
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Updates:</div> We may update these terms
|
||||||
|
as Nessa changes. The current version is always the one at{" "}
|
||||||
|
<A href="/terms" class="text-blue hover-underline-animation">
|
||||||
|
nessa.freno.me/terms
|
||||||
|
</A>
|
||||||
|
, and the "Last Updated" date at the top of this page
|
||||||
|
shows when it last changed.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Continued use:</div> If you keep using
|
||||||
|
Nessa after a change takes effect, you accept the revised terms.
|
||||||
|
For changes that materially reduce your rights or your paid
|
||||||
|
features, we will give notice in the app or by contact before
|
||||||
|
they take effect.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">11.</span> Contact Us
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
<div class="-ml-6">(a) Reaching Out:</div> If you have any
|
||||||
|
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">
|
||||||
|
through the contact form
|
||||||
|
</A>{" "}
|
||||||
|
or email{" "}
|
||||||
|
<a
|
||||||
|
href={`mailto:${SUPPORT_EMAIL}`}
|
||||||
|
class="text-blue hover-underline-animation"
|
||||||
|
>
|
||||||
|
{SUPPORT_EMAIL}
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,10 @@ import { describe, it, expect } from "bun:test";
|
|||||||
import { readdirSync, existsSync, statSync } from "fs";
|
import { readdirSync, existsSync, statSync } from "fs";
|
||||||
|
|
||||||
function isRouteFile(name: string): boolean {
|
function isRouteFile(name: string): boolean {
|
||||||
return name.endsWith(".tsx") || name.endsWith(".ts");
|
// Page routes are components; the `.ts` files beside them are pure content /
|
||||||
|
// metadata modules (content.ts, meta.ts, *-content.ts) with no default
|
||||||
|
// export the router can render, so they are not dispatch targets.
|
||||||
|
return name.endsWith(".tsx");
|
||||||
}
|
}
|
||||||
|
|
||||||
function listRouteFiles(dir: string): string[] {
|
function listRouteFiles(dir: string): string[] {
|
||||||
@@ -25,6 +28,20 @@ function listRouteFiles(dir: string): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listDirectories(dir: string): string[] {
|
||||||
|
try {
|
||||||
|
return readdirSync(dir).filter((name) => {
|
||||||
|
try {
|
||||||
|
return statSync(`${dir}/${name}`).isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hasDispatchRoute(routeName: string, routesDir: string): boolean {
|
function hasDispatchRoute(routeName: string, routesDir: string): boolean {
|
||||||
// Check for src/routes/<name>.tsx
|
// Check for src/routes/<name>.tsx
|
||||||
if (existsSync(`${routesDir}/${routeName}.tsx`)) {
|
if (existsSync(`${routesDir}/${routeName}.tsx`)) {
|
||||||
@@ -41,14 +58,12 @@ describe("subdomain routing dispatch invariant", () => {
|
|||||||
const routesDir = "./src/routes";
|
const routesDir = "./src/routes";
|
||||||
|
|
||||||
it("every subdomain route has a top-level dispatch route", () => {
|
it("every subdomain route has a top-level dispatch route", () => {
|
||||||
const prefixDirs = listRouteFiles(routesDir)
|
// List directory entries before filtering to route files. The previous
|
||||||
.filter((name) => {
|
// shape filtered with `listRouteFiles` first, which only ever returns
|
||||||
try {
|
// `.tsx`/`.ts` names, so `statSync(...).isDirectory()` was never true
|
||||||
return statSync(`${routesDir}/${name}`).isDirectory();
|
// and the assertion ran over an empty list — it passed while `/terms`
|
||||||
} catch {
|
// had no dispatch file (FRE-1670).
|
||||||
return false;
|
const prefixDirs = listDirectories(routesDir)
|
||||||
}
|
|
||||||
})
|
|
||||||
// Exclude non-product subdomains
|
// Exclude non-product subdomains
|
||||||
.filter((name) =>
|
.filter((name) =>
|
||||||
["nessa", "lineage", "gaze", "inputhalo", "nook"].includes(name)
|
["nessa", "lineage", "gaze", "inputhalo", "nook"].includes(name)
|
||||||
|
|||||||
30
src/routes/terms.tsx
Normal file
30
src/routes/terms.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Switch, Match } from "solid-js";
|
||||||
|
import { useSite } from "~/context/SiteContext";
|
||||||
|
import NotFound from "./[...404]";
|
||||||
|
import NessaTermsOfUse from "./nessa/terms";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host-aware terms-of-use route — `/terms`.
|
||||||
|
*
|
||||||
|
* The public browser path `/terms` is what the Nessa app links from
|
||||||
|
* Settings › Legal and from the subscription paywall
|
||||||
|
* (`AppSettings.termsOfServiceURL`). Production Vercel deployments do not
|
||||||
|
* apply the `vercel.json` host rewrites (the Nitro Build Output API config
|
||||||
|
* supersedes them), so the subdomain page needs this top-level dispatch file
|
||||||
|
* to be reachable — without it `nessa.freno.me/terms` is a 404 even though
|
||||||
|
* `src/routes/nessa/terms.tsx` exists.
|
||||||
|
*
|
||||||
|
* Only Nessa publishes terms of use today. Every other site — and the main
|
||||||
|
* site — falls back to the 404 handler rather than borrowing another
|
||||||
|
* product's legal document.
|
||||||
|
*/
|
||||||
|
export default function TermsPage() {
|
||||||
|
const site = useSite();
|
||||||
|
return (
|
||||||
|
<Switch fallback={<NotFound />}>
|
||||||
|
<Match when={site().id === "nessa"}>
|
||||||
|
<NessaTermsOfUse />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user