// 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 // ──────────────────────────────────────────────────────── // The Subscription row used to show a hardcoded "Orbit · $99/mo · renews Jun 11": // a plan that doesn't exist, at a price we don't charge, with an invented date. // Read the real one instead — and deliberately without a price, so this can // never contradict what the App Store / Play actually bills in each country. function planSubtitleFrom(ent) { const plan = (ent && ent.plan) || 'free'; if (plan === 'free' || !ent.active) return 'Free · tap to upgrade'; const name = plan === 'vip' ? 'VIP' : 'Basic'; if (!ent.expiresAt) return name; try { const d = new Date(ent.expiresAt).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }).toUpperCase(); return `${name} · renews ${d}`; } catch (e) { return name; } } function SettingsScreen({ theme, onBack, dark, onChangeDark, accent, onChangeAccent, onOpenAdmin, onOpenCoachQueue, onEditProfile, onOpenSubscription }) { const ent = window.LAUNCH_USER.usePlan ? window.LAUNCH_USER.usePlan() : { plan: 'free', active: false, expiresAt: null }; const planSubtitle = planSubtitleFrom(ent); 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); // ── Store diagnostics (hidden: five taps on the version line) ───────────── // Text of the panel, or null when it's closed. Kept out of the way because // it's a developer tool, not a feature. const [diagTaps, setDiagTaps] = useStateSet(0); const [storeDiag, setStoreDiag] = useStateSet(null); // What the native shell actually exposes. Answers, in order: are we native, // did the RevenueCat plugin make it into the build, and is the public key // loaded? Any one of those being false is what makes the Plans screen say // memberships are unavailable. const runStoreDiag = () => { const lines = []; const C = window.Capacitor || null; const cfg = window.LAUNCH_CONFIG || {}; const I = window.LAUNCH_IAP || null; let platform = 'web'; let native = false; try { platform = C && C.getPlatform ? C.getPlatform() : 'web'; } catch (e) {} try { native = !!(C && C.isNativePlatform && C.isNativePlatform()); } catch (e) {} let available = null; try { available = (C && typeof C.isPluginAvailable === 'function') ? C.isPluginAvailable('Purchases') : 'no isPluginAvailable()'; } catch (e) { available = 'threw: ' + e.message; } let registered = []; try { registered = Object.keys((C && C.Plugins) || {}); } catch (e) {} const key = cfg.REVENUECAT_IOS_KEY || ''; lines.push('Capacitor : ' + (C ? 'present' : 'MISSING')); lines.push('platform : ' + platform); lines.push('isNative : ' + native); lines.push("plugin 'Purchases': " + available); lines.push('plugins loaded : ' + (registered.length ? registered.join(', ') : '(none)')); lines.push('RevenueCat key : ' + (key ? key.slice(0, 10) + '… (' + key.length + ' chars)' : 'EMPTY')); lines.push('LAUNCH_IAP : ' + (I ? 'loaded' : 'MISSING')); lines.push('isAvailable() : ' + (I && I.isAvailable ? I.isAvailable() : 'n/a')); lines.push('canBuyInApp() : ' + (window.LAUNCH_USER ? window.LAUNCH_USER.canBuyInApp() : 'n/a')); // How the plugin proxy was obtained, and what broke if it wasn't. This is // the part that told us registerPlugin was failing while the plugin itself // was perfectly present. if (I && typeof I.diagnostics === 'function') { let d = null; try { d = I.diagnostics(); } catch (e) { d = { pluginError: 'diagnostics threw: ' + e.message }; } lines.push(''); lines.push('registerPlugin : ' + (d.hasRegisterPlugin ? 'is a function' : 'MISSING')); lines.push('plugin proxy : ' + (d.pluginResolved ? 'resolved via ' + d.pluginSource : 'NOT RESOLVED')); lines.push('plugin error : ' + (d.pluginError || 'none')); lines.push('configured for : ' + (d.configuredFor || '(not yet)')); lines.push('last purchase : ' + (d.lastPurchase || '(none this session)')); } lines.push(''); lines.push('Tap TEST STORE to configure the SDK and ask the store for'); lines.push('the products. That is what fails when the products are not'); lines.push('set up in App Store Connect.'); setStoreDiag(lines.join('\n')); }; // Go one step further: start the SDK for this user and read the offerings. // Separated from the summary above because it hits the network and the // store, and its failure means something different (products/entitlements // missing rather than a broken build). const runStoreTest = async () => { const I = window.LAUNCH_IAP; const out = []; if (!I || !I.isAvailable || !I.isAvailable()) { setStoreDiag('Store unavailable — the plugin or the key is missing, so\nthere is nothing to test yet. See the summary above.'); return; } setStoreDiag('Testing the store…'); try { const prices = await window.LAUNCH_USER.iapPrices(); out.push('plan prices : ' + JSON.stringify(prices)); } catch (e) { out.push('plan prices : threw ' + e.message); } try { const items = await window.LAUNCH_USER.iapItemPrices(); out.push('item prices : ' + JSON.stringify(items)); } catch (e) { out.push('item prices : threw ' + e.message); } out.push(''); out.push('Empty prices mean the SDK is fine but the store returned no'); out.push('products — create them in App Store Connect and attach them'); out.push('to the RevenueCat offering.'); setStoreDiag(out.join('\n')); }; 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 ( ); })}
} 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 ( ); })}
{/* 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={planSubtitle} onClick={onOpenSubscription} right={ } /> {/* 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 && (
)} {/* ADMIN — gated */} {onOpenAdmin && (
)} {/* Version — five taps open the store diagnostics (see runStoreDiag). */}
{ const n = diagTaps + 1; if (n >= 5) { setDiagTaps(0); runStoreDiag(); } else setDiagTaps(n); }} style={{ padding: `${theme.gap + 10}px ${theme.pad}px 0`, textAlign: 'center', cursor: 'default', }}>
LAUNCH
VERSION 1.0.0 · BUILD 2487 · MADE IN NASHVILLE
{/* STORE DIAGNOSTICS — hidden panel, five taps on the version line. When in-app purchases come up unavailable on a device, the JS lives inside the binary and there's no console to read, so this is the only way to tell WHICH piece is missing: the native plugin, the RevenueCat key, or the products in App Store Connect. */} {storeDiag && (
Store diagnostics
{storeDiag}
)} {/* 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 ? 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' ? ( <> ) : ( <> )}
); } // ──────────────────────────────────────────────────────── // 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 }) => (
{label}
{value}
); return ( <>
{/* Top bar */}
★ SUPPORT
{(titles[panel] || '').toUpperCase()}
{panel === 'help' && (
{SUPPORT_FAQ.map((item, i) => { const open = openFaq === i; return ( {i > 0 &&
} ); })}
)} {panel === 'contact' && ( <> Questions, feedback or trouble with the app? Reach the Launch team — we usually reply within 4 hours.
)} {panel === 'privacy' && ( <>
LAST UPDATED · JUNE 2026
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 &&
} ))}
)}
{/* In-app legal document reader — read the policy inside the app, then continue. */} {docView && (
{/* Top bar: back · title · open-in-browser fallback */}
{docView.title.toUpperCase()}
{/* Document (embedded page) */}
Loading…