diff --git a/src/components/BotMessageWithBodyInput.tsx b/src/components/BotMessageWithBodyInput.tsx
index de2bedd63..12e03c46e 100644
--- a/src/components/BotMessageWithBodyInput.tsx
+++ b/src/components/BotMessageWithBodyInput.tsx
@@ -62,6 +62,7 @@ type Props = {
zIndex?: number;
bodyStyle?: object;
isBotWelcomeMessage?: boolean;
+ isFormMessage?: boolean;
};
const ImageContainer = styled.div``;
@@ -82,6 +83,7 @@ export default function BotMessageWithBodyInput(props: Props) {
chainTop,
chainBottom,
isBotWelcomeMessage,
+ isFormMessage = false,
} = props;
const nonChainedMessage = chainTop == null && chainBottom == null;
@@ -114,9 +116,10 @@ export default function BotMessageWithBodyInput(props: Props) {
)}
{bodyComponent}
- {enableEmojiFeedback && displayProfileImage && !isBotWelcomeMessage && (
-
- )}
+ {enableEmojiFeedback &&
+ displayProfileImage &&
+ !isBotWelcomeMessage &&
+ !isFormMessage && }
{formatCreatedAtToAMPM(message.createdAt)}
diff --git a/src/components/CustomMessage.tsx b/src/components/CustomMessage.tsx
index dda9cfe64..373416d9f 100644
--- a/src/components/CustomMessage.tsx
+++ b/src/components/CustomMessage.tsx
@@ -8,6 +8,7 @@ import AdminMessage from './AdminMessage';
import BotMessageWithBodyInput from './BotMessageWithBodyInput';
import CurrentUserMessage from './CurrentUserMessage';
import CustomMessageBody from './CustomMessageBody';
+import FormMessage from './FormMessage';
import ParsedBotMessageBody from './ParsedBotMessageBody';
import PendingMessage from './PendingMessage';
import SuggestedReplyMessageBody from './SuggestedReplyMessageBody';
@@ -52,6 +53,23 @@ export default function CustomMessage(props: Props) {
return
;
}
+ if (message.extendedMessage.forms) {
+ const forms = JSON.parse(message.extendedMessage.forms);
+ return (
+ }
+ bodyStyle={{ maxWidth: '320px', width: 'calc(100% - 98px)' }}
+ messageCount={allMessages.length}
+ chainTop={chainTop}
+ chainBottom={chainBottom}
+ isBotWelcomeMessage={isBotWelcomeMessage}
+ isFormMessage={true}
+ />
+ );
+ }
+
// Sent by current user
if ((message as UserMessage).sender.userId !== botUser.userId) {
return (
diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx
new file mode 100644
index 000000000..18ac5a54d
--- /dev/null
+++ b/src/components/FormInput.tsx
@@ -0,0 +1,133 @@
+import Icon, { IconTypes, IconColors } from '@sendbird/uikit-react/ui/Icon';
+import {
+ default as UIKitLabel,
+ LabelTypography,
+ LabelColors,
+} from '@sendbird/uikit-react/ui/Label';
+import { ReactElement, ChangeEvent, ReactNode } from 'react';
+import styled, { css } from 'styled-components';
+
+export interface InputLabelProps {
+ children: ReactNode;
+}
+
+const Label = styled(UIKitLabel)`
+ font-size: 11px;
+ position: relative;
+ bottom: 4px;
+`;
+
+export const InputLabel = ({ children }: InputLabelProps): ReactElement => (
+
+);
+
+const Root = styled.div>`
+ padding-bottom: 12px;
+ width: 100%;
+ .sendbird-input .sendbird-input__input {
+ background-color: #fff;
+ border: ${({ hasError }) =>
+ `solid 1px ${hasError ? '#DE360B' : 'rgba(0, 0, 0, 0.12)'}`};
+ &:focus {
+ border: ${({ hasError }) => (hasError ? 'solid 1px #DE360B' : 'none')};
+ box-shadow: none;
+ }
+ &:disabled {
+ pointer-events: none;
+ background-color: #fff;
+ }
+ }
+`;
+
+const Input = styled.input`
+ ::placeholder {
+ color: rgba(0, 0, 0, 0.38);
+ }
+`;
+
+const ErrorLabel = styled(Label)`
+ position: relative;
+ top: 0;
+ color: #de360b;
+`;
+
+const CheckIcon = styled(Icon)`
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+`;
+
+const InputContainer = styled.div`
+ position: relative;
+`;
+export interface InputProps {
+ name: string;
+ type: string;
+ required?: boolean;
+ disabled?: boolean;
+ isValid?: boolean;
+ hasError?: boolean;
+ value?: string;
+ placeHolder?: string;
+ onChange?: (event: ChangeEvent) => void;
+}
+const FormInput = (props: InputProps) => {
+ const {
+ name,
+ required,
+ disabled,
+ hasError,
+ isValid,
+ value,
+ type,
+ onChange,
+ placeHolder,
+ } = props;
+
+ return (
+
+
+ {required ? `${name} *` : name}
+
+ {
+ onChange?.(event);
+ }}
+ placeholder={!disabled ? placeHolder : ''}
+ />
+ {isValid && (
+
+ )}
+
+ {hasError && (
+
+ Please check the value
+
+ )}
+
+
+ );
+};
+
+export default FormInput;
diff --git a/src/components/FormMessage.tsx b/src/components/FormMessage.tsx
new file mode 100644
index 000000000..886d9f1ff
--- /dev/null
+++ b/src/components/FormMessage.tsx
@@ -0,0 +1,167 @@
+import Button from '@sendbird/uikit-react/ui/Button';
+import Label, {
+ LabelTypography,
+ LabelColors,
+} from '@sendbird/uikit-react/ui/Label';
+import { useCallback, useState, useEffect } from 'react';
+// eslint-disable-next-line import/no-unresolved
+import { EveryMessage } from 'SendbirdUIKitGlobal';
+import styled from 'styled-components';
+
+import Input from './FormInput';
+
+const Root = styled.div`
+ max-width: 244px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 16px 12px;
+ gap: 8px;
+ border-radius: 16px;
+ background-color: #eeeeee;
+`;
+
+const SubmitButton = styled(Button)`
+ width: 100%;
+`;
+
+interface Field {
+ key: string;
+ title: string;
+ placeholder: string;
+ required: boolean;
+ regex: RegExp;
+ input_type: string;
+}
+interface Props {
+ message: EveryMessage;
+ form: {
+ key: string;
+ fields: Field[];
+ /** ubmitted data */
+ data: Record;
+ };
+}
+
+type FormValues = Record<
+ string,
+ { value: string; required: boolean; hasError: boolean; isValid: boolean }
+>;
+
+export default function FormMessage(props: Props) {
+ const {
+ message,
+ form: { fields, key: formKey, data: submittedData },
+ } = props;
+ const [formValues, setInputValue] = useState(() =>
+ fields.reduce(
+ (acc, { key, required }) => ({
+ ...acc,
+ [key]: { value: '', required, hasError: false, isValid: false },
+ }),
+ {}
+ )
+ );
+
+ useEffect(() => {
+ if (submittedData) {
+ setInputValue((prev) =>
+ Object.entries(prev).reduce((acc, [key, value]) => {
+ return {
+ ...acc,
+ [key]: {
+ ...value,
+ isValid:
+ submittedData?.[key] != null && submittedData[key] !== '',
+ },
+ };
+ }, {} as FormValues)
+ );
+ }
+ }, [submittedData]);
+
+ const handleSubmit = useCallback(async () => {
+ try {
+ // If any of required fields are not even touched but the user tries to submit the form,
+ const invalidRequiredFields = Object.keys(formValues).filter(
+ (key) => formValues[key].required && formValues[key].value.length === 0
+ );
+ invalidRequiredFields.forEach((key) => {
+ setInputValue((prev) => ({
+ ...prev,
+ [key]: { ...prev[key], hasError: true },
+ }));
+ });
+ if (invalidRequiredFields.length > 0) {
+ return;
+ }
+
+ // If any of required fields are not valid,
+ const hasError = Object.values(formValues).some(
+ ({ hasError }) => hasError
+ );
+ if (hasError) {
+ return;
+ }
+ const answers = Object.entries(formValues).reduce(
+ (acc, [key, { value }]) => {
+ return {
+ ...acc,
+ [key]: value,
+ };
+ },
+ {} as Record
+ );
+
+ await message.submitForm({
+ formId: formKey,
+ answers,
+ });
+ } catch (error) {
+ console.error(error);
+ }
+ }, [formValues, message.messageId, message.submitForm, formKey]);
+
+ const allFieldsValid = Object.values(formValues).every(
+ (field) => field.isValid
+ );
+
+ return (
+
+ {fields.map(
+ ({ title, placeholder, key, required, regex, input_type }) => (
+ {
+ const value = event.target.value;
+ const hasError = regex
+ ? regex.test(value)
+ : required && value === '';
+ setInputValue(() => ({
+ ...formValues,
+ [key]: { ...formValues[key], value, hasError },
+ }));
+ }}
+ />
+ )
+ )}
+ {!allFieldsValid && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/ParsedBotMessageBody.tsx b/src/components/ParsedBotMessageBody.tsx
index 809e23dcf..a9fabf70a 100644
--- a/src/components/ParsedBotMessageBody.tsx
+++ b/src/components/ParsedBotMessageBody.tsx
@@ -54,7 +54,9 @@ type MetaData = {
export default function ParsedBotMessageBody(props: Props) {
const { message, tokens } = props;
const { enableSourceMessage } = useConstantState();
- const data_ = (message as UserMessage).data as string;
+ const data_ =
+ (message as UserMessage).data === '' ? '{}' : (message.data as string);
+
const data: MetaData = JSON.parse(data_);
const sources: Source[] = Array.isArray(data['metadatas'])
? data['metadatas']