Username is required: blocking first step on every sign-in until one is saved

showOnboard(required) has no skip and no dismiss; a suggestion from the email
prefix is prefilled (GET /api/my/username-suggest, made unique); the member
area does not render until the username exists. Welcome tour and login ad still
run once for invite arrivals afterwards. Wallet-only sessions are unaffected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-12 15:32:42 -05:00
parent 6996186061
commit a385fbf4c2
3 changed files with 35 additions and 10 deletions
+19 -5
View File
@@ -523,12 +523,18 @@
if ($('mcFinish')) $('mcFinish').hidden = !walletOnly;
if (!signedIn) return;
if ($('adminLink')) $('adminLink').hidden = !me.isAdmin; // admin portal link, only for ADMIN_EMAIL
// arrived from an invite page: run onboarding once (username, welcome tour, login ad)
// REQUIRED first step (Marty, 2026-09-12): no username, nothing else. The modal cannot be
// skipped or dismissed; it resolves only when a username is saved, then the area renders.
if (!me.username) {
if (!render.gating) { render.gating = true; showOnboard(true).then(() => { render.gating = false; render(); }); }
return;
}
// arrived from an invite page: run the welcome tour and login ad once
if (/[?&]welcome=1/.test(location.search) && !render.welcomed) {
render.welcomed = true;
history.replaceState(null, '', '/my' + (location.hash || ''));
(async () => {
try { if (!me.username) await showOnboard(); if (!(await showGauntlet())) await showLoginAd(); } catch (e) {}
try { if (!(await showGauntlet())) await showLoginAd(); } catch (e) {}
await render();
})();
}
@@ -1771,15 +1777,23 @@
}));
})();
// new-member onboarding: choose a username, optionally a bio (both skippable)
function showOnboard() {
// new-member onboarding: choose a username (required), optionally a bio.
// required=true: no skip, no backdrop dismiss, prefilled suggestion, resolves only after a save.
function showOnboard(required) {
return new Promise(resolve => {
const m = $('onboardModal'); if (!m) return resolve();
m.hidden = false; $('obErr').hidden = true;
$('obSkip').hidden = !!required;
if (required && !$('obUsername').value) {
fetch('/api/my/username-suggest').then(r => r.json()).then(r => { if (r.suggest && !$('obUsername').value) { $('obUsername').value = r.suggest; $('obUsername').select(); } }).catch(() => {});
}
setTimeout(() => $('obUsername').focus(), 50);
const done = () => { m.hidden = true; resolve(); };
$('obSkip').onclick = done;
$('obSkip').onclick = required ? null : done;
$('obUsername').onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); $('obSave').click(); } };
$('obSave').onclick = async () => {
const u = $('obUsername').value.trim();
if (required && !u) { $('obErr').hidden = false; $('obErr').textContent = 'Pick a username to continue.'; return; }
try {
if (u) await api('/api/my/profile', { username: u });
const bio = $('obBio').value.trim();
+5 -5
View File
@@ -98,15 +98,15 @@
<div id="onboardModal" class="modal-back" hidden>
<div class="modal-card">
<p class="eyebrow">Welcome to InstantAdPay</p>
<h3 style="margin:.2em 0 .5em">Pick your username</h3>
<p class="muted small">This is how your team sees you, and it turns your invite link into a clean vanity link.
You can change it later in Profile.</p>
<h3 style="margin:.2em 0 .5em">Pick your username to continue</h3>
<p class="muted small">This is how your team sees you, and it becomes your invite link and your public page.
It is permanent once saved, so pick the one you want to be known by. We suggested one from your email; keep it or change it.</p>
<p><input id="obUsername" placeholder="Username (3-20 letters, numbers, _)" autocomplete="off" style="width:100%"></p>
<p class="muted small" style="margin:12px 0 4px">Optional — a short bio for your public page (you can skip this).</p>
<p><textarea id="obBio" rows="2" maxlength="600" placeholder="A short bio (optional)" style="width:100%"></textarea></p>
<p id="obErr" class="small" style="color:var(--bad)" hidden></p>
<p style="margin-top:14px;display:flex;gap:10px;justify-content:flex-end">
<button class="btn sec small" id="obSkip" type="button">Skip for now</button>
<button class="btn sec small" id="obSkip" type="button" hidden>Skip for now</button>
<button class="btn" id="obSave" type="button">Save &amp; continue</button>
</p>
</div>
@@ -915,7 +915,7 @@
<script src="/assets/common.js?v=20260912b"></script>
<script src="/assets/wallet.js?v=20260911a"></script>
<script src="/assets/promo.js?v=20260911a"></script>
<script src="/assets/my.js?v=20260912c"></script>
<script src="/assets/my.js?v=20260912e"></script>
<script src="/assets/chat.js?v=20260907l"></script>
</body>
</html>