[object Object]

← back to IWasCute

TK-11412: add /privacy + /terms, and stop the licensor flow claiming a submission that never happened

316515b73725460213d0491be16d08edc3467209 · 2026-09-10 18:06:24 -0700 · Steve Abrams

Two blockers on IWasCute public readiness, both verified live before changing anything.

1. /privacy and /terms did not exist. The login and licensor pages link to both,
   so every visitor following those links got a 404 - and a reachable, accurate
   privacy policy is a hard AdSense requirement, not a nicety. Both pages are
   written from what the code actually does: the two real third parties (AdSense
   ca-pub-5278231299883833 and GA4 G-2DHDBP8R78, both in layout.tsx) and the real
   columns in db/001_schema.sql - email, display name, adult confirmation,
   photographer and copyright fields, the likeness signature, and the separate
   editorial / commercial / AI-training consents. No boilerplate claims about data
   we do not collect.

2. ReviewSubmit simulated a submission. It waited 2.2s then set success on
   Math.random() > 0.05, telling a real person their photos were in the queue and
   that we would email them when cleared - for photographs that were never
   transmitted - and showing a random failure the other 5% of the time. There is
   no /api/licensor/submit endpoint (the only API routes are auth and admin), so
   nothing was ever sent or stored. The flow now ends on an honest preview state
   saying plainly that submissions are not open, nothing was uploaded, and the
   data stayed in the browser.

'success' is deliberately removed from SubmitState rather than left unused: it
would compile fine but render nothing, dropping the user back on the form.

Verified: tsc --noEmit clean, next build succeeds, /privacy and /terms present in
the route table as static pages.

Deploy is NOT included here and stays gated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 316515b73725460213d0491be16d08edc3467209
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 18:06:24 2026 -0700

    TK-11412: add /privacy + /terms, and stop the licensor flow claiming a submission that never happened
    
    Two blockers on IWasCute public readiness, both verified live before changing anything.
    
    1. /privacy and /terms did not exist. The login and licensor pages link to both,
       so every visitor following those links got a 404 - and a reachable, accurate
       privacy policy is a hard AdSense requirement, not a nicety. Both pages are
       written from what the code actually does: the two real third parties (AdSense
       ca-pub-5278231299883833 and GA4 G-2DHDBP8R78, both in layout.tsx) and the real
       columns in db/001_schema.sql - email, display name, adult confirmation,
       photographer and copyright fields, the likeness signature, and the separate
       editorial / commercial / AI-training consents. No boilerplate claims about data
       we do not collect.
    
    2. ReviewSubmit simulated a submission. It waited 2.2s then set success on
       Math.random() > 0.05, telling a real person their photos were in the queue and
       that we would email them when cleared - for photographs that were never
       transmitted - and showing a random failure the other 5% of the time. There is
       no /api/licensor/submit endpoint (the only API routes are auth and admin), so
       nothing was ever sent or stored. The flow now ends on an honest preview state
       saying plainly that submissions are not open, nothing was uploaded, and the
       data stayed in the browser.
    
    'success' is deliberately removed from SubmitState rather than left unused: it
    would compile fine but render nothing, dropping the user back on the form.
    
    Verified: tsc --noEmit clean, next build succeeds, /privacy and /terms present in
    the route table as static pages.
    
    Deploy is NOT included here and stays gated.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 src/app/privacy/page.tsx                 | 133 +++++++++++++++++++++++++++++++
 src/app/terms/page.tsx                   | 127 +++++++++++++++++++++++++++++
 src/components/licensor/ReviewSubmit.tsx |  61 ++++++++------
 3 files changed, 296 insertions(+), 25 deletions(-)

diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx
new file mode 100644
index 0000000..4772f39
--- /dev/null
+++ b/src/app/privacy/page.tsx
@@ -0,0 +1,133 @@
+import type { Metadata } from 'next'
+import Link from 'next/link'
+import Navbar from '@/components/shared/Navbar'
+
+export const metadata: Metadata = {
+  title: 'Privacy Policy — I was Cute',
+  description:
+    'What I was Cute collects, how it is used, who it is shared with, and how to have it removed.',
+}
+
+const UPDATED = 'September 10, 2026'
+
+const SECTIONS: { heading: string; body: string[] }[] = [
+  {
+    heading: 'Current status of this service',
+    body: [
+      'I was Cute is in public preview. The photo-submission flow is a walkthrough of the intended experience: it does not yet send, upload, or store the photographs or release details you enter. Nothing you type into the licensor flow leaves your browser, and no submission is queued for review. We say this here because the rest of this policy describes what we collect once submission is live, and we do not want that read as a description of what is happening today.',
+      'The parts of this policy that apply right now are the account and analytics sections below.',
+    ],
+  },
+  {
+    heading: 'What we collect',
+    body: [
+      'Account information, if you create an account: your email address, a display name, a hashed password (we never store the password itself), whether your email has been verified, whether you have confirmed you are 18 or older, and an optional avatar image.',
+      'Photo submissions, once submission is live: the photographs you choose to upload, and the rights information you provide with them — your relationship to the photo, the photographer’s name, the approximate year it was taken, whether you have the photographer’s permission or hold estate rights, whether you are the person pictured, whether that person is now an adult, which uses you permit (editorial, commercial, AI training), and the name you type as a signature on the likeness release, with the date and time you signed it.',
+      'Review records: the outcome of our rights review, who reviewed it, when, and any notes attached to that decision.',
+    ],
+  },
+  {
+    heading: 'Why we collect it',
+    body: [
+      'The rights information exists so that no photograph is offered for license until its copyright and likeness permissions are documented. That is the entire premise of the service, and it is why the questions are as specific as they are. We use your email to contact you about your own submissions and account.',
+      'We do not sell your personal information, and we do not use your photographs for any purpose you did not explicitly permit in the release. If you do not tick AI training, your photograph is not offered for AI training.',
+    ],
+  },
+  {
+    heading: 'Photographs of children',
+    body: [
+      'This service is built around photographs of people’s own childhoods, which means the images often depict minors. That raises the stakes rather than lowering them. Accounts are for adults only: you must confirm you are 18 or older, and you may only submit a photograph if you are the person pictured, or you hold the rights and the pictured person consents. We do not accept submissions of children who are still minors, and we do not knowingly collect personal information from anyone under 18.',
+      'If you believe a photograph of you was submitted without your consent, contact us and we will remove it. We will not require you to prove ownership of the image to have your own likeness taken down.',
+    ],
+  },
+  {
+    heading: 'Third parties on this site',
+    body: [
+      'Google Analytics 4 (measurement ID G-2DHDBP8R78) records how visitors move through the site — pages viewed, approximate location, device and browser. It is loaded on every page.',
+      'Google AdSense (publisher ID ca-pub-5278231299883833) serves advertising and may use cookies or similar technologies to personalize the ads you see. Google’s use of this data is governed by Google’s own privacy policy, and you can review or change your ad personalization settings at google.com/settings/ads.',
+      'These two are the only third-party services that receive data from your visit. We do not share account details or photographs with them.',
+    ],
+  },
+  {
+    heading: 'Cookies',
+    body: [
+      'Cookies on this site come from the two Google services above (analytics and advertising) and, once accounts are live, from keeping you signed in. You can block or delete cookies in your browser; analytics and advertising cookies are not required for the site to work, though blocking them may affect the ads you are shown.',
+    ],
+  },
+  {
+    heading: 'How long we keep things',
+    body: [
+      'Account records are kept while your account exists. Photographs and their release records are kept while the photograph is listed, and are deleted when you withdraw it — except that we retain the record of the rights decision itself, because it is the evidence that a licensed use was properly cleared at the time it was granted.',
+    ],
+  },
+  {
+    heading: 'Your choices',
+    body: [
+      'You can ask us what we hold about you, ask us to correct it, ask us to delete it, or withdraw a photograph from the marketplace at any time. Withdrawing a photo stops new licenses; it cannot retroactively cancel a license someone already purchased, which is why the permissions you set at submission matter.',
+      'Depending on where you live you may have additional rights under laws such as the GDPR or the CCPA. We apply the choices above to everyone regardless of location.',
+    ],
+  },
+  {
+    heading: 'Contact',
+    body: [
+      'For any privacy question, removal request, or correction, use the contact page. Removal requests are handled as a priority, and you do not need to explain why.',
+    ],
+  },
+]
+
+export default function PrivacyPage() {
+  return (
+    <>
+      <Navbar />
+      <main
+        className="flex-1 px-6 py-16 md:px-12"
+        style={{ background: 'var(--color-cream)', color: 'var(--color-brown)' }}
+      >
+        <div className="mx-auto max-w-2xl">
+          <p
+            className="text-xs uppercase tracking-[0.2em] mb-3"
+            style={{ color: 'var(--color-warm-gray)' }}
+          >
+            Privacy
+          </p>
+          <h1 className="text-4xl font-black tracking-tighter mb-3">Privacy Policy</h1>
+          <p className="text-sm mb-10" style={{ color: 'var(--color-warm-gray)' }}>
+            Last updated {UPDATED}
+          </p>
+
+          {SECTIONS.map(({ heading, body }) => (
+            <section key={heading} className="mb-10">
+              <h2 className="text-xl font-bold mb-3">{heading}</h2>
+              {body.map((p, i) => (
+                <p
+                  key={i}
+                  className="mb-3 leading-relaxed"
+                  style={{ color: 'var(--color-warm-gray)' }}
+                >
+                  {p}
+                </p>
+              ))}
+            </section>
+          ))}
+
+          <div className="flex flex-wrap gap-3 mt-12">
+            <Link
+              href="/terms"
+              className="px-5 py-2.5 rounded-2xl text-sm font-semibold transition-all hover:scale-105"
+              style={{ background: 'var(--color-cream-dark)', color: 'var(--color-brown)' }}
+            >
+              Terms of Service
+            </Link>
+            <Link
+              href="/contact"
+              className="px-5 py-2.5 rounded-2xl text-sm font-semibold transition-all hover:scale-105"
+              style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
+            >
+              Contact us
+            </Link>
+          </div>
+        </div>
+      </main>
+    </>
+  )
+}
diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx
new file mode 100644
index 0000000..ccd53f1
--- /dev/null
+++ b/src/app/terms/page.tsx
@@ -0,0 +1,127 @@
+import type { Metadata } from 'next'
+import Link from 'next/link'
+import Navbar from '@/components/shared/Navbar'
+
+export const metadata: Metadata = {
+  title: 'Terms of Service — I was Cute',
+  description:
+    'The terms that govern using I was Cute: what you keep, what you license, and what we will not do with your photographs.',
+}
+
+const UPDATED = 'September 10, 2026'
+
+const SECTIONS: { heading: string; body: string[] }[] = [
+  {
+    heading: 'Public preview',
+    body: [
+      'I was Cute is in public preview. The licensor flow demonstrates the intended submission experience but does not yet transmit or store photographs, and no photograph can currently be listed, licensed, or earn a royalty. Where these terms describe licensing and payment, they describe how the service will operate when it opens; they are not a representation that it operates that way today.',
+    ],
+  },
+  {
+    heading: 'You keep your photographs',
+    body: [
+      'You do not sell or assign your images to us. You grant a license for specific uses that you choose, and you keep ownership throughout. We act as the marketplace that documents those permissions and connects you with licensees.',
+    ],
+  },
+  {
+    heading: 'What you confirm when you submit',
+    body: [
+      'That you are 18 or older. That you either hold the rights to the photograph or have the permission of the person who does. That you are the person pictured, or that the person pictured has consented to it being listed. That the person pictured is now an adult.',
+      'These are not formalities. A photograph submitted without the pictured person’s consent will be removed, and repeated misrepresentation will end your account.',
+    ],
+  },
+  {
+    heading: 'The permissions you set',
+    body: [
+      'For each photograph you choose which uses are allowed: editorial, commercial, and AI training, each independently. A use you do not select is a use we do not offer. In particular, if you do not select AI training, your photograph is not made available for training datasets.',
+      'You can withdraw a photograph at any time. Withdrawal stops any new license from being granted. It cannot revoke a license already purchased, because a licensee will have relied on it — which is why the permissions you choose at submission are the decision that matters.',
+    ],
+  },
+  {
+    heading: 'Rights review',
+    body: [
+      'Every submission is reviewed before it can be listed. We may decline a photograph for any reason, including incomplete rights information, doubt about consent, or an image that depicts someone who is still a minor. A decline is not an accusation; it means we could not establish the rights to our own satisfaction.',
+    ],
+  },
+  {
+    heading: 'Royalties',
+    body: [
+      'When licensing is live, you receive a royalty when a licensee purchases usage of your photograph. Rates, payment thresholds and schedules will be stated in your account before you list anything, and will not be changed retroactively for licenses already granted.',
+    ],
+  },
+  {
+    heading: 'Acceptable use',
+    body: [
+      'Do not submit photographs you do not have the rights to, images of people who have not consented, images of people who are currently minors, or content that is sexual, exploitative, or unlawful. Do not attempt to scrape, bulk-download, or re-license imagery from this site outside the terms of a purchased license.',
+    ],
+  },
+  {
+    heading: 'Removal',
+    body: [
+      'If you are pictured in a photograph on this service and did not consent to it, contact us and we will remove it. You do not need to own the image or prove anything to have your own likeness taken down.',
+    ],
+  },
+  {
+    heading: 'Changes and contact',
+    body: [
+      'We will update these terms as the service moves out of preview, and the last-updated date above will change with them. Material changes affecting photographs already listed will be communicated to the account that listed them. Questions go through the contact page.',
+    ],
+  },
+]
+
+export default function TermsPage() {
+  return (
+    <>
+      <Navbar />
+      <main
+        className="flex-1 px-6 py-16 md:px-12"
+        style={{ background: 'var(--color-cream)', color: 'var(--color-brown)' }}
+      >
+        <div className="mx-auto max-w-2xl">
+          <p
+            className="text-xs uppercase tracking-[0.2em] mb-3"
+            style={{ color: 'var(--color-warm-gray)' }}
+          >
+            Terms
+          </p>
+          <h1 className="text-4xl font-black tracking-tighter mb-3">Terms of Service</h1>
+          <p className="text-sm mb-10" style={{ color: 'var(--color-warm-gray)' }}>
+            Last updated {UPDATED}
+          </p>
+
+          {SECTIONS.map(({ heading, body }) => (
+            <section key={heading} className="mb-10">
+              <h2 className="text-xl font-bold mb-3">{heading}</h2>
+              {body.map((p, i) => (
+                <p
+                  key={i}
+                  className="mb-3 leading-relaxed"
+                  style={{ color: 'var(--color-warm-gray)' }}
+                >
+                  {p}
+                </p>
+              ))}
+            </section>
+          ))}
+
+          <div className="flex flex-wrap gap-3 mt-12">
+            <Link
+              href="/privacy"
+              className="px-5 py-2.5 rounded-2xl text-sm font-semibold transition-all hover:scale-105"
+              style={{ background: 'var(--color-cream-dark)', color: 'var(--color-brown)' }}
+            >
+              Privacy Policy
+            </Link>
+            <Link
+              href="/contact"
+              className="px-5 py-2.5 rounded-2xl text-sm font-semibold transition-all hover:scale-105"
+              style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
+            >
+              Contact us
+            </Link>
+          </div>
+        </div>
+      </main>
+    </>
+  )
+}
diff --git a/src/components/licensor/ReviewSubmit.tsx b/src/components/licensor/ReviewSubmit.tsx
index 7894385..b9181b5 100644
--- a/src/components/licensor/ReviewSubmit.tsx
+++ b/src/components/licensor/ReviewSubmit.tsx
@@ -24,7 +24,12 @@ interface ReviewSubmitProps {
   onBack: () => void
 }
 
-type SubmitState = 'idle' | 'submitting' | 'success' | 'error'
+// 'preview' replaces 'success' while the app is in public preview: there is no submit
+// endpoint, so the flow must not claim a submission happened. 'success' is deliberately NOT
+// in this union — leaving it would compile fine but render nothing, silently dropping the
+// user back on the form. Re-add it together with its own render branch when a real
+// /api/licensor/submit exists.
+type SubmitState = 'idle' | 'submitting' | 'preview' | 'error'
 
 function SectionCard({
   icon,
@@ -102,24 +107,25 @@ export default function ReviewSubmit({
     .filter(Boolean)
     .join(', ')
 
+  // PUBLIC PREVIEW. There is no /api/licensor/submit endpoint yet (the only API routes on
+  // this app are /api/auth and /api/admin), so nothing is transmitted or stored here.
+  //
+  // This previously simulated a 2.2s upload and then set success on `Math.random() > 0.05`,
+  // which showed a real person "You're in the queue! ... We'll notify you by email" for
+  // photographs that were never sent, and a random failure the other 5% of the time. That is
+  // a false statement to a user about their own photos and their rights release, so it is
+  // replaced with an honest preview outcome until a real submit endpoint exists.
+  //
+  // To make this live: add /api/licensor/submit, POST the FormData, and restore a success
+  // state that reflects the server's actual response.
   const handleSubmit = async () => {
     setSubmitState('submitting')
     setErrorMsg('')
-
-    // Simulated async submission
-    await new Promise((r) => setTimeout(r, 2200))
-
-    // In production: POST to /api/licensor/submit with FormData
-    const success = Math.random() > 0.05 // 95% success rate for demo
-    if (success) {
-      setSubmitState('success')
-    } else {
-      setSubmitState('error')
-      setErrorMsg('Something went wrong. Please try again.')
-    }
+    await new Promise((r) => setTimeout(r, 600))
+    setSubmitState('preview')
   }
 
-  if (submitState === 'success') {
+  if (submitState === 'preview') {
     return (
       <motion.div
         initial={{ opacity: 0, scale: 0.96 }}
@@ -132,19 +138,24 @@ export default function ReviewSubmit({
           animate={{ scale: 1 }}
           transition={{ type: 'spring', stiffness: 300, damping: 22, delay: 0.1 }}
           className="w-24 h-24 rounded-full flex items-center justify-center"
-          style={{ background: 'var(--color-sage)' }}
+          style={{ background: 'var(--color-cream-dark)' }}
         >
-          <CheckCircle2 size={48} style={{ color: 'white' }} />
+          <CheckCircle2 size={48} style={{ color: 'var(--color-brown)' }} />
         </motion.div>
 
         <div className="flex flex-col gap-3 max-w-md">
           <h2 className="text-3xl font-black tracking-tighter" style={{ color: 'var(--color-brown)' }}>
-            You&apos;re in the queue!
+            Submissions aren&apos;t open yet
           </h2>
           <p className="text-base leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
-            Your {photos.length} photo{photos.length !== 1 ? 's' : ''}{' '}
-            {photos.length !== 1 ? 'have' : 'has'} been submitted for rights review. We&apos;ll
-            notify you by email when they&apos;re cleared for licensing.
+            You&apos;ve reached the end of the walkthrough. I was Cute is in public preview, so
+            your {photos.length} photo{photos.length !== 1 ? 's' : ''}{' '}
+            {photos.length !== 1 ? 'were' : 'was'} <strong>not</strong> uploaded and nothing you
+            entered was sent or stored — it stayed in this browser.
+          </p>
+          <p className="text-base leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+            This is what the flow will look like when licensing opens. If you&apos;d like us to
+            tell you when it does, get in touch.
           </p>
         </div>
 
@@ -153,13 +164,13 @@ export default function ReviewSubmit({
           style={{ background: 'var(--color-cream-dark)' }}
         >
           <p className="text-xs font-bold tracking-widest uppercase" style={{ color: 'var(--color-warm-gray)' }}>
-            What happens next
+            How it will work
           </p>
           {[
-            { step: '1', text: 'Rights review (24–48 hours)' },
+            { step: '1', text: 'Rights review of every submission' },
             { step: '2', text: 'Copyright clearance notification' },
             { step: '3', text: 'Photos go live in marketplace' },
-            { step: '4', text: 'Royalties deposited when licensed' },
+            { step: '4', text: 'Royalties when a licensee buys usage' },
           ].map(({ step, text }) => (
             <div key={step} className="flex items-center gap-3">
               <div
@@ -176,11 +187,11 @@ export default function ReviewSubmit({
         </div>
 
         <a
-          href="/dashboard"
+          href="/contact"
           className="inline-flex items-center gap-2 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
           style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
         >
-          Go to My Dashboard
+          Tell me when it opens
           <ExternalLink size={14} />
         </a>
       </motion.div>

← cb37a2a iwascute: pin .deploy.conf PROJECT_NAME=gap-iwascute (real l  ·  back to IWasCute  ·  (newest)