diff --git a/integration/log-in.spec.ts b/integration/log-in.spec.ts index 01584c90c..95683ad74 100644 --- a/integration/log-in.spec.ts +++ b/integration/log-in.spec.ts @@ -516,6 +516,33 @@ test.extend(canLogIn).extend(userIsBlocked)( }, ) +test.extend(canLogIn).extend(areLoggedIn).extend(canChangeEmailAddress)( + 'have to give a valid email address', + async ({ javaScriptEnabled, page }) => { + await page.goto('/my-details/change-email-address') + await page.getByLabel('What is your email address?').fill('not an email address') + + await page.getByRole('button', { name: 'Save and continue' }).click() + + if (javaScriptEnabled) { + await expect(page.getByRole('alert', { name: 'There is a problem' })).toBeFocused() + } else { + await expect(page.getByRole('alert', { name: 'There is a problem' })).toBeInViewport() + } + await expect(page.getByLabel('What is your email address?')).toHaveAttribute('aria-invalid', 'true') + await page.mouse.move(0, 0) + await expect(page).toHaveScreenshot() + + await page + .getByRole('link', { name: 'Enter an email address in the correct format, like name@example.com' }) + .click() + + await expect(page.getByLabel('What is your email address?')).toBeFocused() + await page.mouse.move(0, 0) + await expect(page).toHaveScreenshot() + }, +) + test.extend(canLogIn).extend(areLoggedIn)( 'have to say if you are open for requests', async ({ javaScriptEnabled, page }) => { diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-high-contrast--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-high-contrast--linux.png new file mode 100644 index 000000000..a6f89bc5d --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-high-contrast--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:511aeb8376efea28c16d16dcb3ae0d74dbdca0b560015a175e2115f3100dd3cd +size 41742 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-high-contrast-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-high-contrast-no-JavaScript--linux.png new file mode 100644 index 000000000..7a3ee663c --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-high-contrast-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bbbd877638a2cdf4a53974e12af07fd336c6e9aa0407880c80721e79981e18c +size 41697 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-linux.png new file mode 100644 index 000000000..4908f80f6 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7833b46741e87c5126656f53b489f9da3b5367449d1494a49a557663c9a6412 +size 44760 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-no-JavaScript--linux.png new file mode 100644 index 000000000..464b8d019 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Chrome-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:441d548fb1818a26816b9f4e0a56012aa20f322c6379e5eef2332ba5d3ed2803 +size 44688 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Firefox-linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Firefox-linux.png new file mode 100644 index 000000000..86a56e7dc --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Firefox-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:460a30a7111a1e4959d470d6523bfac2f57fd63ced9447d9dec3027d982463a3 +size 71080 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Firefox-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Firefox-no-JavaScript--linux.png new file mode 100644 index 000000000..682db882c --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Desktop-Firefox-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caca3a9378e0fc1dcbd971c69381f3fe0334b95495642b4ebefc8b892c15690c +size 71195 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Mobile-Chrome-high-contrast--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Mobile-Chrome-high-contrast--linux.png new file mode 100644 index 000000000..a5c86eba4 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Mobile-Chrome-high-contrast--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:951baf320df4a3deebc85b72aba84844da0e230950acbfa8121493d146d43f6c +size 40861 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Mobile-Chrome-high-contrast-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Mobile-Chrome-high-contrast-no-JavaScript--linux.png new file mode 100644 index 000000000..a350758d8 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-Mobile-Chrome-high-contrast-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e89eed5d460496d5b29afedaac8ee9d5a42739f276e16396df3eb7b79b94ca7e +size 40770 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-iPhone-11-linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-iPhone-11-linux.png new file mode 100644 index 000000000..e283b7e9c --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-iPhone-11-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dfb8313876e0d597a933f37883404e111dfa0a668d6974e249aa5c536aaa909 +size 41847 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-iPhone-11-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-iPhone-11-no-JavaScript--linux.png new file mode 100644 index 000000000..7416f7432 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-1-iPhone-11-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa2bfec5749cd22abee1415df6d43b6e73f43f60d3658bee36e96d04a54eadfb +size 41768 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-high-contrast--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-high-contrast--linux.png new file mode 100644 index 000000000..8e5123700 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-high-contrast--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:200eca17c59bcec463f09e3a10b9539e434a9e872d9f1ae4c99e962b060c550d +size 50908 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-high-contrast-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-high-contrast-no-JavaScript--linux.png new file mode 100644 index 000000000..e68cb25e6 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-high-contrast-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2975bf49fed19b1f017cc1823f9feb1f222e40810cb7395dd1350e374fc40f08 +size 49822 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-linux.png new file mode 100644 index 000000000..3f3e95fdf --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90ce242969e8ec694e75ddde4003e0f7836f8067e1034d1e3848941161e5aa37 +size 53669 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-no-JavaScript--linux.png new file mode 100644 index 000000000..74067042b --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Chrome-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71cde53ac841d054922632314612fc815ccc6810229dc8bc46000c6e05eb13dc +size 51304 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Firefox-linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Firefox-linux.png new file mode 100644 index 000000000..83e98ceee --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Firefox-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcbd10125d0ddcb2ded2fb4f23b5f31c7902f31895695a67a7d70efbe1433593 +size 80295 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Firefox-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Firefox-no-JavaScript--linux.png new file mode 100644 index 000000000..b47b5d077 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Desktop-Firefox-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9a1f25f4e6a06d6ece8ebee1a32abfa1e497107ef9e0f58d71e7313c414178f +size 79803 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Mobile-Chrome-high-contrast--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Mobile-Chrome-high-contrast--linux.png new file mode 100644 index 000000000..673b13975 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Mobile-Chrome-high-contrast--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70f30a1e187986251b8656d9eb5e0da1f02b2e62e5e0b7153acdcd73b460741a +size 42864 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Mobile-Chrome-high-contrast-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Mobile-Chrome-high-contrast-no-JavaScript--linux.png new file mode 100644 index 000000000..673b13975 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-Mobile-Chrome-high-contrast-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70f30a1e187986251b8656d9eb5e0da1f02b2e62e5e0b7153acdcd73b460741a +size 42864 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-iPhone-11-linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-iPhone-11-linux.png new file mode 100644 index 000000000..0b93b0fd6 --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-iPhone-11-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f6c3da986df34e66949af4e3f0ac8fbb7f004580113f45cd813e21a2e66a3b7 +size 41412 diff --git a/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-iPhone-11-no-JavaScript--linux.png b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-iPhone-11-no-JavaScript--linux.png new file mode 100644 index 000000000..5738cf15d --- /dev/null +++ b/integration/snapshots/log-in.spec.ts-snapshots/have-to-give-a-valid-email-address-2-iPhone-11-no-JavaScript--linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8e0a52e37335bcbba5be37eba57ccdc3a77baeb74da2aaeffc9037b4abaddfc +size 41272 diff --git a/package-lock.json b/package-lock.json index 657cfd775..fffca5068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@godaddy/terminus": "^4.12.1", + "@hapi/address": "^5.1.1", "@js-temporal/polyfill": "^0.4.4", "@keyv/redis": "^2.8.0", "anonymus": "^2.1.3", @@ -3624,6 +3625,22 @@ "stoppable": "^1.1.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/address/node_modules/@hapi/hoek": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz", + "integrity": "sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==" + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -24825,6 +24842,21 @@ "stoppable": "^1.1.0" } }, + "@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "requires": { + "@hapi/hoek": "^11.0.2" + }, + "dependencies": { + "@hapi/hoek": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz", + "integrity": "sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==" + } + } + }, "@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", diff --git a/package.json b/package.json index 9741918a4..f9198e13e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ }, "dependencies": { "@godaddy/terminus": "^4.12.1", + "@hapi/address": "^5.1.1", "@js-temporal/polyfill": "^0.4.4", "@keyv/redis": "^2.8.0", "anonymus": "^2.1.3", diff --git a/src/email-address.ts b/src/email-address.ts index 9415146c3..acbac795a 100644 --- a/src/email-address.ts +++ b/src/email-address.ts @@ -1,17 +1,25 @@ +import { isEmailValid } from '@hapi/address' import * as RTE from 'fp-ts/ReaderTaskEither' import type * as TE from 'fp-ts/TaskEither' +import { pipe } from 'fp-ts/function' +import * as C from 'io-ts/Codec' +import * as D from 'io-ts/Decoder' import type { Orcid } from 'orcid-id-ts' import type { NonEmptyString } from './string' +export type EmailAddress = NonEmptyString & EmailAddressBrand + export interface GetEmailAddressEnv { - getEmailAddress: (orcid: Orcid) => TE.TaskEither<'not-found' | 'unavailable', NonEmptyString> + getEmailAddress: (orcid: Orcid) => TE.TaskEither<'not-found' | 'unavailable', EmailAddress> } export interface EditEmailAddressEnv extends GetEmailAddressEnv { deleteEmailAddress: (orcid: Orcid) => TE.TaskEither<'unavailable', void> - saveEmailAddress: (orcid: Orcid, EmailAddress: NonEmptyString) => TE.TaskEither<'unavailable', void> + saveEmailAddress: (orcid: Orcid, EmailAddress: EmailAddress) => TE.TaskEither<'unavailable', void> } +export const EmailAddressC = C.fromDecoder(pipe(D.string, D.refine(isEmailAddress, 'EmailAddress'))) + export const getEmailAddress = (orcid: Orcid) => RTE.asksReaderTaskEither(RTE.fromTaskEitherK(({ getEmailAddress }: GetEmailAddressEnv) => getEmailAddress(orcid))) @@ -22,6 +30,14 @@ export const deleteEmailAddress = (orcid: Orcid) => export const saveEmailAddress = ( orcid: Orcid, - emailAddress: NonEmptyString, + emailAddress: EmailAddress, ): RTE.ReaderTaskEither => RTE.asksReaderTaskEither(RTE.fromTaskEitherK(({ saveEmailAddress }) => saveEmailAddress(orcid, emailAddress))) + +function isEmailAddress(value: string): value is EmailAddress { + return isEmailValid(value) +} + +interface EmailAddressBrand { + readonly EmailAddress: unique symbol +} diff --git a/src/my-details-page/change-email-address.ts b/src/my-details-page/change-email-address.ts index 483c14df5..c06da4f10 100644 --- a/src/my-details-page/change-email-address.ts +++ b/src/my-details-page/change-email-address.ts @@ -1,21 +1,31 @@ import { format } from 'fp-ts-routing' +import * as E from 'fp-ts/Either' +import * as I from 'fp-ts/Identity' import * as O from 'fp-ts/Option' import type { Reader } from 'fp-ts/Reader' -import { pipe } from 'fp-ts/function' +import { flow, pipe } from 'fp-ts/function' +import * as s from 'fp-ts/string' import { type ResponseEnded, Status, type StatusOpen } from 'hyper-ts' import type { OAuthEnv } from 'hyper-ts-oauth' import * as RM from 'hyper-ts/ReaderMiddleware' import * as D from 'io-ts/Decoder' +import { get } from 'spectacles-ts' import { P, match } from 'ts-pattern' -import { deleteEmailAddress, getEmailAddress, saveEmailAddress } from '../email-address' +import { + type EmailAddress, + EmailAddressC, + deleteEmailAddress, + getEmailAddress, + saveEmailAddress, +} from '../email-address' import { canChangeEmailAddress } from '../feature-flags' +import { type InvalidE, getInput, hasAnError, invalidE } from '../form' import { html, plainText, sendHtml } from '../html' import { logInAndRedirect } from '../log-in' import { getMethod, notFound, seeOther, serviceUnavailable } from '../middleware' import { type FathomEnv, type PhaseEnv, page } from '../page' import type { PublicUrlEnv } from '../public-url' import { changeEmailAddressMatch, myDetailsMatch } from '../routes' -import { type NonEmptyString, NonEmptyStringC } from '../string' import { type GetUserEnv, type User, getUser } from '../user' export type Env = EnvFor @@ -55,19 +65,45 @@ export const changeEmailAddress = pipe( const showChangeEmailAddressForm = (user: User) => pipe( RM.fromReaderTaskEither(getEmailAddress(user.orcid)), - RM.map(O.some), - RM.orElseW(() => RM.of(O.none)), - RM.chainReaderKW(emailAddress => createFormPage(user, emailAddress)), + RM.orElseW(() => RM.of(undefined)), + RM.chainReaderKW(emailAddress => createFormPage(user, { emailAddress: E.right(emailAddress) })), RM.ichainFirst(() => RM.status(Status.OK)), RM.ichainMiddlewareK(sendHtml), ) -const ChangeEmailAddressFormD = pipe(D.struct({ emailAddress: NonEmptyStringC })) +const showChangeEmailAddressErrorForm = (user: User) => + flow( + RM.fromReaderK((form: ChangeEmailAddressForm) => createFormPage(user, form)), + RM.ichainFirst(() => RM.status(Status.BadRequest)), + RM.ichainMiddlewareK(sendHtml), + ) const handleChangeEmailAddressForm = (user: User) => pipe( - RM.decodeBody(body => ChangeEmailAddressFormD.decode(body)), - RM.orElseW(() => RM.of({ emailAddress: undefined })), + RM.decodeBody(E.right), + RM.map(body => + pipe( + I.Do, + I.let('emailAddress', () => + pipe( + EmailAddressFieldD.decode(body), + E.orElseW(error => + match(getInput('emailAddress')(error)) + .with({ value: P.select() }, flow(invalidE, E.left)) + .when(O.isNone, () => E.right(undefined)) + .exhaustive(), + ), + ), + ), + ), + ), + RM.chainEitherK(fields => + pipe( + E.Do, + E.apS('emailAddress', fields.emailAddress), + E.mapLeft(() => fields), + ), + ), RM.ichainW(({ emailAddress }) => match(emailAddress) .with(P.string, emailAddress => @@ -86,11 +122,37 @@ const handleChangeEmailAddressForm = (user: User) => ) .exhaustive(), ), + RM.orElseW(showChangeEmailAddressErrorForm(user)), ) -function createFormPage(user: User, emailAddress: O.Option) { +const EmailAddressFieldD = pipe( + D.struct({ + emailAddress: pipe( + D.string, + D.map(s.trim), + D.compose( + D.union( + EmailAddressC, + pipe( + D.literal(''), + D.map(() => undefined), + ), + ), + ), + ), + }), + D.map(get('emailAddress')), +) + +interface ChangeEmailAddressForm { + readonly emailAddress: E.Either +} + +function createFormPage(user: User, form: ChangeEmailAddressForm) { + const error = hasAnError(form) + return page({ - title: plainText`What is your email address?`, + title: plainText`${error ? 'Error: ' : ''}What is your email address?`, content: html`