Skip to content

Commit 74f706f

Browse files
feat(sybil-gate): clearer declined-state recovery CTA
The view-gate 'declined' state (signed-in viewer who withheld the account-strength scope) walled users without an obvious fix. Minister now pre-checks the account-strength scope, so re-authenticating shares it. Sharpen the declined primary CTA into an explicit recovery action ('Share your account strength to get in') on the existing authHref link; the null-authHref fallback (Minister disabled -> explanatory copy, no dead button) is unchanged. Presentation only: gate logic (requireViewBucket / gatePageOrRedirect / buildGateView, fail-closed) untouched. Add buildGateView unit coverage locking the declined CTA's authHref data contract in both minister-enabled and -disabled cases.
1 parent 4ef8b6b commit 74f706f

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

src/lib/components/ViewGate.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
view.reason === 'signed-out'
4141
? 'Sign in with Minister'
4242
: view.reason === 'declined'
43-
? 'Share and continue'
43+
? 'Share your account strength to get in'
4444
: view.reason === 'gated'
4545
? 'Re-check my account'
4646
: 'Re-authenticate with Minister'

src/lib/server/sybil/gate.unit.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import { describe, expect, it } from 'vitest';
1111
import { instanceSettings, users } from '$lib/server/db/schema';
1212
import {
13+
buildGateView,
1314
classifyRow,
1415
effectiveBar,
1516
requireViewBucket,
@@ -289,3 +290,29 @@ describe('resolveViewerBucket - collapses to number|null, fail-closed', () => {
289290
expect(await resolveViewerBucket(sess, fakeDb({ throwOn: 'userSelect' }))).toBe(null);
290291
});
291292
});
293+
294+
describe('buildGateView - declined recovery CTA data contract', () => {
295+
// The <ViewGate> declined state renders a primary "share your account strength"
296+
// CTA to `authHref`. That link is only actionable when Minister is enabled; when
297+
// it is not, `authHref` is null and the component renders explanatory copy
298+
// instead of a dead button. These assertions lock the data the CTA depends on.
299+
it('declined + minister enabled -> authHref carries a next back to this page', () => {
300+
const view = buildGateView(
301+
{ ok: false, reason: 'declined', bar: 2 },
302+
{ path: '/s/foo?tab=new', ministerEnabled: true }
303+
);
304+
expect(view.reason).toBe('declined');
305+
expect(view.authHref).toBe(
306+
`/api/auth/oidc/start?next=${encodeURIComponent('/s/foo?tab=new')}`
307+
);
308+
});
309+
310+
it('declined + minister disabled -> authHref null (no dead CTA button)', () => {
311+
const view = buildGateView(
312+
{ ok: false, reason: 'declined', bar: 2 },
313+
{ path: '/s/foo', ministerEnabled: false }
314+
);
315+
expect(view.reason).toBe('declined');
316+
expect(view.authHref).toBeNull();
317+
});
318+
});

0 commit comments

Comments
 (0)