Skip to content

Commit

Permalink
✨ Add Meta Pixel block
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno authored and jmgoncalves97 committed Jan 17, 2025
1 parent 170d09b commit e61e160
Show file tree
Hide file tree
Showing 39 changed files with 826 additions and 38 deletions.
6 changes: 5 additions & 1 deletion apps/builder/src/components/TableList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@chakra-ui/react'
import { TrashIcon, PlusIcon } from '@/components/icons'
import { createId } from '@paralleldrive/cuid2'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'

type ItemWithId<T> = T & { id: string }

Expand Down Expand Up @@ -40,6 +40,10 @@ export const TableList = <T,>({
const [items, setItems] = useState(initialItems)
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)

useEffect(() => {
if (items.length && initialItems.length === 0) setItems(initialItems)
}, [initialItems, items.length])

const createItem = () => {
const id = createId()
const newItem = { id, ...newItemDefaultProps } as ItemWithId<T>
Expand Down
4 changes: 3 additions & 1 deletion apps/builder/src/features/billing/billing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,10 @@ test('should display invoices', async ({ page }) => {
await page.click('text=Settings & Members')
await page.click('text=Billing & Usage')
await expect(page.locator('text="Invoices"')).toBeVisible()
await expect(page.locator('tr')).toHaveCount(2)
await expect(page.locator('tr')).toHaveCount(3)
await expect(page.locator('text="$39.00"')).toBeVisible()
await expect(page.locator('text="$34.00"')).toBeVisible()
await expect(page.locator('text="$174.00"')).toBeVisible()
})

test('custom plans should work', async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { IconProps, Icon } from '@chakra-ui/react'

export const PixelLogo = (props: IconProps) => (
<Icon viewBox="0 0 288 191" fill="none" {...props}>
<path
d="M31.06 125.96C31.06 136.94 33.47 145.37 36.62 150.47C40.75 157.15 46.91 159.98 53.19 159.98C61.29 159.98 68.7 157.97 82.98 138.22C94.42 122.39 107.9 100.17 116.97 86.24L132.33 62.64C143 46.25 155.35 28.03 169.51 15.68C181.07 5.6 193.54 0 206.09 0C227.16 0 247.23 12.21 262.59 35.11C279.4 60.19 287.56 91.78 287.56 124.38C287.56 143.76 283.74 158 277.24 169.25C270.96 180.13 258.72 191 238.13 191V159.98C255.76 159.98 260.16 143.78 260.16 125.24C260.16 98.82 254 69.5 240.43 48.55C230.8 33.69 218.32 24.61 204.59 24.61C189.74 24.61 177.79 35.81 164.36 55.78C157.22 66.39 149.89 79.32 141.66 93.91L132.6 109.96C114.4 142.23 109.79 149.58 100.69 161.71C84.74 182.95 71.12 191 53.19 191C31.92 191 18.47 181.79 10.14 167.91C3.34 156.6 0 141.76 0 124.85L31.06 125.96Z"
fill="#0081FB"
/>
<path
d="M24.4902 37.3C38.7302 15.35 59.2802 0 82.8502 0C96.5002 0 110.07 4.04 124.24 15.61C139.74 28.26 156.26 49.09 176.87 83.42L184.26 95.74C202.1 125.46 212.25 140.75 218.19 147.96C225.83 157.22 231.18 159.98 238.13 159.98C255.76 159.98 260.16 143.78 260.16 125.24L287.56 124.38C287.56 143.76 283.74 158 277.24 169.25C270.96 180.13 258.72 191 238.13 191C225.33 191 213.99 188.22 201.45 176.39C191.81 167.31 180.54 151.18 171.87 136.68L146.08 93.6C133.14 71.98 121.27 55.86 114.4 48.56C107.01 40.71 97.5102 31.23 82.3502 31.23C70.0802 31.23 59.6602 39.84 50.9402 53.01L24.4902 37.3Z"
fill="url(#paint0_linear_1302_7)"
/>
<path
d="M82.35 31.23C70.08 31.23 59.66 39.84 50.94 53.01C38.61 71.62 31.06 99.34 31.06 125.96C31.06 136.94 33.47 145.37 36.62 150.47L10.14 167.91C3.34 156.6 0 141.76 0 124.85C0 94.1 8.44 62.05 24.49 37.3C38.73 15.35 59.28 0 82.85 0L82.35 31.23Z"
fill="url(#paint1_linear_1302_7)"
/>
<defs>
<linearGradient
id="paint0_linear_1302_7"
x1="61.0002"
y1="117"
x2="259"
y2="127"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0064E1" />
<stop offset="0.4" stop-color="#0064E1" />
<stop offset="0.83" stop-color="#0073EE" />
<stop offset="1" stop-color="#0082FB" />
</linearGradient>
<linearGradient
id="paint1_linear_1302_7"
x1="45"
y1="139"
x2="45"
y2="66"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0082FB" />
<stop offset="1" stop-color="#0064E0" />
</linearGradient>
</defs>
</Icon>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { PixelBlock } from '@typebot.io/schemas'

type Props = {
options: PixelBlock['options']
}

export const PixelNodeBody = ({ options }: Props) => (
<Text
color={options.eventType || options.pixelId ? 'currentcolor' : 'gray.500'}
noOfLines={1}
>
{options.eventType
? `Track "${options.eventType}"`
: options.pixelId
? 'Init Pixel'
: 'Configure...'}
</Text>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { DropdownList } from '@/components/DropdownList'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { TableList, TableListItemProps } from '@/components/TableList'
import { TextLink } from '@/components/TextLink'
import { TextInput } from '@/components/inputs'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { Select } from '@/components/inputs/Select'
import { Stack, Text } from '@chakra-ui/react'
import { isDefined, isEmpty } from '@typebot.io/lib'
import {
PixelBlock,
pixelEventTypes,
pixelObjectProperties,
} from '@typebot.io/schemas'
import React, { useMemo } from 'react'

const pixelReferenceUrl =
'https://developers.facebook.com/docs/meta-pixel/reference#standard-events'

type Props = {
options?: PixelBlock['options']
onOptionsChange: (options: PixelBlock['options']) => void
}

type Item = NonNullable<PixelBlock['options']['params']>[number]

export const PixelSettings = ({ options, onOptionsChange }: Props) => {
const updatePixelId = (pixelId: string) =>
onOptionsChange({
...options,
pixelId: isEmpty(pixelId) ? undefined : pixelId,
})

const updateIsTrackingEventEnabled = (isChecked: boolean) =>
onOptionsChange({
...options,
params: isChecked && !options?.params ? [] : undefined,
})

const updateEventType = (
_: string | undefined,
eventType?: (typeof pixelEventTypes)[number] | 'Custom'
) =>
onOptionsChange({
...options,
params: [],
eventType,
})

const updateParams = (params: PixelBlock['options']['params']) =>
onOptionsChange({
...options,
params,
})

const updateEventName = (name: string) => {
if (options?.eventType !== 'Custom') return
onOptionsChange({
...options,
name: isEmpty(name) ? undefined : name,
})
}

const Item = useMemo(
() =>
function Component(props: TableListItemProps<Item>) {
return <ParamItem {...props} eventType={options?.eventType} />
},
[options?.eventType]
)

return (
<Stack spacing={4}>
<TextInput
defaultValue={options?.pixelId ?? ''}
onChange={updatePixelId}
withVariableButton={false}
placeholder='Pixel ID (e.g. "123456789")'
/>
<SwitchWithRelatedSettings
label={'Track event'}
initialValue={isDefined(options?.params)}
onCheckChange={updateIsTrackingEventEnabled}
>
<Text fontSize="sm" color="gray.500">
Read the{' '}
<TextLink href={pixelReferenceUrl} isExternal>
reference
</TextLink>{' '}
to better understand the available options.
</Text>
<Select
items={['Custom', ...pixelEventTypes] as const}
selectedItem={options?.eventType}
placeholder="Select event type"
onSelect={updateEventType}
/>
{options?.eventType === 'Custom' && (
<TextInput
defaultValue={options.name ?? ''}
onChange={updateEventName}
placeholder="Event name"
/>
)}
{options?.eventType &&
(options.eventType === 'Custom' ||
pixelObjectProperties.filter((prop) =>
prop.associatedEvents.includes(options.eventType)
).length > 0) && (
<TableList
initialItems={options?.params ?? []}
Item={Item}
onItemsChange={updateParams}
addLabel="Add parameter"
/>
)}
</SwitchWithRelatedSettings>
</Stack>
)
}

type ParamItemProps = {
item: Item
eventType: 'Custom' | (typeof pixelEventTypes)[number] | undefined
onItemChange: (item: Item) => void
}

const ParamItem = ({ item, eventType, onItemChange }: ParamItemProps) => {
const possibleObjectProps =
eventType && eventType !== 'Custom'
? pixelObjectProperties.filter((prop) =>
prop.associatedEvents.includes(eventType)
)
: []

const currentObject = possibleObjectProps.find(
(prop) => prop.key === item.key
)

const updateKey = (key: string) =>
onItemChange({
...item,
key,
})

const updateValue = (value: string) =>
onItemChange({
...item,
value,
})

if (!eventType) return null

return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
{eventType === 'Custom' ? (
<TextInput
defaultValue={item.key}
onChange={updateKey}
placeholder="Key"
/>
) : (
<DropdownList
currentItem={item.key}
items={possibleObjectProps.map((prop) => prop.key)}
onItemSelect={updateKey}
placeholder="Select key"
/>
)}
{currentObject?.type === 'code' ? (
<CodeEditor
lang={'javascript'}
defaultValue={item.value}
onChange={updateValue}
/>
) : (
<TextInput
defaultValue={item.value}
onChange={updateValue}
placeholder="Value"
/>
)}
</Stack>
)
}
37 changes: 37 additions & 0 deletions apps/builder/src/features/blocks/integrations/pixel/pixel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { IntegrationBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'

test.describe('Pixel block', () => {
test('its configuration should work', async ({ page }) => {
const typebotId = createId()
await createTypebots([
{
id: typebotId,
...parseDefaultGroupWithBlock({
type: IntegrationBlockType.PIXEL,
options: {},
}),
},
])

await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.getByPlaceholder('Pixel ID (e.g. "123456789")').fill('pixelid')
await expect(page.getByText('Init Pixel')).toBeVisible()
await page.getByText('Track event').click()
await page.getByPlaceholder('Select event type').click()
await page.getByRole('menuitem', { name: 'Lead' }).click()
await expect(page.getByText('Track "Lead"')).toBeVisible()
await page.getByRole('button', { name: 'Add parameter' }).click()
await page.getByRole('button', { name: 'Select key' }).click()
await page.getByRole('menuitem', { name: 'currency' }).click()
await page.getByPlaceholder('Value').fill('USD')
await page.getByRole('button', { name: 'Preview' }).click()
await expect(
page.getByText('Pixel is not enabled in Preview mode').nth(1)
).toBeVisible()
})
})
3 changes: 3 additions & 0 deletions apps/builder/src/features/editor/components/BlockIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'
import { PictureChoiceIcon } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon'
import { PixelLogo } from '@/features/blocks/integrations/pixel/components/PixelLogo'

type BlockIconProps = { type: BlockType } & IconProps

Expand Down Expand Up @@ -115,6 +116,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
return <ChatwootLogo {...props} />
case IntegrationBlockType.OPEN_AI:
return <OpenAILogo fill={openAIColor} {...props} />
case IntegrationBlockType.PIXEL:
return <PixelLogo {...props} />
case 'start':
return <FlagIcon {...props} />
}
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/src/features/editor/components/BlockLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,7 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
return <Text fontSize="sm">Chatwoot</Text>
case IntegrationBlockType.OPEN_AI:
return <Text fontSize="sm">OpenAI</Text>
case IntegrationBlockType.PIXEL:
return <Text fontSize="sm">Pixel</Text>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAn
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
import { PictureChoiceNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceNode'
import { PixelNodeBody } from '@/features/blocks/integrations/pixel/components/PixelNodeBody'

type Props = {
block: Block | StartBlock
Expand Down Expand Up @@ -194,6 +195,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
/>
)
}
case IntegrationBlockType.PIXEL: {
return <PixelNodeBody options={block.options} />
}
case 'start': {
return <Text>Start</Text>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/compon
import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTestSettings'
import { PictureChoiceSettings } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceSettings'
import { SettingsHoverBar } from './SettingsHoverBar'
import { PixelSettings } from '@/features/blocks/integrations/pixel/components/PixelSettings'

type Props = {
block: BlockWithOptions
Expand Down Expand Up @@ -311,5 +312,13 @@ export const BlockSettings = ({
/>
)
}
case IntegrationBlockType.PIXEL: {
return (
<PixelSettings
options={block.options}
onOptionsChange={updateOptions}
/>
)
}
}
}
2 changes: 2 additions & 0 deletions apps/builder/src/features/graph/helpers/getHelpDocUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,7 @@ export const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
return 'https://docs.typebot.io/editor/blocks/logic/abTest'
case LogicBlockType.JUMP:
return 'https://docs.typebot.io/editor/blocks/logic/jump'
case IntegrationBlockType.PIXEL:
return 'https://docs.typebot.io/editor/blocks/integrations/pixel'
}
}
Loading

0 comments on commit e61e160

Please sign in to comment.