Policies section New settings: two new dropdowns are under Policies in Manage Business: Name shown on Terms & Privacy: NextChair, My business name, or NextChair and my business name. Contact email on Terms & Privacy: NextChair (info@next-chair.co.uk) or My business email. Fallbacks: both default to NextChair. If you pick the business email but haven’t set one, the form warns you and the pages fall back to the NextChair address. Where the name appears: the choice changes the name in the header link, footer, “we/us” definition, privacy controller line and intro on both pages. Business email: it replaces the address in the complaints section and the privacy contact lines. Database change: this needed a new migration, 20261017120000_add_legal_display_settings. Existing businesses default to NextChair, so nothing changes for them. Run prisma migrate deploy on your server. Terms and placeholders Sections 15 (Governing law) and 16 (Contact us) are removed from terms/page.tsx, so the Terms now have 14 sections. The [LEGAL ENTITY NAME] and [REGISTERED ADDRESS] placeholders are gone, along with the unused governing-law setting. The contact email is now info@next-chair.co.uk in legal-config.ts. Login page sign-in-form.tsx now shows “By continuing, you agree to our Terms of Service and Privacy Policy”, with both as links.
This commit is contained in:
14 files changed
+707
-482
No files matched your search
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `BusinessSettings` ADD COLUMN `legalContactEmail` ENUM('NEXTCHAIR', 'BUSINESS') NOT NULL DEFAULT 'NEXTCHAIR',
|
||||
ADD COLUMN `legalNameDisplay` ENUM('NEXTCHAIR', 'BUSINESS', 'BOTH') NOT NULL DEFAULT 'NEXTCHAIR';
|
||||
@@ -223,6 +223,17 @@ enum CalendarProvider {
|
||||
GOOGLE
|
||||
}
|
||||
|
||||
enum LegalNameDisplay {
|
||||
NEXTCHAIR
|
||||
BUSINESS
|
||||
BOTH
|
||||
}
|
||||
|
||||
enum LegalContactEmail {
|
||||
NEXTCHAIR
|
||||
BUSINESS
|
||||
}
|
||||
|
||||
/// Per-appointment sync outcome, not the connection's overall health (see
|
||||
/// StaffCalendarConnection.lastSyncError for that).
|
||||
enum CalendarSyncStatus {
|
||||
@@ -403,6 +414,14 @@ model BusinessSettings {
|
||||
bookingTerms String? @db.Text
|
||||
privacyPolicyUrl String?
|
||||
|
||||
/// Who the public Terms of Service / Privacy Policy pages name as "we" —
|
||||
/// see src/features/legal/legal-identity.ts. NEXTCHAIR is the default so
|
||||
/// existing businesses keep today's wording.
|
||||
legalNameDisplay LegalNameDisplay @default(NEXTCHAIR)
|
||||
/// Which inbox those pages give for legal/privacy requests. BUSINESS uses
|
||||
/// Business.email and falls back to NextChair's when that is empty.
|
||||
legalContactEmail LegalContactEmail @default(NEXTCHAIR)
|
||||
|
||||
/// Phase 9 — Google reviews visibility policy, applied on top of the
|
||||
/// per-review Review.isHidden flag. Null minRating = no floor.
|
||||
reviewsMinRating Int?
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { LEGAL } from "@/features/legal/legal-config";
|
||||
import { getLegalIdentity } from "@/features/legal/legal-identity";
|
||||
|
||||
/**
|
||||
* Shell for the public legal pages (/terms, /privacy). Deliberately static
|
||||
* and independent of any business's branding — these documents are the
|
||||
* platform operator's, and must stay readable by guests, signed-in users
|
||||
* and (via the proxy's install-exempt list) before installation completes.
|
||||
* Shell for the public legal pages (/terms, /privacy). Readable by guests,
|
||||
* signed-in users and (via the proxy's install-exempt list) before
|
||||
* installation completes — getLegalIdentity() falls back to NextChair then.
|
||||
*/
|
||||
export default function LegalLayout({ children }: { children: React.ReactNode }) {
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function LegalLayout({ children }: { children: React.ReactNode }) {
|
||||
const { name } = await getLegalIdentity();
|
||||
return (
|
||||
<div className="bg-stone flex min-h-screen flex-col">
|
||||
<header className="border-line border-b bg-white">
|
||||
<div className="mx-auto flex max-w-3xl items-center justify-between px-6 py-4">
|
||||
<Link href="/" className="text-ink flex items-center gap-2 text-sm font-bold">
|
||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||
Back to {LEGAL.productName}
|
||||
Back to {name}
|
||||
</Link>
|
||||
<nav aria-label="Legal" className="text-muted flex gap-5 text-sm">
|
||||
<Link href="/terms" className="hover:text-ink">
|
||||
@@ -31,7 +33,7 @@ export default function LegalLayout({ children }: { children: React.ReactNode })
|
||||
{children}
|
||||
</main>
|
||||
<footer className="border-line text-muted border-t px-6 py-6 text-center text-xs">
|
||||
© {new Date().getFullYear()} {LEGAL.legalName}
|
||||
© {new Date().getFullYear()} {name}
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
+244
-216
@@ -1,247 +1,275 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { LegalDocument, type LegalSectionDef } from "@/features/legal/components/legal-document";
|
||||
import { LEGAL } from "@/features/legal/legal-config";
|
||||
import { getLegalIdentity } from "@/features/legal/legal-identity";
|
||||
import type { LegalIdentity } from "@/features/legal/resolve-legal-identity";
|
||||
|
||||
export const metadata: Metadata = { title: "Privacy Policy" };
|
||||
// Names the business per its Policies settings, so it must be read per request.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const sections: LegalSectionDef[] = [
|
||||
{
|
||||
id: "who-we-are",
|
||||
title: "Who is responsible for your data",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
{LEGAL.productName} is a booking platform used by many independent Businesses. Two parties handle your data, in
|
||||
different roles:
|
||||
</p>
|
||||
function buildSections({ name, email }: LegalIdentity): LegalSectionDef[] {
|
||||
return [
|
||||
{
|
||||
id: "who-we-are",
|
||||
title: "Who is responsible for your data",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
This booking platform is used by independent Businesses. Two parties handle your data,
|
||||
in different roles:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>The Business you book with</strong> decides why and how your booking details
|
||||
are used for your appointments — it is an independent <em>controller</em> of that
|
||||
data. It sees your name, contact details and booking history, and may keep notes about
|
||||
you.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The operator of the Platform</strong> ({name}, “we”). For your
|
||||
account, sign-in and the running and security of the Platform, we are a controller.
|
||||
For the booking data a Business keeps in the Platform, we act as its{" "}
|
||||
<em>processor</em>, on its instructions.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Contact us about privacy at <a href={`mailto:${email}`}>{email}</a>.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "data-we-collect",
|
||||
title: "What we collect",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
<strong>You give us:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>Name, email address and, if you provide it, phone number.</li>
|
||||
<li>Your bookings: services, staff member, date and time, and any message you add.</li>
|
||||
<li>Preferences, such as a preferred barber, and notification settings.</li>
|
||||
<li>A profile picture, if you upload one.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Created while you use the Platform:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>Appointment history and status (confirmed, cancelled, completed, no-show).</li>
|
||||
<li>Loyalty points and rewards, if the Business runs a loyalty programme.</li>
|
||||
<li>
|
||||
Notes a Business keeps about its clients to deliver the service (for example,
|
||||
preferred style).
|
||||
</li>
|
||||
<li>
|
||||
Sign-in codes (short-lived and stored in hashed form) and session information that
|
||||
keeps you signed in.
|
||||
</li>
|
||||
<li>
|
||||
A push-notification address for your device, if you switch on push notifications.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>For Staff and owners:</strong> work schedule, services offered and, if
|
||||
connected, the Google Calendar account used to sync appointments.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Clients added by a Business:</strong> a Business may add or import its existing
|
||||
clients (name, contact details, history) into the Platform. In that case the Business is
|
||||
responsible for having the right to do so and for telling those people.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "why",
|
||||
title: "How and why we use it",
|
||||
body: (
|
||||
<ul>
|
||||
<li>
|
||||
<strong>The Business you book with</strong> decides why and how your booking details are used for your appointments —
|
||||
it is an independent <em>controller</em> of that data. It sees your name, contact details and booking history, and may
|
||||
keep notes about you.
|
||||
<strong>To provide the booking service</strong> — create and manage appointments, show
|
||||
availability and sign you in.
|
||||
<em> Basis:</em> performing our contract with you.
|
||||
</li>
|
||||
<li>
|
||||
<strong>{LEGAL.legalName}</strong> ({LEGAL.address}) runs the Platform. For your account, sign-in and the running and
|
||||
security of the Platform, we are a controller. For the booking data a Business keeps in the Platform, we act as its{" "}
|
||||
<em>processor</em>, on its instructions.
|
||||
<strong>To send booking messages</strong> — confirmations, reminders, changes and
|
||||
cancellations by email, push, or a WhatsApp message from the Business. <em>Basis:</em>{" "}
|
||||
performing the contract, and legitimate interest in reducing missed appointments.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To keep the Platform safe</strong> — prevent abuse, fraud and unauthorised
|
||||
access, and fix errors. <em>Basis:</em> legitimate interest.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To meet legal obligations</strong> — such as accounting, or responding to lawful
|
||||
requests. <em>Basis:</em> legal obligation.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Optional features</strong> — such as push notifications or Google Calendar sync.{" "}
|
||||
<em>Basis:</em> your consent, which you can withdraw at any time.
|
||||
</li>
|
||||
</ul>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "sharing",
|
||||
title: "Who we share it with",
|
||||
body: (
|
||||
<>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>The Business you book with</strong>, and its Staff, who need your details to
|
||||
serve you.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Service providers</strong> that run the Platform for us, such as hosting,
|
||||
email delivery and push-notification services, bound to use the data only for that
|
||||
purpose.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Google</strong>, if a Staff member or owner connects Google Calendar —
|
||||
appointment details are written to their calendar.
|
||||
</li>
|
||||
<li>
|
||||
<strong>WhatsApp</strong>, if a Business messages you there — your phone number and
|
||||
the message are handled by WhatsApp under its own terms.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Authorities</strong>, where the law requires it.
|
||||
</li>
|
||||
</ul>
|
||||
<p>We do not sell your personal data and we do not use it for third-party advertising.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "cookies",
|
||||
title: "Cookies and similar technologies",
|
||||
body: (
|
||||
<>
|
||||
<p>We use only what the Platform needs to work:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Session cookie</strong> — keeps you signed in. Without it you could not use
|
||||
your account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Local storage and the app cache</strong> — remember small settings and let the
|
||||
installable app load offline.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
These are strictly necessary and do not need consent. We do not use advertising or
|
||||
tracking cookies. If we ever add analytics, we will update this policy and ask for your
|
||||
consent where the law requires it.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "retention",
|
||||
title: "How long we keep it",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
We keep your account and booking data while your account is active and the Business
|
||||
needs it to serve you. Sign-in codes expire within minutes and are cleared soon after.
|
||||
</p>
|
||||
<p>
|
||||
When you ask us to delete your account we remove or anonymise your data, except what we
|
||||
must keep by law (for example, records needed for accounting or to resolve a dispute). A
|
||||
Business may keep its own records of past appointments according to its legal duties.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
title: "Security",
|
||||
body: (
|
||||
<p>
|
||||
Contact us about privacy at <a href={`mailto:${LEGAL.contactEmail}`}>{LEGAL.contactEmail}</a>.
|
||||
We protect your data with encrypted connections, access controls limited to what each role
|
||||
needs, and hashed sign-in codes. No system is perfectly secure; if a breach affecting you
|
||||
occurs we will notify you and the relevant authority as the law requires.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "data-we-collect",
|
||||
title: "What we collect",
|
||||
body: (
|
||||
<>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "transfers",
|
||||
title: "International transfers",
|
||||
body: (
|
||||
<p>
|
||||
<strong>You give us:</strong>
|
||||
Some of our providers (for example Google or WhatsApp, when you use those features) may
|
||||
process data outside your country. Where they do, we rely on appropriate safeguards such
|
||||
as standard contractual clauses, as required by applicable data protection law.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Name, email address and, if you provide it, phone number.</li>
|
||||
<li>Your bookings: services, staff member, date and time, and any message you add.</li>
|
||||
<li>Preferences, such as a preferred barber, and notification settings.</li>
|
||||
<li>A profile picture, if you upload one.</li>
|
||||
</ul>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "rights",
|
||||
title: "Your rights",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
Depending on where you live (including under the GDPR and equivalent laws), you can:
|
||||
</p>
|
||||
<ul>
|
||||
<li>access the personal data we hold about you;</li>
|
||||
<li>correct data that is wrong or incomplete;</li>
|
||||
<li>ask us to delete your data;</li>
|
||||
<li>
|
||||
restrict or object to certain uses, including those based on legitimate interest;
|
||||
</li>
|
||||
<li>receive your data in a portable format;</li>
|
||||
<li>withdraw consent at any time, where we rely on it.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Email <a href={`mailto:${email}`}>{email}</a> to exercise any of these. For data a
|
||||
Business controls, we may pass your request to that Business. We will reply within one
|
||||
month. You also have the right to complain to your local data protection authority.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "children",
|
||||
title: "Children",
|
||||
body: (
|
||||
<p>
|
||||
<strong>Created while you use the Platform:</strong>
|
||||
The Platform is not directed at children under 16, and we do not knowingly collect their
|
||||
data. If you believe a child has given us personal data, contact us and we will delete it.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Appointment history and status (confirmed, cancelled, completed, no-show).</li>
|
||||
<li>Loyalty points and rewards, if the Business runs a loyalty programme.</li>
|
||||
<li>Notes a Business keeps about its clients to deliver the service (for example, preferred style).</li>
|
||||
<li>Sign-in codes (short-lived and stored in hashed form) and session information that keeps you signed in.</li>
|
||||
<li>A push-notification address for your device, if you switch on push notifications.</li>
|
||||
</ul>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "changes",
|
||||
title: "Changes to this policy",
|
||||
body: (
|
||||
<p>
|
||||
<strong>For Staff and owners:</strong> work schedule, services offered and, if connected, the Google Calendar account used
|
||||
to sync appointments.
|
||||
We will post any update here with a new “last updated” date and tell you about
|
||||
significant changes through the Platform or by email. See also our{" "}
|
||||
<Link href="/terms">Terms of Service</Link>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Clients added by a Business:</strong> a Business may add or import its existing clients (name, contact details,
|
||||
history) into the Platform. In that case the Business is responsible for having the right to do so and for telling those
|
||||
people.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "why",
|
||||
title: "How and why we use it",
|
||||
body: (
|
||||
<ul>
|
||||
<li>
|
||||
<strong>To provide the booking service</strong> — create and manage appointments, show availability and sign you in.
|
||||
<em> Basis:</em> performing our contract with you.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To send booking messages</strong> — confirmations, reminders, changes and cancellations by email, push, or a
|
||||
WhatsApp message from the Business. <em>Basis:</em> performing the contract, and legitimate interest in reducing missed
|
||||
appointments.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To keep the Platform safe</strong> — prevent abuse, fraud and unauthorised access, and fix errors.{" "}
|
||||
<em>Basis:</em> legitimate interest.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To meet legal obligations</strong> — such as accounting, or responding to lawful requests. <em>Basis:</em> legal
|
||||
obligation.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Optional features</strong> — such as push notifications or Google Calendar sync. <em>Basis:</em> your consent,
|
||||
which you can withdraw at any time.
|
||||
</li>
|
||||
</ul>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "sharing",
|
||||
title: "Who we share it with",
|
||||
body: (
|
||||
<>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>The Business you book with</strong>, and its Staff, who need your details to serve you.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Service providers</strong> that run the Platform for us, such as hosting, email delivery and push-notification
|
||||
services, bound to use the data only for that purpose.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Google</strong>, if a Staff member or owner connects Google Calendar — appointment details are written to their
|
||||
calendar.
|
||||
</li>
|
||||
<li>
|
||||
<strong>WhatsApp</strong>, if a Business messages you there — your phone number and the message are handled by WhatsApp
|
||||
under its own terms.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Authorities</strong>, where the law requires it.
|
||||
</li>
|
||||
</ul>
|
||||
<p>We do not sell your personal data and we do not use it for third-party advertising.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "cookies",
|
||||
title: "Cookies and similar technologies",
|
||||
body: (
|
||||
<>
|
||||
<p>We use only what the Platform needs to work:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Session cookie</strong> — keeps you signed in. Without it you could not use your account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Local storage and the app cache</strong> — remember small settings and let the installable app load offline.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
These are strictly necessary and do not need consent. We do not use advertising or tracking cookies. If we ever add
|
||||
analytics, we will update this policy and ask for your consent where the law requires it.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "retention",
|
||||
title: "How long we keep it",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
We keep your account and booking data while your account is active and the Business needs it to serve you. Sign-in codes
|
||||
expire within minutes and are cleared soon after.
|
||||
</p>
|
||||
<p>
|
||||
When you ask us to delete your account we remove or anonymise your data, except what we must keep by law (for example,
|
||||
records needed for accounting or to resolve a dispute). A Business may keep its own records of past appointments
|
||||
according to its legal duties.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
title: "Security",
|
||||
body: (
|
||||
<p>
|
||||
We protect your data with encrypted connections, access controls limited to what each role needs, and hashed sign-in codes.
|
||||
No system is perfectly secure; if a breach affecting you occurs we will notify you and the relevant authority as the law
|
||||
requires.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "transfers",
|
||||
title: "International transfers",
|
||||
body: (
|
||||
<p>
|
||||
Some of our providers (for example Google or WhatsApp, when you use those features) may process data outside your
|
||||
country. Where they do, we rely on appropriate safeguards such as standard contractual clauses, as required by applicable
|
||||
data protection law.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "rights",
|
||||
title: "Your rights",
|
||||
body: (
|
||||
<>
|
||||
<p>Depending on where you live (including under the GDPR and equivalent laws), you can:</p>
|
||||
<ul>
|
||||
<li>access the personal data we hold about you;</li>
|
||||
<li>correct data that is wrong or incomplete;</li>
|
||||
<li>ask us to delete your data;</li>
|
||||
<li>restrict or object to certain uses, including those based on legitimate interest;</li>
|
||||
<li>receive your data in a portable format;</li>
|
||||
<li>withdraw consent at any time, where we rely on it.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Email <a href={`mailto:${LEGAL.contactEmail}`}>{LEGAL.contactEmail}</a> to exercise any of these. For data a Business
|
||||
controls, we may pass your request to that Business. We will reply within one month. You also have the right to complain
|
||||
to your local data protection authority.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "children",
|
||||
title: "Children",
|
||||
body: (
|
||||
<p>
|
||||
The Platform is not directed at children under 16, and we do not knowingly collect their data. If you believe a child has
|
||||
given us personal data, contact us and we will delete it.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "changes",
|
||||
title: "Changes to this policy",
|
||||
body: (
|
||||
<p>
|
||||
We will post any update here with a new “last updated” date and tell you about significant changes through the
|
||||
Platform or by email. See also our <Link href="/terms">Terms of Service</Link>.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
];
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
export default async function PrivacyPage() {
|
||||
const { name, email } = await getLegalIdentity();
|
||||
return (
|
||||
<LegalDocument
|
||||
title="Privacy Policy"
|
||||
intro={
|
||||
<p>
|
||||
This policy explains what personal data {LEGAL.productName} and the Businesses that use it collect when you book an
|
||||
appointment, why, and the choices you have. We have tried to keep it plain and specific.
|
||||
This policy explains what personal data {name} and the Businesses that use the Platform
|
||||
collect when you book an appointment, why, and the choices you have. We have tried to keep
|
||||
it plain and specific.
|
||||
</p>
|
||||
}
|
||||
sections={sections}
|
||||
sections={buildSections({ name, email })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+256
-244
@@ -1,277 +1,289 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { LegalDocument, type LegalSectionDef } from "@/features/legal/components/legal-document";
|
||||
import { LEGAL } from "@/features/legal/legal-config";
|
||||
import { getLegalIdentity } from "@/features/legal/legal-identity";
|
||||
import type { LegalIdentity } from "@/features/legal/resolve-legal-identity";
|
||||
|
||||
export const metadata: Metadata = { title: "Terms of Service" };
|
||||
// Names the business per its Policies settings, so it must be read per request.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const sections: LegalSectionDef[] = [
|
||||
{
|
||||
id: "definitions",
|
||||
title: "Definitions",
|
||||
body: (
|
||||
<ul>
|
||||
<li>
|
||||
<strong>“{LEGAL.productName}”, “we”, “us”</strong> — {LEGAL.legalName}, {LEGAL.address}.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Platform”</strong> — the {LEGAL.productName} website, web app and installable app, through which
|
||||
appointments are booked and managed.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Business”</strong> — a salon, barbershop or other service provider that uses the Platform to take
|
||||
bookings.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Staff”</strong> — people who work for a Business and use the Platform to manage schedules and
|
||||
appointments.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Customer”, “you”</strong> — anyone who books an appointment through the Platform, with or
|
||||
without an account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Services”</strong> — the haircuts, treatments and other services a Business offers.
|
||||
</li>
|
||||
</ul>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "about",
|
||||
title: "About these Terms",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
These Terms govern your use of the Platform. By creating an account, booking an appointment or otherwise using the
|
||||
Platform, you agree to them. If you do not agree, please do not use the Platform.
|
||||
</p>
|
||||
<p>
|
||||
You must be at least 16 years old to create an account or book on your own behalf. If you are younger, a parent or
|
||||
guardian must book for you.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "our-role",
|
||||
title: "What NextChair does — and doesn’t do",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
{LEGAL.productName} provides the software that connects Customers with Businesses: viewing services and availability,
|
||||
booking and managing appointments, receiving reminders and, where a Business enables it, collecting loyalty points.
|
||||
</p>
|
||||
<p>
|
||||
<strong>We are not the provider of the Services.</strong> Each appointment is an agreement between you and the Business.
|
||||
The Business is responsible for the Services, their quality, their pricing, and any payment taken in person. We are an
|
||||
intermediary and are not a party to that agreement.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "accounts",
|
||||
title: "Your account",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
You can book as a guest where a Business allows it, or create an account. We sign you in with a one-time code sent to
|
||||
your email address, so there is no password to remember — but you must keep access to that email address secure.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Give accurate contact details so the Business can reach you about your appointment.</li>
|
||||
<li>You are responsible for activity under your account. Tell us promptly if you think it has been accessed without permission.</li>
|
||||
<li>One person, one account. Do not share your account or create accounts for others without their knowledge.</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "bookings",
|
||||
title: "Bookings, changes and cancellations",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
A booking is confirmed when the Platform shows it as confirmed or you receive a confirmation. Some Businesses approve
|
||||
bookings manually; in that case the booking is pending until they accept it.
|
||||
</p>
|
||||
function buildSections({ name, email }: LegalIdentity): LegalSectionDef[] {
|
||||
return [
|
||||
{
|
||||
id: "definitions",
|
||||
title: "Definitions",
|
||||
body: (
|
||||
<ul>
|
||||
<li>
|
||||
Each Business sets its own cancellation window, no-show policy and any deposits or fees. These are shown during booking
|
||||
and apply to your appointment.
|
||||
<strong>“we”, “us”, “our”</strong> — {name}.
|
||||
</li>
|
||||
<li>
|
||||
If you cannot attend, cancel or reschedule as early as you can, through the Platform or by contacting the Business.
|
||||
<strong>“Platform”</strong> — the website, web app and installable app
|
||||
through which appointments are booked and managed.
|
||||
</li>
|
||||
<li>
|
||||
A Business may need to change or cancel an appointment (for example due to illness). They will contact you and offer a
|
||||
new time or a cancellation where possible.
|
||||
<strong>“Business”</strong> — a salon, barbershop or other service provider
|
||||
that uses the Platform to take bookings.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Staff”</strong> — people who work for a Business and use the
|
||||
Platform to manage schedules and appointments.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Customer”, “you”</strong> — anyone who books an
|
||||
appointment through the Platform, with or without an account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>“Services”</strong> — the haircuts, treatments and other services a
|
||||
Business offers.
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "payments",
|
||||
title: "Prices and payments",
|
||||
body: (
|
||||
<p>
|
||||
Prices are set by each Business and shown at booking. Unless the Platform explicitly says otherwise, payment is made to the
|
||||
Business directly, at the appointment. We do not process payments for Services and are not responsible for refunds or
|
||||
payment disputes with a Business.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "reminders",
|
||||
title: "Reminders and notifications",
|
||||
body: (
|
||||
<>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "about",
|
||||
title: "About these Terms",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
These Terms govern your use of the Platform. By creating an account, booking an
|
||||
appointment or otherwise using the Platform, you agree to them. If you do not agree,
|
||||
please do not use the Platform.
|
||||
</p>
|
||||
<p>
|
||||
You must be at least 16 years old to create an account or book on your own behalf. If
|
||||
you are younger, a parent or guardian must book for you.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "our-role",
|
||||
title: "What we do — and don’t do",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
We provide the software that connects Customers with Businesses: viewing services and
|
||||
availability, booking and managing appointments, receiving reminders and, where a
|
||||
Business enables it, collecting loyalty points.
|
||||
</p>
|
||||
<p>
|
||||
<strong>We are not the provider of the Services.</strong> Each appointment is an
|
||||
agreement between you and the Business. The Business is responsible for the Services,
|
||||
their quality, their pricing, and any payment taken in person. We are an intermediary
|
||||
and are not a party to that agreement.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "accounts",
|
||||
title: "Your account",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
You can book as a guest where a Business allows it, or create an account. We sign you in
|
||||
with a one-time code sent to your email address, so there is no password to remember —
|
||||
but you must keep access to that email address secure.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Give accurate contact details so the Business can reach you about your appointment.
|
||||
</li>
|
||||
<li>
|
||||
You are responsible for activity under your account. Tell us promptly if you think it
|
||||
has been accessed without permission.
|
||||
</li>
|
||||
<li>
|
||||
One person, one account. Do not share your account or create accounts for others
|
||||
without their knowledge.
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "bookings",
|
||||
title: "Bookings, changes and cancellations",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
A booking is confirmed when the Platform shows it as confirmed or you receive a
|
||||
confirmation. Some Businesses approve bookings manually; in that case the booking is
|
||||
pending until they accept it.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Each Business sets its own cancellation window, no-show policy and any deposits or
|
||||
fees. These are shown during booking and apply to your appointment.
|
||||
</li>
|
||||
<li>
|
||||
If you cannot attend, cancel or reschedule as early as you can, through the Platform
|
||||
or by contacting the Business.
|
||||
</li>
|
||||
<li>
|
||||
A Business may need to change or cancel an appointment (for example due to illness).
|
||||
They will contact you and offer a new time or a cancellation where possible.
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "payments",
|
||||
title: "Prices and payments",
|
||||
body: (
|
||||
<p>
|
||||
To reduce missed appointments, the Platform and the Business may send you booking confirmations and reminders by email,
|
||||
push notification, or a message the Business sends you on WhatsApp using the phone number you provided.
|
||||
Prices are set by each Business and shown at booking. Unless the Platform explicitly says
|
||||
otherwise, payment is made to the Business directly, at the appointment. We do not process
|
||||
payments for Services and are not responsible for refunds or payment disputes with a
|
||||
Business.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "reminders",
|
||||
title: "Reminders and notifications",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
To reduce missed appointments, the Platform and the Business may send you booking
|
||||
confirmations and reminders by email, push notification, or a message the Business sends
|
||||
you on WhatsApp using the phone number you provided.
|
||||
</p>
|
||||
<p>
|
||||
These are service messages about your booking. You can turn off push notifications in
|
||||
your device or account settings. Messages sent by a Business through WhatsApp are
|
||||
governed by WhatsApp’s own terms.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "acceptable-use",
|
||||
title: "Acceptable use",
|
||||
body: (
|
||||
<>
|
||||
<p>You agree not to:</p>
|
||||
<ul>
|
||||
<li>make false, spam or repeated bookings you do not intend to keep;</li>
|
||||
<li>harass, threaten or abuse Staff, Businesses or other Customers;</li>
|
||||
<li>attempt to access accounts, data or systems that are not yours;</li>
|
||||
<li>
|
||||
interfere with the Platform, probe it for vulnerabilities without our permission, or
|
||||
use bots or scrapers on it;
|
||||
</li>
|
||||
<li>use the Platform for anything unlawful.</li>
|
||||
</ul>
|
||||
<p>We may suspend or close accounts that break these rules.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "reviews",
|
||||
title: "Reviews and content",
|
||||
body: (
|
||||
<p>
|
||||
These are service messages about your booking. You can turn off push notifications in your device or account settings.
|
||||
Messages sent by a Business through WhatsApp are governed by WhatsApp’s own terms.
|
||||
Where the Platform lets you leave feedback or upload content such as a profile picture,
|
||||
you keep ownership of it but give the Business and us a non-exclusive, worldwide licence
|
||||
to display it on the Platform for as long as it is there. It must be honest, lawful and
|
||||
not infringe anyone’s rights. We may remove content that breaks these Terms.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "acceptable-use",
|
||||
title: "Acceptable use",
|
||||
body: (
|
||||
<>
|
||||
<p>You agree not to:</p>
|
||||
<ul>
|
||||
<li>make false, spam or repeated bookings you do not intend to keep;</li>
|
||||
<li>harass, threaten or abuse Staff, Businesses or other Customers;</li>
|
||||
<li>attempt to access accounts, data or systems that are not yours;</li>
|
||||
<li>interfere with the Platform, probe it for vulnerabilities without our permission, or use bots or scrapers on it;</li>
|
||||
<li>use the Platform for anything unlawful.</li>
|
||||
</ul>
|
||||
<p>We may suspend or close accounts that break these rules.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "reviews",
|
||||
title: "Reviews and content",
|
||||
body: (
|
||||
<p>
|
||||
Where the Platform lets you leave feedback or upload content such as a profile picture, you keep ownership of it but give
|
||||
the Business and us a non-exclusive, worldwide licence to display it on the Platform for as long as it is there. It must be
|
||||
honest, lawful and not infringe anyone’s rights. We may remove content that breaks these Terms.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "ip",
|
||||
title: "Our intellectual property",
|
||||
body: (
|
||||
<p>
|
||||
The Platform, including its software, design, name and logo, belongs to us or our licensors. You may use it for its
|
||||
intended purpose, but you may not copy, modify, resell or reverse-engineer it, except as the law allows.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "liability",
|
||||
title: "Availability and liability",
|
||||
body: (
|
||||
<>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "ip",
|
||||
title: "Our intellectual property",
|
||||
body: (
|
||||
<p>
|
||||
We work to keep the Platform running and accurate, but it is provided “as is” and we cannot promise it will
|
||||
always be available or error-free. Maintenance and outages happen.
|
||||
The Platform, including its software, design, name and logo, belongs to us or our
|
||||
licensors. You may use it for its intended purpose, but you may not copy, modify, resell
|
||||
or reverse-engineer it, except as the law allows.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "liability",
|
||||
title: "Availability and liability",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
We work to keep the Platform running and accurate, but it is provided “as
|
||||
is” and we cannot promise it will always be available or error-free. Maintenance
|
||||
and outages happen.
|
||||
</p>
|
||||
<p>
|
||||
To the extent the law allows, we are not liable for the Services a Business provides,
|
||||
for a Business’s actions or omissions, or for indirect or consequential losses. Nothing
|
||||
in these Terms limits liability that cannot be limited by law, including for death or
|
||||
personal injury caused by negligence, or for fraud. Your statutory consumer rights are
|
||||
not affected.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "privacy",
|
||||
title: "Your personal data",
|
||||
body: (
|
||||
<p>
|
||||
To the extent the law allows, we are not liable for the Services a Business provides, for a Business’s actions or
|
||||
omissions, or for indirect or consequential losses. Nothing in these Terms limits liability that cannot be limited by law,
|
||||
including for death or personal injury caused by negligence, or for fraud. Your statutory consumer rights are not
|
||||
affected.
|
||||
How we and Businesses handle your personal data is explained in our{" "}
|
||||
<Link href="/privacy">Privacy Policy</Link>.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "privacy",
|
||||
title: "Your personal data",
|
||||
body: (
|
||||
<p>
|
||||
How we and Businesses handle your personal data is explained in our <Link href="/privacy">Privacy Policy</Link>.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "complaints",
|
||||
title: "Complaints",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
<strong>About a Service:</strong> raise it with the Business first — they are responsible for the Service.
|
||||
</p>
|
||||
<p>
|
||||
<strong>About the Platform:</strong> email <a href={`mailto:${LEGAL.contactEmail}`}>{LEGAL.contactEmail}</a> with what
|
||||
happened and, if relevant, the date and Business. We aim to reply within 14 days.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "changes",
|
||||
title: "Changes and termination",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
We may update these Terms, for example when we add features or the law changes. We will post the new version here with a
|
||||
new “last updated” date and, for significant changes, tell you through the Platform or by email. If you keep
|
||||
using the Platform after the change, you accept the updated Terms; if you do not agree, you can stop using it and delete
|
||||
your account.
|
||||
</p>
|
||||
<p>
|
||||
You can stop using the Platform at any time. We may suspend or end your access if you break these Terms or if we are
|
||||
required to by law.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "law",
|
||||
title: "Governing law",
|
||||
body: (
|
||||
<p>
|
||||
These Terms are governed by the law of {LEGAL.governingLaw}. This does not remove any mandatory consumer protections you
|
||||
have in the country where you live.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "contact",
|
||||
title: "Contact us",
|
||||
body: (
|
||||
<p>
|
||||
{LEGAL.legalName}
|
||||
<br />
|
||||
{LEGAL.address}
|
||||
<br />
|
||||
<a href={`mailto:${LEGAL.contactEmail}`}>{LEGAL.contactEmail}</a>
|
||||
</p>
|
||||
),
|
||||
},
|
||||
];
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "complaints",
|
||||
title: "Complaints",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
<strong>About a Service:</strong> raise it with the Business first — they are
|
||||
responsible for the Service.
|
||||
</p>
|
||||
<p>
|
||||
<strong>About the Platform:</strong> email <a href={`mailto:${email}`}>{email}</a> with
|
||||
what happened and, if relevant, the date and Business. We aim to reply within 14 days.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "changes",
|
||||
title: "Changes and termination",
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
We may update these Terms, for example when we add features or the law changes. We will
|
||||
post the new version here with a new “last updated” date and, for
|
||||
significant changes, tell you through the Platform or by email. If you keep using the
|
||||
Platform after the change, you accept the updated Terms; if you do not agree, you can
|
||||
stop using it and delete your account.
|
||||
</p>
|
||||
<p>
|
||||
You can stop using the Platform at any time. We may suspend or end your access if you
|
||||
break these Terms or if we are required to by law.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function TermsPage() {
|
||||
export default async function TermsPage() {
|
||||
const identity = await getLegalIdentity();
|
||||
return (
|
||||
<LegalDocument
|
||||
title="Terms of Service"
|
||||
intro={
|
||||
<p>
|
||||
Welcome to {LEGAL.productName}. These Terms explain the rules for using our booking platform, whether you book
|
||||
appointments as a Customer or work for a Business. Please read them — they are written to be readable.
|
||||
Welcome to {identity.name}. These Terms explain the rules for using our booking platform,
|
||||
whether you book appointments as a Customer or work for a Business. Please read them —
|
||||
they are written to be readable.
|
||||
</p>
|
||||
}
|
||||
sections={sections}
|
||||
sections={buildSections(identity)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -56,6 +56,8 @@ export default async function OwnerBusinessDetailsPage() {
|
||||
lateArrivalPolicy: business.settings?.lateArrivalPolicy ?? "",
|
||||
bookingTerms: business.settings?.bookingTerms ?? "",
|
||||
privacyPolicyUrl: business.settings?.privacyPolicyUrl ?? "",
|
||||
legalNameDisplay: business.settings?.legalNameDisplay ?? "NEXTCHAIR",
|
||||
legalContactEmail: business.settings?.legalContactEmail ?? "NEXTCHAIR",
|
||||
}}
|
||||
/>
|
||||
</SettingsPageShell>
|
||||
|
||||
@@ -117,6 +117,18 @@ export function SignInForm() {
|
||||
Create one
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<p className="text-muted mt-6 text-center text-xs leading-relaxed">
|
||||
By continuing, you agree to our{" "}
|
||||
<Link href="/terms" className="underline">
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="/privacy" className="underline">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { LegalContactEmail, LegalNameDisplay } from "@/generated/prisma/enums";
|
||||
import { LEGAL } from "../legal-config";
|
||||
import { resolveLegalIdentity } from "../resolve-legal-identity";
|
||||
|
||||
const base = {
|
||||
businessName: "Cut & Co",
|
||||
businessEmail: "hello@cutandco.example",
|
||||
nameDisplay: LegalNameDisplay.NEXTCHAIR,
|
||||
contactEmail: LegalContactEmail.NEXTCHAIR,
|
||||
};
|
||||
|
||||
describe("resolveLegalIdentity", () => {
|
||||
it("defaults to NextChair's name and email", () => {
|
||||
expect(resolveLegalIdentity(base)).toEqual({ name: "NextChair", email: LEGAL.contactEmail });
|
||||
});
|
||||
|
||||
it("can show the business name instead of NextChair", () => {
|
||||
expect(resolveLegalIdentity({ ...base, nameDisplay: LegalNameDisplay.BUSINESS }).name).toBe(
|
||||
"Cut & Co",
|
||||
);
|
||||
});
|
||||
|
||||
it("can show both names together", () => {
|
||||
expect(resolveLegalIdentity({ ...base, nameDisplay: LegalNameDisplay.BOTH }).name).toBe(
|
||||
"NextChair and Cut & Co",
|
||||
);
|
||||
});
|
||||
|
||||
it("can show the business email instead of NextChair's", () => {
|
||||
expect(resolveLegalIdentity({ ...base, contactEmail: LegalContactEmail.BUSINESS }).email).toBe(
|
||||
"hello@cutandco.example",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to NextChair when the business has no name or email to show", () => {
|
||||
expect(
|
||||
resolveLegalIdentity({
|
||||
businessName: null,
|
||||
businessEmail: " ",
|
||||
nameDisplay: LegalNameDisplay.BOTH,
|
||||
contactEmail: LegalContactEmail.BUSINESS,
|
||||
}),
|
||||
).toEqual({ name: "NextChair", email: LEGAL.contactEmail });
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,7 @@ export function LegalDocument({
|
||||
<p className="text-muted mt-2 text-sm">Last updated {LEGAL.lastUpdated}</p>
|
||||
<div className="text-ink mt-6 space-y-3 leading-relaxed">{intro}</div>
|
||||
|
||||
<nav aria-label="Contents" className="border-line mt-8 rounded-tile border bg-white p-5">
|
||||
<nav aria-label="Contents" className="border-line rounded-tile mt-8 border bg-white p-5">
|
||||
<p className="text-ink mb-3 text-sm font-bold">Contents</p>
|
||||
<ol className="text-muted grid gap-x-8 gap-y-1.5 text-sm sm:grid-cols-2">
|
||||
{sections.map((section, i) => (
|
||||
@@ -46,7 +46,7 @@ export function LegalDocument({
|
||||
<h2 className="text-ink mb-3 text-xl font-bold tracking-tight">
|
||||
{i + 1}. {section.title}
|
||||
</h2>
|
||||
<div className="text-ink space-y-3 leading-relaxed [&_a]:text-accent-foreground [&_a]:underline [&_li]:pl-1 [&_ul]:list-disc [&_ul]:space-y-1.5 [&_ul]:pl-6">
|
||||
<div className="text-ink [&_a]:text-accent-foreground space-y-3 leading-relaxed [&_a]:underline [&_li]:pl-1 [&_ul]:list-disc [&_ul]:space-y-1.5 [&_ul]:pl-6">
|
||||
{section.body}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
/**
|
||||
* Single source of truth for the facts the Terms of Service and Privacy
|
||||
* Policy pages state about the operator. Every value marked TODO is a
|
||||
* placeholder — fill it in (and have the documents reviewed by a lawyer)
|
||||
* before launch.
|
||||
* Platform-level facts the Terms of Service and Privacy Policy state. Which
|
||||
* of these a business actually shows is controlled per business in
|
||||
* Manage Business → Policies — see legal-identity.ts.
|
||||
*/
|
||||
export const LEGAL = {
|
||||
productName: "NextChair",
|
||||
/** TODO: registered legal entity name. */
|
||||
legalName: "NextChair [LEGAL ENTITY NAME]",
|
||||
/** TODO: registered address. */
|
||||
address: "[REGISTERED ADDRESS]",
|
||||
/** TODO: monitored inbox for legal and privacy requests. */
|
||||
contactEmail: "legal@nextchair.example",
|
||||
/** TODO: jurisdiction whose law governs the Terms and whose courts hear disputes. */
|
||||
governingLaw: "[COUNTRY]",
|
||||
contactEmail: "info@next-chair.co.uk",
|
||||
/** Bump whenever either document changes materially. */
|
||||
lastUpdated: "17 October 2026",
|
||||
} as const;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { cache } from "react";
|
||||
import { LegalContactEmail, LegalNameDisplay } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/db/prisma";
|
||||
import { LEGAL } from "./legal-config";
|
||||
import { resolveLegalIdentity, type LegalIdentity } from "./resolve-legal-identity";
|
||||
|
||||
/**
|
||||
* Identity for the public legal pages. Never throws: before installation
|
||||
* (no business row) or with an unreachable database it falls back to the
|
||||
* NextChair defaults, the same way getBranding() does.
|
||||
*/
|
||||
export const getLegalIdentity = cache(async (): Promise<LegalIdentity> => {
|
||||
try {
|
||||
const business = await prisma.business.findFirst({
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
settings: { select: { legalNameDisplay: true, legalContactEmail: true } },
|
||||
},
|
||||
});
|
||||
return resolveLegalIdentity({
|
||||
businessName: business?.name ?? null,
|
||||
businessEmail: business?.email ?? null,
|
||||
nameDisplay: business?.settings?.legalNameDisplay ?? LegalNameDisplay.NEXTCHAIR,
|
||||
contactEmail: business?.settings?.legalContactEmail ?? LegalContactEmail.NEXTCHAIR,
|
||||
});
|
||||
} catch {
|
||||
return { name: LEGAL.productName, email: LEGAL.contactEmail };
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { LegalContactEmail, LegalNameDisplay } from "@/generated/prisma/enums";
|
||||
import { LEGAL } from "./legal-config";
|
||||
|
||||
export interface LegalIdentity {
|
||||
/** The party named as "we" — NextChair, the business, or both. */
|
||||
name: string;
|
||||
/** Where privacy and legal requests go. */
|
||||
email: string;
|
||||
}
|
||||
|
||||
/** Pure so the Policies form and the pages agree on one set of rules. */
|
||||
export function resolveLegalIdentity(input: {
|
||||
businessName: string | null;
|
||||
businessEmail: string | null;
|
||||
nameDisplay: LegalNameDisplay;
|
||||
contactEmail: LegalContactEmail;
|
||||
}): LegalIdentity {
|
||||
const businessName = input.businessName?.trim() || null;
|
||||
const businessEmail = input.businessEmail?.trim() || null;
|
||||
|
||||
let name: string = LEGAL.productName;
|
||||
if (businessName) {
|
||||
if (input.nameDisplay === LegalNameDisplay.BUSINESS) name = businessName;
|
||||
else if (input.nameDisplay === LegalNameDisplay.BOTH)
|
||||
name = `${LEGAL.productName} and ${businessName}`;
|
||||
}
|
||||
|
||||
const email =
|
||||
input.contactEmail === LegalContactEmail.BUSINESS && businessEmail
|
||||
? businessEmail
|
||||
: LEGAL.contactEmail;
|
||||
return { name, email };
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { LegalContactEmail, LegalNameDisplay } from "@/generated/prisma/enums";
|
||||
import { MediaPicker } from "@/features/media/components/media-picker";
|
||||
import { updateBusinessProfileAction, updateBusinessSettingsAction } from "../actions/business";
|
||||
|
||||
@@ -39,6 +40,8 @@ export interface BusinessDetailsInitial {
|
||||
lateArrivalPolicy: string;
|
||||
bookingTerms: string;
|
||||
privacyPolicyUrl: string;
|
||||
legalNameDisplay: LegalNameDisplay;
|
||||
legalContactEmail: LegalContactEmail;
|
||||
}
|
||||
|
||||
export function BusinessDetailsForm({ initial }: { initial: BusinessDetailsInitial }) {
|
||||
@@ -93,6 +96,8 @@ export function BusinessDetailsForm({ initial }: { initial: BusinessDetailsIniti
|
||||
lateArrivalPolicy: form.lateArrivalPolicy || undefined,
|
||||
bookingTerms: form.bookingTerms || undefined,
|
||||
privacyPolicyUrl: form.privacyPolicyUrl || undefined,
|
||||
legalNameDisplay: form.legalNameDisplay,
|
||||
legalContactEmail: form.legalContactEmail,
|
||||
});
|
||||
if (!settingsResult.success) {
|
||||
setError(settingsResult.error ?? "Couldn't save business policies.");
|
||||
@@ -227,6 +232,41 @@ export function BusinessDetailsForm({ initial }: { initial: BusinessDetailsIniti
|
||||
value={form.privacyPolicyUrl}
|
||||
onChange={(e) => set("privacyPolicyUrl", e.target.value)}
|
||||
/>
|
||||
<div className="border-line space-y-4 border-t pt-4">
|
||||
<p className="text-muted ml-1 text-xs leading-relaxed">
|
||||
Your site's{" "}
|
||||
<a href="/terms" target="_blank" rel="noreferrer" className="underline">
|
||||
Terms of Service
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="/privacy" target="_blank" rel="noreferrer" className="underline">
|
||||
Privacy Policy
|
||||
</a>{" "}
|
||||
pages say who is responsible and where to send requests. Choose what they show.
|
||||
</p>
|
||||
<Select
|
||||
label="Name shown on Terms & Privacy"
|
||||
value={form.legalNameDisplay}
|
||||
onChange={(e) => set("legalNameDisplay", e.target.value as LegalNameDisplay)}
|
||||
>
|
||||
<option value={LegalNameDisplay.NEXTCHAIR}>NextChair</option>
|
||||
<option value={LegalNameDisplay.BUSINESS}>My business name</option>
|
||||
<option value={LegalNameDisplay.BOTH}>NextChair and my business name</option>
|
||||
</Select>
|
||||
<Select
|
||||
label="Contact email on Terms & Privacy"
|
||||
value={form.legalContactEmail}
|
||||
onChange={(e) => set("legalContactEmail", e.target.value as LegalContactEmail)}
|
||||
>
|
||||
<option value={LegalContactEmail.NEXTCHAIR}>NextChair (info@next-chair.co.uk)</option>
|
||||
<option value={LegalContactEmail.BUSINESS}>My business email</option>
|
||||
</Select>
|
||||
{form.legalContactEmail === LegalContactEmail.BUSINESS && !form.email.trim() && (
|
||||
<p className="text-warning-foreground ml-1 text-xs">
|
||||
Add an Email under Contact & Address — until then the NextChair address is shown.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import type { PrismaClient } from "@/generated/prisma/client";
|
||||
import { Weekday } from "@/generated/prisma/enums";
|
||||
import type { LegalContactEmail, LegalNameDisplay } from "@/generated/prisma/enums";
|
||||
import { calendarDateOf, minutesFromMidnight, weekdayOf } from "@/lib/time/business-time";
|
||||
import { fail, ok } from "@/lib/validation-result";
|
||||
import type { ValidationResult } from "@/lib/validation-result";
|
||||
@@ -65,6 +66,10 @@ export interface BusinessSettingsInput {
|
||||
lateArrivalPolicy?: string;
|
||||
bookingTerms?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
/** Who the public Terms/Privacy pages name as "we" — see src/features/legal/legal-identity.ts. */
|
||||
legalNameDisplay?: LegalNameDisplay;
|
||||
/** Whose inbox those pages give for legal/privacy requests. */
|
||||
legalContactEmail?: LegalContactEmail;
|
||||
/** Reviews visibility policy — see src/services/reviews. Applied on top of each Review's own isHidden flag. */
|
||||
reviewsMinRating?: number | null;
|
||||
reviewsExcludeRatingOnly?: boolean;
|
||||
|
||||
Reference in new issue
Block a user