// settings.jsx — In-app settings screen (theme, notifications, etc.)
const { useState: useStateSet, useEffect: useEffectSet } = React;
const { Card, TopBar, DisplayHeading, Icon } = window.LAUNCH_SCREENS_1;
// Darken a hex color by `amt` (-100..100). Used to render planet shading.
function shadeColor(hex, amt) {
const m = hex.replace('#', '');
const num = parseInt(m, 16);
let r = (num >> 16) + Math.round(amt * 2.55);
let g = ((num >> 8) & 0xff) + Math.round(amt * 2.55);
let b = (num & 0xff) + Math.round(amt * 2.55);
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
}
// ────────────────────────────────────────────────────────
// SettingsScreen — accessed from Profile
// ────────────────────────────────────────────────────────
function SettingsScreen({ theme, onBack, dark, onChangeDark, accent, onChangeAccent, onOpenAdmin, onOpenCoachQueue, onEditProfile, onOpenSubscription, onOpenPayment }) {
const [notifications, setNotifications] = useStateSet({
streak: true,
lesson: true,
feedback: true,
leaderboard: false,
drops: true,
});
const toggleNotif = (key) => setNotifications(v => ({ ...v, [key]: !v[key] }));
// Which support page is open as an overlay (null = none).
const [supportPanel, setSupportPanel] = useStateSet(null);
// Delete-account confirmation flow open?
const [showDelete, setShowDelete] = useStateSet(false);
const Row = ({ icon, title, sub, right, onClick }) => {
const clickable = typeof onClick === 'function';
const Tag = clickable ? 'button' : 'div';
return (
{icon && (
{icon}
)}
{title}
{sub && (
{sub.toUpperCase()}
)}
{right}
);
};
const Toggle = ({ on, onChange }) => (
);
const Group = ({ label, children }) => (
{label}
{React.Children.map(children, (child, i) => (
{i > 0 &&
}
{child}
))}
);
return (
{/* Top bar */}
★ MISSION CONFIG
SETTINGS
{/* APPEARANCE — the headliner */}
{/* Theme picker — segmented control with previews */}
Theme
CHOOSE HOW LAUNCH LOOKS
{[
{ id: 'light', dark: false, l: 'LIGHT', icon: '☀' },
{ id: 'dark', dark: true, l: 'DARK', icon: '☾' },
{ id: 'auto', dark: null, l: 'AUTO', icon: '◐', sub: 'SYSTEM' },
].map(opt => {
const isActive = opt.id === 'auto' ? false : (dark === opt.dark);
return (
opt.id !== 'auto' && onChangeDark(opt.dark)}
disabled={opt.id === 'auto'}
style={{
padding: 0, border: 'none', cursor: opt.id === 'auto' ? 'not-allowed' : 'pointer',
borderRadius: 14, overflow: 'hidden',
background: 'transparent',
boxShadow: isActive ? `0 0 0 2px ${theme.accent}` : `0 0 0 1px ${theme.border}`,
transition: 'box-shadow .15s',
opacity: opt.id === 'auto' ? 0.5 : 1,
}}>
{/* Mini preview */}
{/* Status bar pill */}
{/* Sample type */}
{opt.icon}
{/* Sample accent dot */}
{opt.l}{isActive && ' ✓'}
{opt.sub && (
{opt.sub}
)}
);
})}
}
title="Reduce motion"
sub="Less animation on splash and transitions"
right={ {}} />}
/>
{/* ACCENT */}
★ PICK YOUR PLANET
{[
{ v: '#C72820', l: 'MARS', sub: 'RED PLANET' },
{ v: '#4A7FD8', l: 'NEPTUNE', sub: 'DEEP BLUE' },
{ v: '#7A5AE0', l: 'NEBULA', sub: 'ULTRAVIOLET' },
{ v: '#E8A736', l: 'SOLAR', sub: 'STARLIGHT' },
].map(c => {
const on = accent === c.v;
return (
onChangeAccent && onChangeAccent(c.v)}
style={{
flex: 1, padding: 0, border: 'none', cursor: 'pointer',
background: 'transparent', textAlign: 'center',
}}>
{/* highlight */}
{/* saturn-style ring for Nebula */}
{c.l === 'NEBULA' && (
)}
{c.l}{on && ' ✓'}
{c.sub}
);
})}
{/* NOTIFICATIONS */}
🔥}
title="Streak reminders"
sub="Daily nudge before you break"
right={ toggleNotif('streak')} />}
/>
📚}
title="Today's lesson"
sub="Drop time: 9:00 AM"
right={ toggleNotif('lesson')} />}
/>
💬}
title="Feedback & reviews"
sub="When your work gets a response"
right={ toggleNotif('feedback')} />}
/>
🏆}
title="Leaderboard updates"
sub="Weekly rankings + position changes"
right={ toggleNotif('leaderboard')} />}
/>
🎵}
title="Squad drops"
sub="New tracks from people you follow"
right={ toggleNotif('drops')} />}
/>
{/* ACCOUNT */}
}
title="Edit profile"
sub="Handle · bio · genres"
onClick={onEditProfile}
right={
}
/>
}
title="Subscription"
sub="Orbit · $99/mo · renews Jun 11"
onClick={onOpenSubscription}
right={
}
/>
}
title="Payment methods"
onClick={onOpenPayment}
right={
}
/>
setShowDelete(true)} style={{
width: '100%', textAlign: 'left', font: 'inherit',
padding: '14px 14px', display: 'flex', alignItems: 'center', gap: 14,
background: 'transparent', border: 'none', cursor: 'pointer',
}}>
Delete account
PERMANENTLY ERASE YOUR DATA
{/* SUPPORT */}
}
title="Help center"
onClick={() => setSupportPanel('help')}
right={
}
/>
}
title="Contact us"
sub="Avg reply: 4 hours"
onClick={() => setSupportPanel('contact')}
right={
}
/>
}
title="Privacy & terms"
onClick={() => setSupportPanel('privacy')}
right={
}
/>
{/* COACH REVIEW QUEUE — gated to coach/admin (always on in demo mode) */}
{onOpenCoachQueue && (
★ COACH
REVIEW QUEUE
MEMBER SONG SUBMISSIONS · SCORE & SEND
OPEN →
)}
{/* ADMIN — gated */}
{onOpenAdmin && (
★ LAUNCH HQ
ADMIN DASHBOARD
INSIGHTS · TRANSACTIONS · PAYOUTS
OPEN →
)}
{/* Version */}
LAUNCH
VERSION 1.0.0 · BUILD 2487 · MADE IN NASHVILLE
{/* SUPPORT OVERLAY — Help center / Contact us / Privacy & terms */}
{supportPanel && (
setSupportPanel(null)}
/>
)}
{/* DELETE ACCOUNT — warning → password → wipe from Supabase */}
{showDelete && (
setShowDelete(false)} />
)}
);
}
// ────────────────────────────────────────────────────────
// DeleteAccountSheet — irreversible account deletion flow.
// step 'warn' → explains the consequences, asks to confirm
// step 'password' → final confirmation: re-enter the account password
// On success the account + all data are wiped from Supabase and the app is
// reloaded back to the login screen.
// ────────────────────────────────────────────────────────
const DANGER = '#E5484D';
function DeleteAccountSheet({ theme, onClose }) {
const U = window.LAUNCH_USER;
const [step, setStep] = useStateSet('warn'); // 'warn' | 'password'
const [password, setPassword] = useStateSet('');
const [confirmText, setConfirmText] = useStateSet(''); // for OAuth accounts (type DELETE)
const [hasPassword, setHasPassword] = useStateSet(true); // email/pw account? (Google = false)
const [busy, setBusy] = useStateSet(false);
const [err, setErr] = useStateSet(null);
// OAuth-only accounts (e.g. Google) have no password → confirm by typing DELETE.
useEffectSet(() => {
let alive = true;
if (U && U.authProviders) {
U.authProviders().then(a => { if (alive && a) setHasPassword(!!a.hasPassword); });
}
return () => { alive = false; };
}, []);
const canConfirm = hasPassword ? !!password : confirmText.trim().toUpperCase() === 'DELETE';
const doDelete = async () => {
if (!canConfirm) {
setErr(hasPassword ? 'Enter your password to confirm.' : 'Type DELETE to confirm.');
return;
}
setBusy(true); setErr(null);
const res = U && U.deleteAccount ? await U.deleteAccount(hasPassword ? password : '') : { error: 'Unavailable.' };
setBusy(false);
if (res && res.ok) {
// Account is gone — hard reload drops back to the welcome / login flow.
try { window.location.reload(); } catch (e) { onClose(); }
} else {
setErr((res && res.error) || 'Could not delete your account.');
}
};
return (
{/* Top bar */}
{step === 'warn' ? 'DANGER ZONE' : 'FINAL CONFIRMATION'}
{/* Warning icon */}
DELETE ACCOUNT
{step === 'warn' && (
<>
This will permanently delete your account.
All of your data will be erased from the app — your profile, songs, submissions,
reviews, points and progress. This action cannot be undone.
Are you sure you want to continue?
>
)}
{step === 'password' && (
<>
{hasPassword
? 'Enter your account password to permanently delete your account.'
: 'Type DELETE to permanently delete your account.'}
{hasPassword ? 'PASSWORD' : 'TYPE "DELETE"'}
{ hasPassword ? setPassword(e.target.value) : setConfirmText(e.target.value); setErr(null); }}
onKeyDown={(e) => { if (e.key === 'Enter' && !busy) doDelete(); }}
placeholder={hasPassword ? 'Your password' : 'DELETE'}
style={{
width: '100%', padding: '14px 16px', borderRadius: 12,
background: theme.surface, border: `1px solid ${err ? DANGER : theme.border}`,
color: theme.text, fontFamily: theme.fontBody, fontSize: 15, outline: 'none',
}}
/>
{err && (
{err}
)}
>
)}
{/* Sticky actions */}
{step === 'warn' ? (
<>
{ setErr(null); setStep('password'); }} style={{
width: '100%', padding: '15px 18px', borderRadius: 12, border: 'none',
background: DANGER, color: '#fff',
fontFamily: theme.fontDisplay, fontSize: 15, letterSpacing: 1,
textTransform: 'uppercase', cursor: 'pointer',
}}>Yes, delete my account
Cancel
>
) : (
<>
{busy ? 'DELETING…' : 'Delete my account permanently'}
{ setStep('warn'); setErr(null); }} disabled={busy} style={{
width: '100%', padding: '14px 18px', borderRadius: 12,
background: 'transparent', border: `1px solid ${theme.border}`, color: theme.text,
fontFamily: theme.fontMono, fontSize: 12, letterSpacing: 1, fontWeight: 700,
textTransform: 'uppercase', cursor: busy ? 'not-allowed' : 'pointer',
}}>Back
>
)}
);
}
// ────────────────────────────────────────────────────────
// SupportSheet — full-screen overlay for the SUPPORT rows.
// Self-contained: no app wiring needed, opens over Settings.
// ────────────────────────────────────────────────────────
const SUPPORT_FAQ = [
{
q: 'How do my lessons and streak work?',
a: 'Complete your daily lesson to keep your streak alive. Miss a day and the streak resets, but your total XP and progress are always saved.',
},
{
q: 'How do I book a session with a coach?',
a: 'Open your Profile, scroll to the coaches block, and tap Book a session. You can pick a time that matches the coach’s availability.',
},
{
q: 'How do community points work?',
a: 'You earn points from lessons, streaks and community activity. Redeem them for perks from the Feed tab. Some rewards may require an active subscription.',
},
{
q: 'How do I change or cancel my subscription?',
a: 'Go to Settings → Account → Subscription to switch plans. Your plan stays active until the end of the current billing period.',
},
{
q: 'I found a bug or my screen is blank.',
a: 'Try closing and reopening the app first. If it persists, use Contact us below and tell us what screen you were on — it helps us fix it fast.',
},
];
function SupportSheet({ theme, panel, onClose }) {
const titles = { help: 'Help center', contact: 'Contact us', privacy: 'Privacy & terms' };
const [openFaq, setOpenFaq] = useStateSet(null);
const [docView, setDocView] = useStateSet(null); // legal doc open in the in-app reader (null = none)
const Section = ({ children }) => (
{children}
);
const Para = ({ children, mono }) => (
{children}
);
const ContactRow = ({ label, value, href }) => (
);
return (
<>
{/* Top bar */}
★ SUPPORT
{(titles[panel] || '').toUpperCase()}
{panel === 'help' && (
{SUPPORT_FAQ.map((item, i) => {
const open = openFaq === i;
return (
{i > 0 &&
}
setOpenFaq(open ? null : i)} style={{
width: '100%', textAlign: 'left', background: 'transparent',
border: 'none', cursor: 'pointer', padding: '16px',
display: 'flex', flexDirection: 'column', gap: 8,
}}>
{open && (
{item.a}
)}
);
})}
)}
{panel === 'contact' && (
<>
Questions, feedback or trouble with the app? Reach the Launch team — we usually reply within 4 hours.
>
)}
{panel === 'privacy' && (
<>
Launch collects only the information needed to run your account: your name, handle,
email, lesson progress and the content you submit. We never sell your personal data.
Your songs and submissions stay private to you and the coaches you share them with.
Subscriptions renew automatically until cancelled and are governed by our standard
Terms of Service. You can request deletion of your account and data at any time by
contacting support.
{/* Legal documents — open the full policies on launchnash.com (new tab). */}
LEGAL DOCUMENTS
{[
{ title: 'Privacy Policy', href: 'https://www.launchnash.com/english-privacy-policy' },
{ title: 'Terms of Service', href: 'https://www.launchnash.com/terms-of-service' },
{ title: 'Community Guidelines', href: 'https://www.launchnash.com/community-guidelines' },
{ title: 'Copyright & DMCA Policy', href: 'https://www.launchnash.com/copyright-dmca-policy' },
{ title: 'Song Submission & Evaluation Agreement', href: 'documents/Song_Submission_Agreement_Launch.pdf' },
].map((doc, i) => (
{i > 0 &&
}
setDocView(doc)} style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 12,
background: 'transparent', border: 'none', cursor: 'pointer',
textAlign: 'left', padding: '15px 16px',
}}>
{doc.title}
))}
>
)}
{/* In-app legal document reader — read the policy inside the app, then continue. */}
{docView && (
{/* Top bar: back · title · open-in-browser fallback */}
setDocView(null)} style={{
width: 38, height: 38, borderRadius: 12,
background: theme.surface, border: `1px solid ${theme.border}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', flexShrink: 0, padding: 0,
}}>
{docView.title.toUpperCase()}
{/* Document (embedded page) */}
{/* Continue back into the app */}
setDocView(null)} style={{
width: '100%', padding: '15px 18px', borderRadius: 12, border: 'none',
background: theme.accent, color: theme.onAccent,
fontFamily: theme.fontDisplay, fontSize: 15, letterSpacing: 1,
textTransform: 'uppercase', cursor: 'pointer',
}}>✓ Done — Back to app
)}
>
);
}
// ────────────────────────────────────────────────────────
// EditProfileScreen — edit display name, handle & bio.
// Reached from Settings → Account → Edit profile. Persists via
// window.LAUNCH_USER.saveProfile so the name shown across the app updates.
// ────────────────────────────────────────────────────────
const AVATAR_COLORS = ['#C72820', '#7A5AE0', '#CAFF33', '#FBBF24', '#22C55E', '#EC4899', '#06B6D4', '#F97316'];
function EditProfileScreen({ theme, onBack }) {
const user = window.LAUNCH_USER.useUser();
const [name, setName] = useStateSet(user.displayName || '');
const [handle, setHandle] = useStateSet(user.handle || '');
const [bio, setBio] = useStateSet(user.bio || '');
const [color, setColor] = useStateSet(user.color || AVATAR_COLORS[0]);
const [saving, setSaving] = useStateSet(false);
const initials = window.LAUNCH_USER.initialsFrom(name, handle);
const save = async () => {
setSaving(true);
try {
await window.LAUNCH_USER.saveProfile({
displayName: name.trim(),
handle: handle.trim().toLowerCase().replace(/[^a-z0-9._]/g, ''),
bio: bio.trim(),
color,
});
} catch (e) { /* saveProfile already falls back to local cache */ }
setSaving(false);
onBack && onBack();
};
const Label = ({ children }) => (
{children}
);
const inputStyle = {
width: '100%', boxSizing: 'border-box',
padding: '14px', borderRadius: 12,
border: `1px solid ${theme.border}`,
fontFamily: theme.fontBody, fontSize: 15,
};
return (
{/* Top bar */}
★ YOUR IDENTITY
EDIT PROFILE
);
}
window.LAUNCH_SCREENS_8 = { SettingsScreen, EditProfileScreen };