From e703824f599dad636f24967c6a490e1b10f525db Mon Sep 17 00:00:00 2001 From: TurtleWolfe Date: Sun, 16 Aug 2026 20:58:26 +0000 Subject: [PATCH] fix(#773): the checkout said "you agree to our terms" and there were no terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The storefront sells one-time work up to $3,500 and monthly plans up to $249 with nothing published: no refund policy, no scope definition, no cancellation or auto-renewal terms, no limitation of liability. /terms/, /terms-of-service/ and /legal/ all 404'd and no terms-shaped route existed under any name. WORSE THAN MISSING. PaymentConsentModal told every buyer: "By accepting, you agree to our payment processing terms." ...and linked only to the Privacy Policy. The product asserted agreement to a document that did not exist — a representation to the customer that could not be produced if a dispute asked for it. That sentence is now true: it links to the thing being agreed to, and Privacy stays alongside it because they answer different questions. The ticket said "link it from the footer next to Privacy". THERE IS NO SUCH LINK — src/config/footer-links.ts holds three EXTERNAL sites and GlobalNav carries no legal links, so /privacy, /cookies, /comment-policy and /accessibility are reachable only from in-page links. The terms link therefore goes where privacy already is and where it actually matters: the consent modal a buyer sees before paying. Corrected on the issue rather than silently doing something different. Covers services offered, payment, refunds, subscription renewal and cancellation, scope changes, IP ownership of delivered work, acceptable use, limitation of liability, third-party services, and governing law (Tennessee, matching the business address). NO PRICES ARE RESTATED. The `products` table is the only authority on price; writing figures into a legal page creates a second one that drifts. It points at /pricing instead. THIS IS A STARTING POINT AND THE PAGE SAYS SO IN A COMMENT. It is written to be accurate about what this business does rather than comprehensive. Someone selling at these amounts should have it reviewed. The gap worth closing first is that there was nothing at all. Guard: a render test asserting the modal links to /terms — pinned on the LINK, not the wording, so the sentence can be reworded but a modal claiming agreement must point at what is being agreed to. Mutation-tested by restoring the old copy: 1 failed, then green again. Follows src/app/privacy/'s structure and tone, carries its own canonical (#668), and joins the contrast sweep automatically since that enumerates src/app/**/page.tsx. pnpm test 4669/4669 · type-check clean · lint clean Closes #773 Refs #629, #769 Co-Authored-By: Claude Opus 5 (1M context) --- src/app/terms/page.tsx | 273 ++++++++++++++++++ .../PaymentConsentModal.test.tsx | 20 ++ .../PaymentConsentModal.tsx | 18 +- 3 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 src/app/terms/page.tsx diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx new file mode 100644 index 00000000..18a5673b --- /dev/null +++ b/src/app/terms/page.tsx @@ -0,0 +1,273 @@ +import React from 'react'; +import { Metadata } from 'next'; +import Link from 'next/link'; + +export const metadata: Metadata = { + // This route claims its own URL (#668). + alternates: { canonical: '/terms/' }, + openGraph: { url: '/terms/' }, + title: 'Terms of Service - ScriptHammer', + description: + 'The terms that govern purchases of ScriptHammer services and products, including payment, refunds, cancellation and ownership of delivered work.', +}; + +export default function TermsOfServicePage() { + // WHY THIS PAGE EXISTS (#773). The storefront sells one-time work up to $3,500 + // and monthly plans up to $249, and there were no published terms at all — no + // refund policy, no scope definition, no cancellation or auto-renewal terms, no + // limitation of liability. Worse, the payment consent modal already told every + // buyer "By accepting, you agree to our payment processing terms" while linking + // only to the Privacy Policy, so the product asserted agreement to a document + // that did not exist. + // + // THIS IS A STARTING POINT, NOT LEGAL ADVICE. It is written to be accurate about + // what this business actually does rather than to be comprehensive. Anyone + // selling at these amounts should have it reviewed. Prices and plan names below + // are stated in general terms on purpose — the `products` table is the only + // authority on price, and duplicating figures here would create a second one + // that drifts. + const lastUpdated = '2026-08-16'; + + return ( +
+
+

+ Terms of Service +

+
+ +
+

+ Last updated: {lastUpdated} +

+ +
+

1. Who these terms are between

+

+ These terms govern your purchase and use of services and digital + products from ScriptHammer (“we”, “us”). By + placing an order, or by accepting the payment consent prompt at + checkout, you agree to them. If you are buying on behalf of a + company, you confirm you are authorised to accept these terms for + it. +

+

+ How we handle your personal information is covered separately in our{' '} + + Privacy Policy + + . +

+
+ +
+

2. What we sell

+

+ We offer one-time project work — discovery + sessions, landing pages and full business sites — alongside + digital products and{' '} + monthly plans covering hosting, updates and + support. Current prices, inclusions and availability are shown on + our{' '} + + pricing page + + , which is the authoritative statement of what a given item costs at + the time you buy it. +

+

+ Some items are marked as not yet available. Those cannot be + purchased, and nothing on this page should be read as an offer to + sell them. +

+
+ +
+

3. Payment

+

+ Prices are in US dollars unless stated otherwise. Card payments are + processed by our payment providers; we never receive or store your + full card details. One-time work is charged when you place the + order. Where an item is offered on a deposit basis, the deposit is + charged up front and the balance becomes due as described at + checkout. +

+

+ The price shown at checkout is the price charged.{' '} + Amounts are determined on our servers from our catalogue, not from + anything your browser sends. +

+
+ +
+

4. Subscriptions, renewal and cancellation

+

+ Monthly plans renew automatically each month at the + then-current price until you cancel. We will tell you in advance of + any price change, and you may cancel before it takes effect. +

+

+ You can cancel at any time from your account. Cancellation takes + effect at the end of the billing period you have already paid for — + you keep access until then, and you are not charged again. We do not + pro-rate partial months. +

+

+ We may suspend or cancel a plan if payment fails repeatedly. Where + that happens you will have a grace period to update your payment + details before access ends. +

+
+ +
+

5. Refunds

+

+ Before work begins on a one-time project, you may + cancel for a full refund. +

+

+ Once work has begun, we refund the portion not yet + performed. Deposits covering work already carried out are not + refundable. +

+

+ Digital products delivered immediately are not + refundable once downloaded or accessed, except where the product is + faulty or not as described. +

+

+ Subscriptions are not refunded for the current + period; cancelling stops the next charge. +

+

+ Voluntary contributions (for example the tip jar) are not + refundable. +

+

+ None of this limits rights you have under consumer law that cannot + be excluded by agreement. +

+
+ +
+

6. Scope, changes and your input

+

+ Project work is delivered against the scope agreed in writing before + it starts. Work outside that scope is a change: we will quote it, + and it proceeds only once you approve the quote. +

+

+ Delivery depends on your input — content, access, credentials, and + timely feedback. Where those are delayed, timelines move + accordingly. If a project stalls on our side beyond an agreed date, + you may cancel under section 5. +

+
+ +
+

7. Ownership of delivered work

+

+ You own what you paid us to make — the code, + content and configuration produced specifically for you — once the + work is paid for in full. +

+

+ We retain ownership of our pre-existing and general-purpose + materials: templates, libraries, tooling and know-how that existed + before your project or that we develop for general use. You receive + a perpetual, non-exclusive licence to use those as part of the + delivered work. +

+

+ Third-party components keep their own licences. Open-source + dependencies remain governed by those licences, not by this + agreement. +

+

+ You retain ownership of everything you supply to us, and you confirm + you have the right to supply it. +

+
+ +
+

8. Acceptable use

+

+ You agree not to use our services to break the law, to infringe + someone else’s rights, or to send unsolicited bulk messages. + We may decline or discontinue work that would require us to do any + of those things. +

+
+ +
+

9. Warranties and limitation of liability

+

+ We provide our services with reasonable skill and care. Beyond that, + and to the extent the law allows, services and products are provided + “as is” without further warranty — + including any implied warranty of merchantability or fitness for a + particular purpose. +

+

+ + Our total liability for any claim is limited to the amount you + paid us for the item the claim relates to + + , in the twelve months before the claim arose. We are not liable for + indirect or consequential losses, including lost profits, lost + revenue or lost data. +

+

+ Nothing here excludes liability that cannot lawfully be excluded, + including for fraud or for death or personal injury caused by + negligence. +

+
+ +
+

10. Third-party services

+

+ Delivered work may rely on third-party services — hosting, payment + processing, email delivery, analytics and similar. Those services + are governed by their own terms, and we are not responsible for + their availability or their changes. Where a third-party service we + selected becomes unavailable, we will work with you on a + replacement; that work is scoped under section 6. +

+
+ +
+

11. Changes to these terms

+

+ We may update these terms. The version in force for a purchase is + the one published when you placed that order. Material changes + affecting an active subscription take effect at your next renewal, + and we will tell you before that happens. +

+
+ +
+

12. Governing law

+

+ These terms are governed by the laws of the State of Tennessee, USA, + and the courts of that state have jurisdiction — without affecting + any right you have to bring a claim where you live. +

+
+ +
+

13. Contact

+

+ Questions about these terms, an order, or a refund should go through + our{' '} + + contact page + + . We would much rather hear from you directly than have you raise a + dispute with your bank — we can usually fix things faster. +

+
+
+
+ ); +} diff --git a/src/components/payment/PaymentConsentModal/PaymentConsentModal.test.tsx b/src/components/payment/PaymentConsentModal/PaymentConsentModal.test.tsx index 96a8626f..cf018b69 100644 --- a/src/components/payment/PaymentConsentModal/PaymentConsentModal.test.tsx +++ b/src/components/payment/PaymentConsentModal/PaymentConsentModal.test.tsx @@ -63,6 +63,26 @@ describe('PaymentConsentModal', () => { expect(screen.getByText(customMessage)).toBeInTheDocument(); }); + /** + * The modal tells the buyer they are agreeing to terms. #773: it said exactly + * that while linking only to the Privacy Policy, and no terms page existed — + * a representation to the customer that could not be produced if a dispute + * asked for it. + * + * Asserting the LINK, not the wording: the sentence may be reworded, but a + * modal that claims agreement must point at the thing being agreed to. + */ + it('links to the Terms of Service it says you are agreeing to (#773)', () => { + render(); + + const terms = screen.getByRole('link', { name: /terms of service/i }); + expect(terms).toHaveAttribute('href', '/terms'); + + // Privacy must survive alongside it — they answer different questions. + const privacy = screen.getByRole('link', { name: /privacy policy/i }); + expect(privacy).toHaveAttribute('href', '/privacy'); + }); + it('renders accept and decline buttons', () => { render(); diff --git a/src/components/payment/PaymentConsentModal/PaymentConsentModal.tsx b/src/components/payment/PaymentConsentModal/PaymentConsentModal.tsx index b03f158e..10caba6a 100644 --- a/src/components/payment/PaymentConsentModal/PaymentConsentModal.tsx +++ b/src/components/payment/PaymentConsentModal/PaymentConsentModal.tsx @@ -188,9 +188,23 @@ export const PaymentConsentModal: React.FC = ({ - {/* Privacy Notice */} + {/* + This said "you agree to our payment processing terms" while linking + only to the Privacy Policy — asserting agreement to a document that did + not exist (#773). Both are linked now, so the sentence is true and the + terms a buyer is said to accept can actually be produced if a dispute + asks for them. + */}

- By accepting, you agree to our payment processing terms. + By accepting, you agree to our{' '} + + Terms of Service + + .
Read our{' '}