2
0

🐛 (payment) Fix payment redirection

This commit is contained in:
Baptiste Arnaud
2024-06-26 10:59:11 +02:00
parent 6db0464fd7
commit 6af47a8cfe
16 changed files with 182 additions and 38 deletions

View File

@ -0,0 +1,94 @@
{
"version": "6",
"id": "clxvjgawa0003lu4yp38homts",
"name": "Lead Generation",
"events": [
{
"id": "k6kY6gwRE6noPoYQNGzgUq",
"outgoingEdgeId": "oNvqaqNExdSH2kKEhKZHuE",
"graphCoordinates": { "x": 0, "y": 0 },
"type": "start"
}
],
"groups": [
{
"id": "kinRXxYop2X4d7F9qt8WNB",
"title": "Welcome",
"graphCoordinates": { "x": 1, "y": 148 },
"blocks": [
{
"id": "sc1y8VwDabNJgiVTBi4qtif",
"type": "text",
"content": {
"richText": [
{
"type": "p",
"children": [
{ "text": "Welcome to " },
{ "bold": true, "text": "AA" },
{ "text": " (Awesome Agency)" }
]
}
]
}
},
{
"id": "xccq75m3fe7l6ads4wcvian0",
"type": "payment input",
"options": {
"credentialsId": "stripe",
"amount": "10"
}
},
{
"id": "wo8xv8kqc8dpwadvoepv3bua",
"type": "text",
"content": {
"richText": [
{ "type": "p", "children": [{ "text": "Thank you!" }] }
]
}
}
]
}
],
"edges": [
{
"id": "oNvqaqNExdSH2kKEhKZHuE",
"from": { "eventId": "k6kY6gwRE6noPoYQNGzgUq" },
"to": { "groupId": "kinRXxYop2X4d7F9qt8WNB" }
}
],
"variables": [
{
"id": "vml1jtton0xpmnk2vkwog488g",
"name": "Attachments",
"isSessionVariable": true
},
{
"id": "giiLFGw5xXBCHzvp1qAbdX",
"name": "Name",
"isSessionVariable": true
},
{
"id": "v3VFChNVSCXQ2rXv4DrJ8Ah",
"name": "Email",
"isSessionVariable": true
}
],
"theme": {},
"selectedThemeTemplateId": null,
"settings": {},
"createdAt": "2024-06-26T07:53:07.882Z",
"updatedAt": "2024-06-26T08:12:50.867Z",
"icon": "🤝",
"folderId": null,
"publicId": "lead-generation-38homts",
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
}

View File

@ -0,0 +1,22 @@
import { createId } from '@paralleldrive/cuid2'
import test, { expect } from '@playwright/test'
import { getTestAsset } from './utils/playwright'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
test('Payment redirection should work', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(getTestAsset('typebots/payment.json'), {
id: typebotId,
publicId: `${typebotId}-public`,
})
await page.goto(`/${typebotId}-public`)
const paypalButton = page
.frameLocator('iframe[title="Secure payment input frame"]')
.getByTestId('paypal')
await expect(paypalButton).toBeVisible()
await page.waitForTimeout(1000)
await paypalButton.click()
await page.getByRole('button', { name: 'Pay $' }).click()
await page.getByRole('link', { name: 'Authorize Test Payment' }).click()
await expect(page.getByText('Thank you!')).toBeVisible()
})

View File

@ -226,7 +226,7 @@ export const continueBotFlow = async (
return { return {
...chatReply, ...chatReply,
lastMessageNewFormat: lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined, formattedReply !== reply?.text ? formattedReply : undefined,
} }
} }
@ -235,7 +235,7 @@ export const continueBotFlow = async (
messages: [], messages: [],
newSessionState, newSessionState,
lastMessageNewFormat: lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined, formattedReply !== reply?.text ? formattedReply : undefined,
visitedEdges, visitedEdges,
setVariableHistory, setVariableHistory,
} }
@ -272,7 +272,8 @@ export const continueBotFlow = async (
return { return {
...chatReply, ...chatReply,
lastMessageNewFormat: formattedReply !== reply ? formattedReply : undefined, lastMessageNewFormat:
formattedReply !== reply?.text ? formattedReply : undefined,
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.3.0", "version": "0.3.1",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -4,7 +4,7 @@ import { isDefined, isNotDefined, isNotEmpty } from '@typebot.io/lib'
import { startChatQuery } from '@/queries/startChatQuery' import { startChatQuery } from '@/queries/startChatQuery'
import { ConversationContainer } from './ConversationContainer' import { ConversationContainer } from './ConversationContainer'
import { setIsMobile } from '@/utils/isMobileSignal' import { setIsMobile } from '@/utils/isMobileSignal'
import { BotContext, InitialChatReply, OutgoingLog } from '@/types' import { BotContext, OutgoingLog } from '@/types'
import { ErrorMessage } from './ErrorMessage' import { ErrorMessage } from './ErrorMessage'
import { import {
getExistingResultIdFromStorage, getExistingResultIdFromStorage,
@ -15,7 +15,12 @@ import {
} from '@/utils/storage' } from '@/utils/storage'
import { setCssVariablesValue } from '@/utils/setCssVariablesValue' import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
import immutableCss from '../assets/immutable.css' import immutableCss from '../assets/immutable.css'
import { Font, InputBlock, StartFrom } from '@typebot.io/schemas' import {
Font,
InputBlock,
StartChatResponse,
StartFrom,
} from '@typebot.io/schemas'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { HTTPError } from 'ky' import { HTTPError } from 'ky'
import { injectFont } from '@/utils/injectFont' import { injectFont } from '@/utils/injectFont'
@ -55,7 +60,7 @@ export type BotProps = {
export const Bot = (props: BotProps & { class?: string }) => { export const Bot = (props: BotProps & { class?: string }) => {
const [initialChatReply, setInitialChatReply] = createSignal< const [initialChatReply, setInitialChatReply] = createSignal<
InitialChatReply | undefined StartChatResponse | undefined
>() >()
const [customCss, setCustomCss] = createSignal('') const [customCss, setCustomCss] = createSignal('')
const [isInitialized, setIsInitialized] = createSignal(false) const [isInitialized, setIsInitialized] = createSignal(false)
@ -245,7 +250,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
} }
type BotContentProps = { type BotContentProps = {
initialChatReply: InitialChatReply initialChatReply: StartChatResponse
context: BotContext context: BotContext
class?: string class?: string
progressBarRef?: HTMLDivElement progressBarRef?: HTMLDivElement

View File

@ -3,6 +3,7 @@ import {
InputBlock, InputBlock,
Theme, Theme,
ChatLog, ChatLog,
StartChatResponse,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { import {
createEffect, createEffect,
@ -14,12 +15,7 @@ import {
} from 'solid-js' } from 'solid-js'
import { continueChatQuery } from '@/queries/continueChatQuery' import { continueChatQuery } from '@/queries/continueChatQuery'
import { ChatChunk } from './ChatChunk' import { ChatChunk } from './ChatChunk'
import { import { BotContext, ChatChunk as ChatChunkType, OutgoingLog } from '@/types'
BotContext,
ChatChunk as ChatChunkType,
InitialChatReply,
OutgoingLog,
} from '@/types'
import { isNotDefined } from '@typebot.io/lib' import { isNotDefined } from '@typebot.io/lib'
import { executeClientSideAction } from '@/utils/executeClientSideActions' import { executeClientSideAction } from '@/utils/executeClientSideActions'
import { LoadingChunk } from './LoadingChunk' import { LoadingChunk } from './LoadingChunk'
@ -61,7 +57,7 @@ const parseDynamicTheme = (
}) })
type Props = { type Props = {
initialChatReply: InitialChatReply initialChatReply: StartChatResponse
context: BotContext context: BotContext
onNewInputBlock?: (inputBlock: InputBlock) => void onNewInputBlock?: (inputBlock: InputBlock) => void
onAnswer?: (answer: { message: string; blockId: string }) => void onAnswer?: (answer: { message: string; blockId: string }) => void

View File

@ -64,6 +64,7 @@ export const StripePaymentForm = (props: Props) => {
setPaymentInProgressInStorage({ setPaymentInProgressInStorage({
sessionId: props.context.sessionId, sessionId: props.context.sessionId,
resultId: props.context.resultId,
typebot: props.context.typebot, typebot: props.context.typebot,
}) })
const { postalCode, ...address } = const { postalCode, ...address } =

View File

@ -1,9 +1,8 @@
import { BotContext } from '@/types' import { StartChatResponse } from '@typebot.io/schemas'
export const setPaymentInProgressInStorage = (state: { export const setPaymentInProgressInStorage = (
sessionId: string state: Pick<StartChatResponse, 'typebot' | 'sessionId' | 'resultId'>
typebot: BotContext['typebot'] ) => {
}) => {
sessionStorage.setItem('typebotPaymentInProgress', JSON.stringify(state)) sessionStorage.setItem('typebotPaymentInProgress', JSON.stringify(state))
} }

View File

@ -1,4 +1,4 @@
import { BotContext, InitialChatReply } from '@/types' import { BotContext } from '@/types'
import { guessApiHost } from '@/utils/guessApiHost' import { guessApiHost } from '@/utils/guessApiHost'
import { isNotDefined, isNotEmpty } from '@typebot.io/lib' import { isNotDefined, isNotEmpty } from '@typebot.io/lib'
import { import {
@ -6,7 +6,9 @@ import {
removePaymentInProgressFromStorage, removePaymentInProgressFromStorage,
} from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage' } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
import { import {
ContinueChatResponse,
StartChatInput, StartChatInput,
StartChatResponse,
StartFrom, StartFrom,
StartPreviewChatInput, StartPreviewChatInput,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
@ -65,9 +67,14 @@ export async function startChatQuery({
timeout: false, timeout: false,
} }
) )
.json<InitialChatReply>() .json<ContinueChatResponse>()
return { data } return {
data: {
...data,
...paymentInProgressState,
} satisfies StartChatResponse,
}
} catch (error) { } catch (error) {
return { error } return { error }
} }
@ -94,7 +101,7 @@ export async function startChatQuery({
timeout: false, timeout: false,
} }
) )
.json<InitialChatReply>() .json<StartChatResponse>()
return { data } return { data }
} catch (error) { } catch (error) {
@ -138,7 +145,7 @@ export async function startChatQuery({
) )
throw new CorsError(corsAllowOrigin) throw new CorsError(corsAllowOrigin)
return { data: await response.json<InitialChatReply>() } return { data: await response.json<StartChatResponse>() }
} catch (error) { } catch (error) {
return { error } return { error }
} }

View File

@ -7,7 +7,7 @@ export type InputSubmitContent = {
} }
export type BotContext = { export type BotContext = {
typebot: InitialChatReply['typebot'] typebot: StartChatResponse['typebot']
resultId?: string resultId?: string
isPreview: boolean isPreview: boolean
apiHost?: string apiHost?: string
@ -15,11 +15,6 @@ export type BotContext = {
storage: 'local' | 'session' | undefined storage: 'local' | 'session' | undefined
} }
export type InitialChatReply = StartChatResponse & {
typebot: NonNullable<StartChatResponse['typebot']>
sessionId: NonNullable<StartChatResponse['sessionId']>
}
export type OutgoingLog = { export type OutgoingLog = {
status: string status: string
description: string description: string

View File

@ -1,4 +1,4 @@
import { InitialChatReply } from '@/types' import { StartChatResponse } from '@typebot.io/schemas/features/chat/schema'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants' import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
const storageResultIdKey = 'resultId' const storageResultIdKey = 'resultId'
@ -38,13 +38,13 @@ export const getInitialChatReplyFromStorage = (
sessionStorage.getItem(`typebot-${typebotId}-initialChatReply`) ?? sessionStorage.getItem(`typebot-${typebotId}-initialChatReply`) ??
localStorage.getItem(`typebot-${typebotId}-initialChatReply`) localStorage.getItem(`typebot-${typebotId}-initialChatReply`)
if (!rawInitialChatReply) return if (!rawInitialChatReply) return
return JSON.parse(rawInitialChatReply) as InitialChatReply return JSON.parse(rawInitialChatReply) as StartChatResponse
} catch { } catch {
/* empty */ /* empty */
} }
} }
export const setInitialChatReplyInStorage = ( export const setInitialChatReplyInStorage = (
initialChatReply: InitialChatReply, initialChatReply: StartChatResponse,
{ {
typebotId, typebotId,
storage, storage,

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/nextjs", "name": "@typebot.io/nextjs",
"version": "0.3.0", "version": "0.3.1",
"description": "Convenient library to display typebots on your Next.js website", "description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.3.0", "version": "0.3.1",
"description": "Convenient library to display typebots on your React app", "description": "Convenient library to display typebots on your React app",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -5,6 +5,8 @@ import {
WorkspaceRole, WorkspaceRole,
} from '@typebot.io/prisma' } from '@typebot.io/prisma'
import { encrypt } from '@typebot.io/lib/api/encryption/encrypt' import { encrypt } from '@typebot.io/lib/api/encryption/encrypt'
import { env } from '@typebot.io/env'
import { StripeCredentials } from '@typebot.io/schemas'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@ -134,6 +136,16 @@ const setupCredentials = async () => {
refresh_token: refresh_token:
'1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29G69iEhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek', '1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29G69iEhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek',
}) })
const { encryptedData: stripeEncryptedData, iv: stripeIv } = await encrypt({
test: {
publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
secretKey: env.STRIPE_SECRET_KEY,
},
live: {
publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? '',
secretKey: env.STRIPE_SECRET_KEY ?? '',
},
} satisfies StripeCredentials['data'])
return prisma.credentials.createMany({ return prisma.credentials.createMany({
data: [ data: [
{ {
@ -143,6 +155,14 @@ const setupCredentials = async () => {
workspaceId: proWorkspaceId, workspaceId: proWorkspaceId,
iv, iv,
}, },
{
id: 'stripe',
name: 'Test',
type: 'stripe',
data: stripeEncryptedData,
workspaceId: proWorkspaceId,
iv: stripeIv,
},
], ],
}) })
} }

View File

@ -10,7 +10,8 @@
"@playwright/test": "1.43.1", "@playwright/test": "1.43.1",
"@typebot.io/lib": "workspace:*", "@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*", "@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*" "@typebot.io/schemas": "workspace:*",
"@typebot.io/env": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.4.2", "@types/node": "20.4.2",

3
pnpm-lock.yaml generated
View File

@ -1820,6 +1820,9 @@ importers:
'@playwright/test': '@playwright/test':
specifier: 1.43.1 specifier: 1.43.1
version: 1.43.1 version: 1.43.1
'@typebot.io/env':
specifier: workspace:*
version: link:../env
'@typebot.io/lib': '@typebot.io/lib':
specifier: workspace:* specifier: workspace:*
version: link:../lib version: link:../lib