Member profiles are optional: an invitation on the dashboard, never a gate
Manson's objection was that requiring a username and a verified email pulls the
build back toward a centralized database of members. He is right, and the
communication gap is real too, so the answer is to ask well rather than to force.
Nothing about holding a position, getting paid, reading the org, the training or
the tools depends on contact details any more. There is no onboarding gate: a
brand-new member registers, lands on their page and is never stopped by a modal.
The dashboard offers a dismissable card ("Not now" snoozes it for a week) that
leads with the thing members actually want, a note the moment a payout lands in
their wallet, and says outright that everything works the same without it. The
inbox is the one place that asks, because a message cannot be delivered to
someone who left no way to reach them, and even there it is an invitation.
The card sits above the tab strip rather than inside the dashboard pane: the page
opens on the pitch tab, so an invitation parked in the dashboard would never be
seen by the new members it is aimed at.
For leaders, /api/public/reach answers "how many of my org can I reach off the
site", scoped by chain.isInTeam so it leaks nothing upward or sideways. That
makes coverage a leader's own problem to solve by asking, not a rule imposed on
members.
Fixes a real bug found by the rewritten suite: the dismissable flag double-booked
as "single-field edit", so saving a username in the opt-in flow closed the dialog
instead of advancing to the email step. Split into oneShot; the suite now asserts
the advance as a regression.
QA, all green: profiles-unit 28, signin-fallback 7, gate-e2e 33 (rewritten to
assert the opposite of what it used to: no forced modal, dismissable everywhere,
visitors unaffected), join-flow 12 cold / 11 refuse / 12 warm.
qa/reseed.sh carries two hard-won guards: never name a shell variable TMP on
Windows (it inherits the system temp dir and rm -rf wipes it), and never pkill.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
-5
@@ -33,6 +33,16 @@ const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> '
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
|
||||
// The event flyer and upgrade promo are full-screen overlays that legitimately cover
|
||||
// /my once per browser/session. They are not under test here and they swallow clicks.
|
||||
await ctx.addInitScript(() => {
|
||||
const g = Storage.prototype.getItem;
|
||||
Storage.prototype.getItem = function (k) {
|
||||
if (/^rmc-promo-/.test(k)) return 'seen';
|
||||
if (/^rmc-announce-/.test(k)) return 'done';
|
||||
return g.call(this, k);
|
||||
};
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
|
||||
// real signing, called from the page (exposeFunction sidesteps the page's strict CSP)
|
||||
@@ -108,16 +118,26 @@ if (SCENARIO === 'sign') {
|
||||
t('a signature was requested right after the join', (qa.signAsked || 0) >= 1, JSON.stringify(qa));
|
||||
t('an inbox session was created', hasSession, cookies.map(c => c.name).join(','));
|
||||
await page.waitForTimeout(2500);
|
||||
const gate = await page.evaluate(() => !!document.querySelector('#pgCard'));
|
||||
t('the profile gate appears on their dashboard', gate);
|
||||
const card = await page.evaluate(() => { const c = document.querySelector('#pgCard'); return c ? c.innerText.replace(/\s+/g, ' ') : ''; });
|
||||
t('the gate is for THEIR position', new RegExp('position #' + NEW_ID, 'i').test(card), card.slice(0, 90));
|
||||
// Since 2026-09-17 contact details are optional, so a brand-new member must NOT be
|
||||
// met by a modal. They get a dismissable invitation on their own dashboard instead.
|
||||
t('NO modal is forced on a brand-new member', !(await page.evaluate(() => !!document.querySelector('#pgCard'))));
|
||||
const invite = await page.evaluate(() => !!document.getElementById('pcGo'));
|
||||
t('the optional invitation is offered instead', invite);
|
||||
t('and it can be declined', await page.evaluate(() => !!document.getElementById('pcNo')));
|
||||
if (invite) {
|
||||
await page.click('#pcGo'); await page.waitForTimeout(1400);
|
||||
const card = await page.evaluate(() => { const c = document.querySelector('#pgCard'); return c ? c.innerText.replace(/\s+/g, ' ') : ''; });
|
||||
t('opening it addresses THEIR position', new RegExp('position #' + NEW_ID, 'i').test(card), card.slice(0, 90));
|
||||
t('and they can close it again', await page.evaluate(() => !!document.getElementById('pgClose')));
|
||||
}
|
||||
} else {
|
||||
t('the signature was refused by the wallet', (qa.signRefused || 0) >= 1, JSON.stringify(qa));
|
||||
t('REGRESSION: the join still completed and redirected', /\/my\//.test(page.url()), page.url());
|
||||
t('no inbox session, as expected', !hasSession);
|
||||
const gate = await page.evaluate(() => !!document.querySelector('#pgCard'));
|
||||
t('no gate without a session, page still usable', !gate);
|
||||
t('no modal without a session, page still usable', !gate);
|
||||
t('and no invitation either, since we cannot know who they are',
|
||||
!(await page.evaluate(() => !!document.getElementById('pcGo'))));
|
||||
const txt = await page.evaluate(() => document.body.innerText);
|
||||
t('the dashboard still renders for them', txt.length > 300, 'len ' + txt.length);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user