web: PRO needs a proven address — email verification gates the studio
A signed-in account is served exactly like a guest until it opens the verification link: watermarked 2048px export, no saving, no PRO frames, GPS stamp or HDF. SMTP is declared in .env; with SMTP_HOST unset the link goes to the container log. Allowlisted admins count as verified.
This commit is contained in:
@@ -180,7 +180,9 @@ export function Landing() {
|
||||
const [temp, setTemp] = useState(0);
|
||||
const [sat, setSat] = useState(100);
|
||||
const [con, setCon] = useState(100);
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
// `lead` is the short bold tag in front of the message; the marketing lines
|
||||
// share "Coming soon —", the verification ones carry their own.
|
||||
const [toast, setToast] = useState<{ lead: string; text: string } | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [photos, setPhotos] = useState<Photo[]>([]);
|
||||
const [ratings, setRatings] = useState<Record<string, Rating>>({});
|
||||
@@ -197,8 +199,13 @@ export function Landing() {
|
||||
|
||||
const preset = PRESETS.find((p) => p.id === presetId) ?? PRESETS[0];
|
||||
|
||||
const showToast = useCallback((text: Txt | string) => {
|
||||
setToast(typeof text === 'string' ? text : c(text));
|
||||
const toastLead: Txt = { en: 'Coming soon —', vi: 'Sắp có —' };
|
||||
|
||||
const showToast = useCallback((text: Txt | string, lead?: Txt) => {
|
||||
setToast({
|
||||
lead: c(lead ?? toastLead),
|
||||
text: typeof text === 'string' ? text : c(text),
|
||||
});
|
||||
clearTimeout(toastTimer.current);
|
||||
toastTimer.current = setTimeout(() => setToast(null), 3400);
|
||||
}, [c]);
|
||||
@@ -219,6 +226,32 @@ export function Landing() {
|
||||
reload();
|
||||
}, [reload]);
|
||||
|
||||
// The verification link in the letter redirects here with `?verified=1` (or
|
||||
// `0` for a stale or unknown token). Say which it was, re-read the session so
|
||||
// a just-proven account comes back PRO, and drop the flag so a reload does
|
||||
// not repeat the toast.
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const flag = params.get('verified');
|
||||
if (flag === null) return;
|
||||
params.delete('verified');
|
||||
const rest = params.toString();
|
||||
window.history.replaceState(null, '', `${window.location.pathname}${rest ? `?${rest}` : ''}${window.location.hash}`);
|
||||
if (flag === '1') {
|
||||
showToast(
|
||||
{ en: 'Your email is confirmed — the studio is unlocked.', vi: 'Đã xác thực email — studio đã mở khoá.' },
|
||||
{ en: 'Verified —', vi: 'Đã xác thực —' },
|
||||
);
|
||||
reload();
|
||||
} else {
|
||||
showToast(
|
||||
{ en: 'That verification link is invalid or has expired.', vi: 'Liên kết xác thực không hợp lệ hoặc đã hết hạn.' },
|
||||
{ en: 'Not verified —', vi: 'Chưa xác thực —' },
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// One vote. The visitor may change their mind: the API upserts under the same
|
||||
// address hash, so a second click replaces the first rather than stacking. A
|
||||
// vote that never lands is silently dropped — it is not worth a toast.
|
||||
@@ -949,6 +982,7 @@ export function Landing() {
|
||||
{auth ? (
|
||||
<AuthModal
|
||||
initialMode={auth}
|
||||
email={user?.email}
|
||||
onClose={() => setAuth(null)}
|
||||
onDone={() => {
|
||||
setAuth(null);
|
||||
@@ -959,7 +993,7 @@ export function Landing() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{toast ? <div className="lp-toast" role="status"><b>{c({ en: 'Coming soon —', vi: 'Sắp có —' })}</b> {toast}</div> : null}
|
||||
{toast ? <div className="lp-toast" role="status"><b>{toast.lead}</b> {toast.text}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user