commit 8c07deca97f89dc33ee08523bbb8efea37287488
parent 7595228344db6e6a9ce934c1cb1a068153ced89f
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 10 Apr 2025 15:47:27 -0300
postal code support
Diffstat:
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'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'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";