diff --git a/src/modules/invitations/tests/invitations.account.view.unit.tests.js b/src/modules/invitations/tests/invitations.account.view.unit.tests.js index 63cb501e0..b7227dea0 100644 --- a/src/modules/invitations/tests/invitations.account.view.unit.tests.js +++ b/src/modules/invitations/tests/invitations.account.view.unit.tests.js @@ -12,6 +12,11 @@ vi.mock('../../../lib/services/config', () => ({ }, })); +const authStoreMock = vi.hoisted(() => ({ serverConfig: null })); +vi.mock('../../auth/stores/auth.store', () => ({ + useAuthStore: () => authStoreMock, +})); + const stubs = { // Render the #status slot so the chip template function is invoked coreDataTableComponent: { @@ -44,6 +49,7 @@ const mountView = () => describe('invitations.account.view', () => { let store; beforeEach(() => { + authStoreMock.serverConfig = { sign: { in: true, up: false } }; setActivePinia(createPinia()); store = useInvitationsStore(); store.getInvitations = vi.fn().mockResolvedValue(); @@ -181,4 +187,34 @@ describe('invitations.account.view', () => { // Scaffold contract (#5 not built): the placeholder must never show numbers/fake math expect(placeholder.text()).not.toMatch(/\d/); }); + + it('signup open: replaces the invite form with the informational alert', () => { + authStoreMock.serverConfig = { sign: { in: true, up: true } }; + const wrapper = mountView(); + const alert = wrapper.find('[data-test="referrals-open-signup-alert"]'); + expect(alert.exists()).toBe(true); + expect(alert.text()).toContain('Referral invitations are inactive on this deployment because public signup is open.'); + expect(wrapper.find('form').exists()).toBe(false); + }); + + it('signup closed: renders the invite form, no open-signup alert', () => { + const wrapper = mountView(); + expect(wrapper.find('[data-test="referrals-open-signup-alert"]').exists()).toBe(false); + expect(wrapper.find('form').exists()).toBe(true); + }); + + it('unknown server config (null) is treated as closed — form stays (backend CASL is the real gate)', () => { + authStoreMock.serverConfig = null; + const wrapper = mountView(); + expect(wrapper.vm.signupOpen).toBe(false); + expect(wrapper.find('form').exists()).toBe(true); + }); + + it('keeps the My referrals list visible read-only when signup is open', () => { + authStoreMock.serverConfig = { sign: { in: true, up: true } }; + store.invitations = [{ id: '1', usedAt: null, expiresAt: '2999-01-01' }]; + const wrapper = mountView(); + expect(wrapper.text()).toContain('My referrals'); + expect(wrapper.find('[data-test="referrals-summary"]').exists()).toBe(true); + }); }); diff --git a/src/modules/invitations/views/invitations.account.view.vue b/src/modules/invitations/views/invitations.account.view.vue index 9dab7e8d6..b75e3509e 100644 --- a/src/modules/invitations/views/invitations.account.view.vue +++ b/src/modules/invitations/views/invitations.account.view.vue @@ -23,35 +23,51 @@ Invite a contact -

- Share the platform with someone you know. They'll receive an email invitation to join. -

- -
- - - - Send invite - -
-
+ + + Referral invitations are inactive on this deployment because public signup is open. + + @@ -96,6 +112,7 @@ * Module dependencies. */ import { useInvitationsStore } from '../stores/invitations.store'; +import { useAuthStore } from '../../auth/stores/auth.store'; import { inviteStatus as deriveInviteStatus } from '../lib/invitations.status'; import coreDataTableComponent from '../../core/components/core.datatable.component.vue'; @@ -170,6 +187,19 @@ export default { { total: this.invitations.length, joined: 0, pending: 0, expired: 0, revoked: 0 }, ); }, + /** + * @desc Whether the deployment has PUBLIC signup open, from the server auth + * config (GET /api/auth/config → authStore.serverConfig.sign.up; loaded by the + * global router guard for any logged-in navigation). When open, the referral + * substrate is inert by design — a presented token is resolved but never + * claimed/finalized — so the invite form is replaced by an informational + * state. Uses strict equality (`=== true`) so an unknown/missing + * serverConfig keeps the form shown (the backend policy is the actual gate). + * @returns {boolean} + */ + signupOpen() { + return useAuthStore().serverConfig?.sign?.up === true; + }, }, methods: { /**