mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:12:02 +00:00
Add Cody verify your email popup and notice for dotcom (#52165)
Add alerts, notices, popups dotcom for restricting Cody access to logged-in users with verified emails only. https://www.loom.com/share/865d8621dbd440a3ae926a647ce4adca design: https://www.figma.com/file/DIk3eGdgKszigQVec7Y8SD/Frictionless-cody?type=design&node-id=767-30229&t=ee1VyzmpjtZhgrNq-0 ## Test plan - sign up with a new account
This commit is contained in:
parent
08bce28d41
commit
fbe9f63296
@ -37,6 +37,8 @@ interface ChatProps extends ChatClassNames {
|
||||
copyButtonOnSubmit?: CopyButtonProps['copyButtonOnSubmit']
|
||||
suggestions?: string[]
|
||||
setSuggestions?: (suggestions: undefined | []) => void
|
||||
needsEmailVerification?: boolean
|
||||
needsEmailVerificationNotice?: React.FunctionComponent
|
||||
}
|
||||
|
||||
interface ChatClassNames extends TranscriptItemClassNames {
|
||||
@ -51,6 +53,7 @@ export interface ChatUITextAreaProps {
|
||||
autoFocus: boolean
|
||||
value: string
|
||||
required: boolean
|
||||
disabled?: boolean
|
||||
onInput: React.FormEventHandler<HTMLElement>
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLElement>, caretPosition: number | null) => void
|
||||
}
|
||||
@ -117,6 +120,8 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
|
||||
copyButtonOnSubmit,
|
||||
suggestions,
|
||||
setSuggestions,
|
||||
needsEmailVerification = false,
|
||||
needsEmailVerificationNotice: NeedsEmailVerificationNotice,
|
||||
}) => {
|
||||
const [inputRows, setInputRows] = useState(5)
|
||||
const [historyIndex, setHistoryIndex] = useState(inputHistory.length)
|
||||
@ -198,33 +203,45 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
|
||||
)
|
||||
|
||||
const transcriptWithWelcome = useMemo<ChatMessage[]>(
|
||||
() => [{ speaker: 'assistant', displayText: welcomeText(afterTips) }, ...transcript],
|
||||
() => [
|
||||
{
|
||||
speaker: 'assistant',
|
||||
displayText: welcomeText(afterTips),
|
||||
},
|
||||
...transcript,
|
||||
],
|
||||
[afterTips, transcript]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles.innerContainer)}>
|
||||
<Transcript
|
||||
transcript={transcriptWithWelcome}
|
||||
messageInProgress={messageInProgress}
|
||||
messageBeingEdited={messageBeingEdited}
|
||||
setMessageBeingEdited={setMessageBeingEdited}
|
||||
fileLinkComponent={fileLinkComponent}
|
||||
codeBlocksCopyButtonClassName={codeBlocksCopyButtonClassName}
|
||||
transcriptItemClassName={transcriptItemClassName}
|
||||
humanTranscriptItemClassName={humanTranscriptItemClassName}
|
||||
transcriptItemParticipantClassName={transcriptItemParticipantClassName}
|
||||
transcriptActionClassName={transcriptActionClassName}
|
||||
className={styles.transcriptContainer}
|
||||
textAreaComponent={TextArea}
|
||||
EditButtonContainer={EditButtonContainer}
|
||||
editButtonOnSubmit={editButtonOnSubmit}
|
||||
FeedbackButtonsContainer={FeedbackButtonsContainer}
|
||||
feedbackButtonsOnSubmit={feedbackButtonsOnSubmit}
|
||||
copyButtonOnSubmit={copyButtonOnSubmit}
|
||||
submitButtonComponent={SubmitButton}
|
||||
chatInputClassName={chatInputClassName}
|
||||
/>
|
||||
{needsEmailVerification && NeedsEmailVerificationNotice ? (
|
||||
<div className="flex-1">
|
||||
<NeedsEmailVerificationNotice />
|
||||
</div>
|
||||
) : (
|
||||
<Transcript
|
||||
transcript={transcriptWithWelcome}
|
||||
messageInProgress={messageInProgress}
|
||||
messageBeingEdited={messageBeingEdited}
|
||||
setMessageBeingEdited={setMessageBeingEdited}
|
||||
fileLinkComponent={fileLinkComponent}
|
||||
codeBlocksCopyButtonClassName={codeBlocksCopyButtonClassName}
|
||||
transcriptItemClassName={transcriptItemClassName}
|
||||
humanTranscriptItemClassName={humanTranscriptItemClassName}
|
||||
transcriptItemParticipantClassName={transcriptItemParticipantClassName}
|
||||
transcriptActionClassName={transcriptActionClassName}
|
||||
className={styles.transcriptContainer}
|
||||
textAreaComponent={TextArea}
|
||||
EditButtonContainer={EditButtonContainer}
|
||||
editButtonOnSubmit={editButtonOnSubmit}
|
||||
FeedbackButtonsContainer={FeedbackButtonsContainer}
|
||||
feedbackButtonsOnSubmit={feedbackButtonsOnSubmit}
|
||||
copyButtonOnSubmit={copyButtonOnSubmit}
|
||||
submitButtonComponent={SubmitButton}
|
||||
chatInputClassName={chatInputClassName}
|
||||
/>
|
||||
)}
|
||||
|
||||
<form className={classNames(styles.inputRow, inputRowClassName)}>
|
||||
{suggestions !== undefined && suggestions.length !== 0 && SuggestionButton ? (
|
||||
@ -247,6 +264,7 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
|
||||
value={formInput}
|
||||
autoFocus={true}
|
||||
required={true}
|
||||
disabled={needsEmailVerification}
|
||||
onInput={({ target }) => {
|
||||
const { value } = target as HTMLInputElement
|
||||
inputHandler(value)
|
||||
@ -256,7 +274,7 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
|
||||
<SubmitButton
|
||||
className={styles.submitButton}
|
||||
onClick={onChatSubmit}
|
||||
disabled={!!messageInProgress}
|
||||
disabled={!!messageInProgress || needsEmailVerification}
|
||||
/>
|
||||
</div>
|
||||
{contextStatus && (
|
||||
|
||||
@ -187,18 +187,15 @@ export const LegacyLayout: FC<LegacyLayoutProps> = props => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isCodyStandalonePage && (
|
||||
<GlobalAlerts
|
||||
authenticatedUser={props.authenticatedUser}
|
||||
isSourcegraphDotCom={props.isSourcegraphDotCom}
|
||||
/>
|
||||
)}
|
||||
{!isCodyStandalonePage && <GlobalAlerts authenticatedUser={props.authenticatedUser} />}
|
||||
{!isSiteInit &&
|
||||
!isSignInOrUp &&
|
||||
!props.isSourcegraphDotCom &&
|
||||
!disableFeedbackSurvey &&
|
||||
!isCodyStandalonePage && <SurveyToast authenticatedUser={props.authenticatedUser} />}
|
||||
{props.isSourcegraphDotCom && props.authenticatedUser && <CodySurveyToast />}
|
||||
{props.isSourcegraphDotCom && props.authenticatedUser && (
|
||||
<CodySurveyToast authenticatedUser={props.authenticatedUser} />
|
||||
)}
|
||||
{!isSiteInit && !isSignInOrUp && !isCodyStandalonePage && (
|
||||
<GlobalNavbar
|
||||
{...props}
|
||||
|
||||
@ -142,3 +142,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cody-message-header {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
height: 2rem;
|
||||
svg {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,11 +13,13 @@ import {
|
||||
} from '@sourcegraph/cody-ui/src/Chat'
|
||||
import { FileLinkProps } from '@sourcegraph/cody-ui/src/chat/ContextFiles'
|
||||
import { CODY_TERMS_MARKDOWN } from '@sourcegraph/cody-ui/src/terms'
|
||||
import { Button, Icon, TextArea, Link } from '@sourcegraph/wildcard'
|
||||
import { Button, Icon, TextArea, Link, Tooltip, Alert, Text, H2 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { CodyPageIcon } from '../../chat/CodyPageIcon'
|
||||
import { useChatStoreState } from '../../stores/chat'
|
||||
import { useCodySidebarStore } from '../../stores/sidebar'
|
||||
import { useIsCodyEnabled } from '../../useIsCodyEnabled'
|
||||
|
||||
import styles from './ChatUi.module.scss'
|
||||
|
||||
@ -35,6 +37,7 @@ export const ChatUI = (): JSX.Element => {
|
||||
transcriptId,
|
||||
transcriptHistory,
|
||||
} = useChatStoreState()
|
||||
const { needsEmailVerification } = useIsCodyEnabled()
|
||||
|
||||
const [formInput, setFormInput] = useState('')
|
||||
const [inputHistory, setInputHistory] = useState<string[] | []>(() =>
|
||||
@ -75,6 +78,8 @@ export const ChatUI = (): JSX.Element => {
|
||||
transcriptActionClassName={styles.transcriptAction}
|
||||
FeedbackButtonsContainer={FeedbackButtons}
|
||||
feedbackButtonsOnSubmit={onFeedbackSubmit}
|
||||
needsEmailVerification={needsEmailVerification}
|
||||
needsEmailVerificationNotice={NeedsEmailVerificationNotice}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -164,8 +169,10 @@ export const AutoResizableTextArea: React.FC<AutoResizableTextAreaProps> = ({
|
||||
onInput,
|
||||
onKeyDown,
|
||||
className,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { inputNeedsFocus, setFocusProvided } = useCodySidebarStore()
|
||||
const { needsEmailVerification } = useIsCodyEnabled()
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const { width = 0 } = useResizeObserver({ ref: textAreaRef })
|
||||
|
||||
@ -202,16 +209,37 @@ export const AutoResizableTextArea: React.FC<AutoResizableTextAreaProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
rows={1}
|
||||
autoFocus={false}
|
||||
required={true}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={onInput}
|
||||
/>
|
||||
<Tooltip content={needsEmailVerification ? 'Verify your email to use Cody.' : ''}>
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
rows={1}
|
||||
autoFocus={false}
|
||||
required={true}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={onInput}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const NeedsEmailVerificationNotice: React.FunctionComponent = () => (
|
||||
<div className="p-3">
|
||||
<H2 className={classNames('d-flex gap-1 align-items-center mb-3', styles.codyMessageHeader)}>
|
||||
<CodyPageIcon /> Cody
|
||||
</H2>
|
||||
<Alert variant="warning">
|
||||
<Text className="mb-0">Verify email</Text>
|
||||
<Text className="mb-0">
|
||||
Using Cody requires a verified email.{' '}
|
||||
<Link to={`${window.context.currentUser?.settingsURL}/emails`} target="_blank" rel="noreferrer">
|
||||
Resend email verification
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -8,7 +8,7 @@ import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
|
||||
import { TelemetryService } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
|
||||
import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
|
||||
import { Alert, Form, Input, LoadingSpinner, Text, Badge, useSessionStorage } from '@sourcegraph/wildcard'
|
||||
import { Alert, Form, Input, LoadingSpinner, Text, Badge, Link, useSessionStorage } from '@sourcegraph/wildcard'
|
||||
|
||||
import { BrandLogo } from '../../components/branding/BrandLogo'
|
||||
import { useURLSyncedString } from '../../hooks/useUrlSyncedString'
|
||||
@ -31,8 +31,6 @@ export const CodySearchPage: React.FunctionComponent<CodeSearchPageProps> = ({ a
|
||||
eventLogger.logPageView('CodySearch')
|
||||
}, [])
|
||||
|
||||
const enabled = useIsCodyEnabled()
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
/** The value entered by the user in the query input */
|
||||
@ -109,20 +107,14 @@ export const CodySearchPage: React.FunctionComponent<CodeSearchPageProps> = ({ a
|
||||
<div className={classNames('d-flex flex-column align-items-center px-3', searchPageStyles.searchPage)}>
|
||||
<BrandLogo className={searchPageStyles.logo} isLightTheme={isLightTheme} variant="logo" />
|
||||
<div className="text-muted mt-3 mr-sm-2 pr-2 text-center">Searching millions of public repositories</div>
|
||||
{enabled.search ? (
|
||||
<SearchInput
|
||||
value={input}
|
||||
onChange={onInputChange}
|
||||
onSubmit={onSubmit}
|
||||
loading={loading}
|
||||
error={inputError}
|
||||
className={classNames('mt-5 w-100', styles.inputContainer)}
|
||||
/>
|
||||
) : (
|
||||
<Alert variant="info" className="mt-5">
|
||||
Cody is not enabled on this Sourcegraph instance.
|
||||
</Alert>
|
||||
)}
|
||||
<SearchInput
|
||||
value={input}
|
||||
onChange={onInputChange}
|
||||
onSubmit={onSubmit}
|
||||
loading={loading}
|
||||
error={inputError}
|
||||
className={classNames('mt-5 w-100', styles.inputContainer)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -135,6 +127,7 @@ const SearchInput: React.FunctionComponent<{
|
||||
onSubmit: () => void
|
||||
className?: string
|
||||
}> = ({ value, loading, error, onChange, onSubmit: parentOnSubmit, className }) => {
|
||||
const cody = useIsCodyEnabled()
|
||||
const onInput = useCallback<React.FormEventHandler<HTMLInputElement>>(
|
||||
event => {
|
||||
onChange(event.currentTarget.value)
|
||||
@ -150,13 +143,25 @@ const SearchInput: React.FunctionComponent<{
|
||||
[parentOnSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
return cody.search ? (
|
||||
<Form onSubmit={onSubmit} className={className}>
|
||||
{cody.needsEmailVerification && (
|
||||
<Alert variant="warning">
|
||||
<Text className="mb-0">Verify email</Text>
|
||||
<Text className="mb-0">
|
||||
Using Cody requires a verified email.{' '}
|
||||
<Link to={`${window.context.currentUser?.settingsURL}/emails`} target="_blank" rel="noreferrer">
|
||||
Resend email verification
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<Input
|
||||
inputClassName={styles.input}
|
||||
value={value}
|
||||
onInput={onInput}
|
||||
disabled={loading}
|
||||
disabled={loading || cody.needsEmailVerification}
|
||||
autoFocus={true}
|
||||
placeholder="Search for code or files in natural language..."
|
||||
/>
|
||||
@ -174,5 +179,9 @@ const SearchInput: React.FunctionComponent<{
|
||||
<LoadingSpinner className="mt-2 d-block mx-auto" />
|
||||
) : null}
|
||||
</Form>
|
||||
) : (
|
||||
<Alert variant="info" className="mt-5">
|
||||
Cody is not enabled on this Sourcegraph instance.
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import { isErrorLike } from '@sourcegraph/common'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { EventName } from '../../util/constants'
|
||||
import { CodeMirrorEditor } from '../components/CodeMirrorEditor'
|
||||
import { useIsCodyEnabled } from '../useIsCodyEnabled'
|
||||
import { useIsCodyEnabled, isEmailVerificationNeeded } from '../useIsCodyEnabled'
|
||||
|
||||
import { EditorStore, useEditorStore } from './editor'
|
||||
|
||||
@ -71,6 +71,7 @@ const sortSliceTranscriptHistory = (transcriptHistory: TranscriptJSON[]): Transc
|
||||
.slice(0, SAVE_MAX_TRANSCRIPT_HISTORY)
|
||||
|
||||
export const useChatStoreState = create<CodyChatStore>((set, get): CodyChatStore => {
|
||||
const needsEmailVerification = isEmailVerificationNeeded()
|
||||
const fetchTranscriptHistory = (): TranscriptJSON[] => {
|
||||
try {
|
||||
const json = JSON.parse(
|
||||
@ -106,6 +107,10 @@ export const useChatStoreState = create<CodyChatStore>((set, get): CodyChatStore
|
||||
}
|
||||
|
||||
const clearHistory = (): void => {
|
||||
if (needsEmailVerification) {
|
||||
return
|
||||
}
|
||||
|
||||
const { client, onEvent } = get()
|
||||
saveTranscriptHistory([])
|
||||
if (client && !isErrorLike(client)) {
|
||||
@ -115,6 +120,10 @@ export const useChatStoreState = create<CodyChatStore>((set, get): CodyChatStore
|
||||
}
|
||||
|
||||
const deleteHistoryItem = (id: string): void => {
|
||||
if (needsEmailVerification) {
|
||||
return
|
||||
}
|
||||
|
||||
const { transcriptId } = get()
|
||||
const transcriptHistory = fetchTranscriptHistory()
|
||||
|
||||
@ -128,6 +137,12 @@ export const useChatStoreState = create<CodyChatStore>((set, get): CodyChatStore
|
||||
|
||||
const submitMessage = (text: string): void => {
|
||||
const { client, onEvent, getChatContext } = get()
|
||||
|
||||
if (needsEmailVerification) {
|
||||
onEvent?.('submit')
|
||||
return
|
||||
}
|
||||
|
||||
if (client && !isErrorLike(client)) {
|
||||
const { codebase, filePath } = getChatContext()
|
||||
eventLogger.log(EventName.CODY_SIDEBAR_SUBMIT, {
|
||||
@ -142,6 +157,12 @@ export const useChatStoreState = create<CodyChatStore>((set, get): CodyChatStore
|
||||
|
||||
const editMessage = (text: string): void => {
|
||||
const { client, onEvent, getChatContext } = get()
|
||||
|
||||
if (needsEmailVerification) {
|
||||
onEvent?.('submit')
|
||||
return
|
||||
}
|
||||
|
||||
if (client && !isErrorLike(client)) {
|
||||
const { codebase, filePath } = getChatContext()
|
||||
eventLogger.log(EventName.CODY_SIDEBAR_EDIT, {
|
||||
@ -162,6 +183,12 @@ export const useChatStoreState = create<CodyChatStore>((set, get): CodyChatStore
|
||||
}
|
||||
): Promise<void> => {
|
||||
const { client, getChatContext, onEvent } = get()
|
||||
|
||||
if (needsEmailVerification) {
|
||||
onEvent?.('submit')
|
||||
return
|
||||
}
|
||||
|
||||
if (client && !isErrorLike(client)) {
|
||||
const { codebase, filePath } = getChatContext()
|
||||
eventLogger.log(EventName.CODY_SIDEBAR_RECIPE, { repo: codebase, path: filePath, recipeId })
|
||||
@ -178,6 +205,11 @@ export const useChatStoreState = create<CodyChatStore>((set, get): CodyChatStore
|
||||
return
|
||||
}
|
||||
|
||||
if (needsEmailVerification) {
|
||||
onEvent?.('submit')
|
||||
return
|
||||
}
|
||||
|
||||
if (oldClient && !isErrorLike(oldClient)) {
|
||||
oldClient.reset()
|
||||
}
|
||||
|
||||
@ -5,9 +5,21 @@ const notEnabled = {
|
||||
sidebar: false,
|
||||
search: false,
|
||||
editorRecipes: false,
|
||||
needsEmailVerification: false,
|
||||
}
|
||||
|
||||
export const useIsCodyEnabled = (): { chat: boolean; sidebar: boolean; search: boolean; editorRecipes: boolean } => {
|
||||
interface IsCodyEnabled {
|
||||
chat: boolean
|
||||
sidebar: boolean
|
||||
search: boolean
|
||||
editorRecipes: boolean
|
||||
needsEmailVerification: boolean
|
||||
}
|
||||
|
||||
export const isEmailVerificationNeeded = (): boolean =>
|
||||
window.context?.sourcegraphDotComMode && !window.context?.currentUser?.hasVerifiedEmail
|
||||
|
||||
export const useIsCodyEnabled = (): IsCodyEnabled => {
|
||||
const [chatEnabled] = useFeatureFlag('cody-web-chat')
|
||||
const [searchEnabled] = useFeatureFlag('cody-web-search')
|
||||
const [sidebarEnabled] = useFeatureFlag('cody-web-sidebar')
|
||||
@ -18,18 +30,11 @@ export const useIsCodyEnabled = (): { chat: boolean; sidebar: boolean; search: b
|
||||
return notEnabled
|
||||
}
|
||||
|
||||
if (
|
||||
window.context?.sourcegraphDotComMode &&
|
||||
!window.context?.currentUser?.siteAdmin &&
|
||||
!window.context?.currentUser?.hasVerifiedEmail
|
||||
) {
|
||||
return notEnabled
|
||||
}
|
||||
|
||||
return {
|
||||
chat: chatEnabled || allEnabled,
|
||||
sidebar: sidebarEnabled || allEnabled,
|
||||
search: searchEnabled || allEnabled,
|
||||
editorRecipes: (editorRecipesEnabled && sidebarEnabled) || allEnabled,
|
||||
needsEmailVerification: isEmailVerificationNeeded(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { parseISO } from 'date-fns'
|
||||
@ -24,7 +24,6 @@ import styles from './GlobalAlerts.module.scss'
|
||||
|
||||
interface Props {
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
isSourcegraphDotCom: boolean
|
||||
}
|
||||
|
||||
// NOTE: The name of the query is also added in the refreshSiteFlags() function
|
||||
@ -41,23 +40,13 @@ const QUERY = gql`
|
||||
/**
|
||||
* Fetches and displays relevant global alerts at the top of the page
|
||||
*/
|
||||
export const GlobalAlerts: React.FunctionComponent<Props> = ({ authenticatedUser, isSourcegraphDotCom }) => {
|
||||
export const GlobalAlerts: React.FunctionComponent<Props> = ({ authenticatedUser }) => {
|
||||
const settings = useSettings()
|
||||
const { data } = useQuery<GlobalAlertsSiteFlagsResult, GlobalAlertsSiteFlagsVariables>(QUERY, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
const siteFlagsValue = data?.site
|
||||
|
||||
const verifyEmailProps = useMemo(() => {
|
||||
if (!authenticatedUser || !isSourcegraphDotCom) {
|
||||
return
|
||||
}
|
||||
return {
|
||||
emails: authenticatedUser.emails.filter(({ verified }) => !verified).map(({ email }) => email),
|
||||
settingsURL: authenticatedUser.settingsURL as string,
|
||||
}
|
||||
}, [authenticatedUser, isSourcegraphDotCom])
|
||||
|
||||
return (
|
||||
<div className={classNames('test-global-alert', styles.globalAlerts)}>
|
||||
{siteFlagsValue && (
|
||||
@ -118,9 +107,7 @@ export const GlobalAlerts: React.FunctionComponent<Props> = ({ authenticatedUser
|
||||
</DismissibleAlert>
|
||||
)}
|
||||
<Notices alertClassName={styles.alert} location="top" />
|
||||
{!!verifyEmailProps?.emails.length && (
|
||||
<VerifyEmailNotices alertClassName={styles.alert} {...verifyEmailProps} />
|
||||
)}
|
||||
<VerifyEmailNotices authenticatedUser={authenticatedUser} alertClassName={styles.alert} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
@ -7,8 +7,9 @@ import { Notice } from '@sourcegraph/shared/src/schema/settings.schema'
|
||||
import { useSettings } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { Alert, AlertProps, Markdown } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../auth'
|
||||
import { useIsCodyEnabled } from '../cody/useIsCodyEnabled'
|
||||
import { DismissibleAlert } from '../components/DismissibleAlert'
|
||||
import { useFeatureFlag } from '../featureFlags/useFeatureFlag'
|
||||
|
||||
import styles from './Notices.module.scss'
|
||||
|
||||
@ -83,10 +84,8 @@ export const Notices: React.FunctionComponent<React.PropsWithChildren<Props>> =
|
||||
|
||||
interface VerifyEmailNoticesProps {
|
||||
className?: string
|
||||
/** Apply this class name to each notice (alongside .alert). */
|
||||
alertClassName?: string
|
||||
emails: string[]
|
||||
settingsURL: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,35 +94,24 @@ interface VerifyEmailNoticesProps {
|
||||
export const VerifyEmailNotices: React.FunctionComponent<VerifyEmailNoticesProps> = ({
|
||||
className,
|
||||
alertClassName,
|
||||
emails,
|
||||
settingsURL,
|
||||
authenticatedUser,
|
||||
}) => {
|
||||
const [isEmailVerificationAlertEnabled, status] = useFeatureFlag('ab-email-verification-alert')
|
||||
const codyEnabled = useIsCodyEnabled()
|
||||
|
||||
const notices: Notice[] = useMemo(() => {
|
||||
if (status !== 'loaded' || !isEmailVerificationAlertEnabled) {
|
||||
return []
|
||||
}
|
||||
return emails.map(
|
||||
(email): Notice => ({
|
||||
message: `Please, <a href="${settingsURL}/emails">verify your email</a> <strong>${email
|
||||
.split('@')
|
||||
.join('\\@')}</strong>`,
|
||||
location: 'top',
|
||||
dismissible: false,
|
||||
})
|
||||
if (codyEnabled.needsEmailVerification && authenticatedUser) {
|
||||
return (
|
||||
<div className={classNames(styles.notices, className)}>
|
||||
<NoticeAlert
|
||||
className={alertClassName}
|
||||
notice={{
|
||||
location: 'top',
|
||||
message: `**NEW**: Cody, our new AI Assistant is available to use for free, simply verify your email address. Didn't get an email? [Resend verification email](${authenticatedUser?.settingsURL}/emails)`,
|
||||
dismissible: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}, [emails, isEmailVerificationAlertEnabled, settingsURL, status])
|
||||
|
||||
if (notices.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.notices, className)}>
|
||||
{notices.map(notice => (
|
||||
<NoticeAlert key={notice.message} testId="notice-alert" className={alertClassName} notice={notice} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
|
||||
import { mdiEmail } from '@mdi/js'
|
||||
|
||||
import { asError, ErrorLike } from '@sourcegraph/common'
|
||||
import { gql, useMutation } from '@sourcegraph/http-client'
|
||||
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
|
||||
import { Checkbox, Form, H3, Modal, Text, useLocalStorage } from '@sourcegraph/wildcard'
|
||||
import { Checkbox, Form, H3, Modal, Text, Button, Icon, useLocalStorage } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { CodyPageIcon } from '../../cody/chat/CodyPageIcon'
|
||||
import { useIsCodyEnabled } from '../../cody/useIsCodyEnabled'
|
||||
import { LoaderButton } from '../../components/LoaderButton'
|
||||
import { SubmitCodySurveyResult, SubmitCodySurveyVariables } from '../../graphql-operations'
|
||||
import { resendVerificationEmail } from '../../user/settings/emails/UserEmail'
|
||||
|
||||
const SUBMIT_CODY_SURVEY = gql`
|
||||
mutation SubmitCodySurvey($isForWork: Boolean!, $isForPersonal: Boolean!) {
|
||||
@ -47,7 +54,10 @@ const CodySurveyToastInner: React.FC<{ onSubmitEnd: () => void }> = ({ onSubmitE
|
||||
|
||||
return (
|
||||
<Modal position="center" aria-label="Welcome message">
|
||||
<H3 className="mb-4">Quick question...</H3>
|
||||
<H3 className="mb-4 d-flex align-items-center">
|
||||
<CodyPageIcon />
|
||||
<span>Just one more thing...</span>
|
||||
</H3>
|
||||
<Text className="mb-3">How will you be using Cody, our AI assistant?</Text>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Checkbox
|
||||
@ -74,6 +84,68 @@ const CodySurveyToastInner: React.FC<{ onSubmitEnd: () => void }> = ({ onSubmitE
|
||||
)
|
||||
}
|
||||
|
||||
const CodyVerifyEmailToast: React.FC<{ onNext: () => void; authenticatedUser: AuthenticatedUser }> = ({
|
||||
onNext,
|
||||
authenticatedUser,
|
||||
}) => {
|
||||
const [sending, setSending] = useState(false)
|
||||
const [resentEmailTo, setResentEmailTo] = useState<string | null>(null)
|
||||
const [resendEmailError, setResendEmailError] = useState<ErrorLike | null>(null)
|
||||
const resend = useCallback(async () => {
|
||||
const email = (authenticatedUser.emails || []).find(({ verified }) => !verified)?.email
|
||||
if (email) {
|
||||
setSending(true)
|
||||
await resendVerificationEmail(authenticatedUser.id, email, {
|
||||
onSuccess: () => {
|
||||
setResentEmailTo(email)
|
||||
setResendEmailError(null)
|
||||
setSending(false)
|
||||
},
|
||||
onError: (errors: ErrorLike) => {
|
||||
setResendEmailError(asError(errors))
|
||||
setResentEmailTo(null)
|
||||
setSending(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [authenticatedUser])
|
||||
|
||||
return (
|
||||
<Modal position="center" aria-label="Welcome message">
|
||||
<H3 className="mb-4">
|
||||
<Icon svgPath={mdiEmail} className="mr-2" aria-hidden={true} />
|
||||
Verify your email address
|
||||
</H3>
|
||||
<Text>To use Cody, our AI Assistent, you'll need to verify your email address.</Text>
|
||||
<Text className="d-flex align-items-center">
|
||||
<span className="mr-1">Didn't get an email?</span>
|
||||
{sending ? (
|
||||
<span>Sending...</span>
|
||||
) : (
|
||||
<>
|
||||
<span>Click to </span>
|
||||
<Button variant="link" className="p-0 ml-1" onClick={resend}>
|
||||
resend
|
||||
</Button>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
{resentEmailTo && (
|
||||
<Text>
|
||||
Sent verification email to <strong>{resentEmailTo}</strong>.
|
||||
</Text>
|
||||
)}
|
||||
{resendEmailError && <Text>{resendEmailError.message}.</Text>}
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<Button variant="primary" onClick={onNext}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCodySurveyToast = (): {
|
||||
show: boolean
|
||||
dismiss: () => void
|
||||
@ -83,7 +155,16 @@ export const useCodySurveyToast = (): {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const [shouldShowCodySurvey, setShouldShowCodySurvey] = useLocalStorage('cody.survey.show', false)
|
||||
const [hasSubmitted, setHasSubmitted] = useTemporarySetting('cody.survey.submitted', false)
|
||||
const dismiss = useCallback(() => setHasSubmitted(true), [setHasSubmitted])
|
||||
const dismiss = useCallback(() => {
|
||||
setHasSubmitted(true)
|
||||
setShouldShowCodySurvey(false)
|
||||
}, [setHasSubmitted, setShouldShowCodySurvey])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowCodySurvey && hasSubmitted) {
|
||||
setShouldShowCodySurvey(false)
|
||||
}
|
||||
}, [shouldShowCodySurvey, hasSubmitted, setShouldShowCodySurvey])
|
||||
|
||||
return {
|
||||
// we calculate "show" value based whether this a new signup and whether they already have submitted survey
|
||||
@ -93,11 +174,21 @@ export const useCodySurveyToast = (): {
|
||||
}
|
||||
}
|
||||
|
||||
export const CodySurveyToast: React.FC = () => {
|
||||
export const CodySurveyToast: React.FC<{
|
||||
authenticatedUser?: AuthenticatedUser
|
||||
}> = ({ authenticatedUser }) => {
|
||||
const { show, dismiss } = useCodySurveyToast()
|
||||
const codyEnabled = useIsCodyEnabled()
|
||||
const [showVerifyEmail, setShowVerifyEmail] = useState(show && codyEnabled.needsEmailVerification)
|
||||
const dismissVerifyEmail = useCallback(() => setShowVerifyEmail(false), [setShowVerifyEmail])
|
||||
|
||||
if (!show) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (showVerifyEmail && authenticatedUser) {
|
||||
return <CodyVerifyEmailToast onNext={dismissVerifyEmail} authenticatedUser={authenticatedUser} />
|
||||
}
|
||||
|
||||
return <CodySurveyToastInner onSubmitEnd={dismiss} />
|
||||
}
|
||||
|
||||
@ -193,7 +193,7 @@ export const Layout: React.FC<LegacyLayoutProps> = props => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<GlobalAlerts authenticatedUser={props.authenticatedUser} isSourcegraphDotCom={props.isSourcegraphDotCom} />
|
||||
<GlobalAlerts authenticatedUser={props.authenticatedUser} />
|
||||
{!isSiteInit && !isSignInOrUp && !props.isSourcegraphDotCom && !disableFeedbackSurvey && (
|
||||
<SurveyToast authenticatedUser={props.authenticatedUser} />
|
||||
)}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { FunctionComponent, useState, useCallback } from 'react'
|
||||
|
||||
import { asError, ErrorLike } from '@sourcegraph/common'
|
||||
import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
|
||||
@ -28,6 +28,33 @@ interface Props {
|
||||
onEmailResendVerification?: () => void
|
||||
}
|
||||
|
||||
export const resendVerificationEmail = async (
|
||||
userID: string,
|
||||
email: string,
|
||||
options?: { onSuccess: () => void; onError: (error: ErrorLike) => void }
|
||||
): Promise<void> => {
|
||||
try {
|
||||
dataOrThrowErrors(
|
||||
await requestGraphQL<ResendVerificationEmailResult, ResendVerificationEmailVariables>(
|
||||
gql`
|
||||
mutation ResendVerificationEmail($userID: ID!, $email: String!) {
|
||||
resendVerificationEmail(user: $userID, email: $email) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ userID, email }
|
||||
).toPromise()
|
||||
)
|
||||
|
||||
eventLogger.log('UserEmailAddressVerificationResent')
|
||||
|
||||
options?.onSuccess?.()
|
||||
} catch (error) {
|
||||
options?.onError?.(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const UserEmail: FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
user,
|
||||
email: { email, isPrimary, verified, verificationPending, viewerCanManuallyVerify },
|
||||
@ -39,10 +66,13 @@ export const UserEmail: FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleError = (error: ErrorLike): void => {
|
||||
onError(asError(error))
|
||||
setIsLoading(false)
|
||||
}
|
||||
const handleError = useCallback(
|
||||
(error: ErrorLike): void => {
|
||||
onError(asError(error))
|
||||
setIsLoading(false)
|
||||
},
|
||||
[onError, setIsLoading]
|
||||
)
|
||||
|
||||
const removeEmail = async (): Promise<void> => {
|
||||
setIsLoading(true)
|
||||
@ -106,31 +136,16 @@ export const UserEmail: FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const resendEmailVerification = async (email: string): Promise<void> => {
|
||||
const resendEmail = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
dataOrThrowErrors(
|
||||
await requestGraphQL<ResendVerificationEmailResult, ResendVerificationEmailVariables>(
|
||||
gql`
|
||||
mutation ResendVerificationEmail($user: ID!, $email: String!) {
|
||||
resendVerificationEmail(user: $user, email: $email) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ user, email }
|
||||
).toPromise()
|
||||
)
|
||||
|
||||
setIsLoading(false)
|
||||
eventLogger.log('UserEmailAddressVerificationResent')
|
||||
|
||||
onEmailResendVerification?.()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
await resendVerificationEmail(user, email, {
|
||||
onSuccess: () => {
|
||||
setIsLoading(false)
|
||||
onEmailResendVerification?.()
|
||||
},
|
||||
onError: handleError,
|
||||
})
|
||||
}, [user, email, onEmailResendVerification, handleError])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -162,7 +177,7 @@ export const UserEmail: FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
<span className={styles.dot}>• </span>
|
||||
<Button
|
||||
className="p-0"
|
||||
onClick={() => resendEmailVerification(email)}
|
||||
onClick={resendEmail}
|
||||
disabled={isLoading || disableControls}
|
||||
variant="link"
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user