@ -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 }
|
||||
|
||||
@ -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>
|
||||
|
@ -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 }) => {
|
||||
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
@ -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()
|
||||
})
|
||||
})
|
@ -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
|
||||
|
||||
@ -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} />
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -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>
|
||||
}
|
||||
|
@ -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
|
||||
@ -311,5 +312,13 @@ export const BlockSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.PIXEL: {
|
||||
return (
|
||||
<PixelSettings
|
||||
options={block.options}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ export const FlutterFlowLogo = (props: IconProps) => (
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M60.6347 0C62.6766 0 64.5668 1.10496 65.5244 2.90118C66.4419 4.6217 66.403 6.64918 65.4227 8.33205L65.3893 8.38864L56.7972 22.7617C55.8256 24.387 54.0715 25.4014 52.1968 25.423L52.1356 25.4234L42.3241 25.4233L46.4021 34.7593L46.4183 34.7867L46.4434 34.8308C47.4038 36.5666 47.3804 38.6304 46.3844 40.3403L46.3509 40.3972L37.7588 54.7702C36.7872 56.3955 35.0332 57.4099 33.1585 57.4315L33.0973 57.4319L21.0549 57.4318L12.3978 66.5627L12.3814 66.5797C11.5668 67.4078 10.4661 67.8698 9.31846 67.8698C9.03158 67.8698 8.74452 67.8408 8.46053 67.7829C7.06066 67.4971 5.89759 66.5365 5.33774 65.2117L5.32195 65.1739L0.766133 54.7156L0.742168 54.6755L0.717276 54.6324C0.713141 54.625 0.708936 54.6175 0.704544 54.6095C-0.255836 52.8739 -0.232574 50.8101 0.763263 49.1001L0.796818 49.0432L9.13021 35.1029L2.98975 21.0452L3.10227 20.9879L3.10057 20.9794C2.84075 19.6863 3.0499 18.3378 3.71716 17.1567L3.75401 17.0925L3.78807 17.0347L12.3802 2.66159C13.3519 1.03641 15.106 0.0219807 16.9806 0H17.0418H60.6347ZM15.5195 57.3226L6.39039 57.3224L9.0717 63.525L9.07829 63.5415C9.12274 63.6526 9.19463 63.7134 9.30164 63.7354C9.39709 63.7551 9.47432 63.7348 9.54736 63.6682L9.56013 63.6558L15.5195 57.3226ZM41.6168 35.9985H14.0216C13.9912 35.9985 13.9607 35.9995 13.9303 36.0016L13.9219 36.0022L21.4883 53.3099H33.0959C33.5731 53.3099 34.0327 53.0544 34.2941 52.6441L34.3148 52.6105L42.9193 38.2287C43.176 37.7997 43.2044 37.3019 43.001 36.854C42.7665 36.3375 42.2221 35.9985 41.6168 35.9985ZM11.1531 39.5525L4.32399 51.0629C4.05225 51.5211 4.0365 52.0576 4.27984 52.5298L4.30565 52.5778L4.3194 52.6013L4.34697 52.6464L4.40158 52.7276L4.46255 52.8069L4.52088 52.8735L4.531 52.8843L4.57045 52.9243L4.60472 52.9569C4.81954 53.1519 5.08509 53.2709 5.37798 53.3015L5.44378 53.3071L5.48188 53.3091L5.53015 53.3099H17.1171L11.1531 39.5525ZM9.35736 25.4512L12.3566 32.3299L12.4151 32.3099C12.933 32.1366 13.4746 32.0427 14.022 32.0341L14.1042 32.0335L40.8529 32.0334L37.9829 25.4513L9.35736 25.4512ZM60.5585 4.01257H32.8649L40.4335 21.3239H52.037C52.515 21.3239 52.975 21.0683 53.2365 20.6578L53.2573 20.6242L61.8623 6.24261C62.1189 5.81371 62.1473 5.31633 61.9441 4.8688C61.7144 4.36289 61.1876 4.02709 60.5971 4.01303L60.5585 4.01257ZM28.6254 4.01257H17.0861C16.6116 4.01257 16.1539 4.26756 15.8936 4.67744L15.8729 4.71101L7.29049 19.0929C7.03426 19.5222 7.00595 20.0207 7.20912 20.4692C7.43797 20.9746 7.96188 21.3095 8.54887 21.3235L8.58723 21.3239H36.1742L28.6254 4.01257Z"
|
||||
fill="#4B39EF"
|
||||
/>
|
||||
|
@ -27,6 +27,6 @@ test.describe.parallel('Templates page', () => {
|
||||
await page.click('text=Customer Support')
|
||||
await expect(page.locator('text=How can I help you?')).toBeVisible()
|
||||
await page.click('text=Use this template')
|
||||
await expect(page).toHaveURL(new RegExp(`/edit`))
|
||||
await expect(page).toHaveURL(new RegExp(`/edit`), { timeout: 10000 })
|
||||
})
|
||||
})
|
||||
|
@ -141,6 +141,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
||||
return defaultChatwootOptions
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return {}
|
||||
case IntegrationBlockType.PIXEL:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,15 @@ The Google Analytics integration block allows you to track a Google Analytics ev
|
||||
alt="Google Analytics block"
|
||||
/>
|
||||
|
||||
:::note
|
||||
This block is not executed in Preview mode. To test it, you need to launch the published bot.
|
||||
:::
|
||||
|
||||
When your flow contains a Google Analytics block, under the hood it:
|
||||
|
||||
- Initialize GA and track a "Page view" event on page load.
|
||||
- Track the event if any when the block is executed.
|
||||
|
||||
## Track conversions with Google Ads
|
||||
|
||||
To track conversions for your Google Ad, you can add a Google Analytics block whenever you'd like to trigger the conversion event with the following properties:
|
||||
@ -15,3 +24,7 @@ To track conversions for your Google Ad, you can add a Google Analytics block wh
|
||||
- Event action: conversion
|
||||
- Send to: <YOUR_AW_ID>
|
||||
- Value (optional): a number to quantify the conversion
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
To help you debug how your Google Analytics behaves, I suggest you add the [Google Analytics Debugger](https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna) extension to your browser.
|
||||
|
22
apps/docs/docs/editor/blocks/integrations/pixel.md
Normal file
22
apps/docs/docs/editor/blocks/integrations/pixel.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Meta pixel
|
||||
|
||||
The Pixel integration block allows you to add a Meta pixel to your bot and track specific events.
|
||||
|
||||
<img
|
||||
src="/img/blocks/integrations/pixel.png"
|
||||
width="600"
|
||||
alt="Pixel block"
|
||||
/>
|
||||
|
||||
:::note
|
||||
This block is not executed in Preview mode. To test it, you need to launch the published bot.
|
||||
:::
|
||||
|
||||
When your flow contains a pixel block, under the hood it:
|
||||
|
||||
- Initialize the pixel and track "PageView" event on page load.
|
||||
- Track the event if any when the block is executed.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
To help you debug how your pixel behaves, I suggest you add the [Facebook Pixel Helper](https://chrome.google.com/webstore/detail/facebook-pixel-helper/fdgfkebogiimcoedlicjlajpkdmockpc) extension to your browser.
|
BIN
apps/docs/static/img/blocks/integrations/pixel.png
vendored
Normal file
BIN
apps/docs/static/img/blocks/integrations/pixel.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 125 KiB |
@ -1,23 +1,21 @@
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
|
||||
|
||||
export const executeGoogleAnalyticsBlock = (
|
||||
{ typebot: { variables } }: SessionState,
|
||||
{ typebot: { variables }, result }: SessionState,
|
||||
block: GoogleAnalyticsBlock
|
||||
): ExecuteIntegrationResponse => {
|
||||
const googleAnalytics = deepParseVariables(variables)(block.options)
|
||||
if (!result) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const googleAnalytics = deepParseVariables(variables, {
|
||||
guessCorrectTypes: true,
|
||||
removeEmptyStrings: true,
|
||||
})(block.options)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: [
|
||||
{
|
||||
googleAnalytics: {
|
||||
...googleAnalytics,
|
||||
value: isNotEmpty(googleAnalytics.value as string)
|
||||
? Number(googleAnalytics.value)
|
||||
: undefined,
|
||||
},
|
||||
googleAnalytics,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||
import { PixelBlock, SessionState } from '@typebot.io/schemas'
|
||||
|
||||
export const executePixelBlock = (
|
||||
{ typebot: { variables }, result }: SessionState,
|
||||
block: PixelBlock
|
||||
): ExecuteIntegrationResponse => {
|
||||
if (!result) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const pixel = deepParseVariables(variables, {
|
||||
guessCorrectTypes: true,
|
||||
removeEmptyStrings: true,
|
||||
})(block.options)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: [
|
||||
{
|
||||
pixel: {
|
||||
...pixel,
|
||||
pixelId: block.options.pixelId,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
@ -6,6 +6,9 @@ import {
|
||||
ChatReply,
|
||||
chatReplySchema,
|
||||
ChatSession,
|
||||
GoogleAnalyticsBlock,
|
||||
IntegrationBlockType,
|
||||
PixelBlock,
|
||||
ResultInSession,
|
||||
sendMessageInputSchema,
|
||||
SessionState,
|
||||
@ -180,10 +183,40 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
|
||||
'setVariable' in action || 'streamOpenAiChatCompletion' in action
|
||||
)
|
||||
|
||||
const startClientSideAction = clientSideActions ?? []
|
||||
|
||||
const parsedStartPropsActions = parseStartClientSideAction(typebot)
|
||||
|
||||
const startLogs = logs ?? []
|
||||
|
||||
if (isDefined(parsedStartPropsActions)) {
|
||||
if (!result) {
|
||||
if ('startPropsToInject' in parsedStartPropsActions) {
|
||||
const { customHeadCode, googleAnalyticsId, pixelId, gtmId } =
|
||||
parsedStartPropsActions.startPropsToInject
|
||||
let toolsList = ''
|
||||
if (customHeadCode) toolsList += 'Custom head code, '
|
||||
if (googleAnalyticsId) toolsList += 'Google Analytics, '
|
||||
if (pixelId) toolsList += 'Pixel, '
|
||||
if (gtmId) toolsList += 'Google Tag Manager, '
|
||||
toolsList = toolsList.slice(0, -2)
|
||||
startLogs.push({
|
||||
description: `${toolsList} ${
|
||||
toolsList.includes(',') ? 'are not' : 'is not'
|
||||
} enabled in Preview mode`,
|
||||
status: 'info',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
startClientSideAction.push(parsedStartPropsActions)
|
||||
}
|
||||
}
|
||||
|
||||
if (!input && !clientSideActionsNeedSessionId)
|
||||
return {
|
||||
messages,
|
||||
clientSideActions,
|
||||
clientSideActions:
|
||||
startClientSideAction.length > 0 ? startClientSideAction : undefined,
|
||||
typebot: {
|
||||
id: typebot.id,
|
||||
settings: deepParseVariables(newSessionState.typebot.variables)(
|
||||
@ -194,7 +227,7 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
|
||||
),
|
||||
},
|
||||
dynamicTheme: parseDynamicThemeReply(newSessionState),
|
||||
logs,
|
||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||
}
|
||||
|
||||
const session = (await prisma.chatSession.create({
|
||||
@ -217,9 +250,10 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
|
||||
},
|
||||
messages,
|
||||
input,
|
||||
clientSideActions,
|
||||
clientSideActions:
|
||||
startClientSideAction.length > 0 ? startClientSideAction : undefined,
|
||||
dynamicTheme: parseDynamicThemeReply(newSessionState),
|
||||
logs,
|
||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||
} satisfies ChatReply
|
||||
}
|
||||
|
||||
@ -403,3 +437,38 @@ const parseDynamicThemeReply = (
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const parseStartClientSideAction = (
|
||||
typebot: StartTypebot
|
||||
): NonNullable<ChatReply['clientSideActions']>[number] | undefined => {
|
||||
const blocks = typebot.groups.flatMap((group) => group.blocks)
|
||||
const startPropsToInject = {
|
||||
customHeadCode: typebot.settings.metadata.customHeadCode,
|
||||
gtmId: typebot.settings.metadata.googleTagManagerId,
|
||||
googleAnalyticsId: (
|
||||
blocks.find(
|
||||
(block) =>
|
||||
block.type === IntegrationBlockType.GOOGLE_ANALYTICS &&
|
||||
block.options.trackingId
|
||||
) as GoogleAnalyticsBlock | undefined
|
||||
)?.options.trackingId,
|
||||
pixelId: (
|
||||
blocks.find(
|
||||
(block) =>
|
||||
block.type === IntegrationBlockType.PIXEL && block.options.pixelId
|
||||
) as PixelBlock | undefined
|
||||
)?.options.pixelId,
|
||||
}
|
||||
|
||||
if (
|
||||
!startPropsToInject.customHeadCode &&
|
||||
!startPropsToInject.gtmId &&
|
||||
!startPropsToInject.googleAnalyticsId &&
|
||||
!startPropsToInject.pixelId
|
||||
)
|
||||
return
|
||||
|
||||
return {
|
||||
startPropsToInject,
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +165,11 @@ const parseBubbleBlock =
|
||||
(block: BubbleBlock): ChatReply['messages'][0] => {
|
||||
switch (block.type) {
|
||||
case BubbleBlockType.TEXT:
|
||||
return deepParseVariables(variables, { takeLatestIfList: true })(block)
|
||||
return deepParseVariables(
|
||||
variables,
|
||||
{},
|
||||
{ takeLatestIfList: true }
|
||||
)(block)
|
||||
case BubbleBlockType.EMBED: {
|
||||
const message = deepParseVariables(variables)(block)
|
||||
return {
|
||||
|
@ -4,6 +4,7 @@ import { executeWebhookBlock } from '@/features/blocks/integrations/webhook/exec
|
||||
import { executeChatwootBlock } from '@/features/blocks/integrations/chatwoot/executeChatwootBlock'
|
||||
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock'
|
||||
import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSheets/executeGoogleSheetBlock'
|
||||
import { executePixelBlock } from '@/features/blocks/integrations/pixel/executePixelBlock'
|
||||
import {
|
||||
IntegrationBlock,
|
||||
IntegrationBlockType,
|
||||
@ -30,5 +31,7 @@ export const executeIntegration =
|
||||
return executeWebhookBlock(state, block)
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return executeOpenAIBlock(state, block)
|
||||
case IntegrationBlockType.PIXEL:
|
||||
return executePixelBlock(state, block)
|
||||
}
|
||||
}
|
||||
|
@ -4,21 +4,38 @@ import {
|
||||
parseVariables,
|
||||
ParseVariablesOptions,
|
||||
} from './parseVariables'
|
||||
import { parseGuessedTypeFromString } from './parseGuessedTypeFromString'
|
||||
|
||||
type DeepParseOptions = {
|
||||
guessCorrectTypes?: boolean
|
||||
removeEmptyStrings?: boolean
|
||||
}
|
||||
|
||||
export const deepParseVariables =
|
||||
(
|
||||
variables: Variable[],
|
||||
options: ParseVariablesOptions = defaultParseVariablesOptions
|
||||
deepParseOptions: DeepParseOptions = {
|
||||
guessCorrectTypes: false,
|
||||
removeEmptyStrings: false,
|
||||
},
|
||||
parseVariablesOptions: ParseVariablesOptions = defaultParseVariablesOptions
|
||||
) =>
|
||||
<T extends Record<string, unknown>>(object: T): T =>
|
||||
Object.keys(object).reduce<T>((newObj, key) => {
|
||||
const currentValue = object[key]
|
||||
|
||||
if (typeof currentValue === 'string') {
|
||||
const parsedVariable = parseVariables(variables, options)(currentValue)
|
||||
const parsedVariable = parseVariables(
|
||||
variables,
|
||||
parseVariablesOptions
|
||||
)(currentValue)
|
||||
if (deepParseOptions.removeEmptyStrings && parsedVariable === '')
|
||||
return newObj
|
||||
return {
|
||||
...newObj,
|
||||
[key]: parsedVariable,
|
||||
[key]: deepParseOptions.guessCorrectTypes
|
||||
? parseGuessedTypeFromString(parsedVariable)
|
||||
: parsedVariable,
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,14 +44,21 @@ export const deepParseVariables =
|
||||
...newObj,
|
||||
[key]: deepParseVariables(
|
||||
variables,
|
||||
options
|
||||
deepParseOptions,
|
||||
parseVariablesOptions
|
||||
)(currentValue as Record<string, unknown>),
|
||||
}
|
||||
|
||||
if (currentValue instanceof Array)
|
||||
return {
|
||||
...newObj,
|
||||
[key]: currentValue.map(deepParseVariables(variables, options)),
|
||||
[key]: currentValue.map(
|
||||
deepParseVariables(
|
||||
variables,
|
||||
deepParseOptions,
|
||||
parseVariablesOptions
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
return { ...newObj, [key]: currentValue }
|
||||
|
@ -0,0 +1,12 @@
|
||||
export const parseGuessedTypeFromString = (value: string): unknown => {
|
||||
if (value === 'undefined') return undefined
|
||||
return safeJsonParse(value)
|
||||
}
|
||||
|
||||
const safeJsonParse = (value: string): unknown => {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ export const parseVariables =
|
||||
return text.replace(
|
||||
pattern,
|
||||
(_full, nameInCurlyBraces, _dollarSign, nameInTemplateLitteral) => {
|
||||
const dollarSign = _dollarSign ?? ''
|
||||
const dollarSign = (_dollarSign ?? '') as string
|
||||
const matchedVarName = nameInCurlyBraces ?? nameInTemplateLitteral
|
||||
const variable = variables.find((variable) => {
|
||||
return (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { LiteBadge } from './LiteBadge'
|
||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { injectCustomHeadCode, isNotEmpty } from '@typebot.io/lib'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
|
||||
import { ConversationContainer } from './ConversationContainer'
|
||||
import { setIsMobile } from '@/utils/isMobileSignal'
|
||||
@ -89,8 +89,6 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
groupId: data.input.groupId,
|
||||
})
|
||||
if (data.logs) props.onNewLogs?.(data.logs)
|
||||
const customHeadCode = data.typebot.settings.metadata.customHeadCode
|
||||
if (customHeadCode) injectCustomHeadCode(customHeadCode)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
|
@ -5,7 +5,5 @@ export const executeGoogleAnalyticsBlock = async (
|
||||
options: GoogleAnalyticsOptions
|
||||
) => {
|
||||
if (!options?.trackingId) return
|
||||
const { default: initGoogleAnalytics } = await import('@/lib/gtag')
|
||||
await initGoogleAnalytics(options.trackingId)
|
||||
sendGaEvent(options)
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { trackPixelEvent } from '@/lib/pixel'
|
||||
import { isEmpty } from '@typebot.io/lib/utils'
|
||||
import type { PixelBlock } from '@typebot.io/schemas'
|
||||
|
||||
export const executePixel = async (options: PixelBlock['options']) => {
|
||||
if (isEmpty(options?.pixelId)) return
|
||||
trackPixelEvent(options)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { isEmpty } from '@typebot.io/lib/utils'
|
||||
import type { GoogleAnalyticsOptions } from '@typebot.io/schemas'
|
||||
|
||||
declare const gtag: (
|
||||
@ -11,7 +12,7 @@ declare const gtag: (
|
||||
}
|
||||
) => void
|
||||
|
||||
const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
export const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
const existingScript = document.getElementById('gtag')
|
||||
if (!existingScript) {
|
||||
@ -37,11 +38,9 @@ const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
export const sendGaEvent = (options: GoogleAnalyticsOptions) => {
|
||||
if (!options) return
|
||||
gtag('event', options.action, {
|
||||
event_category: options.label?.length ? options.category : undefined,
|
||||
event_label: options.label?.length ? options.label : undefined,
|
||||
event_category: isEmpty(options.category) ? undefined : options.category,
|
||||
event_label: isEmpty(options.label) ? undefined : options.label,
|
||||
value: options.value as number,
|
||||
send_to: options.sendTo?.length ? options.sendTo : undefined,
|
||||
send_to: isEmpty(options.sendTo) ? undefined : options.sendTo,
|
||||
})
|
||||
}
|
||||
|
||||
export default initGoogleAnalytics
|
||||
|
23
packages/embeds/js/src/lib/gtm.ts
Normal file
23
packages/embeds/js/src/lib/gtm.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const gtmHeadSnippet = (
|
||||
googleTagManagerId: string
|
||||
) => `<!-- Google Tag Manager -->
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','${googleTagManagerId}');
|
||||
<!-- End Google Tag Manager -->`
|
||||
|
||||
export const gtmBodyElement = (googleTagManagerId: string) => {
|
||||
if (document.getElementById('gtm-noscript')) return ''
|
||||
const noScriptElement = document.createElement('noscript')
|
||||
noScriptElement.id = 'gtm-noscript'
|
||||
const iframeElement = document.createElement('iframe')
|
||||
iframeElement.src = `https://www.googletagmanager.com/ns.html?id=${googleTagManagerId}`
|
||||
iframeElement.height = '0'
|
||||
iframeElement.width = '0'
|
||||
iframeElement.style.display = 'none'
|
||||
iframeElement.style.visibility = 'hidden'
|
||||
noScriptElement.appendChild(iframeElement)
|
||||
return noScriptElement
|
||||
}
|
41
packages/embeds/js/src/lib/pixel.ts
Normal file
41
packages/embeds/js/src/lib/pixel.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { PixelBlock } from '@typebot.io/schemas'
|
||||
|
||||
declare const fbq: (
|
||||
arg0: string,
|
||||
arg1: string,
|
||||
arg2: Record<string, string> | undefined
|
||||
) => void
|
||||
|
||||
export const initPixel = (pixelId: string) => {
|
||||
const script = document.createElement('script')
|
||||
script.innerHTML = `!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', '${pixelId}');
|
||||
fbq('track', 'PageView');`
|
||||
document.head.appendChild(script)
|
||||
|
||||
const noscript = document.createElement('noscript')
|
||||
noscript.innerHTML = `<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1"/>`
|
||||
document.head.appendChild(noscript)
|
||||
}
|
||||
|
||||
export const trackPixelEvent = (options: PixelBlock['options']) => {
|
||||
if (!options.eventType) return
|
||||
const params = options.params?.length
|
||||
? options.params.reduce<Record<string, string>>((obj, param) => {
|
||||
if (!param.key || !param.value) return obj
|
||||
return { ...obj, [param.key]: param.value }
|
||||
}, {})
|
||||
: undefined
|
||||
if (options.eventType === 'Custom') {
|
||||
if (!options.name) return
|
||||
fbq('trackCustom', options.name, params)
|
||||
}
|
||||
fbq('track', options.eventType, params)
|
||||
}
|
@ -6,8 +6,10 @@ import { executeScript } from '@/features/blocks/logic/script/executeScript'
|
||||
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
|
||||
import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait'
|
||||
import { executeWebhook } from '@/features/blocks/integrations/webhook/executeWebhook'
|
||||
import { executePixel } from '@/features/blocks/integrations/pixel/executePixel'
|
||||
import { ClientSideActionContext } from '@/types'
|
||||
import type { ChatReply, ReplyLog } from '@typebot.io/schemas'
|
||||
import { injectStartProps } from './injectStartProps'
|
||||
|
||||
export const executeClientSideAction = async (
|
||||
clientSideAction: NonNullable<ChatReply['clientSideActions']>[0],
|
||||
@ -58,4 +60,10 @@ export const executeClientSideAction = async (
|
||||
const response = await executeWebhook(clientSideAction.webhookToExecute)
|
||||
return { replyToSend: response }
|
||||
}
|
||||
if ('startPropsToInject' in clientSideAction) {
|
||||
return injectStartProps(clientSideAction.startPropsToInject)
|
||||
}
|
||||
if ('pixel' in clientSideAction) {
|
||||
return executePixel(clientSideAction.pixel)
|
||||
}
|
||||
}
|
||||
|
20
packages/embeds/js/src/utils/injectStartProps.ts
Normal file
20
packages/embeds/js/src/utils/injectStartProps.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/* eslint-disable solid/reactivity */
|
||||
import { initGoogleAnalytics } from '@/lib/gtag'
|
||||
import { gtmBodyElement } from '@/lib/gtm'
|
||||
import { initPixel } from '@/lib/pixel'
|
||||
import { injectCustomHeadCode, isNotEmpty } from '@typebot.io/lib/utils'
|
||||
import { StartPropsToInject } from '@typebot.io/schemas'
|
||||
|
||||
export const injectStartProps = async (
|
||||
startPropsToInject: StartPropsToInject
|
||||
) => {
|
||||
const customHeadCode = startPropsToInject.customHeadCode
|
||||
if (isNotEmpty(customHeadCode)) injectCustomHeadCode(customHeadCode)
|
||||
const gtmId = startPropsToInject.gtmId
|
||||
if (isNotEmpty(gtmId)) document.body.prepend(gtmBodyElement(gtmId))
|
||||
const googleAnalyticsId = startPropsToInject.googleAnalyticsId
|
||||
if (isNotEmpty(googleAnalyticsId))
|
||||
await initGoogleAnalytics(googleAnalyticsId)
|
||||
const pixelId = startPropsToInject.pixelId
|
||||
if (isNotEmpty(pixelId)) initPixel(pixelId)
|
||||
}
|
@ -8,4 +8,5 @@ export enum IntegrationBlockType {
|
||||
MAKE_COM = 'Make.com',
|
||||
PABBLY_CONNECT = 'Pabbly',
|
||||
CHATWOOT = 'Chatwoot',
|
||||
PIXEL = 'Pixel',
|
||||
}
|
||||
|
@ -7,3 +7,5 @@ export * from './pabblyConnect'
|
||||
export * from './sendEmail'
|
||||
export * from './webhook'
|
||||
export * from './zapier'
|
||||
export * from './pixel/schemas'
|
||||
export * from './pixel/constants'
|
||||
|
134
packages/schemas/features/blocks/integrations/pixel/constants.ts
Normal file
134
packages/schemas/features/blocks/integrations/pixel/constants.ts
Normal file
@ -0,0 +1,134 @@
|
||||
// Reference: https://developers.facebook.com/docs/meta-pixel/reference#standard-events
|
||||
|
||||
export const pixelEventTypes = [
|
||||
'Lead',
|
||||
'Contact',
|
||||
'CompleteRegistration',
|
||||
'Schedule',
|
||||
'SubmitApplication',
|
||||
'ViewContent',
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CustomizeProduct',
|
||||
'Donate',
|
||||
'FindLocation',
|
||||
'InitiateCheckout',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'StartTrial',
|
||||
'Subscribe',
|
||||
] as const
|
||||
|
||||
export const allEventTypes = ['Custom', ...pixelEventTypes] as const
|
||||
|
||||
export const pixelObjectProperties: {
|
||||
key: string
|
||||
type: 'text' | 'code'
|
||||
associatedEvents: (typeof pixelEventTypes)[number][]
|
||||
}[] = [
|
||||
{
|
||||
key: 'content_category',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToWishlist',
|
||||
'InitiateCheckout',
|
||||
'Lead',
|
||||
'Search',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_ids',
|
||||
type: 'code',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'InitiateCheckout',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content_name',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CompleteRegistration',
|
||||
'Lead',
|
||||
'Purchase',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contents',
|
||||
type: 'code',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'InitiateCheckout',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'currency',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CompleteRegistration',
|
||||
'InitiateCheckout',
|
||||
'Lead',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'StartTrial',
|
||||
'Subscribe',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'num_items',
|
||||
type: 'text',
|
||||
associatedEvents: ['InitiateCheckout', 'Purchase'],
|
||||
},
|
||||
{
|
||||
key: 'predicted_ltv',
|
||||
type: 'text',
|
||||
associatedEvents: ['StartTrial', 'Subscribe'],
|
||||
},
|
||||
{
|
||||
key: 'search_string',
|
||||
type: 'text',
|
||||
associatedEvents: ['Search'],
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
type: 'text',
|
||||
associatedEvents: ['CompleteRegistration'],
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
type: 'text',
|
||||
associatedEvents: [
|
||||
'AddPaymentInfo',
|
||||
'AddToCart',
|
||||
'AddToWishlist',
|
||||
'CompleteRegistration',
|
||||
'InitiateCheckout',
|
||||
'Lead',
|
||||
'Purchase',
|
||||
'Search',
|
||||
'StartTrial',
|
||||
'Subscribe',
|
||||
'ViewContent',
|
||||
],
|
||||
},
|
||||
]
|
@ -0,0 +1,51 @@
|
||||
import { z } from 'zod'
|
||||
import { pixelEventTypes } from './constants'
|
||||
import { blockBaseSchema } from '../../baseSchemas'
|
||||
import { IntegrationBlockType } from '../enums'
|
||||
|
||||
const basePixelOptionSchema = z.object({
|
||||
pixelId: z.string().optional(),
|
||||
params: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
key: z.string().optional(),
|
||||
value: z.any().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const initialPixelOptionSchema = basePixelOptionSchema.merge(
|
||||
z.object({
|
||||
eventType: z.undefined(),
|
||||
})
|
||||
)
|
||||
|
||||
const standardPixelEventOptionSchema = basePixelOptionSchema.merge(
|
||||
z.object({
|
||||
eventType: z.enum(pixelEventTypes),
|
||||
})
|
||||
)
|
||||
|
||||
const customPixelOptionSchema = basePixelOptionSchema.merge(
|
||||
z.object({
|
||||
eventType: z.enum(['Custom']),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export const pixelOptionsSchema = z.discriminatedUnion('eventType', [
|
||||
initialPixelOptionSchema,
|
||||
standardPixelEventOptionSchema,
|
||||
customPixelOptionSchema,
|
||||
])
|
||||
|
||||
export const pixelBlockSchema = blockBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.enum([IntegrationBlockType.PIXEL]),
|
||||
options: pixelOptionsSchema,
|
||||
})
|
||||
)
|
||||
|
||||
export type PixelBlock = z.infer<typeof pixelBlockSchema>
|
@ -1,9 +1,9 @@
|
||||
import { ZodDiscriminatedUnionOption, z } from 'zod'
|
||||
import { z } from 'zod'
|
||||
import { BubbleBlockType } from './bubbles/enums'
|
||||
import { choiceInputSchema } from './inputs/choice'
|
||||
import { InputBlockType } from './inputs/enums'
|
||||
import { IntegrationBlockType } from './integrations/enums'
|
||||
import { ConditionBlock, conditionBlockSchema } from './logic/condition'
|
||||
import { conditionBlockSchema } from './logic/condition'
|
||||
import { LogicBlockType } from './logic/enums'
|
||||
import { blockBaseSchema } from './baseSchemas'
|
||||
import { startBlockSchema } from './start/schemas'
|
||||
@ -31,6 +31,7 @@ import {
|
||||
googleSheetsBlockSchema,
|
||||
makeComBlockSchema,
|
||||
pabblyConnectBlockSchema,
|
||||
pixelBlockSchema,
|
||||
sendEmailBlockSchema,
|
||||
webhookBlockSchema,
|
||||
zapierBlockSchema,
|
||||
@ -125,6 +126,7 @@ export const blockSchema = z.discriminatedUnion('type', [
|
||||
sendEmailBlockSchema,
|
||||
webhookBlockSchema,
|
||||
zapierBlockSchema,
|
||||
pixelBlockSchema,
|
||||
])
|
||||
|
||||
export type Block = z.infer<typeof blockSchema>
|
||||
|
@ -2,6 +2,7 @@ import { z } from 'zod'
|
||||
import {
|
||||
googleAnalyticsOptionsSchema,
|
||||
paymentInputRuntimeOptionsSchema,
|
||||
pixelOptionsSchema,
|
||||
redirectOptionsSchema,
|
||||
} from './blocks'
|
||||
import { publicTypebotSchema } from './publicTypebot'
|
||||
@ -197,6 +198,13 @@ export const sendMessageInputSchema = z.object({
|
||||
|
||||
const runtimeOptionsSchema = paymentInputRuntimeOptionsSchema.optional()
|
||||
|
||||
const startPropsToInjectSchema = z.object({
|
||||
googleAnalyticsId: z.string().optional(),
|
||||
pixelId: z.string().optional(),
|
||||
gtmId: z.string().optional(),
|
||||
customHeadCode: z.string().optional(),
|
||||
})
|
||||
|
||||
const clientSideActionSchema = z
|
||||
.object({
|
||||
lastBubbleBlockId: z.string().optional(),
|
||||
@ -247,6 +255,16 @@ const clientSideActionSchema = z
|
||||
webhookToExecute: executableWebhookSchema,
|
||||
})
|
||||
)
|
||||
.or(
|
||||
z.object({
|
||||
startPropsToInject: startPropsToInjectSchema,
|
||||
})
|
||||
)
|
||||
.or(
|
||||
z.object({
|
||||
pixel: pixelOptionsSchema,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export const chatReplySchema = z.object({
|
||||
@ -282,3 +300,4 @@ export type StartParams = z.infer<typeof startParamsSchema>
|
||||
export type RuntimeOptions = z.infer<typeof runtimeOptionsSchema>
|
||||
export type StartTypebot = z.infer<typeof startTypebotSchema>
|
||||
export type ReplyLog = z.infer<typeof replyLogSchema>
|
||||
export type StartPropsToInject = z.infer<typeof startPropsToInjectSchema>
|
||||
|
Reference in New Issue
Block a user