// 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 (
);
};
/* ============ 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 (
{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 (
);
if (kind === 'trending') return (
);
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" />
{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 }) => (
);
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 (
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'}
}
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]}}
{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) => (
))}
{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 }) => (
{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 });