taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 8c07deca97f89dc33ee08523bbb8efea37287488
parent 7595228344db6e6a9ce934c1cb1a068153ced89f
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 10 Apr 2025 15:47:27 -0300

postal code support

Diffstat:
Mpackages/challenger-ui/src/pages/AskChallenge.tsx | 339++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/taler-util/src/taler-form-attributes.ts | 6++++++
Mpackages/taler-util/src/types-taler-challenger.ts | 5+++--
Mpackages/web-util/src/index.browser.ts | 1+
4 files changed, 216 insertions(+), 135 deletions(-)

diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -22,12 +22,15 @@ import { import { Attention, Button, - ErrorLoading, + countryNameList, + FormDesign, + FormUI, LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, Time, useChallengerApiContext, + useForm, useLocalNotificationHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -37,6 +40,10 @@ import { useChallengeSession } from "../hooks/challenge.js"; import { SessionId, useSessionState } from "../hooks/session.js"; import { doAutoFocus } from "./AnswerChallenge.js"; import { ErrorLoadingWithDebug } from "./ErrorLoadingWithDebug.js"; +import { ChallengerApi } from "@gnu-taler/taler-util"; +import { TalerFormAttributes } from "@gnu-taler/taler-util"; +import { InternationalizationAPI } from "@gnu-taler/taler-util"; +import { assertUnreachable } from "@gnu-taler/taler-util"; export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; @@ -58,44 +65,11 @@ export function AskChallenge({ const { i18n } = useTranslationContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); - const [address, setEmail] = useState<string | undefined>(); - const [repeat, setRepeat] = useState<string | undefined>(); - const [remember, setRemember] = useState<boolean>(false); + // const [address, setEmail] = useState<string | undefined>(); const [addrIndex, setAddrIndex] = useState<number | undefined>(); - const restrictionKey = config.address_type; - const result = useChallengeSession(session); - const restriction = !config.restrictions - ? undefined - : config.restrictions[restrictionKey]; - const regexText = - restriction && restriction.regex ? restriction.regex : undefined; - - let restrictionRG; - if (regexText) { - try { - restrictionRG = new RegExp(regexText); - } catch (e) { - return ( - <Attention title={i18n.str`Server configuration error`} type="danger"> - <i18n.Translate> - Invalid server regular expression configuration. Server restriction - is "{regexText}" but it didn't compile: {String(e)} - </i18n.Translate> - </Attention> - ); - } - } else { - restrictionRG = EMAIL_REGEX; - } - - const restrictionHint = - restriction && restriction.hint - ? restriction.hint - : i18n.str`invalid email`; - if (!result) { return ( <div> @@ -112,7 +86,7 @@ export function AskChallenge({ return ( <Attention type="danger" - title={i18n.str`Couldn't get the information`} + title={i18n.str`Couldn't get information about the validation process`} > <i18n.Translate>Bad request</i18n.Translate> </Attention> @@ -122,7 +96,7 @@ export function AskChallenge({ return ( <Attention type="danger" - title={i18n.str`Couldn't get the information`} + title={i18n.str`Couldn't get information about the validation process`} > <i18n.Translate>Not found</i18n.Translate> </Attention> @@ -132,7 +106,7 @@ export function AskChallenge({ return ( <Attention type="danger" - title={i18n.str`Couldn't get the information`} + title={i18n.str`Couldn't get information about the validation process`} > <i18n.Translate>Not acceptable</i18n.Translate> </Attention> @@ -142,7 +116,7 @@ export function AskChallenge({ return ( <Attention type="danger" - title={i18n.str`Couldn't get the information`} + title={i18n.str`Couldn't get information about the validation process`} > <i18n.Translate>Too many request</i18n.Translate> </Attention> @@ -152,7 +126,7 @@ export function AskChallenge({ return ( <Attention type="danger" - title={i18n.str`Couldn't get the information`} + title={i18n.str`Couldn't get information about the validation process`} > <i18n.Translate>Server error</i18n.Translate> </Attention> @@ -163,34 +137,66 @@ export function AskChallenge({ const lastStatus = result.body; + const restrictionKey = config.address_type; + const restriction = !config.restrictions + ? undefined + : config.restrictions[restrictionKey]; + const regexText = + restriction && restriction.regex ? restriction.regex : undefined; + const restrictionHint = + restriction && restriction.hint + ? restriction.hint + : i18n.str`invalid field`; + + let restrictionRG; + if (regexText) { + try { + restrictionRG = new RegExp(regexText); + } catch (e) { + return ( + <Attention title={i18n.str`Server configuration error`} type="danger"> + <i18n.Translate> + Invalid server regular expression configuration. Server restriction + is "{regexText}" but it didn't compile: {String(e)} + </i18n.Translate> + </Attention> + ); + } + } else { + restrictionRG = EMAIL_REGEX; + } + + const design = getFormDesignBasedOnAddressType(i18n, config.address_type); + const form = useForm(design, lastStatus.last_address ?? {}); + const prevAddr = !lastStatus?.last_address ? undefined : lastStatus.last_address[restrictionKey]; - const errors = undefinedIfEmpty({ - address: !address - ? i18n.str`required` - : !restrictionRG.test(address) - ? restrictionHint - : prevAddr !== undefined && address === prevAddr - ? i18n.str`can't use the same address` - : undefined, - repeat: !repeat - ? i18n.str`required` - : address !== repeat - ? i18n.str`doesn't match` - : undefined, - }); - - const contact = address ? { [restrictionKey]: address } : undefined; - - const usableAddrs = - !state?.lastAddress || !state.lastAddress.length - ? [] - : state.lastAddress.filter((d) => !!d.address[restrictionKey]); + // const errors = undefinedIfEmpty({ + // address: !address + // ? i18n.str`required` + // : !restrictionRG.test(address) + // ? restrictionHint + // : prevAddr !== undefined && address === prevAddr + // ? i18n.str`can't use the same address` + // : undefined, + // }); + + // const contact = address ? { [restrictionKey]: address } : undefined; + + // const usableAddrs = + // !state?.lastAddress || !state.lastAddress.length + // ? [] + // : state.lastAddress.filter((d) => !!d.address[restrictionKey]); + + const contact = + form.status.status === "fail" + ? undefined + : (form.status.result as Record<string, string>); const onSend = - errors || !contact + form.status.errors || !contact ? undefined : withErrorHandler( async () => { @@ -200,9 +206,9 @@ export function AskChallenge({ if (ok.body.type === "completed") { completed(ok.body); } else { - if (remember) { - saveAddress(config.address_type, contact); - } + // if (remember) { + // saveAddress(config.address_type, contact); + // } sent(ok.body); } onSendSuccesful(); @@ -268,7 +274,7 @@ export function AskChallenge({ </Fragment> )} - {!usableAddrs.length ? undefined : ( + {/* {!usableAddrs.length ? undefined : ( <div class="mx-auto max-w-xl mt-4"> <h3> <i18n.Translate>Previous address</i18n.Translate> @@ -291,7 +297,6 @@ export function AskChallenge({ onClick={() => { setAddrIndex(idx); setEmail(addr.address[restrictionKey]); - setRepeat(addr.address[restrictionKey]); }} class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 active:ring-2 active:ring-indigo-600 active:ring-offset-2" /> @@ -330,7 +335,6 @@ export function AskChallenge({ onClick={() => { setAddrIndex(undefined); setEmail(undefined); - setRepeat(undefined); }} class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 active:ring-2 active:ring-indigo-600 active:ring-offset-2" /> @@ -345,10 +349,9 @@ export function AskChallenge({ </div> </fieldset> </div> - )} + )} */} - <form - method="POST" + {/* <form class="mx-auto mt-4 max-w-xl " onSubmit={(e) => { e.preventDefault(); @@ -363,6 +366,8 @@ export function AskChallenge({ switch (config.address_type) { case "email": return i18n.str`Email`; + case "address": + return i18n.str`Address`; case "phone": return i18n.str`Phone`; } @@ -379,6 +384,8 @@ export function AskChallenge({ switch (config.address_type) { case "email": return "email"; + case "address": + return "address"; case "phone": return "phone"; } @@ -398,68 +405,33 @@ export function AskChallenge({ </div> </div> - {lastStatus.fix_address || addrIndex !== undefined ? undefined : ( - <div class="sm:col-span-2"> - <label - for="repeat-address" - class="block text-sm font-semibold leading-6 text-gray-900" - > - {(function (): TranslatedString { - switch (config.address_type) { - case "email": - return i18n.str`Repeat email`; - case "phone": - return i18n.str`Repeat phone`; - } - })()} - </label> - <div class="mt-2.5"> - <input - type="text" - name="repeat-address" - id="repeat-address" - value={repeat ?? ""} - onChange={(e) => { - setRepeat(e.currentTarget.value); - }} - autocomplete={(function (): string { - switch (config.address_type) { - case "email": - return "email"; - case "phone": - return "phone"; - } - })()} - class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - /> - <ShowInputErrorLabel - message={errors?.repeat} - isDirty={repeat !== undefined} - /> - </div> - </div> - )} + - {lastStatus === undefined ? undefined : ( - <p class="mt-2 text-sm leading-6 text-gray-400"> - {lastStatus.changes_left < 1 ? ( - <i18n.Translate> - You can&#39;t change the contact address anymore. - </i18n.Translate> - ) : lastStatus.changes_left === 1 ? ( - <i18n.Translate> - You can change the contact address one last time. - </i18n.Translate> - ) : ( - <i18n.Translate> - You can change the contact address {lastStatus.changes_left}{" "} - more times. - </i18n.Translate> - )} - </p> - )} + + </form> */} + <div class="mx-auto mt-4 max-w-xl "> + <FormUI design={design} model={form.model} /> + </div> - <div class="flex items-center justify-between py-2"> + {lastStatus === undefined ? undefined : ( + <p class="mt-2 text-sm leading-6 text-gray-400"> + {lastStatus.changes_left < 1 ? ( + <i18n.Translate> + You can&#39;t change the contact address anymore. + </i18n.Translate> + ) : lastStatus.changes_left === 1 ? ( + <i18n.Translate> + You can change the contact address one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can change the contact address {lastStatus.changes_left}{" "} + more times. + </i18n.Translate> + )} + </p> + )} + {/* <div class="flex items-center justify-between py-2"> <span class="flex flex-grow flex-col"> <span class="text-sm text-black font-medium leading-6 " @@ -489,8 +461,7 @@ export function AskChallenge({ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" ></span> </button> - </div> - </form> + </div> */} <div class="mx-auto mt-4 max-w-xl "> {!prevAddr ? ( <div class="mt-10"> @@ -504,6 +475,9 @@ export function AskChallenge({ switch (config.address_type) { case "email": return i18n.str`Send email`; + case "postal": + case "postal-ch": + return i18n.str`Send letter`; case "phone": return i18n.str`Send SMS`; } @@ -522,6 +496,9 @@ export function AskChallenge({ switch (config.address_type) { case "email": return i18n.str`Change email`; + case "postal": + case "postal-ch": + return i18n.str`Change address`; case "phone": return i18n.str`Change phone`; } @@ -542,3 +519,99 @@ export function undefinedIfEmpty<T extends object>(obj: T): T | undefined { ? obj : undefined; } + +const ADDRESS_EXAMPLE_INTERNATIONAL = `John Doe +Grunerstraße 1 +4. OG rechts +12345 City_name +country_name `; + +const ADDRESS_EXAMPLE_CH = `Florian Dold +Grunerstraße 1 +4. OG rechts +12345 City_name +country_name `; + +function getFormDesignBasedOnAddressType( + i18n: InternationalizationAPI, + type: ChallengerApi.ChallengerTermsOfServiceResponse["address_type"], +): FormDesign { + switch (type) { + case "email": + return { + type: "single-column", + fields: [ + { + type: "text", + id: TalerFormAttributes.CONTACT_EMAIL, + required: true, + label: i18n.str`Email`, + }, + ], + }; + case "phone": + return { + type: "single-column", + fields: [ + { + type: "text", + id: TalerFormAttributes.CONTACT_PHONE, + required: true, + label: i18n.str`Phone`, + }, + ], + }; + case "postal": + return { + type: "single-column", + fields: [ + { + type: "text", + id: TalerFormAttributes.CONTACT_NAME, + required: true, + label: i18n.str`Contact name`, + placeholder: i18n.str`Person full name or name of the business`, + }, + { + type: "textArea", + id: TalerFormAttributes.ADDRESS_LINES, + required: true, + label: i18n.str`Address`, + placeholder: ADDRESS_EXAMPLE_INTERNATIONAL, + }, + { + id: TalerFormAttributes.ADDRESS_COUNTRY, + label: i18n.str`Country`, + type: "selectOne", + choices: countryNameList(i18n), + required: true, + preferredChoiceVals: ["CH", "DE"], + }, + ], + }; + + case "postal-ch": + return { + type: "single-column", + fields: [ + { + type: "text", + id: TalerFormAttributes.CONTACT_PERSON_NAME, + required: true, + label: i18n.str`Contact name`, + placeholder: i18n.str`Your full name`, + }, + { + type: "textArea", + id: TalerFormAttributes.ADDRESS_LINES, + required: true, + label: i18n.str`Address`, + placeholder: ADDRESS_EXAMPLE_CH, + }, + ], + }; + default: { + assertUnreachable(type); + } + } +} diff --git a/packages/taler-util/src/taler-form-attributes.ts b/packages/taler-util/src/taler-form-attributes.ts @@ -615,6 +615,12 @@ export const TalerFormAttributes = { */ CONTACT_PHONE: "CONTACT_PHONE" as const, /** + * Description: Natural person full-name or name of the business to contact. + * + * GANA Type: String + */ + CONTACT_NAME: "CONTACT_NAME" as const, + /** * Description: Country where the individual or business resides. Format is 2-letter ISO country-code. * * GANA Type: CountryCode diff --git a/packages/taler-util/src/types-taler-challenger.ts b/packages/taler-util/src/types-taler-challenger.ts @@ -60,7 +60,7 @@ export interface ChallengerTermsOfServiceResponse { restrictions: Record<string, Restriction> | undefined; // @since v2. - address_type: "email" | "phone" | "address"; + address_type: "email" | "phone" | "postal" | "postal-ch"; } export interface ChallengeSetupResponse { @@ -210,7 +210,8 @@ export const codecForChallengerTermsOfServiceResponse = codecForEither( codecForConstString("phone"), codecForConstString("email"), - codecForConstString("address"), + codecForConstString("postal"), + codecForConstString("postal-ch"), ), ) .build("ChallengerApi.ChallengerTermsOfServiceResponse"); diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts @@ -4,6 +4,7 @@ export * from "./utils/http-impl.browser.js"; export * from "./utils/http-impl.sw.js"; export * from "./utils/observable.js"; export * from "./utils/route.js"; +export * from "./utils/select-ui-lists.js"; export * from "./context/index.js"; export * from "./components/index.js"; export * from "./forms/index.js";