2
0

(paymentInput) Handle Stripe redirection

Closes #631
This commit is contained in:
Baptiste Arnaud
2023-07-27 17:25:02 +02:00
parent e499478dee
commit c99298e49b
15 changed files with 109 additions and 21 deletions

View File

@ -1387,6 +1387,9 @@
},
"amount": {
"type": "string"
},
"retryMessageContent": {
"type": "string"
}
},
"required": [

View File

@ -981,6 +981,9 @@
},
"amount": {
"type": "string"
},
"retryMessageContent": {
"type": "string"
}
},
"required": [
@ -4392,6 +4395,9 @@
},
"amount": {
"type": "string"
},
"retryMessageContent": {
"type": "string"
}
},
"required": [

View File

@ -12,6 +12,7 @@ import {
SessionState,
SetVariableBlock,
WebhookBlock,
defaultPaymentInputOptions,
} from '@typebot.io/schemas'
import { isInputBlock, byId } from '@typebot.io/lib'
import { executeGroup } from './executeGroup'
@ -167,7 +168,7 @@ const parseRetryMessage = (
const retryMessage =
'retryMessageContent' in block.options && block.options.retryMessageContent
? block.options.retryMessageContent
: 'Invalid message. Please, try again.'
: parseDefaultRetryMessage(block)
return {
messages: [
{
@ -182,6 +183,15 @@ const parseRetryMessage = (
}
}
const parseDefaultRetryMessage = (block: InputBlock): string => {
switch (block.type) {
case InputBlockType.PAYMENT:
return defaultPaymentInputOptions.retryMessageContent as string
default:
return 'Invalid message. Please, try again.'
}
}
const saveAnswer =
(state: SessionState, block: InputBlock, itemId?: string) =>
async (reply: string): Promise<SessionState> => {
@ -271,6 +281,8 @@ export const isReplyValid = (inputValue: string, block: Block): boolean => {
return validatePhoneNumber(inputValue)
case InputBlockType.URL:
return validateUrl(inputValue)
case InputBlockType.PAYMENT:
return inputValue !== 'fail'
}
return true
}

View File

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

View File

@ -47,6 +47,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
const typebotIdFromProps =
typeof props.typebot === 'string' ? props.typebot : undefined
const { data, error } = await getInitialChatReplyQuery({
stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined,
typebot: props.typebot,
apiHost: props.apiHost,
isPreview: props.isPreview ?? false,
@ -134,8 +135,9 @@ export const Bot = (props: BotProps & { class?: string }) => {
apiHost: props.apiHost,
isPreview:
typeof props.typebot !== 'string' || (props.isPreview ?? false),
typebotId: initialChatReply.typebot.id,
resultId: initialChatReply.resultId,
sessionId: initialChatReply.sessionId,
typebot: initialChatReply.typebot,
}}
onNewInputBlock={props.onNewInputBlock}
onNewLogs={props.onNewLogs}

View File

@ -54,7 +54,7 @@ export const FileUploadForm = (props: Props) => {
setIsUploading(true)
const urls = await uploadFiles({
basePath: `${props.context.apiHost ?? guessApiHost()}/api/typebots/${
props.context.typebotId
props.context.typebot.id
}/blocks/${props.block.id}`,
files: [
{
@ -79,7 +79,7 @@ export const FileUploadForm = (props: Props) => {
setIsUploading(true)
const urls = await uploadFiles({
basePath: `${props.context.apiHost ?? guessApiHost()}/api/typebots/${
props.context.typebotId
props.context.typebot.id
}/blocks/${props.block.id}`,
files: files.map((file) => ({
file: file,

View File

@ -4,6 +4,10 @@ import type { Stripe, StripeElements } from '@stripe/stripe-js'
import { BotContext } from '@/types'
import type { PaymentInputOptions, RuntimeOptions } from '@typebot.io/schemas'
import { loadStripe } from '@/lib/stripe'
import {
removePaymentInProgressFromStorage,
setPaymentInProgressInStorage,
} from '../helpers/paymentInProgressStorage'
type Props = {
context: BotContext
@ -51,11 +55,14 @@ export const StripePaymentForm = (props: Props) => {
setIsLoading(true)
setPaymentInProgressInStorage({
sessionId: props.context.sessionId,
typebot: props.context.typebot,
})
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
// TO-DO: Handle redirection correctly.
return_url: props.context.apiHost,
return_url: window.location.href,
payment_method_data: {
billing_details: {
name: props.options.additionalInformation?.name,
@ -71,6 +78,7 @@ export const StripePaymentForm = (props: Props) => {
},
redirect: 'if_required',
})
removePaymentInProgressFromStorage()
setIsLoading(false)
if (error?.type === 'validation_error') return

View File

@ -0,0 +1,15 @@
import { BotContext } from '@/types'
export const setPaymentInProgressInStorage = (state: {
sessionId: string
typebot: BotContext['typebot']
}) => {
sessionStorage.setItem('typebotPaymentInProgress', JSON.stringify(state))
}
export const getPaymentInProgressInStorage = () =>
sessionStorage.getItem('typebotPaymentInProgress')
export const removePaymentInProgressFromStorage = () => {
sessionStorage.removeItem('typebotPaymentInProgress')
}

View File

@ -13,6 +13,7 @@ import { PreviewMessage, PreviewMessageProps } from './PreviewMessage'
import { isDefined } from '@typebot.io/lib'
import { BubbleParams } from '../types'
import { Bot, BotProps } from '../../../components/Bot'
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
export type BubbleProps = BotProps &
BubbleParams & {
@ -51,6 +52,8 @@ export const Bubble = (props: BubbleProps) => {
const autoShowDelay = bubbleProps.autoShowDelay
const previewMessageAutoShowDelay =
bubbleProps.previewMessage?.autoShowDelay
const paymentInProgress = getPaymentInProgressInStorage()
if (paymentInProgress) openBot()
if (isDefined(autoShowDelay)) {
setTimeout(() => {
openBot()

View File

@ -11,6 +11,7 @@ import { CommandData } from '../../commands'
import { isDefined, isNotDefined } from '@typebot.io/lib'
import { PopupParams } from '../types'
import { Bot, BotProps } from '../../../components/Bot'
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
export type PopupProps = BotProps &
PopupParams & {
@ -41,7 +42,8 @@ export const Popup = (props: PopupProps) => {
)
onMount(() => {
if (popupProps.defaultOpen) openBot()
const paymentInProgress = getPaymentInProgressInStorage()
if (popupProps.defaultOpen || paymentInProgress) openBot()
window.addEventListener('message', processIncomingEvent)
const autoShowDelay = popupProps.autoShowDelay
if (isDefined(autoShowDelay)) {

View File

@ -1,7 +1,11 @@
import { InitialChatReply } from '@/types'
import { BotContext, InitialChatReply } from '@/types'
import { guessApiHost } from '@/utils/guessApiHost'
import type { SendMessageInput, StartParams } from '@typebot.io/schemas'
import { isNotDefined, isNotEmpty, sendRequest } from '@typebot.io/lib'
import {
getPaymentInProgressInStorage,
removePaymentInProgressFromStorage,
} from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
export async function getInitialChatReplyQuery({
typebot,
@ -10,17 +14,29 @@ export async function getInitialChatReplyQuery({
prefilledVariables,
startGroupId,
resultId,
stripeRedirectStatus,
}: StartParams & {
stripeRedirectStatus?: string
apiHost?: string
}) {
if (isNotDefined(typebot))
throw new Error('Typebot ID is required to get initial messages')
return sendRequest<InitialChatReply>({
const paymentInProgressStateStr = getPaymentInProgressInStorage() ?? undefined
const paymentInProgressState = paymentInProgressStateStr
? (JSON.parse(paymentInProgressStateStr) as {
sessionId: string
typebot: BotContext['typebot']
})
: undefined
if (paymentInProgressState) removePaymentInProgressFromStorage()
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sendMessage`,
body: {
startParams: {
startParams: paymentInProgressState
? undefined
: {
isPreview,
typebot,
prefilledVariables,
@ -28,6 +44,24 @@ export async function getInitialChatReplyQuery({
resultId,
isStreamEnabled: true,
},
sessionId: paymentInProgressState?.sessionId,
message: paymentInProgressState
? stripeRedirectStatus === 'failed'
? 'fail'
: 'Success'
: undefined,
} satisfies SendMessageInput,
})
return {
data: data
? {
...data,
...(paymentInProgressState
? { typebot: paymentInProgressState.typebot }
: {}),
}
: undefined,
error,
}
}

View File

@ -6,10 +6,11 @@ export type InputSubmitContent = {
}
export type BotContext = {
typebotId: string
typebot: InitialChatReply['typebot']
resultId?: string
isPreview: boolean
apiHost?: string
sessionId: string
}
export type InitialChatReply = ChatReply & {

View File

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

View File

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

View File

@ -42,6 +42,7 @@ export const paymentInputOptionsSchema = optionBaseSchema.merge(
credentialsId: z.string().optional(),
currency: z.string(),
amount: z.string().optional(),
retryMessageContent: z.string().optional(),
})
)
@ -77,6 +78,7 @@ export const stripeCredentialsSchema = z
export const defaultPaymentInputOptions: PaymentInputOptions = {
provider: PaymentProvider.STRIPE,
labels: { button: 'Pay', success: 'Success' },
retryMessageContent: 'Payment failed. Please, try again.',
currency: 'USD',
}