Skip to content
28 changes: 3 additions & 25 deletions aselo-webchat-react-app/src/components/PreEngagementFormPhase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ import { Button } from '@twilio-paste/core/button';
import { useDispatch, useSelector } from 'react-redux';
import { Text } from '@twilio-paste/core/text';

import { sessionDataHandler } from '../sessionDataHandler';
import { addNotification, changeEngagementPhase, updatePreEngagementDataField } from '../store/actions/genericActions';
import { initSession } from '../store/actions/initActions';
import { AppState, EngagementPhase } from '../store/definitions';
import { submitAndInitChatThunk, updatePreEngagementDataField } from '../store/actions/genericActions';
import { AppState } from '../store/definitions';
import { Header } from './Header';
import { notifications } from '../notifications';
import { NotificationBar } from './NotificationBar';
import { fieldStyles, titleStyles, formStyles } from './styles/PreEngagementFormPhase.styles';
import LocalizedTemplate from '../localization/LocalizedTemplate';
Expand All @@ -36,8 +33,6 @@ export const PreEngagementFormPhase = () => {
const { preEngagementFormDefinition } = useSelector((state: AppState) => state.config);
const dispatch = useDispatch();

const { friendlyName } = preEngagementData;

const getItem = (inputName: string) => preEngagementData[inputName] ?? {};
const setItemValue = (payload: { name: string; value: string | boolean }) => {
dispatch(updatePreEngagementDataField(payload));
Expand All @@ -46,24 +41,7 @@ export const PreEngagementFormPhase = () => {

const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
dispatch(changeEngagementPhase({ phase: EngagementPhase.Loading }));
try {
const data = await sessionDataHandler.fetchAndStoreNewSession({
formData: {
...preEngagementData,
friendlyName,
},
});
dispatch(
initSession({
token: data.token,
conversationSid: data.conversationSid,
}),
);
} catch (err) {
dispatch(addNotification(notifications.failedToInitSessionNotification((err as Error).message)));
dispatch(changeEngagementPhase({ phase: EngagementPhase.PreEngagementForm }));
}
await dispatch(submitAndInitChatThunk() as any);
};

if (!preEngagementFormDefinition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ describe('Pre Engagement Form Phase', () => {

expect(fetchAndStoreNewSessionSpy).toHaveBeenCalledWith({
formData: {
friendlyName: { value: name, error: null, dirty: true },
query: { value: query, error: null, dirty: true },
email: { value: email, error: null, dirty: true },
friendlyName: name,
query,
email,
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ const validateEmailPattern = ({
return null;
}

const matches = (value as string).match(EMAIL_PATTERN);
if (!value) {
return null;
}

const matches = (value as string)?.match(EMAIL_PATTERN);

if (Boolean(matches?.length)) {
return null;
Expand Down
9 changes: 9 additions & 0 deletions aselo-webchat-react-app/src/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ const failedToInitSessionNotification = (error: string): Notification => ({
type: 'error',
});

const formValidationErrorNotification = (): Notification => ({
id: `FailedToInitSessionNotification`,
dismissible: true,
message: 'Some fields contain errors',
type: 'error',
timeout: 5000,
});

export const notifications = {
fileAttachmentAlreadyAttachedNotification,
fileAttachmentInvalidSizeNotification,
Expand All @@ -106,4 +114,5 @@ export const notifications = {
fileDownloadInvalidTypeNotification,
noConnectionNotification,
failedToInitSessionNotification,
formValidationErrorNotification,
};
50 changes: 50 additions & 0 deletions aselo-webchat-react-app/src/store/actions/genericActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import {
} from './actionTypes';
import { MESSAGES_LOAD_COUNT } from '../../constants';
import { validateInput } from '../../components/forms/formInputs/validation';
import { sessionDataHandler } from '../../sessionDataHandler';
import { getDefaultValue } from '../../components/forms/formInputs';
import { initSession } from './initActions';
import { notifications } from '../../notifications';

export function changeEngagementPhase({ phase }: { phase: EngagementPhase }) {
return {
Expand Down Expand Up @@ -136,3 +140,49 @@ export const updatePreEngagementDataField = ({
});
};
};

const getInitialItem = (definition: PreEngagementFormItem): PreEngagementDataItem => ({
error: null,
dirty: false,
value: getDefaultValue(definition),
});

export const submitAndInitChatThunk = (): ThunkAction<void, AppState, unknown, AnyAction> => {
return async (dispatch, getState) => {
const state = getState();
const definition = state.config.preEngagementFormDefinition?.fields || [];
const data = definition.reduce<PreEngagementData>((accum, def) => {
const item = state.session.preEngagementData[def.name];
const error = validateInput({ definition: def, value: item?.value });
return { ...accum, [def.name]: { ...(item || getInitialItem(def)), error } };
}, {});

dispatch(updatePreEngagementData(data));

const hasError = Object.values(data).some(i => Boolean(i.error));
if (hasError) {
dispatch(addNotification(notifications.formValidationErrorNotification()));
return;
}

dispatch(changeEngagementPhase({ phase: EngagementPhase.Loading }));
try {
const preEngagementDataValues = Object.entries(data).reduce(
(accum, [name, { value }]) => ({ ...accum, [name]: value }),
{},
);
const sessionData = await sessionDataHandler.fetchAndStoreNewSession({
formData: preEngagementDataValues,
});
dispatch(
initSession({
token: sessionData.token,
conversationSid: sessionData.conversationSid,
}),
);
} catch (err) {
dispatch(addNotification(notifications.failedToInitSessionNotification((err as Error).message)));
dispatch(changeEngagementPhase({ phase: EngagementPhase.PreEngagementForm }));
}
};
};
2 changes: 0 additions & 2 deletions aselo-webchat-react-app/src/store/actions/initActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
ACTION_LOAD_CONFIG_REQUEST,
ACTION_LOAD_CONFIG_SUCCESS,
ACTION_LOAD_CONFIG_FAILURE,
ACTION_UPDATE_PRE_ENGAGEMENT_DATA,
} from './actionTypes';
import { addNotification, changeEngagementPhase } from './genericActions';
import { MESSAGES_LOAD_COUNT } from '../../constants';
Expand All @@ -38,7 +37,6 @@ import { sessionDataHandler } from '../../sessionDataHandler';
import { createParticipantNameMap } from '../../utils/participantNameMap';
import { getDefinitionVersion } from '../../services/configService';
import type { AppState } from '../store';
import { getDefaultValue } from '../../components/forms/formInputs';

export const initConfigThunk = (
config: Omit<ConfigState, 'preEngagementForm'>,
Expand Down
90 changes: 40 additions & 50 deletions lambdas/account-scoped/src/webchatAuthentication/initWebchat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,48 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import { getAccountAuthToken, getTwilioClient } from '@tech-matters/twilio-configuration';
import { getTwilioClient } from '@tech-matters/twilio-configuration';
import type { AccountScopedHandler, HttpError } from '../httpTypes';
import type { AccountSID, ConversationSID } from '@tech-matters/twilio-types';
import { isErr, newErr, newOk, Result } from '../Result';

import { createToken, TOKEN_TTL_IN_SECONDS } from './createToken';

const contactWebchatOrchestrator = async (
accountSid: AccountSID,
addressSid: string,
formData: any,
customerFriendlyName: string,
): Promise<Result<HttpError, { conversationSid: ConversationSID; identity: string }>> => {
const contactWebchatOrchestrator = async ({
addressSid,
accountSid,
formData,
customerFriendlyName,
}: {
accountSid: AccountSID;
addressSid: string;
formData: Record<string, any>;
customerFriendlyName: string;
}): Promise<
Result<HttpError, { conversationSid: ConversationSID; identity: string }>
> => {
console.info('Calling Webchat Orchestrator');

const params = new URLSearchParams();
params.append('AddressSid', addressSid);
params.append('ChatFriendlyName', 'Webchat widget');
params.append('CustomerFriendlyName', customerFriendlyName);
params.append(
'PreEngagementData',
JSON.stringify({
...formData,
friendlyName: customerFriendlyName,
}),
);
const authToken = await getAccountAuthToken(accountSid);
try {
const client = await getTwilioClient(accountSid);
const orchestratorResponse = await client.flexApi.v2.webChannels.create({
customerFriendlyName,
addressSid,
preEngagementData: JSON.stringify(formData),
uiVersion: process.env.WEBCHAT_VERSION || '1.0.0',
chatFriendlyName: 'Webchat widget',
});
console.info('Webchat Orchestrator successfully called', orchestratorResponse);

const res = await fetch(`https://flex-api.twilio.com/v2/WebChats`, {
method: 'POST',
headers: {
Authorization:
'Basic ' + Buffer.from(accountSid + ':' + authToken).toString('base64'),
'ui-version': process.env.WEBCHAT_VERSION || '1.0.0',
},
body: params,
});
if (!res.ok) {
const bodyError = await res.text();
console.error(
'Error calling https://flex-api.twilio.com/v2/WebChats',
accountSid,
bodyError,
);
const { conversationSid, identity } = orchestratorResponse;

return newOk({
conversationSid: conversationSid as ConversationSID,
identity,
});
} catch (err) {
const bodyError = err instanceof Error ? err.message : String(err);
console.error('Error creating web channel', accountSid, bodyError);
return newErr({
message: bodyError,
error: {
Expand All @@ -66,16 +64,6 @@ const contactWebchatOrchestrator = async (
},
});
}
const orchestratorResponse = (await res.json()) as any;

console.info('Webchat Orchestrator successfully called', orchestratorResponse);

const { conversation_sid: conversationSid, identity } = orchestratorResponse;

return newOk({
conversationSid,
identity,
});
};

const sendUserMessage = async (
Expand Down Expand Up @@ -116,18 +104,20 @@ const sendWelcomeMessage = async (
export const initWebchatHandler: AccountScopedHandler = async (request, accountSid) => {
console.info('Initiating webchat', accountSid);

const customerFriendlyName = request.body?.formData?.friendlyName || 'Customer';
const formData = JSON.parse(request.body?.PreEngagementData);
const customerFriendlyName =
formData?.friendlyName || request.body?.CustomerFriendlyName || 'Customer';

let conversationSid: ConversationSID;
let identity;

// Hit Webchat Orchestration endpoint to generate conversation and get customer participant sid
const result = await contactWebchatOrchestrator(
const result = await contactWebchatOrchestrator({
accountSid,
'IG1ba46f2d6828b42ddd363f5045138044', // Obvs needs to be SSM parameter
request.body?.formData,
addressSid: 'IG1ba46f2d6828b42ddd363f5045138044', // Obvs needs to be SSM parameter
formData,
customerFriendlyName,
);
});
if (isErr(result)) {
return result;
}
Expand Down
Loading