diff --git a/fixtures/js-stubs/user.tsx b/fixtures/js-stubs/user.tsx index c33f5f6359ed65..1fcede9a24b6a1 100644 --- a/fixtures/js-stubs/user.tsx +++ b/fixtures/js-stubs/user.tsx @@ -22,7 +22,13 @@ export function UserFixture(params: Partial = {}): User { authenticators: [], canReset2fa: false, dateJoined: '2020-01-01T00:00:00.000Z', - emails: [], + emails: [ + { + id: '1', + email: 'foo@example.com', + is_verified: true, + }, + ], experiments: [], has2fa: false, identities: [], diff --git a/static/app/components/modals/inviteMembersModal/index.spec.tsx b/static/app/components/modals/inviteMembersModal/index.spec.tsx index f82ae6a6b4af47..5a12d79f26a903 100644 --- a/static/app/components/modals/inviteMembersModal/index.spec.tsx +++ b/static/app/components/modals/inviteMembersModal/index.spec.tsx @@ -12,9 +12,13 @@ import InviteMembersModal from 'sentry/components/modals/inviteMembersModal'; import {ORG_ROLES} from 'sentry/constants'; import TeamStore from 'sentry/stores/teamStore'; import type {DetailedTeam, Scope} from 'sentry/types'; +import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import useOrganization from 'sentry/utils/useOrganization'; jest.mock('sentry/utils/useOrganization'); +jest.mock('sentry/utils/isActiveSuperuser', () => ({ + isActiveSuperuser: jest.fn(), +})); describe('InviteMembersModal', function () { const styledWrapper = styled(c => c.children); @@ -22,13 +26,32 @@ describe('InviteMembersModal', function () { type MockApiResponseFn = ( client: typeof MockApiClient, orgSlug: string, + is_superuser: boolean, + verified_email: boolean, roles?: object[] ) => jest.Mock; - const defaultMockOrganizationRoles: MockApiResponseFn = (client, orgSlug, roles) => { + const defaultMockOrganizationRoles: MockApiResponseFn = ( + client, + orgSlug, + is_superuser, + verified_email, + roles + ) => { return client.addMockResponse({ url: `/organizations/${orgSlug}/members/me/`, method: 'GET', - body: {orgRoleList: roles}, + body: { + user: { + isSuperuser: is_superuser, + emails: [ + { + email: 'test@dev.getsentry.net', + is_verified: verified_email, + }, + ], + }, + orgRoleList: roles, + }, }); }; @@ -50,6 +73,8 @@ describe('InviteMembersModal', function () { const setupView = ({ orgTeams = [TeamFixture()], orgAccess = ['member:write'], + is_superuser = false, + verified_email = false, roles = [ { id: 'admin', @@ -69,11 +94,13 @@ describe('InviteMembersModal', function () { modalProps = defaultMockModalProps, mockApiResponses = [defaultMockOrganizationRoles], }: { + is_superuser?: boolean; mockApiResponses?: MockApiResponseFn[]; modalProps?: ComponentProps; orgAccess?: Scope[]; orgTeams?: DetailedTeam[]; roles?: object[]; + verified_email?: boolean; } = {}) => { const org = OrganizationFixture({access: orgAccess, teams: orgTeams}); TeamStore.reset(); @@ -82,7 +109,9 @@ describe('InviteMembersModal', function () { MockApiClient.clearMockResponses(); const mocks: jest.Mock[] = []; mockApiResponses.forEach(mockApiResponse => { - mocks.push(mockApiResponse(MockApiClient, org.slug, roles)); + mocks.push( + mockApiResponse(MockApiClient, org.slug, is_superuser, verified_email, roles) + ); }); jest.mocked(useOrganization).mockReturnValue(org); @@ -106,7 +135,7 @@ describe('InviteMembersModal', function () { }; it('renders', async function () { - setupView(); + setupView({verified_email: true}); await waitFor(() => { // Starts with one invite row expect(screen.getByRole('listitem')).toBeInTheDocument(); @@ -120,9 +149,7 @@ describe('InviteMembersModal', function () { }); it('renders for superuser', async function () { - jest.mock('sentry/utils/isActiveSuperuser', () => ({ - isActiveSuperuser: jest.fn(), - })); + jest.mocked(isActiveSuperuser).mockReturnValueOnce(true); const errorResponse: MockApiResponseFn = (client, orgSlug, _) => { return client.addMockResponse({ diff --git a/static/app/components/modals/inviteMembersModal/inviteMembersModalview.tsx b/static/app/components/modals/inviteMembersModal/inviteMembersModalview.tsx index b2350000a2e386..64eff970a52b13 100644 --- a/static/app/components/modals/inviteMembersModal/inviteMembersModalview.tsx +++ b/static/app/components/modals/inviteMembersModal/inviteMembersModalview.tsx @@ -20,6 +20,7 @@ import {IconAdd} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Member} from 'sentry/types/organization'; +import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; interface Props { Footer: ModalRenderProps['Footer']; @@ -64,8 +65,6 @@ export default function InviteMembersModalView({ willInvite, error, }: Props) { - const disableInputs = sendingInvites || complete; - const inviteEmails = invites.map(inv => inv.email); const hasDuplicateEmails = inviteEmails.length !== new Set(inviteEmails).size; const isValidInvites = invites.length > 0 && !hasDuplicateEmails; @@ -76,18 +75,29 @@ export default function InviteMembersModalView({ ) : null; + const userEmails = member?.user?.emails; + const isVerified = userEmails ? userEmails.some(element => element.is_verified) : false; + + const isSuperUser = isActiveSuperuser(); + + const disableInputs = sendingInvites || complete || (!isVerified && !isSuperUser); + return ( {errorAlert} {t('Invite New Members')} - {willInvite ? ( - {t('Invite new members by email to join your organization.')} - ) : ( + {!isVerified && !isSuperUser ? ( + + {t('Please verify your email before inviting other users.')} + + ) : !willInvite && isVerified && !isSuperUser ? ( {t( 'You can’t invite users directly, but we’ll forward your request to an org owner or manager for approval.' )} + ) : ( + {t('Invite new members by email to join your organization.')} )} {headerInfo}