One way in: email sign-in only; wallet-only sessions finish setup by email

- Remove the "sign in with just your wallet" door from the sign-in card.
- A wallet-only session (no account) is walked to the email card with a
  finish-setup note; verifying the code links that wallet to the account
  and retires the wallet-only session, so member #, purchases and payouts
  stay attached and username/profile work.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-09 05:32:30 -05:00
parent c0bd4fcbdd
commit dd2ee4b77e
3 changed files with 20 additions and 15 deletions
+5 -11
View File
@@ -467,9 +467,13 @@
async function render() { async function render() {
const me = await IAP.refreshNavWallet(); const me = await IAP.refreshNavWallet();
const signedIn = me && me.signedIn; // one way in: email. A wallet-only session (no account) is sent back to the
// email card with a finish-setup note; verifying the code links that wallet.
const walletOnly = !!(me && me.signedIn && !me.email);
const signedIn = me && me.signedIn && !walletOnly;
$('authArea').hidden = !!signedIn; $('authArea').hidden = !!signedIn;
$('memberArea').hidden = !signedIn; $('memberArea').hidden = !signedIn;
if ($('mcFinish')) $('mcFinish').hidden = !walletOnly;
if (!signedIn) return; if (!signedIn) return;
if ($('adminLink')) $('adminLink').hidden = !me.isAdmin; // admin portal link, only for ADMIN_EMAIL if ($('adminLink')) $('adminLink').hidden = !me.isAdmin; // admin portal link, only for ADMIN_EMAIL
setPane(location.hash.slice(1) || 'overview'); setPane(location.hash.slice(1) || 'overview');
@@ -1403,16 +1407,6 @@
if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad
await render(); await render();
})); }));
$('walletSigninLink').addEventListener('click', async e => {
e.preventDefault();
try {
IAP.status('Check your wallet for the free sign-in signature…');
await IAPWallet.signIn();
IAP.status('Signed in with your wallet.', 'ok');
if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad
await render();
} catch (err) { IAP.status((err && err.message) || String(err), 'bad'); }
});
$('linkBtn').addEventListener('click', busy($('linkBtn'), async () => { $('linkBtn').addEventListener('click', busy($('linkBtn'), async () => {
IAP.status('Check your wallet for the free link signature…'); IAP.status('Check your wallet for the free link signature…');
await IAPWallet.signIn(); // server binds the wallet to the signed-in email account await IAPWallet.signIn(); // server binds the wallet to the signed-in email account
+3 -3
View File
@@ -27,6 +27,8 @@
<b id="sponsorNoteName" style="font-size:19px;color:var(--mint)"></b></p> <b id="sponsorNoteName" style="font-size:19px;color:var(--mint)"></b></p>
</div> </div>
<div class="card" id="magicCard" hidden> <div class="card" id="magicCard" hidden>
<p id="mcFinish" class="small" style="color:var(--amber);margin:0 0 10px" hidden>Your wallet is signed in, but it has no account yet.
Add your email to finish setting up. Your wallet, member number and purchases stay attached.</p>
<h3>Sign in or join free</h3> <h3>Sign in or join free</h3>
<p class="muted small">Type your email and we send a 6-digit code. No password to invent, <p class="muted small">Type your email and we send a 6-digit code. No password to invent,
no password to forget. New emails get a free account automatically.</p> no password to forget. New emails get a free account automatically.</p>
@@ -50,8 +52,6 @@
<button class="btn sec" id="loginBtn">Log in</button> <button class="btn sec" id="loginBtn">Log in</button>
</div> </div>
</div> </div>
<p class="small muted">Crypto-native? You can also <a href="#" id="walletSigninLink">sign in with just your wallet</a>.
One free signature, no email needed.</p>
<p class="small muted"><a href="/">← Back to the site</a></p> <p class="small muted"><a href="/">← Back to the site</a></p>
<div class="card" id="adSlotLogin" hidden></div> <div class="card" id="adSlotLogin" hidden></div>
</div> </div>
@@ -697,7 +697,7 @@
</div> </div>
<script src="/assets/common.js?v=20260909a"></script> <script src="/assets/common.js?v=20260909a"></script>
<script src="/assets/wallet.js?v=20260908t"></script> <script src="/assets/wallet.js?v=20260908t"></script>
<script src="/assets/my.js?v=20260909a"></script> <script src="/assets/my.js?v=20260909b"></script>
<script src="/assets/chat.js?v=20260907l"></script> <script src="/assets/chat.js?v=20260907l"></script>
</body> </body>
</html> </html>
+12 -1
View File
@@ -568,6 +568,16 @@ const server = http.createServer(async (req, res) => {
const ref = parseCookies(req)['iap.sponsor'] || ''; const ref = parseCookies(req)['iap.sponsor'] || '';
const r = await accounts.ensure(e, ref); // first touch wins; existing accounts unchanged const r = await accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r); if (r.error) return json(res, 400, r);
// a wallet-only session (signed with a wallet, no account) finishing setup:
// adopt that wallet into the email account so member #, purchases and
// payouts stay attached, then retire the wallet-only session
const prior = await auth.fromRequest(req);
if (prior && prior.address && !prior.email) {
const lr = await accounts.linkWallet(e, prior.address);
if (lr.error) return json(res, 400, lr);
r.account = lr.account || await accounts.byEmail(e);
await auth.logout(req);
}
if (r.created) { sendWelcome(e, ref).catch(() => {}); } // sponsor notified at username set (/api/my/profile) if (r.created) { sendWelcome(e, ref).catch(() => {}); } // sponsor notified at username set (/api/my/profile)
if (r.created && b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent, new joins only if (r.created && b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent, new joins only
let memberId = 0; let memberId = 0;
@@ -577,7 +587,8 @@ const server = http.createServer(async (req, res) => {
} }
// -- wallet auth: link-to-account when an email session exists, or // -- wallet auth: link-to-account when an email session exists, or
// wallet-first sign-in for crypto-native users // wallet-first sign-in (no UI door since 2026-09-09; a wallet-only session
// is walked to the email card, which adopts the wallet on verify)
if (p === '/api/auth/challenge' && req.method === 'POST') { if (p === '/api/auth/challenge' && req.method === 'POST') {
const b = await readBody(req); const b = await readBody(req);
const r = auth.makeChallenge(b.address); const r = auth.makeChallenge(b.address);