From 39628a56013298170dd6022206a4b65a48739cc8 Mon Sep 17 00:00:00 2001
From: Jonas Brunvoll Larsson <59939294+jonasbrunvoll@users.noreply.github.com>
Date: Wed, 25 Sep 2024 20:21:56 +0200
Subject: [PATCH] chore: rewrite ticket control to use one state machine for
all forms (#353)
* Rewrite to use only one state machine for all ticket control forms
* Add temporary success message.
* Specifies which inputs that should be validated when submitting a form from tricketControll
* Update validation for travelGuarantee.
---
src/page-modules/contact/index.ts | 11 +-
src/page-modules/contact/layouts/index.ts | 5 -
.../layouts/ticket-control-page-layout.tsx | 63 ---
.../means-of-transport-form-machine.ts | 5 +-
.../complaint/complaintFormMachine.ts | 193 ---------
.../contact/ticket-control/events.ts | 1 +
.../feedback/feedbackformMachine.ts | 169 --------
.../index.tsx => forms/feeComplaintForm.tsx} | 39 +-
.../index.tsx => forms/feedbackForm.tsx} | 38 +-
.../index.tsx => forms/postponePayment.tsx} | 44 +-
.../contact/ticket-control/index.ts | 1 -
.../contact/ticket-control/index.tsx | 76 ++++
.../postponePaymentFormMachine.ts | 118 ------
.../ticket-control-form-machine.ts | 395 ++++++++++++++++++
.../travel-guarantee/form-selector.tsx | 2 +-
.../travelGuaranteeFormMachine.ts | 5 +-
.../validation/commonInputValidator.ts | 8 +-
.../travelGuaranteeInputValidator.ts | 6 +-
.../ticket-control/complaint/index.tsx | 27 --
.../contact/ticket-control/feedback/index.tsx | 27 --
src/pages/contact/ticket-control/index.tsx | 4 +-
.../ticket-control/postpone-payment/index.tsx | 28 --
src/translations/pages/contact.ts | 28 +-
23 files changed, 540 insertions(+), 753 deletions(-)
delete mode 100644 src/page-modules/contact/layouts/ticket-control-page-layout.tsx
delete mode 100644 src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts
delete mode 100644 src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts
rename src/page-modules/contact/ticket-control/{complaint/index.tsx => forms/feeComplaintForm.tsx} (93%)
rename src/page-modules/contact/ticket-control/{feedback/index.tsx => forms/feedbackForm.tsx} (89%)
rename src/page-modules/contact/ticket-control/{postpone-paymnet/index.tsx => forms/postponePayment.tsx} (74%)
delete mode 100644 src/page-modules/contact/ticket-control/index.ts
create mode 100644 src/page-modules/contact/ticket-control/index.tsx
delete mode 100644 src/page-modules/contact/ticket-control/postpone-paymnet/postponePaymentFormMachine.ts
create mode 100644 src/page-modules/contact/ticket-control/ticket-control-form-machine.ts
delete mode 100644 src/pages/contact/ticket-control/complaint/index.tsx
delete mode 100644 src/pages/contact/ticket-control/feedback/index.tsx
delete mode 100644 src/pages/contact/ticket-control/postpone-payment/index.tsx
diff --git a/src/page-modules/contact/index.ts b/src/page-modules/contact/index.ts
index e28c6868..de84e695 100644
--- a/src/page-modules/contact/index.ts
+++ b/src/page-modules/contact/index.ts
@@ -1,14 +1,7 @@
-export {
- ContactPageLayout,
- type ContactPageLayoutProps,
- TicketControlPageLayout,
- type TicketControlPageLayoutProps,
-} from './layouts';
+export { ContactPageLayout, type ContactPageLayoutProps } from './layouts';
-export { PostponePaymentForm } from './ticket-control';
export { RefundForm } from './travel-guarantee';
-export { FeeComplaintForm } from './ticket-control/complaint';
export { type Line } from './server/journey-planner/validators';
export { shouldShowContactPage } from './utils';
-export { FeedbackForm } from './ticket-control/feedback';
export { MeansOfTransportContent } from './means-of-transport';
+export { default as TicketControlPageContent } from './ticket-control';
diff --git a/src/page-modules/contact/layouts/index.ts b/src/page-modules/contact/layouts/index.ts
index 5867b8c5..f15aff2c 100644
--- a/src/page-modules/contact/layouts/index.ts
+++ b/src/page-modules/contact/layouts/index.ts
@@ -2,8 +2,3 @@ export {
default as ContactPageLayout,
type ContactPageLayoutProps,
} from './contact-page-layout';
-
-export {
- default as TicketControlPageLayout,
- type TicketControlPageLayoutProps,
-} from './ticket-control-page-layout';
diff --git a/src/page-modules/contact/layouts/ticket-control-page-layout.tsx b/src/page-modules/contact/layouts/ticket-control-page-layout.tsx
deleted file mode 100644
index 401d0790..00000000
--- a/src/page-modules/contact/layouts/ticket-control-page-layout.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { PropsWithChildren } from 'react';
-import { SectionCard } from '../components/section-card';
-import { useRouter } from 'next/router';
-import { PageText, useTranslation } from '@atb/translations';
-import { Checkbox } from '../components/input/checkbox';
-import { Input } from '../components/input';
-import { RadioInput } from '../components/input/radio';
-
-export type TicketControlPageLayoutProps = PropsWithChildren<{
- title: string;
-}>;
-
-function TicketControlPageLayout({ children }: TicketControlPageLayoutProps) {
- const { t } = useTranslation();
- const router = useRouter();
-
- return (
-
-
-
-
- router.push('/contact/ticket-control/complaint', undefined, {
- shallow: true,
- })
- }
- name="complaint"
- />
-
- router.push(
- '/contact/ticket-control/postpone-payment',
- undefined,
- {
- shallow: true,
- },
- )
- }
- name="postpone-payment"
- />
-
- router.push('/contact/ticket-control/feedback', undefined, {
- shallow: true,
- })
- }
- name="feedback"
- />
-
-
-
- {children}
-
- );
-}
-
-export default TicketControlPageLayout;
diff --git a/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts b/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts
index d9710ee4..af9a5fc1 100644
--- a/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts
+++ b/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts
@@ -60,7 +60,10 @@ export const meansOfTransportFormMachine = setup({
events: meansOfTransportFormEvents,
},
guards: {
- validateInputs: ({ context }) => commonInputValidator(context),
+ validateInputs: ({ context }) => {
+ context.errorMessages = commonInputValidator(context);
+ return Object.keys(context.errorMessages).length > 0 ? false : true;
+ },
},
actions: {
onInputChange: assign(({ context, event }) => {
diff --git a/src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts b/src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts
deleted file mode 100644
index 900893e5..00000000
--- a/src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-import { assign, fromPromise, setup } from 'xstate';
-import { commonInputValidator, InputErrorMessages } from '../../validation';
-import { convertFilesToBase64 } from '../../utils';
-import { ticketControlFormEvents } from '../events';
-
-type APIParams = {
- feeNumber: string;
- appPhoneNumber: string | undefined;
- customerNumber: string | undefined;
- travelCardNumber: string | undefined;
- feedback: string;
- firstName: string;
- lastName: string;
- address: string;
- postalCode: string;
- city: string;
- email: string;
- phoneNumber: string;
- bankAccountNumber: string;
- IBAN: string;
- SWIFT: string;
- attachments?: File[];
-};
-
-type ContextProps = {
- agreesFirstAgreement: boolean;
- agreesSecondAgreement: boolean;
- isAppTicketStorageMode: boolean;
- hasInternationalBankAccount: boolean;
- errorMessages: InputErrorMessages;
-} & APIParams;
-
-export const formMachine = setup({
- types: {
- context: {} as ContextProps,
- events: ticketControlFormEvents,
- },
- guards: {
- validateInputs: ({ context }) => commonInputValidator(context),
- },
- actions: {
- cleanErrorMessages: assign({
- errorMessages: () => ({}),
- }),
-
- onInputChange: assign(({ context, event }) => {
- if (event.type === 'ON_INPUT_CHANGE') {
- const { inputName, value } = event;
- // Remove errorMessages if any
- context.errorMessages[inputName] = [];
- return {
- ...context,
- [inputName]: value,
- };
- }
- return context;
- }),
- },
- actors: {
- callAPI: fromPromise(
- async ({
- input: {
- feeNumber,
- appPhoneNumber,
- customerNumber,
- travelCardNumber,
- feedback,
- firstName,
- lastName,
- address,
- postalCode,
- city,
- email,
- phoneNumber,
- bankAccountNumber,
- IBAN,
- SWIFT,
- attachments,
- },
- }: {
- input: APIParams;
- }) => {
- const base64EncodedAttachments = await convertFilesToBase64(
- attachments || [],
- );
- return await fetch('/api/contact/ticket-control', {
- method: 'POST',
- body: JSON.stringify({
- feeNumber: feeNumber,
- appPhoneNumber: appPhoneNumber,
- customerNumber: customerNumber,
- travelCardNumber: travelCardNumber,
- additionalInfo: feedback,
- firstName: firstName,
- lastName: lastName,
- address: address,
- postalCode: postalCode,
- city: city,
- email: email,
- phoneNumber: phoneNumber,
- bankAccountNumber: bankAccountNumber,
- IBAN: IBAN,
- SWIFT: SWIFT,
- attachments: base64EncodedAttachments,
- }),
- })
- .then((response) => {
- // throw an error to force onError
- if (!response.ok) throw new Error('Failed to call API');
- return response.ok;
- })
- .catch((error) => {
- throw error;
- });
- },
- ),
- },
-}).createMachine({
- id: 'feeComplaintForm',
- initial: 'editing',
- context: {
- feeNumber: '',
- appPhoneNumber: undefined,
- customerNumber: undefined,
- travelCardNumber: undefined,
- feedback: '',
- firstName: '',
- lastName: '',
- address: '',
- postalCode: '',
- city: '',
- email: '',
- phoneNumber: '',
- bankAccountNumber: '',
- IBAN: '',
- SWIFT: '',
- agreesFirstAgreement: false,
- agreesSecondAgreement: false,
- hasInternationalBankAccount: false,
- isAppTicketStorageMode: true,
- errorMessages: {},
- },
- states: {
- editing: {
- entry: 'cleanErrorMessages',
- on: {
- ON_INPUT_CHANGE: {
- actions: 'onInputChange',
- },
- VALIDATE: {
- guard: 'validateInputs',
- target: 'submitting',
- },
- },
- },
-
- submitting: {
- invoke: {
- src: 'callAPI',
- input: ({ context }) => ({
- feeNumber: context.feeNumber,
- appPhoneNumber: context.appPhoneNumber,
- customerNumber: context.customerNumber,
- travelCardNumber: context.travelCardNumber,
- feedback: context.feedback,
- firstName: context.firstName,
- lastName: context.lastName,
- address: context.address,
- postalCode: context.postalCode,
- city: context.city,
- email: context.email,
- phoneNumber: context.phoneNumber,
- bankAccountNumber: context.bankAccountNumber,
- IBAN: context.IBAN,
- SWIFT: context.SWIFT,
- attachments: context.attachments,
- }),
-
- onDone: {
- target: 'success',
- },
-
- onError: {
- target: 'editing',
- },
- },
- },
-
- success: {
- type: 'final',
- },
- },
-});
diff --git a/src/page-modules/contact/ticket-control/events.ts b/src/page-modules/contact/ticket-control/events.ts
index ee68ad7b..f0f342ca 100644
--- a/src/page-modules/contact/ticket-control/events.ts
+++ b/src/page-modules/contact/ticket-control/events.ts
@@ -3,6 +3,7 @@ import { commonEvents } from '../commoneEvents';
const ticketControlSpecificFormEvents = {} as {
type: 'ON_INPUT_CHANGE';
inputName:
+ | 'formType'
| 'feeNumber'
| 'invoiceNumber'
| 'appPhoneNumber'
diff --git a/src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts b/src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts
deleted file mode 100644
index bd89555b..00000000
--- a/src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-import { TransportModeType } from '@atb-as/config-specs';
-import { Line } from '../../server/journey-planner/validators';
-import { assign, fromPromise, setup } from 'xstate';
-import { commonInputValidator, InputErrorMessages } from '../../validation';
-import {
- convertFilesToBase64,
- getCurrentDateString,
- getCurrentTimeString,
-} from '../../utils';
-import { ticketControlFormEvents } from '../events';
-
-type APIParams = {
- transportMode: TransportModeType | undefined;
- line: Line | undefined;
- fromStop: Line['quays'][0] | undefined;
- toStop: Line['quays'][0] | undefined;
- date: string;
- plannedDepartureTime: string;
- feedback: string;
- firstName: string;
- lastName: string;
- email: string;
- attachments?: File[];
-};
-
-type ContextProps = {
- errorMessages: InputErrorMessages;
-} & APIParams;
-
-export const formMachine = setup({
- types: {
- context: {} as ContextProps,
- events: ticketControlFormEvents,
- },
- guards: {
- validateInputs: ({ context }) => commonInputValidator(context),
- },
- actions: {
- onInputChange: assign(({ context, event }) => {
- if (event.type === 'ON_INPUT_CHANGE') {
- const { inputName, value } = event;
- // Remove errorMessages if any
- context.errorMessages[inputName] = [];
- return {
- ...context,
- [inputName]: value,
- };
- }
- return context;
- }),
-
- cleanErrorMessages: assign({
- errorMessages: () => ({}),
- }),
- },
- actors: {
- callAPI: fromPromise(
- async ({
- input: {
- transportMode,
- line,
- fromStop,
- toStop,
- date,
- plannedDepartureTime,
- feedback,
- firstName,
- lastName,
- email,
- attachments,
- },
- }: {
- input: APIParams;
- }) => {
- const base64EncodedAttachments = await convertFilesToBase64(
- attachments || [],
- );
- return await fetch('/api/contact/ticket-control', {
- method: 'POST',
- body: JSON.stringify({
- transportMode: transportMode,
- line: line?.id,
- fromStop: fromStop?.id,
- toStop: toStop?.id,
- date: date,
- plannedDepartureTime: plannedDepartureTime,
- feedback: feedback,
- firstName: firstName,
- lastName: lastName,
- email: email,
- attachments: base64EncodedAttachments,
- }),
- })
- .then((response) => {
- // throw an error to force onError
- if (!response.ok) throw new Error('Failed to call API');
- return response.ok;
- })
- .catch((error) => {
- console.log(error);
- throw error;
- });
- },
- ),
- },
-}).createMachine({
- /** @xstate-layout N4IgpgJg5mDOIC5QDMyQEYEMDGBrAYgPYBOAtgHQAuxmAbmADYCyhEYAxLGJQCo33NWYANoAGALqJQAB0KwAlpXmEAdlJAAPRAFoAjADYATOV0BOAKwBmXYfMAaEAE9Eu8+QDsugCwAOL5fdzAF8gh1QMHAISCgZ5FQ4uSgAZOJEJdVkFJVV1LQRbHw9Rd1FrWwdnfN1yfVFzL30fQJCwtAgsPCIycjZpTGJKAFdiMCTCbExslU5uABEwPoHh0fHJ5RUxSSQQTMV13J1dS0sTUVcjeycXdy9yUtNRC5aQcPbIrop+4nlaTAYxiZTGaUACCxG+v3+qymmwycj2OW2eW0lh8hVM3ke5SuCEshn0RTKwWeKiE8G2rw6UTIcKy+yROn8hWOpj8AUulW0ng8PnM51sz0p72iVH4jBYbFpCLUDIQKIe5BZbMCFRcbk8vn8zVCLzaVI+5Fi8SlUwOCH0pnIPn0eJ8Ng5iEM1Vq9Ua2taEU6It6-SGIwBa0RMnhptlhlElt5pnZqoQBgJNh8DyeOqFXu6Xx+fwDoeDdKDmh0ln0CYxXixDrjPlEivdus91IosEG6FIikoJvpoGR1nIvlEDSala8DUVfPMKZCQA */
- id: 'feedbackForm',
- initial: 'editing',
- context: {
- transportMode: undefined,
- line: undefined,
- fromStop: undefined,
- toStop: undefined,
- date: getCurrentDateString(),
- plannedDepartureTime: getCurrentTimeString(),
- feedback: '',
- firstName: '',
- lastName: '',
- email: '',
- errorMessages: {},
- },
-
- states: {
- editing: {
- entry: 'cleanErrorMessages',
- on: {
- ON_INPUT_CHANGE: {
- actions: 'onInputChange',
- },
- VALIDATE: {
- guard: 'validateInputs',
- target: 'submitting',
- },
- },
- },
-
- submitting: {
- invoke: {
- src: 'callAPI',
- input: ({ context }) => ({
- transportMode: context.transportMode,
- line: context.line,
- fromStop: context.fromStop,
- toStop: context.toStop,
- date: context.date,
- plannedDepartureTime: context.plannedDepartureTime,
- feedback: context.feedback,
- firstName: context.firstName,
- lastName: context.lastName,
- email: context.email,
- attachments: context.attachments,
- }),
-
- onDone: {
- target: 'success',
- },
-
- onError: {
- target: 'editing',
- },
- },
- },
-
- success: {
- type: 'final',
- },
- },
-});
diff --git a/src/page-modules/contact/ticket-control/complaint/index.tsx b/src/page-modules/contact/ticket-control/forms/feeComplaintForm.tsx
similarity index 93%
rename from src/page-modules/contact/ticket-control/complaint/index.tsx
rename to src/page-modules/contact/ticket-control/forms/feeComplaintForm.tsx
index 23327611..00acc161 100644
--- a/src/page-modules/contact/ticket-control/complaint/index.tsx
+++ b/src/page-modules/contact/ticket-control/forms/feeComplaintForm.tsx
@@ -1,35 +1,23 @@
-import { FormEventHandler, useState } from 'react';
import style from '../../contact.module.css';
-import { Button } from '@atb/components/button';
import { Input } from '../../components/input';
import { SectionCard } from '../../components/section-card';
import { PageText, TranslatedString, useTranslation } from '@atb/translations';
-import { useMachine } from '@xstate/react';
-import { formMachine } from './complaintFormMachine';
import { Checkbox } from '../../components/input/checkbox';
import { Typo } from '@atb/components/typography';
import { RadioInput } from '../../components/input/radio';
import { Textarea } from '../../components/input/textarea';
import ErrorMessage from '../../components/input/error-message';
import { FileInput } from '../../components/input/file';
-import { input } from '@testing-library/user-event/dist/cjs/event/input.js';
+import { ContextProps } from '../ticket-control-form-machine';
+import { ticketControlFormEvents } from '../events';
-export const FeeComplaintForm = () => {
- const { t } = useTranslation();
- const [state, send] = useMachine(formMachine);
-
- // Local state to force re-render to display errors.
- const [forceRerender, setForceRerender] = useState(false);
-
- const onSubmit: FormEventHandler = async (e) => {
- e.preventDefault();
- send({ type: 'VALIDATE' });
+type FeeComplaintFormProps = {
+ state: { context: ContextProps };
+ send: (event: typeof ticketControlFormEvents) => void;
+};
- // Force a re-render with dummy state.
- if (Object.keys(state.context.errorMessages).length > 0) {
- setForceRerender(!forceRerender);
- }
- };
+export const FeeComplaintForm = ({ state, send }: FeeComplaintFormProps) => {
+ const { t } = useTranslation();
const FirstAgreement = () => {
return (
@@ -119,7 +107,7 @@ export const FeeComplaintForm = () => {
{state.context.agreesFirstAgreement && }
{state.context.agreesSecondAgreement && (
-
+
)}
);
diff --git a/src/page-modules/contact/ticket-control/feedback/index.tsx b/src/page-modules/contact/ticket-control/forms/feedbackForm.tsx
similarity index 89%
rename from src/page-modules/contact/ticket-control/feedback/index.tsx
rename to src/page-modules/contact/ticket-control/forms/feedbackForm.tsx
index a3265fab..f2ec5c06 100644
--- a/src/page-modules/contact/ticket-control/feedback/index.tsx
+++ b/src/page-modules/contact/ticket-control/forms/feedbackForm.tsx
@@ -1,38 +1,27 @@
-import { FormEventHandler, useState } from 'react';
-import { Button } from '@atb/components/button';
import { SectionCard } from '../../components/section-card';
import { ComponentText, PageText, useTranslation } from '@atb/translations';
import { useLines } from '../../lines/use-lines';
import { TransportModeType } from '@atb-as/config-specs';
import { Input } from '../../components/input';
-import { formMachine } from './feedbackformMachine';
-import { useMachine } from '@xstate/react';
import { Line } from '../..';
import { Textarea } from '../../components/input/textarea';
import Select from '../../components/input/select';
import { Typo } from '@atb/components/typography';
import { FileInput } from '../../components/input/file';
+import { ticketControlFormEvents } from '../events';
+import { ContextProps } from '../ticket-control-form-machine';
-export const FeedbackForm = () => {
+type FeedbackFormProps = {
+ state: { context: ContextProps };
+ send: (event: typeof ticketControlFormEvents) => void;
+};
+
+export const FeedbackForm = ({ state, send }: FeedbackFormProps) => {
const { t } = useTranslation();
const { getLinesByMode, getQuaysByLine } = useLines();
- const [state, send] = useMachine(formMachine);
-
- // Local state to force re-render to display errors.
- const [forceRerender, setForceRerender] = useState(false);
-
- const onSubmit: FormEventHandler = async (e) => {
- e.preventDefault();
- send({ type: 'VALIDATE' });
-
- // Force a re-render with dummy state.
- if (Object.keys(state.context.errorMessages).length > 0) {
- setForceRerender(!forceRerender);
- }
- };
return (
-