// QReward Customer — Onboarding: Splash, Tour, Login, Account creation const { useState: uS1, useEffect: uE1, useRef: uR1 } = React; /* ============ SPLASH ============ */ const SplashScreen = ({ onDone }) => { uE1(() => { const t = setTimeout(onDone, 1600); return () => clearTimeout(t); }, []); return (
r
QReward
); }; /* ============ FIRST-RUN TOUR ============ */ const TourScreen = ({ onDone, lang }) => { const [i, setI] = uS1(0); const slides = [ { art: 'wallet', hl: lang === 'jp' ? 'すべてのポイントを\nひとつに' : 'All your points,\nin one wallet', body: lang === 'jp' ? 'LifeCardとQUALIAのポイントを、QRewardでまとめて管理。' : 'QReward brings your LifeCard and QUALIA points together in one place.' }, { art: 'trending', hl: lang === 'jp' ? '使うほど\n貯まる' : 'Earn as you\nspend', body: lang === 'jp' ? '提携カードを連携すると、毎日のお買い物がポイントに変わります。' : 'Link your partner cards and everyday spending turns into points automatically.' }, { art: 'gift', hl: lang === 'jp' ? 'ポイントで\n特別な体験を' : 'Redeem for\nthings you love', body: lang === 'jp' ? 'マーケットプレイスで、ギフト・家電・体験などと交換できます。' : 'Spend points in the marketplace on gift cards, electronics, experiences and more.' }, ]; const s = slides[i]; const last = i === slides.length - 1; return (
{slides.map((_, k) =>
)}
{s.hl}
{s.body}
last ? onDone() : setI(i + 1)}> {last ? (lang === 'jp' ? 'はじめる' : 'Get started') : (lang === 'jp' ? '次へ' : 'Next')}
); }; const TourArt = ({ kind }) => { // Stylized cohesive illustrations (line-weight consistent) if (kind === 'wallet') return ( r ); if (kind === 'trending') return ( r ); return ( ); }; /* ============ LOGIN ============ */ const LoginScreen = ({ onLogin, onCreate, lang, hasBiometric = true }) => { const [email, setEmail] = uS1('giri.tanaka@gmail.com'); const [pw, setPw] = uS1(''); const [showPw, setShowPw] = uS1(false); const [remember, setRemember] = uS1(true); const [state, setState] = uS1('fresh'); // fresh | submitting | error | success | bioPrompt const [attempts, setAttempts] = uS1(0); const submit = () => { setState('submitting'); setTimeout(() => { if (pw === 'wrong' || (pw.length > 0 && pw.length < 4)) { const a = attempts + 1; setAttempts(a); setState('error'); } else { setState('success'); setTimeout(onLogin, 500); } }, 900); }; const locked = attempts >= 5; return (
r
{lang === 'jp' ? 'おかえりなさい' : 'Welcome back'}
{state === 'error' && (
{locked ? (lang === 'jp' ? '試行回数が多すぎます。パスワードをリセットしてください。' : 'Too many attempts. Try “Forgot password”.') : (lang === 'jp' ? 'メールアドレスまたはパスワードが正しくありません。' : 'Invalid email or password.')}
)}
setEmail(e.target.value)} type="email" inputMode="email" />
setPw(e.target.value)} type={showPw ? 'text' : 'password'} placeholder="••••••••" />
{state === 'submitting' ? : state === 'success' ? : L(lang, 'signIn')}
{hasBiometric && ( <>
{lang === 'jp' ? 'または' : 'or'}
{ setState('success'); setTimeout(onLogin, 600); }}> {lang === 'jp' ? 'Face IDでサインイン' : 'Use Face ID'}
)}
{lang === 'jp' ? 'はじめての方は' : 'New here?'}
); }; /* ============ ACCOUNT CREATION (7 steps) ============ */ const SignupFlow = ({ onDone, onCancel, lang }) => { const [step, setStep] = uS1(0); // 0..6 const total = 7; const next = () => setStep(s => Math.min(total - 1, s + 1)); const back = () => step === 0 ? onCancel() : setStep(s => s - 1); return (
{step < 6 && (
)}
{step === 0 && } {step === 1 && } {step === 2 && } {step === 3 && } {step === 4 && } {step === 5 && } {step === 6 && }
); }; const StepHead = ({ title, sub }) => (
{title}
{sub &&
{sub}
}
); const SignupDetails = ({ onNext, lang }) => { const [user, setUser] = uS1(''); const [email, setEmail] = uS1(''); const [phone, setPhone] = uS1(''); const [pw, setPw] = uS1(''); const [pw2, setPw2] = uS1(''); const [showPw, setShowPw] = uS1(false); const userTaken = user.toLowerCase() === 'giri' || user.toLowerCase() === 'admin'; const userOk = user.length >= 3 && !userTaken; const emailOk = /\S+@\S+\.\S+/.test(email); const pwScore = [pw.length >= 8, /[A-Z]/.test(pw) && /[a-z]/.test(pw), /\d/.test(pw), /[^A-Za-z0-9]/.test(pw)].filter(Boolean).length; const pwLabels = ['', 'weak', 'fair', 'good', 'strong']; const matchOk = pw2.length > 0 && pw === pw2; const valid = userOk && emailOk && phone.length >= 10 && pwScore >= 3 && matchOk; return (
setEmail(e.target.value)} type="email" placeholder="you@example.com" /> {emailOk && }
setUser(e.target.value)} placeholder="giri_t" style={userTaken ? { borderColor: 'var(--red-500)' } : null} /> {user.length >= 3 && (userOk ? : )}
{userTaken &&
{lang === 'jp' ? 'このユーザー名は使用されています' : 'That username is taken'}
} {userOk &&
{lang === 'jp' ? '利用可能です' : 'Available'}
}
+81
setPhone(e.target.value.replace(/\D/g, ''))} type="tel" inputMode="numeric" placeholder="90-1234-5678" />
setPw(e.target.value)} type={showPw ? 'text' : 'password'} placeholder="••••••••" />
{[1, 2, 3, 4].map(k =>
= k ? 'on ' + pwLabels[pwScore] : ''}`}>
)}
{lang === 'jp' ? '8文字以上、大文字・小文字・数字・記号を含む' : 'At least 8 characters with upper, lower, digit & symbol.'} {pw && · {pwLabels[pwScore]}}
setPw2(e.target.value)} type={showPw ? 'text' : 'password'} placeholder="••••••••" style={pw2 && !matchOk ? { borderColor: 'var(--red-500)' } : null} /> {matchOk && }
{L(lang, 'continue')}
); }; const SignupOTP = ({ onNext, email, lang }) => { const [digits, setDigits] = uS1(['', '', '', '', '', '']); const [err, setErr] = uS1(false); const [countdown, setCountdown] = uS1(60); uE1(() => { if (countdown <= 0) return; const t = setTimeout(() => setCountdown(c => c - 1), 1000); return () => clearTimeout(t); }, [countdown]); const filled = digits.filter(Boolean).length; const press = (n) => { const idx = digits.findIndex(d => d === ''); if (idx === -1) return; const nd = [...digits]; nd[idx] = String(n); setDigits(nd); if (idx === 5) { setTimeout(() => { if (nd.join('') === '123456' || nd.join('').length === 6) { onNext(); } else { setErr(true); setTimeout(() => { setDigits(['', '', '', '', '', '']); setErr(false); }, 500); } }, 200); } }; const del = () => { const idx = [...digits].reverse().findIndex(d => d !== ''); if (idx === -1) return; const real = 5 - idx; const nd = [...digits]; nd[real] = ''; setDigits(nd); }; return (
{lang === 'jp' ? 'に送信した6桁のコードを入力' : 'Enter the 6-digit code we sent to'} {email}
{digits.map((d, i) => (
{d}
))}
{countdown > 0 ? <>{lang === 'jp' ? 'コード再送信' : 'Resend code'} 0:{String(countdown).padStart(2, '0')} : }
); }; const Numpad = ({ onPress, onDel }) => (
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => )}
); const SignupConsent = ({ onNext, lang }) => { const [c1, setC1] = uS1(false); const [c2, setC2] = uS1(false); const points = [ { icon: 'wallet', t: lang === 'jp' ? 'ポイント残高の管理と本人確認に使用します' : 'Used to manage your points balance and verify your identity' }, { icon: 'link', t: lang === 'jp' ? '連携した提携先とのみ共有します(LifeCard、QUALIA)' : 'Shared only with partners you link (LifeCard, QUALIA)' }, { icon: 'shield', t: lang === 'jp' ? '設定からいつでもデータをエクスポート・削除できます' : 'Export or delete your data anytime from Settings' }, ]; return (
{lang === 'jp' ? 'データについて、わかりやすく' : 'Your data, plainly'}
{points.map((p, i) => (
{p.t}
))}
{lang === 'jp' ? '同意して続ける' : 'Agree & continue'}
); }; const SignupPIN = ({ onNext, lang }) => { const [pin, setPin] = uS1([]); const [confirm, setConfirm] = uS1([]); const [phase, setPhase] = uS1('set'); // set | confirm const [err, setErr] = uS1(false); const cur = phase === 'set' ? pin : confirm; const press = (n) => { if (cur.length >= 6) return; const nc = [...cur, n]; phase === 'set' ? setPin(nc) : setConfirm(nc); if (nc.length === 6) { setTimeout(() => { if (phase === 'set') { setPhase('confirm'); } else { if (nc.join('') === pin.join('')) { onNext(); } else { setErr(true); setTimeout(() => { setConfirm([]); setErr(false); }, 500); } } }, 150); } }; const del = () => phase === 'set' ? setPin(pin.slice(0, -1)) : setConfirm(confirm.slice(0, -1)); return (
{[0, 1, 2, 3, 4, 5].map(i =>
i ? (err ? 'error' : 'filled') : ''}`}>
)}
); }; const SignupBiometric = ({ onNext, lang }) => (
{lang === 'jp' ? 'Face IDで\nすばやくサインイン' : 'Sign in faster\nwith Face ID'}
{lang === 'jp' ? '次回からワンタップでサインイン。\n設定からいつでも変更できます。' : 'Skip the password next time. This is optional and you can change it anytime in Settings.'}
{lang === 'jp' ? 'Face IDを有効化' : 'Enable Face ID'}
); const SignupQualia = ({ onNext, lang }) => (
Q
r
{lang === 'jp' ? 'QUALIA会員IDを\n連携' : 'Link your QUALIA\nmember ID'}
{lang === 'jp' ? 'ポイントの同期や取引履歴の表示ができます。' : 'Sync your points and see your full transaction history automatically.'}
{lang === 'jp' ? '今すぐ連携' : 'Link now'}
); const SignupWelcome = ({ onDone, lang }) => { uE1(() => { const t = setTimeout(onDone, 2400); return () => clearTimeout(t); }, []); return (
{lang === 'jp' ? `ようこそ、${DATA.user.nameJp}さん` : `Welcome to QReward, ${DATA.user.name}`}
{lang === 'jp' ? 'すべての準備が整いました。' : "You're all set. Let's go."}
); }; const Confetti = () => { const bits = Array.from({ length: 28 }); const colors = ['#1f4fd1', '#f5b942', '#5eea98', '#ec4899', '#8b5cf6']; return (
{bits.map((_, i) => { const left = Math.random() * 100, delay = Math.random() * 0.5, dur = 1.4 + Math.random() * 1.2; const c = colors[i % colors.length], size = 6 + Math.random() * 6; return
; })}
); }; Object.assign(window, { SplashScreen, TourScreen, LoginScreen, SignupFlow });