-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add /{id}/exit/email and /{id}/exit/status routes * add exit options definition * add exitOptions typings * add exitService * add pageExitedOn and cacheService.setExitState * inline documentation * add documentation * add ExitService tests * tidy exit options * tidy exit options * update FormModel.exitOptions type * tidying and adding inline docs * add refactor notes * clear user's state * remove exit confirmation page configurations for now *feat: allow safelist to be parsed as JSON array * save exit response to user's state * use updated state for exit response * test(exit): add test for back link, add test for initialised session * add additional documentation for exiting an initialised form * add step for exit form * add utility property "formPath" to exit request * add formPath to exit request * add form name to render context
- Loading branch information
1 parent
ee316e4
commit c25fed2
Showing
32 changed files
with
1,280 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# Exiting | ||
|
||
This feature allows the user to exit a form. Their data will be sent to the configured URL. It is not the Runner's | ||
responsibility to persist this data. | ||
|
||
You can use this feature alongside [session-initialisation.md](./session-initialisation.md) to allow users to "save and return". | ||
|
||
If the `exitOptions` property is present on the form, the form will be considered as "allowing exits". You must remove this | ||
to disable it. | ||
|
||
If exits are allowed, on all question pages, a button "save and come back later" will be displayed below the continue button. | ||
When the user selects this | ||
|
||
1. They will be taken to a page /{id}/exit/email, where /{id} is the form's ID (i.e. the form's file name), which asks for the user's email address. | ||
2. If the email address is valid, a POST request will be made to the configured URL with the user's data | ||
3. If the POST request was successful (2xx) code, the user will be redirected to a page /{id}/exit/status | ||
- If the persistence API returned with a `redirectUrl` in the response body, the user will be redirected to this URL | ||
- If no redirectUrl was returned, the user will be shown a page detailing the email address it was sent to. | ||
- If the persistence API returned with an `expiry` in the response body, the user will be shown a message detailing how long they have to return | ||
4. The user's cache will then be cleared | ||
|
||
## Configuration | ||
|
||
The exit feature is configured in the form JSON on the `exitOptions` property. | ||
|
||
```json5 | ||
{ | ||
pages: [], | ||
lists: [], | ||
// ..etc | ||
exitOptions: { | ||
url: "your-persistence-api:3005", | ||
format: "WEBHOOK", // can be "WEBHOOK" or "STATE" | ||
}, | ||
} | ||
``` | ||
|
||
`exitOptions.format` can be `WEBHOOK` or `STATE`. This will control in which format the data is sent exitOptions.url. | ||
|
||
## Data sent | ||
|
||
Depending on which `exitOptions.format` is configured for the form, the format the users' answers are in will be different. | ||
|
||
With both formats, the request body will always include `exitState` and `formPath`, which is the form's id. | ||
|
||
```json5 | ||
{ | ||
exitState: { | ||
exitEmailAddress: "[email protected]", // the email address the user entered on /{id}/exit/email | ||
pageExitedOn: "/form-a/your-address", // the page the user chose to exit on | ||
}, | ||
formPath: "/form-a", | ||
//... | ||
} | ||
``` | ||
|
||
### ExitOptions.format - STATE | ||
|
||
This mirrors the data in the user's state for the form that they chose to exit on. For example, if the user is filling out | ||
two forms at once, /form-a and /form-b, if the user chose to exit on /form-a, only data from /form-a will be sent. | ||
|
||
```json5 | ||
{ | ||
exitState: { | ||
exitEmailAddress: "[email protected]", | ||
pageExitedOn: "/test/how-many-people", | ||
}, | ||
progress: ["/test/uk-passport", "/test/how-many-people"], | ||
checkBeforeYouStart: { ukPassport: true }, | ||
applicantDetails: { numberOfApplicants: "1 or fewer" }, | ||
formPath: "/test", | ||
} | ||
``` | ||
|
||
This may be an easier format for the persistence API to parse, however you must convert this data back into the webhook. | ||
|
||
In future, the initialise session and webhook output formats may support this "flatter" format for easier persistence. | ||
Note that this data format does not include information like the page or component titles. | ||
|
||
### ExitOptions.format - WEBHOOK | ||
|
||
This mirrors the same format that data is sent when configuring a webhook output. You may just choose to store the data | ||
as is, until the user decides to return. This makes it simpler calling [session-initialisation.md](./session-initialisation.md). | ||
|
||
```json5 | ||
{ | ||
name: "Digital Form Builder - Runner test", | ||
metadata: {}, | ||
questions: [ | ||
{ | ||
category: "checkBeforeYouStart", | ||
question: "Do you have a UK passport?", | ||
fields: [ | ||
{ | ||
key: "ukPassport", | ||
title: "Do you have a UK passport?", | ||
type: "list", | ||
}, | ||
], | ||
index: 0, | ||
}, | ||
{ | ||
category: "applicantDetails", | ||
question: "How many applicants are there?", | ||
fields: [ | ||
{ | ||
key: "numberOfApplicants", | ||
title: "How many applicants are there?", | ||
type: "list", | ||
answer: "1 or fewer", | ||
}, | ||
], | ||
index: 0, | ||
}, | ||
], | ||
exitState: { | ||
exitEmailAddress: "[email protected]", | ||
pageExitedOn: "/test/how-many-people", | ||
}, | ||
formPath: "/test", | ||
} | ||
``` | ||
|
||
## Persistence API response | ||
|
||
The persistence API (exitOptions.url) should return a 2xx status code if the data was successfully persisted. What | ||
"successfully persisted" means depends on your implementation and requirements. This may mean successfully stored in your database, | ||
or successfully inserted into a queue. | ||
|
||
The response body must be JSON, and can some or none include the following properties: | ||
|
||
- `redirectUrl` - a URL to redirect the user to after the data has been persisted. This can be used to redirect the user to a | ||
"success" page, or to a "hub" or "account" page. | ||
- The redirectUrl must be on the `safelist` (`SAFELIST` environment variable) to allow redirects to it. This is to protect the user from being redirected to an unknown URL. | ||
if the URL is not on the safelist, the user will be shown the runner's success page. | ||
- `expiry` - The ISO date-time string of when the user's data will be deleted. This will be parsed in the format d MMMM yyyy (e.g. 9 July 2024) and shown to the user. | ||
|
||
```json5 | ||
{ | ||
redirectUrl: "https://your-service.service.gov.uk/success", | ||
expiry: "2024-07-09T00:00:00Z", | ||
} | ||
``` | ||
|
||
The user will be shown a generic error page if the request failed to send, or the API responded with a non 2xx code. | ||
|
||
## Initialising session and exiting | ||
|
||
When initialising a session you can configure the `callbackUrl`. This will send data to a different URL from | ||
what is configured in the form JSON's webhook output. | ||
|
||
Currently, when exiting an initialised session, the data will be sent to the URL configured in `exitOptions.url`. It will not be sent to the `callbackUrl`. | ||
|
||
If you need to initialise a session, allow a user to exit, and still be able to identify the user, you should initialise | ||
the session with `metadata`. The metadata should include an identifier so your API can match and merge their data if required. | ||
|
||
When exiting the form, the user's last known page is given to you in the `exitState` object in `pageExitedOn` property, | ||
`pageExitedOn` is given in the format `/{formId}/{pagePath}`, e.g. `/test/uk-passport`. If you want to return the user back | ||
to this page wth initialised sessions, you can use the `redirectPath` option in the session initialisation payload. | ||
Note that in the POST `/session/{formId}` payload, the `redirectPath` is relative to the formId, so you must remove `/{formId}` | ||
from `/test/uk-passport` so the user is redirected to the correct page, e.g. `/uk-passport`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
Feature: Exit | ||
As a user, | ||
I want to be able to exit the service, | ||
so that I can save my progress and return at a later date. | ||
|
||
Background: | ||
Given the form "exit-expiry" exists | ||
|
||
Scenario: Service can be exited with date displayed | ||
When I navigate to the "exit-expiry" form | ||
Then I see "Save and come back later" | ||
When I choose "lisbon" | ||
And I select the button "Save and come back later" | ||
And I enter "[email protected]" for "Enter your email address" | ||
And I select the button "Save and exit" | ||
Then I see "9 July 2024" | ||
And I see "[email protected]" | ||
|
||
|
||
Scenario: A user can start exiting, then go back to the form | ||
When I navigate to the "exit-expiry" form | ||
Then I see "Save and come back later" | ||
When I choose "lisbon" | ||
And I select the button "Save and come back later" | ||
And I go back | ||
Then I see "First page" | ||
|
||
Scenario: An initialised session can be exited | ||
Given the session is initialised for the exit form | ||
When I go to the initialised session URL | ||
And I select the button "Save and come back later" | ||
And I enter "[email protected]" for "Enter your email address" | ||
And I select the button "Save and exit" | ||
Then I see "Your application to exit test has been saved" | ||
# TODO: Mock the API in the e2e process so we can check for correct data sent. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { When } from "@badeball/cypress-cucumber-preprocessor"; | ||
|
||
When("the session is initialised for the exit form", () => { | ||
const url = `${Cypress.env("RUNNER_URL")}/session/exit-expiry`; | ||
cy.request("POST", url, { | ||
options: { | ||
callbackUrl: "http://localhost", | ||
redirectPath: "/second-page", | ||
}, | ||
metadata: { | ||
id: "abcdef", | ||
}, | ||
questions: [ | ||
{ | ||
fields: [ | ||
{ | ||
key: "whichConsulate", | ||
answer: "lisbon", | ||
}, | ||
], | ||
index: 0, | ||
}, | ||
{ | ||
category: "yourDetails", | ||
fields: [ | ||
{ | ||
key: "fullName", | ||
answer: "first last", | ||
}, | ||
], | ||
}, | ||
], | ||
}).then((res) => { | ||
cy.wrap(res.body.token).as("token"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
{ | ||
"metadata": { | ||
"caseType": "generic" | ||
}, | ||
"startPage": "/first-page", | ||
"pages": [ | ||
{ | ||
"title": "First page", | ||
"path": "/first-page", | ||
"components": [ | ||
{ | ||
"name": "whichConsulate", | ||
"options": {}, | ||
"type": "RadiosField", | ||
"title": "which consulate", | ||
"list": "IpQxvK", | ||
"schema": {} | ||
} | ||
], | ||
"next": [ | ||
{ | ||
"path": "/second-page" | ||
} | ||
] | ||
}, | ||
{ | ||
"section": "yourDetails", | ||
"title": "Second page", | ||
"path": "/second-page", | ||
"components": [ | ||
{ | ||
"name": "fullName", | ||
"title": "Your full name", | ||
"options": {}, | ||
"type": "TextField", | ||
"schema": {} | ||
} | ||
], | ||
"next": [ | ||
{ | ||
"path": "/summary" | ||
} | ||
] | ||
}, | ||
{ | ||
"title": "Summary", | ||
"path": "/summary", | ||
"controller": "./pages/summary.js", | ||
"components": [] | ||
} | ||
], | ||
"specialPages": {}, | ||
"lists": [ | ||
{ | ||
"title": "which consulate", | ||
"name": "IpQxvK", | ||
"type": "string", | ||
"items": [ | ||
{ | ||
"text": "lisbon", | ||
"value": "lisbon" | ||
}, | ||
{ | ||
"text": "portimao", | ||
"value": "portimao" | ||
} | ||
] | ||
} | ||
], | ||
"sections": [ | ||
{ | ||
"name": "yourDetails", | ||
"title": "Your details" | ||
} | ||
], | ||
"conditions": [ | ||
{ | ||
"displayName": "isLisbon", | ||
"name": "isLisbon", | ||
"value": { | ||
"name": "isLisbon", | ||
"conditions": [ | ||
{ | ||
"field": { | ||
"name": "whichConsulate", | ||
"type": "RadiosField", | ||
"display": "which consulate" | ||
}, | ||
"operator": "is", | ||
"value": { | ||
"type": "Value", | ||
"value": "lisbon", | ||
"display": "lisbon" | ||
} | ||
} | ||
] | ||
} | ||
}, | ||
{ | ||
"displayName": "isPortimao", | ||
"name": "isPortimao", | ||
"value": { | ||
"name": "isPortimao", | ||
"conditions": [ | ||
{ | ||
"field": { | ||
"name": "whichConsulate", | ||
"type": "RadiosField", | ||
"display": "which consulate" | ||
}, | ||
"operator": "is", | ||
"value": { | ||
"type": "Value", | ||
"value": "portimao", | ||
"display": "portimao" | ||
} | ||
} | ||
] | ||
} | ||
} | ||
], | ||
"fees": [], | ||
"outputs": [], | ||
|
||
"version": 2, | ||
"skipSummary": false, | ||
"exitOptions": { | ||
"format": "WEBHOOK", | ||
"url": "https://61bca17e-fe74-40e0-9c15-a901ad120eca.mock.pstmn.io/exit/expiry" | ||
}, | ||
"name": "exit test" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.