// screen-more-auth.jsx — Lainnya menu + Login + Onboarding // ─── LAINNYA / MORE MENU ───────────────────────────────────────── function ScreenMore({ theme, accent, scenario, onNavigate, onLogout, auth }) { const isAuth = !!(auth && auth.family); const [members, setMembers] = React.useState(null); const familyId = auth && auth.family && auth.family.id; React.useEffect(function() { if (!familyId || !window.sbGetMembers) return; sbGetMembers(familyId).then(function(r){ if(!r.error) setMembers(r.data||[]); }); }, [familyId]); const realMembers = members || (auth && auth.profile ? [auth.profile] : null); const me = isAuth && auth.profile ? { name: auth.profile.full_name||'Kamu', short: (auth.profile.full_name||'').split(' ')[0]||'Kamu', color:'#10b981' } : scenario.members[0]; return (

Lainnya

{/* Family card */}
{isAuth ? auth.family.name : scenario.family.name}
{isAuth ? auth.family.invite_code : scenario.family.code}
{isAuth ? (realMembers||[me]).length : scenario.members.length} anggota aktif
{/* Grouped menu */} onNavigate('budget')} /> onNavigate('goals')} /> onNavigate('debts')} /> onNavigate('fire')} /> onNavigate('receipt')} /> onNavigate('profile')} /> onNavigate('accounts')} /> onNavigate('security')} /> onNavigate('export')} /> onNavigate('help')} />
INDIECASH v0.4 · made by INDIE Group
); } function MenuGroup({ theme, title, children }) { return (
{title}
{children}
); } function MenuItem({ theme, accent, icon, label, hint, color, last, onClick }) { return ( ); } // ─── LOGIN ─────────────────────────────────────────────────────── function ScreenLogin({ theme, accent, onContinue, auth }) { const T = theme; const [mode, setMode] = React.useState('main'); // main | password | magic const [email, setEmail] = React.useState(''); const [password, setPass] = React.useState(''); const [isSignup, setIsSignup] = React.useState(false); const [sent, setSent] = React.useState(false); const [loading, setLoading] = React.useState(false); const [err, setErr] = React.useState(''); const [showPass, setShowPass] = React.useState(false); const validEmail = email.includes('@') && email.includes('.'); const realAuth = auth || {}; function reset() { setErr(''); setLoading(false); } async function handlePassword(e) { e && e.preventDefault(); if (!validEmail || password.length < 6) return; setLoading(true); setErr(''); if (!window.SB) { setErr('Koneksi belum siap'); setLoading(false); return; } try { if (isSignup) { var r = await window.SB.auth.signUp({ email, password, options: { emailRedirectTo: window.location.origin } }); if (r.error) throw r.error; // Jika email confirm OFF → user langsung punya session if (r.data && r.data.session) { // Langsung masuk — useAuth akan detect via onAuthStateChange setLoading(false); return; } // Jika email confirm ON → tampilkan pesan cek email setSent(true); setLoading(false); return; } else { var r = await window.SB.auth.signInWithPassword({ email, password }); if (r.error) throw r.error; // Login sukses — App detect session change otomatis } } catch(e) { var msg = e.message || 'Gagal login'; if (msg.includes('Invalid login credentials')) msg = 'Email atau password salah'; if (msg.includes('Email not confirmed')) msg = 'Email belum diverifikasi — cek inbox kamu'; if (msg.includes('User already registered')) msg = 'Email sudah terdaftar — coba login'; setErr(msg); setLoading(false); } } async function handleMagicLink(e) { e && e.preventDefault(); if (!validEmail) return; setLoading(true); setErr(''); try { var r = await window.SB.auth.signInWithOtp({ email, options: { emailRedirectTo: window.location.origin } }); if (r.error) throw r.error; setSent(true); setLoading(false); } catch(e) { var msg = e.message || 'Gagal'; if (msg.includes('rate limit')) msg = 'Terlalu banyak percobaan, tunggu beberapa menit'; setErr(msg); setLoading(false); } } async function handleGoogle() { setLoading(true); setErr(''); try { if (!window.SB) throw new Error('Koneksi belum siap'); var r = await window.SB.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: window.location.origin }, }); if (r.error) throw r.error; } catch(e) { setErr(e.message || 'Gagal login dengan Google'); setLoading(false); } } // ── Sent confirmation ─────────────────────────────────────────── if (sent) return (
{isSignup ? '🎉' : '✉️'}
{isSignup ? 'Cek email kamu!' : 'Link terkirim!'}
{isSignup ? <>Kami kirim link verifikasi ke {email}. Klik link tersebut lalu kembali ke sini untuk login. : <>Kami kirim magic link ke {email}. Klik link di email untuk masuk. }
); // ── Shared styles ─────────────────────────────────────────────── const inputStyle = { width:'100%', height:50, borderRadius:12, border:`1.5px solid ${T.border}`, background:T.bg, color:T.fg1, fontSize:15, fontWeight:500, padding:'0 14px', outline:'none', fontFamily:'inherit', transition:'border-color 0.15s', }; const btnPrimary = (active) => ({ width:'100%', height:52, borderRadius:14, border:0, cursor: active && !loading ? 'pointer' : 'not-allowed', background: active ? `linear-gradient(135deg,${accent.grad[0]},${accent.grad[1]})` : T.surfaceAlt, color: active ? '#fff' : T.fg4, fontSize:15, fontWeight:700, letterSpacing:'-0.005em', boxShadow: active ? `0 6px 20px ${accent[500]}44` : 'none', display:'flex', alignItems:'center', justifyContent:'center', gap:8, opacity: loading ? 0.7 : 1, transition:'all 0.15s', }); const spinner = (
); // ── Main screen ───────────────────────────────────────────────── if (mode === 'main') return (
{/* Brand */}
Catatan keuangan keluarga

Selamat datang

Masuk atau daftar untuk mulai catat keuangan keluarga

{/* Google */} {/* Email + Password */} {/* Magic link */}
{/* Divider + demo */}
); // ── Password mode ─────────────────────────────────────────────── if (mode === 'password') return (

{isSignup ? 'Buat akun baru' : 'Masuk dengan password'}

{isSignup ? 'Daftar dengan email dan password' : 'Masuk ke akun INDIECASH kamu'}

Email
setEmail(e.target.value)} placeholder="kamu@email.com" autoFocus style={inputStyle} />
Password
setPass(e.target.value)} placeholder={isSignup ? 'Minimal 6 karakter' : 'Password kamu'} style={{ ...inputStyle, paddingRight:44 }} />
{isSignup && password.length > 0 && password.length < 6 && (
Minimal 6 karakter
)}
{err && (
{err}
)}
{/* Toggle signup/login */}
{isSignup ? ( <>Sudah punya akun?{' '} ) : ( <>Belum punya akun?{' '} )}
{/* Forgot password placeholder */} {!isSignup && (
)}
); // ── Magic link mode ───────────────────────────────────────────── return (

Kirim Magic Link

Kami kirim link login ke email kamu — tanpa perlu password.

Email
setEmail(e.target.value)} placeholder="kamu@email.com" autoFocus style={inputStyle} />
{err && (
{err}
)}
); } // ─── ONBOARDING ─────────────────────────────────────────────────── function ScreenOnboarding({ theme, accent, onPick, auth }) { const T = theme; const [step, setStep] = React.useState('choice'); const [familyName, setFamilyName] = React.useState(''); const [inviteCode, setInviteCode] = React.useState(''); const [loading, setLoading] = React.useState(false); const [err, setErr] = React.useState(''); const realAuth = auth || {}; async function handleCreate() { if (!familyName.trim()) return; setLoading(true); setErr(''); if (realAuth.createFamily && realAuth.user) { var r = await realAuth.createFamily(familyName.trim(), realAuth.user.id); if (r.error) { setErr(r.error.message || 'Gagal membuat keluarga'); setLoading(false); return; } } onPick('create'); setLoading(false); } async function handleJoin() { if (inviteCode.length < 6) return; setLoading(true); setErr(''); if (realAuth.joinFamily && realAuth.user) { var r = await realAuth.joinFamily(inviteCode, realAuth.user.id); if (r.error) { setErr(r.error.message || 'Kode tidak valid'); setLoading(false); return; } } onPick('join'); setLoading(false); } const spinner = (
); return (

Mulai catat
bareng keluarga

Buat keluarga baru, atau gabung pakai kode dari pasangan/anggota lain.

{/* Card: Buat baru */} {/* Card: Gabung */}
{/* Create family sheet */} {step === 'create' && (
{ if(e.target===e.currentTarget) setStep('choice'); }}>
Nama Keluarga
setFamilyName(e.target.value)} autoFocus onKeyDown={e => e.key==='Enter' && handleCreate()} style={{ width:'100%', border:`1.5px solid ${T.border}`, borderRadius:12, padding:'12px 14px', fontSize:15, fontFamily:'inherit', background:T.bg, color:T.fg1, outline:'none', marginBottom:12 }}/> {err &&
{err}
}
)} {/* Join family sheet */} {step === 'join' && (
{ if(e.target===e.currentTarget) setStep('choice'); }}>
Masukkan Kode
Minta kode undangan dari admin keluarga kamu
setInviteCode(e.target.value.toUpperCase())} maxLength={8} autoFocus onKeyDown={e => e.key==='Enter' && handleJoin()} style={{ width:'100%', border:`1.5px solid ${T.border}`, borderRadius:12, padding:'12px 14px', fontSize:22, fontWeight:800, fontFamily:'monospace', letterSpacing:'0.25em', textAlign:'center', background:T.bg, color:T.fg1, outline:'none', marginBottom:12 }}/> {err &&
{err}
}
)}
💡
Kenapa keluarga? Semua anggota lihat saldo & transaksi yang sama — nggak ada lagi "lupa kasih tahu pasangan."
); } Object.assign(window, { ScreenMore, ScreenLogin, ScreenOnboarding });