// screens3.jsx — Pro Review (paid evaluation service)
const { useState: useState3 } = React;
const { Card, Chip, CoverArt, Waveform, TopBar, IconBtn, DisplayHeading, Icon } = window.LAUNCH_SCREENS_1;
const TIERS = [
{
id: 'quick',
name: 'QUICK SCORE',
price: 11,
eta: '48 HOURS',
blurb: 'Fast point-based score. 5 categories, 1\u201310 each. Hit 40+/50 to unlock rewards.',
perks: [
'Song Structure \u00b7 1\u201310',
'Title / Hook \u00b7 1\u201310',
'Idea \u00b7 1\u201310',
'Lyrics \u00b7 1\u201310',
'Melody \u00b7 1\u201310',
'Score 40+ \u2192 rewards & bonus XP'
],
accent: false
},
{
id: 'full',
name: 'IN-DEPTH EVAL',
price: 40,
eta: '5 DAYS',
blurb: 'Deep written breakdown + voice note from a Launch mentor. Mix, songwriting, vocals, arrangement.',
perks: [
'Mentor-matched to your genre',
'Written breakdown (~600 words)',
'5-min voice walkthrough',
'Stems-level feedback',
'Full rubric (out of 50)'
],
accent: true,
badge: 'MOST PICKED'
}];
const REVIEWERS = [
{ lbl: 'JB', color: '#C72820', name: 'Jay Brunswick', displayName: 'Jay', role: 'RIAA Gold · 170+ cuts · 2 Billboard Top 40' },
{ lbl: 'PG', color: '#7A5AE0', name: 'PG Banker', displayName: 'PG', role: 'Logic Pro certified · 1,000+ placements' }];
const RECENT_REVIEWS = [
{
avatar: 'KP', color: '#C72820', name: 'kai.park',
track: 'NEON HOURS', tier: 'IN-DEPTH EVAL',
quote: '"Top-line is sticky as hell. Pull the 2k from your vocal bus — it\'s fighting the synth lead."',
reviewer: 'JAY BRUNSWICK', stars: 5
},
{
avatar: 'IL', color: '#FBBF24', name: 'isla.loop',
track: 'LOW TIDE', tier: 'A&R SUBMIT',
quote: '"Scored 9.1 — pitched to two labels. Hold tight."',
reviewer: 'A&R PANEL', stars: 5, ar: true
}];
function Stars({ n, color, dim }) {
return (
{[0, 1, 2, 3, 4].map((i) =>
)}
);
}
// ─────────────────────────────────────────────────────────────
// Pro Review screen — 2 modes: 'browse' and 'submit'
// ─────────────────────────────────────────────────────────────
function ReviewScreen({ theme, copy, onBack, onOpenEvaluations }) {
const [mode, setMode] = useState3('browse'); // 'browse' | 'submit' | 'success'
const [tier, setTier] = useState3('full');
const [trackTitle, setTrackTitle] = useState3('');
const [notes, setNotes] = useState3('');
const [trackUploaded, setTrackUploaded] = useState3(false);
const [audioFile, setAudioFile] = useState3(null);
const [coachLbl, setCoachLbl] = useState3('JB');
const [reviewers, setReviewers] = useState3(REVIEWERS); // demo fallback until the table loads
const [submitting, setSubmitting] = useState3(false);
const [submitErr, setSubmitErr] = useState3(null);
const [agreementGate, setAgreementGate] = useState3(false); // consent gate before upload
const [consented, setConsented] = useState3(false); // read+agreed THIS submission — re-prompts on every upload
const [creditCents, setCreditCents] = useState3(0); // review-only credit from redeemed points
React.useEffect(() => {
let alive = true;
if (window.LAUNCH_USER.fetchMyReviewCredit) window.LAUNCH_USER.fetchMyReviewCredit().then(c => { if (alive) setCreditCents(c || 0); });
return () => { alive = false; };
}, []);
const fileRef = React.useRef(null);
// Load the admin-managed coach roster (Supabase `coaches` table). Falls back
// to the built-in REVIEWERS list in demo mode / on error.
React.useEffect(() => {
let alive = true;
window.LAUNCH_USER.fetchCoaches().then((list) => {
if (!alive || !list || !list.length) return;
setReviewers(list);
setCoachLbl((prev) => list.some((r) => r.lbl === prev) ? prev : list[0].lbl);
});
return () => { alive = false; };
}, []);
const selectedTier = TIERS.find((t) => t.id === tier);
// Plan benefits: basic → 1 free quick ($11); vip → 2 free in-depth ($40).
const ent = window.LAUNCH_USER.usePlan ? window.LAUNCH_USER.usePlan() : { plan: 'free', quickEval: 0, fullEval: 0 };
const freeEvalLeft = tier === 'quick' ? (ent.quickEval || 0) : tier === 'full' ? (ent.fullEval || 0) : 0;
const planCoversThis = freeEvalLeft > 0;
// Review-only credit (from redeemed points) applies ONLY to the $40 tier, and
// only when the plan doesn't already cover it (plan benefit takes priority).
const priceCents = (selectedTier ? selectedTier.price : 0) * 100;
const isReviewTier = tier === 'full';
const creditApplied = (!planCoversThis && isReviewTier) ? Math.min(Math.max(0, creditCents), priceCents) : 0;
const dueCents = planCoversThis ? 0 : Math.max(0, priceCents - creditApplied);
const onPickFile = (e) => {
const f = e.target.files && e.target.files[0];
if (f) { setAudioFile(f); setTrackUploaded(true); setSubmitErr(null); }
};
// Show the Song Submission Agreement BEFORE the file picker opens — the user
// can't even choose an audio file until they've read it and ticked "I agree".
// We prompt on EVERY upload (per-submission `consented` flag), not just the
// first time, even though the acceptance is still recorded in Supabase.
const openFilePicker = () => {
if (!consented) { setAgreementGate(true); return; }
if (fileRef.current) fileRef.current.click();
};
const fmtSize = (b) => b < 1024 * 1024
? (b / 1024).toFixed(0) + ' KB'
: (b / (1024 * 1024)).toFixed(1) + ' MB';
const doSubmit = async () => {
// Consent gate: the user must accept the Song Submission Agreement first.
if (!consented) { setAgreementGate(true); return; }
setSubmitting(true); setSubmitErr(null);
const res = await window.LAUNCH_USER.submitSongForReview({
title: trackTitle, notes, tier, coach: coachLbl, audioFile,
});
if (res && res.error) { setSubmitting(false); setSubmitErr(res.error); return; }
const subId = res.submission && res.submission.id;
// 1) Plan benefit: a free Quick (basic) or In-Depth (vip) eval — no payment.
// Works in both live and demo (claimFreeEval handles both).
if (planCoversThis && subId && window.LAUNCH_USER.claimFreeEval) {
const fr = await window.LAUNCH_USER.claimFreeEval(subId, tier);
if (fr && fr.ok) { setSubmitting(false); (setConsented(false), setMode('success')); return; }
// claim failed → fall through to the normal paid flow
}
// Live mode → charge via Stripe Checkout (redirects out and back).
if (window.LAUNCH_USER.isConfigured()) {
// If account credit (from redeemed points) covers the full price, apply it
// directly — no Stripe, no payment page.
if (subId && priceCents > 0 && creditApplied >= priceCents && window.LAUNCH_USER.payReviewWithCredit) {
const pc = await window.LAUNCH_USER.payReviewWithCredit(subId, priceCents);
if (pc && pc.ok && pc.covered) { setSubmitting(false); (setConsented(false), setMode('success')); return; }
// credit didn't cover (e.g. stale balance) → fall through to checkout
}
const itemMap = { quick: 'quick-score', full: 'full-review' };
const co = await window.LAUNCH_USER.startCheckout({
item: itemMap[tier] || 'quick-score',
submissionId: subId,
});
if (co && co.error) { setSubmitting(false); setSubmitErr('Saved, but checkout failed: ' + co.error); return; }
// Account credit fully covered it server-side → no Stripe redirect.
if (co && co.done) { setSubmitting(false); (setConsented(false), setMode('success')); return; }
return; // leaving the page for Stripe
}
// Demo mode → simulated success.
setSubmitting(false);
(setConsented(false), setMode('success'));
};
// ───────── SUCCESS ─────────
if (mode === 'success') {
return (
} />
SUBMITTED · {dueCents <= 0 ? 'COVERED BY CREDIT' : '$' + (dueCents / 100).toFixed(0) + ' CHARGED'}
YOUR TRACK IS IN THE QUEUE
Expect your review within {selectedTier.eta.toLowerCase()} . We'll notify you the moment it drops.
ORDER · LCH-2487
Track
{trackTitle || 'Untitled Demo v3'}
Tier
{selectedTier.name}
ETA
{selectedTier.eta}
BACK TO HOME
);
}
// ───────── SUBMIT FORM ─────────
if (mode === 'submit') {
const canSubmit = trackTitle.trim() && audioFile && !submitting;
return (
{/* Top bar with back */}
setMode('browse')} style={{
width: 38, height: 38, borderRadius: 12,
background: theme.surface, border: `1px solid ${theme.border}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer'
}}>
STEP 2 / 2
{selectedTier.name} · ${selectedTier.price}
DROP YOUR TRACK
{/* Upload area */}
{trackUploaded ?
<>
✓ UPLOADED{audioFile ? ' · ' + fmtSize(audioFile.size) : ''}
{audioFile ? audioFile.name : 'your track'}
> :
<>
UPLOAD A TRACK
WAV · MP3 · AIFF · 50MB MAX
>
}
{/* Form fields */}
TRACK TITLE
setTrackTitle(e.target.value)}
placeholder="e.g. NEON HOURS"
style={{
width: '100%', padding: '14px 16px', borderRadius: 12,
background: theme.surface, border: `1px solid ${theme.border}`,
color: theme.text, fontFamily: theme.fontBody, fontSize: 15,
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = theme.accent}
onBlur={(e) => e.target.style.borderColor = theme.border} />
WHAT DO YOU WANT FEEDBACK ON?
{/* Reviewer preference */}
PICK YOUR COACH
{reviewers.map((r, i) => {
const picked = coachLbl === r.lbl;
return (
setCoachLbl(r.lbl)} style={{
flexShrink: 0, padding: '8px 12px 10px', borderRadius: 12,
background: picked ? `${theme.accent}15` : theme.surface,
border: `${picked ? 2 : 1}px solid ${picked ? theme.accent : theme.border}`,
display: 'flex', alignItems: 'center', gap: 8, minWidth: 200,
cursor: 'pointer', textAlign: 'left'
}}>
{r.lbl}
{r.displayName || r.name}
{r.role.toUpperCase()}
{picked && ✓ }
);
})}
{/* Sticky pay bar */}
{(planCoversThis || creditApplied > 0) && !submitErr &&
{planCoversThis
? `INCLUDED · ${String(ent.plan || '').toUpperCase()} PLAN · ${freeEvalLeft} LEFT`
: `ACCOUNT CREDIT −$${(creditApplied / 100).toFixed(0)}`}
}
0) ? 2 : 0 }}>
{submitErr ? '⚠ ' + submitErr.toUpperCase() : (dueCents <= 0 ? (planCoversThis ? 'FREE WITH YOUR PLAN' : 'COVERED BY CREDIT') : 'TOTAL · ' + selectedTier.eta)}
${(dueCents / 100).toFixed(0)}.00
{submitting ? 'SENDING…' : (dueCents <= 0 ? 'SUBMIT · FREE →' : 'SUBMIT & PAY →')}
{/* Consent gate — must be accepted before picking a file or submitting */}
{agreementGate && window.LAUNCH_AGREEMENT &&
setAgreementGate(false)}
onAccepted={() => { setAgreementGate(false); setConsented(true); if (fileRef.current) fileRef.current.click(); }}
/>
}
);
}
// ───────── BROWSE / TIER PICKER ─────────
return (
{/* Top bar with back */}
onOpenEvaluations && onOpenEvaluations()} style={{
padding: '7px 12px', borderRadius: 99, background: theme.surface,
border: `1px solid ${theme.border}`, color: theme.accent, cursor: 'pointer',
fontFamily: theme.fontMono, fontSize: 10, letterSpacing: 1, fontWeight: 700
}}>MY REVIEWS →
{/* Hero */}
PRO REVIEW
GET YOUR TRACK HEARD
Real feedback from signed producers, mix engineers, and A&R. Pay once. No subscriptions. Top scores get pitched to labels.
{/* Tier cards */}
{TIERS.map((t) => {
const selected = tier === t.id;
return (
setTier(t.id)}
style={{
background: t.accent ? theme.accent : theme.surface,
color: t.accent ? theme.onAccent : theme.text,
border: selected ?
`2px solid ${t.accent ? '#0A0A0A' : theme.accent}` :
`1px solid ${theme.border}`,
borderRadius: theme.radius, padding: 16,
cursor: 'pointer', textAlign: 'left',
position: 'relative'
}}>
{t.badge &&
{t.badge}
}
{t.blurb}
{t.perks.map((p, i) =>
)}
{selected &&
SELECTED ✓
}
);
})}
{/* Recent reviews */}
RECENT REVIEWS · FROM THE SQUAD
{RECENT_REVIEWS.map((r, i) =>
{r.avatar}
{r.name} · {r.track}
{r.tier} {r.ar && '· PITCHED'}
{r.quote}
— {r.reviewer}
)}
{/* Trust strip */}
{[
{ n: '12K+', l: 'TRACKS\nREVIEWED' },
{ n: '47', l: 'LABEL\nPLACEMENTS' },
{ n: '4.9', l: 'AVG\nRATING' }].
map((s, i) =>
)}
{/* Sticky CTA */}
SELECTED · {selectedTier.eta}
{selectedTier.name} · ${selectedTier.price}
setMode('submit')} style={{
background: theme.accent, color: theme.onAccent,
fontFamily: theme.fontDisplay, fontSize: 14, letterSpacing: 1,
padding: '14px 18px', borderRadius: 12, border: 'none', cursor: 'pointer',
textTransform: 'uppercase'
}}>CONTINUE →
);
}
// ─────────────────────────────────────────────────────────────
// "Pro Review" CTA — drop into other screens as a promo card
// ─────────────────────────────────────────────────────────────
function ProReviewCTA({ theme, onOpen, variant = 'card' }) {
if (variant === 'inline') {
return (
WANT IT REVIEWED BY A PRO?
FROM $11 · 48H TURNAROUND
GO →
);
}
return (
{/* Decorative accent bar */}
SUBMIT A TRACK FOR REAL FEEDBACK
Signed producers, mix engineers, A&R. Top scores pitched to labels.
);
}
window.LAUNCH_SCREENS_3 = { ReviewScreen, ProReviewCTA };