(github.com/companieshouse/ch-account-ui)
This is a Next.js project bootstrapped
with create-next-app
.
- UI Architectural Decisions
- ch-account-ui - github
- forgerock-cloud-config - github
- Forgerock Platform UI Admin - Dev
Ensure you are running the correct verions of Node (v18.8.0) via Node Version Manager
nvm install
Next, install the dependencies.
npm install
If you get a peer dependency issue delete the package-lock.json and run:
npm install --legacy-peer-deps
Then, run the development server:
npm run dev
Open http://localhost:3000 with your browser to see the application.
Open http://localhost:3001 to browse components and pages in Storybook.
The application is based on Next.js. Next handles all the bundling etc as well as routing. It uses a mixture of SSR and CSR and builds to a static site for deployment to and S3 bucket. For more info on Next.js see nextjs.org
Components and features are the same thing as far as react is concerned. We use the terms interchangeably here, but the only difference is that a feature is a component that can be dynamically rendered by a JSON definition instead of explicitly on a React page.
The application uses data from the server-side to determine what elements are displayed on particular pages and builds forms dynamically.
For instance, for the login
page, the server might ask us to render a username and password field but no other
information is present. On our login
screen we also need a page title, some descriptive text and a bit of hint text
applied to the username
field. None of that data exists in the data coming back from the server, so we need some way
of describing those extra elements based on the page we are rendering as well. Given that the server pages can be
changed and updated, we wanted a way to respond to those changes without having to create a new corresponding React page
or modifying an existing one when changes happen server-side. This is why we have a dynamic page / features system.
On the server-side (FIDC), there are page flows called journeys. Each journey consists of stages and each stage maps to
a page on the front end and a stage definition in the /services/stages
folder that describes extra components such as
text headings and body text that should be rendered around the server-sent form elements.
Let's use the password reset journey as an example. When the user navigates to the /password-reset/request/
url, the
React page asks the server-side to initiate the CHResetPassword
journey. This request responds with some data that
includes what form fields we need to present to the user as well as the name of the stage
we are on.
The stage name that is returned maps directly to a file in the
/services/stages
folder. If we take a look at the RESET_PASSWORD_1.js
file, it will look something like this:
const RESET_PASSWORD_1 = (lang, tokens) => [
{
component: 'BrowserTitle',
props: {
title: tokens('RESET_PASSWORD_1.[1].PageHeading.enterYourEmailAddress')
}
},
{
conditional: {
prop: '${notifyToken}',
operator: 'eeq',
value: 'forceUpdate'
},
component: 'NotificationBanner',
dynamicProps: {
title: tokens('SHARED.important'),
heading: tokens('RESET_PASSWORD_1.[0].NotificationBanner.yourPasswordNeedsToBeUpdatedPleaseFollowThe')
}
},
{
component: 'PageHeading',
props: {
children: tokens('RESET_PASSWORD_1.[1].PageHeading.enterYourEmailAddress')
}
},
{
component: 'BodyText',
props: {
children: tokens('RESET_PASSWORD_1.[2].BodyText.enterTheEmailAddressYouUsedToCreateYour')
}
},
{
component: 'DisplayUiElements',
props: {
elementProps: {
IDToken1: {
label: tokens('SHARED.emailAddress'),
type: 'email',
autoComplete: 'email'
}
}
}
},
{
component: 'Button',
props: {
children: tokens('SHARED.continue'),
type: 'submit',
testId: 'submitButton'
}
}
]
export default RESET_PASSWORD_1
When the page gets rendered, the app takes this data and renders the corresponding component. You can see that we can
pass props
and children
to the component. These get spread into the component's props and what you provide here will override a
prop if it clashes with one that is already being passed to the component.
The DisplayUiElements
component specifically handles taking the JSON that the server sent and renders the required
form elements. If you do not include this feature in your stage definition then no form elements the server sends will
be rendered (including hidden ones).
You'll notice in the DisplayUiElements
component we can also pass a prop called elementProps
. This allows you to
pass props to the specific form component that the server has told us to render. The props here are targeted by the ID
of the field.
{
"component": "DisplayUiElements",
"props": {
"elementProps": {
"IDToken1": {
"type": "email",
"autoComplete": "email"
}
}
}
}
In the example above, we are saying that "when rendering the field with the ID IDToken1
set the type
and autoComplete
props to
email
". If we open the /components/general-ui/interaction/InputField.js
component, we can see that those props get used when rendering the
<input>
field.
There are also specific props for iterating and conditionally rendering other components.
conditional: {
prop: '${notifyToken}',
operator: 'eeq',
value: 'associateSuccess'
},
iterator: {
prop: '${companies}',
name: 'company',
index: 'index'
},
Where properties within the stage definition need to be dynamic they are included as a template literal and those props defined as dynamic.
dynamicProps: {
href: '${member.detailsPath}'
},
The CORS configuration in the FIDC dev instance allows development locally against that environment. The UI environment
properties are configured by default this way. Once the app is up and running then you should be able to navigate
through the journeys using localhost:3000
Alternatively Storybook on localhost:3001
allows isolated local
development by mocking the data returned from FIDC in the stories.
Each UI journey maps to a Next.js page which is the route for that journey pages/account/home.js
. The page file
contains all the code needed to initiate that journey or page. The page interfaces with the Forgerock SDK and FIDC using
one of two custom hooks.
useFRFlow
(for journeys )useFRAuth
(for standalone pages)
The page consists of two key components <FeatureDynamicView/>
and <Dynamic/>
you can see how these are used
in the existing page examples. FeatureDynamicView is effectively the page wrapper, handling the header footer and
navigation. Dynamic is the main component which translates the stage definition into the rendered React components.
Any code specific to the journey can be applied in these page files for example setting dynamic links as props to be passed down the component tree and exposed to the page components.
Once the page has been configured you will be able to see the response data coming back from FIDC. Once you know the stage name and other data being returned you can start to create the stage definition file. You can also capture that data and create a new story for the page with mock response data in Storybook (see existing examples) which will allow you to work on a stage in isolation.
Example FIDC response for the initial login journey https://idam.amido.aws.chdev.org/am/json/realms/root/realms/alpha/authenticate?authIndexType=service&authIndexValue=CHWebFiling-Login
{
"authId": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoSW5kZXhWYWx1ZSI6IkNIV2ViRmlsaW5nLUxvZ2luIiwib3RrIjoiOGZhbjRhMmtsYTRvNzViZnM2YWpsM2E3NWsiLCJhdXRoSW5kZXhUeXBlIjoic2VydmljZSIsInJlYWxtIjoiL2FscGhhIiwic2Vzc2lvbklkIjoiKkFBSlRTUUFDTURJQUJIUjVjR1VBQ0VwWFZGOUJWVlJJQUFKVE1RQUNNREUuKmV5SjBlWEFpT2lKS1YxUWlMQ0pqZEhraU9pSktWMVFpTENKaGJHY2lPaUpJVXpJMU5pSjkuWlhsS01HVllRV2xQYVVwTFZqRlJhVXhEU214aWJVMXBUMmxLUWsxVVNUUlJNRXBFVEZWb1ZFMXFWVEpKYVhkcFdWZDRia2xxYjJsYVIyeDVTVzR3TGk1dGJ6ZzBXakpIUWt4dmRrOUdiRFJUZUVsdFVrZDNMbk14V1d4Vk5saGpWSGRSVDAwd09GWm5WMUl0T0ZWemRuTm1jblJZZEVoSGFIUk9XRjluYmtKVE9FaFdiVmhYTW1GeWNXMXRORFZ4Y0ZGRWMwczJhbFZmWlZsVFoxWXlTR1Z2YUZaQ1ZWaENaa1JHYjFGTlVGOXVabXhJVUROc1lXeFJhekl3TXpkNVpVNVhYM0ZhVVMxTUxYVXlVQzFDV21waU1tZE5OR3hJV0RkemNXWkJRVXMwYjJkTE1sZHFSbXh3TkRVNVlpMVVPV2xzUVhWMVkzbFJjMW8xYW1wb1ZGZEJjMUJ5WmpkbU9EbExNbWRET1U1bU1saG5aWE5VZERNNGVsRXdaSHAzYVVjMVNsOXVNMnBLWkhCd2JWRmhWMlpGWWxVdFNIaFJZM0F5Y2pVNFduUnBOamhRU1dwdVRqVnBZVmhNVWpaZlFWVkhSSGRDTUV4cVZrNU1hRlpMT1VkaFdXNUVSV0ZqWDIwdE5YcGFRMUl4UlZveVV6VjZPVGhJWmtKd1JFcEdaSFF4VjNoMVUxcFZkelIxYkdkRmRITkdVMWxMYjJWUVowODNUVlZQYW1kNlJWOTRXRkJaWVRkMFlXbEZSbkppU25wQ1ZHOTBaa3MyVlhab1gzRnBXV05PVVZod2IwUlhiblZ6TjBseldrdDFUR2d0WDBaU2QxWmhNemd0VTB4eGJYcFRNMFI1ZWtwbWExcFRUMWhtVDFwSWFGaElZbGhrYXpnd1kwMTZPR2x6U3pGcGVIcFpRM0JDVFhWaU9GOUNXbFU1TFdsVU9FbFFXbEpDUVY5clVWZGhWVEUyWkVFdFZWUjVXa3BuVEZOd09WUkhiRmhYVjNnd05XeDFaSHAzT0hOMVVETjBMV28xTW1WRFEwczRaMEpCYW1aUFpsSTVRVEJMV1dOUGVVZDZNM2RmUldkVk0zbHdkR3N0TVUxak9XcHNWMkYwWHpoRFdqUlZTQzFKVEdKcFFubDFRWFpuVm5KdkxXbDRWR1YxZFdRMWFXZ3lYMVJVZFhRM1JGbDBWbE5HUkRreGRIVkRlVlpsWnpacVRWcGxTRkYxV1V4clpURmZMVlpXVTBKUVZXUkhiR3d0ZURObVExRm9ibTFEWVV4blpqRnBNV0ZyUW5CUE5ucFRUMmRxYjNWM1VHcFZhRUZIUTA1WFZqa3hVV0ZzT0RKeFRsbGtRMUIzVEdkeGEwSkZMbWRzUjAxaWRHVlJkblp0T0dsNlVURm9ZMWhHYzBFLjROUEEtUTRBc0NTaVpNbGJoTkxpZFdhYmVVZ3ZXLUtoSmJVaVNiR29IM28iLCJleHAiOjE2MzA5NDE1NzIsImlhdCI6MTYzMDk0MTI3Mn0.gqpAq8mgVnmtOfVnYWf_ILdt2vIj4IyV5xAOo-dRi1g",
"callbacks": [
{
"type": "NameCallback",
"output": [
{
"name": "prompt",
"value": "User Name"
}
],
"input": [
{
"name": "IDToken1",
"value": ""
}
],
"_id": 0
},
{
"type": "PasswordCallback",
"output": [
{
"name": "prompt",
"value": "Password"
}
],
"input": [
{
"name": "IDToken2",
"value": ""
}
],
"_id": 1
}
],
"stage": "EWF_LOGIN_1",
"header": "Sign In (EWF)",
"description": "New here? <a href=\"#/service/Registration\">Create an account</a><br><a href=\"#/service/ForgottenUsername\">Forgot username?</a><a href=\"#/service/ResetPassword\"> Forgot password?</a>"
}
The UI components are held within the components
folder and are split into logical catagories. Components which
are to be used directly in the stage definitions need to be added to the componentMap services/componentMap.js
.
These components will receive any props passed in the stage definition or any props passed into the <Dynamic/>
component at the page level (the React dev tools are useful here for inspecting the props)
The application relies heavily on the GDS govuk-frontend
library for styling so application specific styling is
minimal and can be found in css/global.scss
. Most of the presentational components are based on
the GDS markup examples. The govuk-frontend
js dependencies are
loaded in _app.js
as a static script.
Unit/Integration tests are configured using Jest and React testing library. Test files .test.js
sit next to the
components in the folder structure. The tests are set to run on commit along with the linting and package audit
using husky to configure the git hooks. They can also be run standalone
with npm test
As the application is only a frontend for the FIDC UI the integration level testing is handled by a
separate application using
selenium https://github.com/companieshouse/taf-idam.
Linting in configured in .eslintrc
and uses the standard linting rules extended with eslint-config-next
In order to support multiple language the application content is tokenised. The WithLang
provider ensures that the current language selection is passed into the component tree as the lang
prop.
The tokens are split into two files.
services/lang/content-tokens.json
- Static content used in the stage definitionsservices/lang/tokens.json
- Dynamic tokens mainly passed from FIDC as error tokens
The dynamic tokens are name spaced to allow for more flexibility. The tokens are matched in the following order based on
the token
returned from FIDC, the Journeys name space and the field name:
${journeyNamespace}_${token}(${fieldName})
${journeyNamespace}_${token}
${token}(${fieldName})
${token}
This structure allows for the most flexibility while still being relatively simple to maintain. It should also make the extraction of the content to a CMS at later date trivial....