Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ export const ReadyWithSuggestions: Story = {
},
}

export const VerifyingWithSuggestions: Story = {
args: {
host: 'cloudflare',
seed: (backend) => {
backend.publishRecords(DOMAIN, halfPublished)
backend.suggestWorkflows(SUGGESTED_WORKFLOWS)
},
aiApproved: true,
},
}

export const Unavailable: Story = { render: () => <UnavailableDomainPage /> }

export const AgentHandoff: Story = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ describe('EmailDomainScene', () => {
expectProgress('8 of 8 found')
expect(screen.getByText('Turn on your workflows now')).toBeInTheDocument()
expect(element('email-domain-verifying-go-to-workflows')).toBeInTheDocument()
expect(element('email-domain-verifying-create-welcome-workflow')).toBeInTheDocument()
expect(element('email-domain-verifying-create-blank-workflow')).toBeInTheDocument()
expect(screen.queryByText('Proves you own the domain')).not.toBeInTheDocument()
click('email-domain-dns-records')
await flush()
expect(screen.getByText('Proves you own the domain')).toBeInTheDocument()

backend.verifyDomain(DOMAIN)
await flush(POLL_MS)
Expand Down Expand Up @@ -224,7 +230,7 @@ describe('EmailDomainScene', () => {
expectStep(/You can send from/)
})

it('replaces the template with workflows suggested from the data, loaded while DNS was checked', async () => {
it('replaces the template with workflows suggested from the data, from the wait for DNS to the last step', async () => {
const capture = jest.spyOn(posthog, 'capture')
const shownEvents = (): unknown[][] =>
capture.mock.calls.filter(([event]) => event === 'workflows data suggestions shown')
Expand All @@ -237,21 +243,33 @@ describe('EmailDomainScene', () => {
])
await open(urls.workflowsEmailDomain(SENDER_ID))
expectStep('Checking your settings…')
expect(screen.queryByText('Welcome new teammates')).not.toBeInTheDocument()
expect(shownEvents()).toHaveLength(0)
expect(screen.getByText('Welcome new teammates')).toBeInTheDocument()
expect(document.querySelector('[data-attr="email-domain-verifying-create-welcome-workflow"]')).toBeNull()
expect(element('email-domain-verifying-skip-suggestions')).toBeInTheDocument()
expect(element('email-domain-verifying-suggested-workflow-choices')).toContainElement(
element('workflows-data-suggestion-ai')
)
expect(shownEvents()).toHaveLength(1)

backend.verifyDomain(DOMAIN)
await flush(POLL_MS)
expectStep(/You can send from/)
expect(screen.getByText('Welcome new teammates')).toBeInTheDocument()
expect(element('workflows-data-suggestion-ai')).toHaveTextContent('Create workflow')
expect(element('email-domain-suggested-workflow-choices')).toContainElement(
element('workflows-data-suggestion-ai')
)
expect(document.querySelector('[data-attr="email-domain-create-welcome-workflow"]')).toBeNull()
expect(document.querySelector('[data-attr="email-domain-create-blank-workflow"]')).toBeNull()
expect(backend.requestsTo('data_suggestions_generate')).toHaveLength(1)
expect(shownEvents()).toEqual([
[
'workflows data suggestions shown',
expect.objectContaining({ surface: 'email_domain_wizard', stages: ['onboarding'] }),
expect.objectContaining({ surface: 'email_domain_wizard', placement: 'verifying' }),
],
[
'workflows data suggestions shown',
expect.objectContaining({ surface: 'email_domain_wizard', placement: 'ready' }),
],
])

Expand Down Expand Up @@ -409,6 +427,38 @@ describe('EmailDomainScene', () => {
}
)

it('opens the DNS records by itself once automatic checks stop with a setting missing', async () => {
seedSender()
backend.publishRecords(DOMAIN, (record) => record.kind !== 'verification')
await open(urls.workflowsEmailDomain(SENDER_ID))
expect(screen.queryByText('Proves you own the domain')).not.toBeInTheDocument()

await flush(31 * 60_000)
expect(screen.getByText('One setting is still missing')).toBeInTheDocument()
expect(screen.getByText('Proves you own the domain')).toBeInTheDocument()
})

it.each<[string, (fake: FakeEmailDomainBackend) => void]>([
['verifying', (fake) => fake.publishRecords(DOMAIN)],
['ready', (fake) => fake.verifyDomain(DOMAIN)],
])('reports a suggestion clicked on the %s step with its placement', async (placement, reachStep) => {
const capture = jest.spyOn(posthog, 'capture')
turnOnSuggestions(true)
seedSender()
reachStep(backend)
backend.suggestWorkflows([SUGGESTED_WORKFLOW])
await open(urls.workflowsEmailDomain(SENDER_ID))

click('workflows-data-suggestion-ai')

expect(capture.mock.calls.filter(([event]) => event === 'workflows data suggestion clicked')).toEqual([
[
'workflows data suggestion clicked',
expect.objectContaining({ surface: 'email_domain_wizard', placement }),
],
])
})

it('falls back to the generic manual setup when host detection fails', async () => {
seedSender()
backend.fail('domain_connect_check', { status: 500, body: { detail: 'DNS lookup timed out' } })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LemonSkeleton } from '@posthog/lemon-ui'
import { urls } from 'scenes/urls'

import { AgentHandoffModal } from './components/AgentHandoffModal'
import { SendOnceVerifiedCard } from './components/SendOnceVerifiedCard'
import { SendOnceVerifiedChoices } from './components/SendOnceVerifiedChoices'
import { StepIndicator } from './components/StepIndicator'
import { emailDomainLogic } from './emailDomainLogic'
import { emailDomainStatusLogic } from './emailDomainStatusLogic'
Expand Down Expand Up @@ -43,7 +43,7 @@ function CurrentStep(): JSX.Element {
return (
<>
<VerifyingStep />
{status?.status !== 'failed' && <SendOnceVerifiedCard />}
{status?.status !== 'failed' && <SendOnceVerifiedChoices />}
</>
)
case 'ready':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useValues } from 'kea'

import { IconCheck, IconWarning } from '@posthog/icons'
import { LemonCollapse, Spinner } from '@posthog/lemon-ui'

import type { EmailDomainStatusRecordApi } from 'products/integrations/frontend/generated/api.schemas'

import { emailDomainAutoConfigureLogic } from '../emailDomainAutoConfigureLogic'
import { emailDomainStatusLogic } from '../emailDomainStatusLogic'
import { DnsRecordRows } from './DnsRecordRows'

interface DnsRecordsSummaryProps {
/** The records to highlight as missing. */
missing: EmailDomainStatusRecordApi[]
/** Opens the records, because the person has to fix some of them. */
needsAttention: boolean
}

function SummaryIcon({ needsAttention }: { needsAttention: boolean }): JSX.Element {
const { allRecordsFound } = useValues(emailDomainStatusLogic)
if (allRecordsFound) {
return <IconCheck className="text-success" />
}
return needsAttention ? <IconWarning className="text-warning" /> : <Spinner textColored />
}

/** The DNS records behind one summary line, which a person expands to copy a record. */
export function DnsRecordsSummary({ missing, needsAttention }: DnsRecordsSummaryProps): JSX.Element {
const { records, foundCount, allRecordsFound } = useValues(emailDomainStatusLogic)
const { hostName } = useValues(emailDomainAutoConfigureLogic)
const host = hostName ?? 'your DNS host'
return (
<LemonCollapse
// Remounts when the records start to need attention, so they open for the person to fix.
key={needsAttention ? 'needs-attention' : 'in-progress'}
defaultActiveKey={needsAttention ? 'records' : undefined}
panels={[
{
key: 'records',
dataAttr: 'email-domain-dns-records',
header: (
<span className="flex items-center gap-2">
<SummaryIcon needsAttention={needsAttention} />
<span>
{allRecordsFound
? `${records.length} settings in place at ${host}`
: `${foundCount} of ${records.length} settings in place at ${host}`}
</span>
</span>
),
content: <DnsRecordRows records={records} missing={missing} showStatus />,
},
]}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LemonButton, LemonTag, Tooltip } from '@posthog/lemon-ui'
import { SuggestionCard } from '../../../setupGuide/suggestions/suggestionCards'
import { WorkflowDataSuggestions } from '../../../setupGuide/suggestions/WorkflowDataSuggestions'
import {
WorkflowDataSuggestionsPlacement,
WorkflowDataSuggestionsSurface,
workflowDataSuggestionsLogic,
} from '../../../setupGuide/suggestions/workflowDataSuggestionsLogic'
Expand All @@ -17,27 +18,45 @@ import { CreateWelcomeWorkflowAction } from './CreateWelcomeWorkflowAction'

const SURFACE: WorkflowDataSuggestionsSurface = 'email_domain_wizard'

/** The ways to start a first workflow from the domain wizard: the best suggestion once loaded, else the welcome template and a blank workflow. */
export function FirstWorkflowChoices(): JSX.Element {
// pinned: data-attr prefixes, autocapture dashboards read them
const DATA_ATTR_PREFIX: Record<WorkflowDataSuggestionsPlacement, string> = {
ready: 'email-domain',
verifying: 'email-domain-verifying',
}

/**
* The ways to start a first workflow from the domain wizard: the best suggestion once loaded, else the welcome
* template and a blank workflow. Each placement, the wait for DNS or the last step, reports its own data-attrs.
*/
export function FirstWorkflowChoices({
placement = 'ready',
}: {
placement?: WorkflowDataSuggestionsPlacement
}): JSX.Element {
const { offersDataSuggestions } = useValues(emailDomainLogic)

// The suggestions logic loads on mount, so it only mounts when the organization allows AI processing.
return offersDataSuggestions ? <ChoicesWithSuggestions /> : <TemplateChoices />
return offersDataSuggestions ? (
<ChoicesWithSuggestions placement={placement} />
) : (
<TemplateChoices prefix={DATA_ATTR_PREFIX[placement]} />
)
}

function ChoicesWithSuggestions(): JSX.Element {
function ChoicesWithSuggestions({ placement }: { placement: WorkflowDataSuggestionsPlacement }): JSX.Element {
const { cards, generating } = useValues(workflowDataSuggestionsLogic({ surface: SURFACE }))
const prefix = DATA_ATTR_PREFIX[placement]
const [skipped, setSkipped] = useState(false)

if (skipped) {
return (
<TemplateChoices>
<TemplateChoices prefix={prefix}>
<LemonButton
type="tertiary"
size="small"
className="self-center"
onClick={() => setSkipped(false)}
data-attr="email-domain-back-to-suggestions"
data-attr={`${prefix}-back-to-suggestions`}
>
Back to the suggestions
</LemonButton>
Expand All @@ -47,15 +66,24 @@ function ChoicesWithSuggestions(): JSX.Element {
// Until suggestions load, and when none come back, the welcome template stays the first choice.
if (generating || cards.length === 0) {
return (
<TemplateChoices>
<TemplateChoices prefix={prefix}>
<WorkflowDataSuggestions surface={SURFACE} />
</TemplateChoices>
)
}
return <SuggestedChoices cards={cards} onSkip={() => setSkipped(true)} />
return <SuggestedChoices placement={placement} cards={cards} onSkip={() => setSkipped(true)} />
}

function SuggestedChoices({ cards, onSkip }: { cards: SuggestionCard[]; onSkip: () => void }): JSX.Element {
function SuggestedChoices({
placement,
cards,
onSkip,
}: {
placement: WorkflowDataSuggestionsPlacement
cards: SuggestionCard[]
onSkip: () => void
}): JSX.Element {
const prefix = DATA_ATTR_PREFIX[placement]
const logic = workflowDataSuggestionsLogic({ surface: SURFACE })
const { buildingKey } = useValues(logic)
const { reportCardsShown } = useActions(logic)
Expand All @@ -64,55 +92,66 @@ function SuggestedChoices({ cards, onSkip }: { cards: SuggestionCard[]; onSkip:

// Only the cards on screen count as shown, so expanding reports the rest.
useEffect(() => {
reportCardsShown((expanded ? cards : [best]).map((card) => card.suggestionId))
}, [cards, best, expanded, reportCardsShown])
reportCardsShown(
(expanded ? cards : [best]).map((card) => card.suggestionId),
placement
)
}, [cards, best, expanded, placement, reportCardsShown])

return (
<div className="flex flex-col gap-3" data-attr="workflows-data-suggestions">
<span className="inline-flex items-center gap-1 self-center text-xs text-tertiary">
<IconSparkles className="size-3" />
Suggested from your project's events
<Tooltip title="AI writes these suggestions from your project's events. Check each draft before you turn it on.">
<LemonTag type="completion" size="small">
Beta
</LemonTag>
</Tooltip>
</span>
<WorkflowSuggestionCard card={best} surface={SURFACE} primary />
{expanded &&
others.map((card) => (
<WorkflowSuggestionCard key={card.key} card={card} surface={SURFACE} primary={false} />
))}
<div className="flex flex-wrap justify-center gap-x-2">
{others.length > 0 && !expanded && (
<div data-attr={`${prefix}-suggested-workflow-choices`}>
<div className="flex flex-col gap-3" data-attr="workflows-data-suggestions">
<span className="inline-flex items-center gap-1 self-center text-xs text-tertiary">
<IconSparkles className="size-3" />
Suggested from your project's events
<Tooltip title="AI writes these suggestions from your project's events. Check each draft before you turn it on.">
<LemonTag type="completion" size="small">
Beta
</LemonTag>
</Tooltip>
</span>
<WorkflowSuggestionCard card={best} surface={SURFACE} placement={placement} primary />
{expanded &&
others.map((card) => (
<WorkflowSuggestionCard
key={card.key}
card={card}
surface={SURFACE}
placement={placement}
primary={false}
/>
))}
<div className="flex flex-wrap justify-center gap-x-2">
{others.length > 0 && !expanded && (
<LemonButton
type="tertiary"
size="small"
onClick={() => setExpanded(true)}
data-attr={`${prefix}-show-more-suggestions`}
>
Show {others.length} more {others.length === 1 ? 'suggestion' : 'suggestions'}
</LemonButton>
)}
<LemonButton
type="tertiary"
size="small"
onClick={() => setExpanded(true)}
data-attr="email-domain-show-more-suggestions"
onClick={onSkip}
disabledReason={buildingKey ? 'Wait until the suggested workflow is built' : undefined}
data-attr={`${prefix}-skip-suggestions`}
>
Show {others.length} more {others.length === 1 ? 'suggestion' : 'suggestions'}
Start without a suggestion
</LemonButton>
)}
<LemonButton
type="tertiary"
size="small"
onClick={onSkip}
disabledReason={buildingKey ? 'Wait until the suggested workflow is built' : undefined}
data-attr="email-domain-skip-suggestions"
>
Start without a suggestion
</LemonButton>
</div>
</div>
</div>
)
}

function TemplateChoices({ children }: { children?: ReactNode }): JSX.Element {
function TemplateChoices({ prefix, children }: { prefix: string; children?: ReactNode }): JSX.Element {
return (
<div className="flex flex-col gap-5">
<CreateWelcomeWorkflowAction data-attr="email-domain-create-welcome-workflow" />
<WorkflowTemplateBlankCard data-attr="email-domain-create-blank-workflow" />
<div className="flex flex-col gap-5" data-attr={`${prefix}-workflow-choices`}>
<CreateWelcomeWorkflowAction data-attr={`${prefix}-create-welcome-workflow`} />
<WorkflowTemplateBlankCard data-attr={`${prefix}-create-blank-workflow`} />
{children}
</div>
)
Expand Down
Loading
Loading