add new GDPR page and handle account creation email
This commit is contained in:
@@ -141,10 +141,6 @@
|
||||
qrDataUrl = await QRCode.toDataURL(url, { width: 200, margin: 1 });
|
||||
}
|
||||
|
||||
function handleParentInvite() {
|
||||
alert('Parent invite coming soon — email would be sent to ' + parentInviteEmail);
|
||||
}
|
||||
|
||||
function handleInviteResult(result: any) {
|
||||
if (result.type === 'success') {
|
||||
const d = result.data as { ok?: boolean; error?: string } | undefined;
|
||||
|
||||
@@ -34,11 +34,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<AuthShell
|
||||
title="Log in"
|
||||
subtitle="Welcome back to FamDone."
|
||||
secondary={secondaryChild}
|
||||
>
|
||||
<AuthShell title="Log in" subtitle="Welcome back to FamDone." secondary={secondaryChild}>
|
||||
{#if showForgot}
|
||||
{#if forgotSuccess}
|
||||
<p class="form-ok">
|
||||
@@ -93,6 +89,10 @@
|
||||
Forgot password?
|
||||
</button>
|
||||
<button type="submit">Log in</button>
|
||||
<p class="privacy-note">
|
||||
We use a cookie to keep you signed in and collect only what we need to run your account. See
|
||||
our <a href="/privacy">Privacy Policy</a>.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<p class="alt">
|
||||
@@ -194,6 +194,21 @@
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-note {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.78rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-note a {
|
||||
color: #4338ca;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.privacy-note a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.alt a {
|
||||
color: #4338ca;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
<script lang="ts">
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import FamDone from '$lib/components/FamDone.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Privacy Policy — FamDone</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="How FamDone collects, uses, and protects your personal data — in plain, GDPR-friendly language."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<main class="privacy">
|
||||
<a class="brand" href="/"><FamDone /></a>
|
||||
<div class="card">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="updated">Last updated: September 2026</p>
|
||||
|
||||
<section>
|
||||
<h2>1. Who we are (the controller)</h2>
|
||||
<p>
|
||||
<FamDone /> is provided by <strong>threejjjs / Interactive Tech</strong>. We are the "data
|
||||
controller" for the personal data you give us. If you have any questions about this policy
|
||||
or your data, you can reach us at
|
||||
<a href="mailto:privacy@threejjjs.xyz">privacy@threejjjs.xyz</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. What data we collect</h2>
|
||||
<p>We keep things minimal. When you create an account we collect:</p>
|
||||
<ul>
|
||||
<li><strong>Email address</strong> — used only as your login identity.</li>
|
||||
<li>
|
||||
<strong>Usernames and family/chore names</strong> — so your account and your family's chores
|
||||
work.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
We do <strong>not</strong> collect any sensitive data (race, health, religion, biometrics, etc.),
|
||||
and we do not use your data for profiling, advertising, or predictions about you.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Why we collect it (the purpose & legal basis)</h2>
|
||||
<p>
|
||||
We process your data solely to provide the service you asked for: to create and maintain
|
||||
your account, and to authenticate you when you log in. The legal basis for this is <strong
|
||||
>contract necessity</strong
|
||||
> (Article 6(1)(b) GDPR) — we process the data needed to run the account you signed up for.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. How long we keep it</h2>
|
||||
<p>
|
||||
We keep your data for as long as your account is active. If you delete your account, (see
|
||||
"Your rights" below) we delete or anonymise your personal data in a reasonable timeframe,
|
||||
unless we're legally required to keep it longer.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Do we share or sell your data?</h2>
|
||||
<p>
|
||||
<strong>No.</strong> We do not sell, rent, or trade your personal data, and we do not share it
|
||||
with third parties for their own marketing purposes. We only share the data that's strictly necessary
|
||||
to run the service (for example, processing a payment you make), and we use your email only to
|
||||
sign you in.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Cookies and login</h2>
|
||||
<p>
|
||||
We use a strictly-necessary session/login cookie so you can stay signed in. This is
|
||||
essential for the service to work and does not require separate consent. We do not use
|
||||
cookies for advertising, analytics, or marketing pixels.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Your rights</h2>
|
||||
<p>Under the GDPR you have the right to:</p>
|
||||
<ul>
|
||||
<li><strong>Access</strong> the personal data we hold about you.</li>
|
||||
<li><strong>Rectify</strong> inaccurate data.</li>
|
||||
<li>Request <strong>deletion</strong> ("right to be forgotten") of your data.</li>
|
||||
<li><strong>Portability</strong> — receive your data in a machine-readable format.</li>
|
||||
<li>Withdraw consent or object to processing where it applies.</li>
|
||||
</ul>
|
||||
<p>
|
||||
To exercise any of these rights, email
|
||||
<a href="mailto:privacy@threejjjs.xyz">privacy@threejjjs.xyz</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>8. International transfers</h2>
|
||||
<p>
|
||||
We store your data on our secure hosting providers. If your data is ever transferred outside
|
||||
the European Economic Area (EEA), we rely on recognised safeguards (such as the relevant
|
||||
adequacy decisions or the EU's Standard Contractual Clauses) to protect it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. Complaints</h2>
|
||||
<p>
|
||||
If you believe we've handled your data incorrectly, please contact us first at
|
||||
<a href="mailto:privacy@threejjjs.xyz">privacy@threejjjs.xyz</a>. You also have the right to
|
||||
lodge a complaint with your local supervisory authority at any time.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. Children's data</h2>
|
||||
<p>
|
||||
A family can add child members under a parent's account. We only keep the child's chosen
|
||||
username so their chores and progress work. As a parent you control this data and can change
|
||||
or delete it from your family settings or by contacting us.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>11. Contact</h2>
|
||||
<p>
|
||||
Questions about this policy or your data? Email
|
||||
<a href="mailto:privacy@threejjjs.xyz">privacy@threejjjs.xyz</a>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem 1rem 0;
|
||||
background: linear-gradient(135deg, #eef2ff 0%, #ffffff 55%, #eef2ff 100%);
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: #1f2937;
|
||||
}
|
||||
.brand {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #4338ca;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 30px rgba(67, 56, 202, 0.08);
|
||||
padding: 2.5rem;
|
||||
}
|
||||
.card h1 {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
.updated {
|
||||
margin: 0 0 2rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.card section {
|
||||
margin: 0 0 1.75rem;
|
||||
}
|
||||
.card section h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
color: #111827;
|
||||
}
|
||||
.card p {
|
||||
margin: 0 0 0.6rem;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.card ul {
|
||||
margin: 0 0 0.6rem;
|
||||
padding-left: 1.25rem;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.card li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.card a {
|
||||
color: #4338ca;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.card a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import { setSessionCookie } from '$lib/server/session';
|
||||
import { issueAccess } from '$lib/server/member-otp';
|
||||
import { slugify, handle, famUsername, handleOf } from '@shared/slugify';
|
||||
import { applyAccessCode } from '$lib/server/access';
|
||||
import { sendWelcomeEmail, sendAccessUnlockedEmail } from '$lib/server/email';
|
||||
import { createEmbeddedCheckoutSession, PLAN_IDS } from '$lib/server/stripe';
|
||||
import type { PlanId } from '$lib/server/stripe';
|
||||
|
||||
@@ -70,6 +71,10 @@ export const actions = {
|
||||
.catch(() => null);
|
||||
if (authResult?.token) setSessionCookie(event.cookies, authResult.token);
|
||||
|
||||
// Welcome email — a best-effort send. Failures must not block signup.
|
||||
const dashboardUrl = `${event.url.origin}/${slug}`;
|
||||
sendWelcomeEmail({ to: email, famName, dashboardUrl }).catch(() => {});
|
||||
|
||||
return { success: true, famSlug: slug, username: handleName };
|
||||
},
|
||||
|
||||
@@ -111,6 +116,27 @@ export const actions = {
|
||||
const result = await applyAccessCode(user.famId, code);
|
||||
if (result.error) return fail(400, { error: result.error });
|
||||
|
||||
// Access code applied successfully — confirm by email (best-effort).
|
||||
// Failure must never block the user continuing to the plan step.
|
||||
if (result.ok) {
|
||||
const fam = await pbAdmin
|
||||
.getOne('fams', user.famId)
|
||||
.catch(() => ({ name: '', slug: user.famId }));
|
||||
const parents = await pbAdmin
|
||||
.getList('users', `famId = '${user.famId}' && role = 'parent'`)
|
||||
.catch(() => []);
|
||||
const parentEmail = (parents[0] as { email?: string } | undefined | null)?.email;
|
||||
if (parentEmail) {
|
||||
sendAccessUnlockedEmail({
|
||||
to: parentEmail,
|
||||
famName: fam.name || '',
|
||||
codeName: result.code?.name || code,
|
||||
codeValue: code,
|
||||
dashboardUrl: `${event.url.origin}/${fam.slug || user.famId}`
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, ...result };
|
||||
},
|
||||
|
||||
@@ -148,4 +174,4 @@ export const actions = {
|
||||
function requireUser(event: RequestEvent) {
|
||||
if (!event.locals.user) throw redirect(303, '/signup');
|
||||
return event.locals.user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,10 @@
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={submitting}>Create my family</button>
|
||||
<p class="privacy-note">
|
||||
By signing up you agree to our <a href="/privacy">Privacy Policy</a>. We collect your email
|
||||
only to create and log into your account, never sell or share your data.
|
||||
</p>
|
||||
</form>
|
||||
<p class="alt">Already have a family? <a href="/login">Log in</a></p>
|
||||
{:else if step === 'child'}
|
||||
@@ -381,6 +385,21 @@
|
||||
font-size: 0.85rem;
|
||||
color: #059669;
|
||||
}
|
||||
.privacy-note {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.78rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-note a {
|
||||
color: #4338ca;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.privacy-note a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user