import {Fragment} from 'react'; import {browserHistory, RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import {urlEncode} from '@sentry/utils'; import {logout} from 'sentry/actionCreators/account'; import {Alert} from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import NarrowLayout from 'sentry/components/narrowLayout'; import {t, tct} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import {space} from 'sentry/styles/space'; import AsyncView from 'sentry/views/asyncView'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; type InviteDetails = { existingMember: boolean; hasAuthProvider: boolean; needs2fa: boolean; needsAuthentication: boolean; needsEmailVerification: boolean; orgSlug: string; requireSso: boolean; ssoProvider?: string; }; type Props = RouteComponentProps<{memberId: string; token: string; orgId?: string}, {}>; type State = AsyncView['state'] & { acceptError: boolean | undefined; accepting: boolean | undefined; inviteDetails: InviteDetails; }; class AcceptOrganizationInvite extends AsyncView { disableErrorReport = false; get orgSlug(): string | null { const {params} = this.props; if (params.orgId) { return params.orgId; } const {customerDomain} = window.__initialData; if (customerDomain?.subdomain) { return customerDomain.subdomain; } return null; } getEndpoints(): ReturnType { const {memberId, token} = this.props.params; if (this.orgSlug) { return [['inviteDetails', `/accept-invite/${this.orgSlug}/${memberId}/${token}/`]]; } return [['inviteDetails', `/accept-invite/${memberId}/${token}/`]]; } getTitle() { return t('Accept Organization Invite'); } makeNextUrl(path: string) { return `${path}?${urlEncode({next: window.location.pathname})}`; } handleLogout = async (e: React.MouseEvent) => { e.preventDefault(); await logout(this.api); window.location.replace(this.makeNextUrl('/auth/login/')); }; handleAcceptInvite = async () => { const {memberId, token} = this.props.params; this.setState({accepting: true}); try { if (this.orgSlug) { await this.api.requestPromise( `/accept-invite/${this.orgSlug}/${memberId}/${token}/`, { method: 'POST', } ); } else { await this.api.requestPromise(`/accept-invite/${memberId}/${token}/`, { method: 'POST', }); } browserHistory.replace(`/${this.state.inviteDetails.orgSlug}/`); } catch { this.setState({acceptError: true}); } this.setState({accepting: false}); }; get existingMemberAlert() { const user = ConfigStore.get('user'); return ( {tct( 'Your account ([email]) is already a member of this organization. [switchLink:Switch accounts]?', { email: user.email, switchLink: ( ), } )} ); } get authenticationActions() { const {inviteDetails} = this.state; return ( {!inviteDetails.requireSso && (

{t( `To continue, you must either create a new account, or login to an existing Sentry account.` )}

)} {inviteDetails.hasAuthProvider && (

{inviteDetails.requireSso ? tct( `Note that [orgSlug] has required Single Sign-On (SSO) using [authProvider]. You may create an account by authenticating with the organization's SSO provider.`, { orgSlug: {inviteDetails.orgSlug}, authProvider: inviteDetails.ssoProvider, } ) : tct( `Note that [orgSlug] has enabled Single Sign-On (SSO) using [authProvider]. You may create an account by authenticating with the organization's SSO provider.`, { orgSlug: {inviteDetails.orgSlug}, authProvider: inviteDetails.ssoProvider, } )}

)} {inviteDetails.hasAuthProvider && ( )} {!inviteDetails.requireSso && ( )} {!inviteDetails.requireSso && ( {t('Login using an existing account')} )}
); } get warning2fa() { const {inviteDetails} = this.state; return (

{tct( 'To continue, [orgSlug] requires all members to configure two-factor authentication.', {orgSlug: inviteDetails.orgSlug} )}

); } get warningEmailVerification() { const {inviteDetails} = this.state; return (

{tct( 'To continue, [orgSlug] requires all members to verify their email address.', {orgSlug: inviteDetails.orgSlug} )}

); } get acceptActions() { const {inviteDetails, accepting} = this.state; return ( {inviteDetails.hasAuthProvider && !inviteDetails.requireSso && (

{tct( `Note that [orgSlug] has enabled Single Sign-On (SSO) using [authProvider]. You may join the organization by authenticating with the organization's SSO provider or via your standard account authentication.`, { orgSlug: {inviteDetails.orgSlug}, authProvider: inviteDetails.ssoProvider, } )}

)} {inviteDetails.hasAuthProvider && !inviteDetails.requireSso && ( )}
); } renderError() { return ( {t('This organization invite link is no longer valid.')} ); } renderBody() { const {inviteDetails, acceptError} = this.state; return ( {acceptError && ( {t('Failed to join this organization. Please try again')} )} {tct('[orgSlug] is using Sentry to track and debug errors.', { orgSlug: {inviteDetails.orgSlug}, })} {inviteDetails.needsAuthentication ? this.authenticationActions : inviteDetails.existingMember ? this.existingMemberAlert : inviteDetails.needs2fa ? this.warning2fa : inviteDetails.needsEmailVerification ? this.warningEmailVerification : inviteDetails.requireSso ? this.authenticationActions : this.acceptActions} ); } } const Actions = styled('div')` display: flex; align-items: center; justify-content: space-between; margin-bottom: ${space(3)}; `; const ActionsLeft = styled('span')` > a { margin-right: ${space(1)}; } `; const InviteDescription = styled('p')` font-size: 1.2em; `; export default AcceptOrganizationInvite;