aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-12-04 13:45:03 -0300
committerSebastian <sebasjm@gmail.com>2023-12-04 13:52:05 -0300
commit7f21700576b4de0f5479ea258b75fb141d18a41b (patch)
tree3f2ae2dc3502a535d2f2ad4e12113c2fd6a8d7f8 /packages
parent2991d354a3e630812f76a725ff535c0b9ab98ff1 (diff)
downloadwallet-core-7f21700576b4de0f5479ea258b75fb141d18a41b.tar.xz
account creation and show login when required
Diffstat (limited to 'packages')
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx22
-rw-r--r--packages/demobank-ui/src/pages/WireTransfer.tsx4
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx17
-rw-r--r--packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx4
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx167
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx4
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx4
-rw-r--r--packages/demobank-ui/src/utils.ts37
8 files changed, 155 insertions, 104 deletions
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index 02ec75dbf..e0ff77417 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -28,9 +28,9 @@ import { assertUnreachable } from "./WithdrawalOperationPage.js";
/**
* Collect and submit login data.
*/
-export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forbidden", onRegister?: () => void }): VNode {
+export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?: boolean, currentUser?: string, onRegister?: () => void }): VNode {
const backend = useBackendState();
- const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
+
const [username, setUsername] = useState<string | undefined>(currentUser);
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
@@ -38,21 +38,11 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb
const [notification, notify, handleError] = useLocalNotification()
const {config} = useBankCoreApiContext();
- /**
- * Register form may be shown in the initialization step.
- * If no register handler then this is invoke
- * to show a session expired or unauthorized
- */
- const isLogginAgain = !onRegister
-
const ref = useRef<HTMLInputElement>(null);
useEffect(function focusInput() {
- if (isLogginAgain && backend.state.status !== "expired") {
- backend.expired()
- window.location.reload()
- }
ref.current?.focus();
}, []);
+
const [busy, setBusy] = useState<Record<string, undefined>>()
const errors = undefinedIfEmpty({
@@ -128,7 +118,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb
id="username"
class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 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"
value={username ?? ""}
- disabled={isLogginAgain}
+ disabled={fixedUser}
enterkeyhint="next"
placeholder="identification"
autocomplete="username"
@@ -170,7 +160,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb
</div>
</div>
- {isLogginAgain ? <div class="flex justify-between">
+ {currentUser ? <div class="flex justify-between">
<button type="submit"
class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
onClick={(e) => {
@@ -189,7 +179,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb
doLogin()
}}
>
- <i18n.Translate>Renew session</i18n.Translate>
+ <i18n.Translate>Check</i18n.Translate>
</button>
</div> : <div>
<button type="submit"
diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx b/packages/demobank-ui/src/pages/WireTransfer.tsx
index a68c085c9..88bdc70a6 100644
--- a/packages/demobank-ui/src/pages/WireTransfer.tsx
+++ b/packages/demobank-ui/src/pages/WireTransfer.tsx
@@ -24,8 +24,8 @@ export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { o
}
if (result.type === "fail") {
switch (result.case) {
- case "unauthorized": return <LoginForm reason="forbidden" />
- case "not-found": return <LoginForm reason="not-found" />
+ case "unauthorized": return <LoginForm currentUser={account}/>
+ case "not-found": return <LoginForm currentUser={account} />
default: assertUnreachable(result)
}
}
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index f8913f0ec..3af619c2d 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -47,6 +47,7 @@ import { useWithdrawalDetails } from "../hooks/access.js";
import { OperationState } from "./OperationState/index.js";
import { OperationNotFound } from "./WithdrawalQRCode.js";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { LoginForm } from "./LoginForm.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
@@ -340,16 +341,16 @@ export function ShouldBeSameUser({ username, children }: { username: string, chi
const { state: credentials } = useBackendState();
const { i18n } = useTranslationContext()
if (credentials.status === "loggedOut") {
- return <Attention type="info" title={i18n.str`Authentication required`}>
- <p>You should login as "{username}"</p>
- </Attention>
+ return <Fragment>
+ <Attention type="info" title={i18n.str`Authentication required`} />
+ <LoginForm currentUser={username} fixedUser/>
+ </Fragment>
}
if (credentials.username !== username) {
- return <Attention type="warning" title={i18n.str`This operation was created with other username`}>
- <p>
- You can switch to account "{username}" and complete the operation.
- </p>
- </Attention>
+ return <Fragment>
+ <Attention type="warning" title={i18n.str`This operation was created with other username`} />
+ <LoginForm currentUser={username} fixedUser/>
+ </Fragment>
}
return <Fragment>
{children}
diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
index 06a88c1c6..d435673a2 100644
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -44,8 +44,8 @@ export function ShowAccountDetails({
}
if (result.type === "fail") {
switch (result.case) {
- case "not-found": return <LoginForm reason="not-found" />
- case "unauthorized": return <LoginForm reason="forbidden" />
+ case "not-found": return <LoginForm currentUser={account} />
+ case "unauthorized": return <LoginForm currentUser={account} />
default: assertUnreachable(result)
}
}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index c8abde74b..e76204a81 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -1,8 +1,8 @@
-import { AmountString, Amounts, PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { AmountString, Amounts, ChallengeContactData, PaytoString, TalerCorebankApi, TranslatedString, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
+import { ErrorMessageMappingFor, PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { useBackendContext } from "../../context/backend.js";
@@ -14,9 +14,14 @@ const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
-export type AccountFormData = TalerCorebankApi.AccountData & { username: string }
+export type AccountFormData = TalerCorebankApi.AccountData & {
+ username: string,
+ debitAmount: string,
+ isExchange: boolean,
+ isPublic: boolean,
+}
-type MM = {
+type ChangeByPurposeType = {
"create": (a: TalerCorebankApi.RegisterAccountRequest | undefined) => void,
"update": (a: TalerCorebankApi.AccountReconfiguration | undefined) => void,
"show": undefined
@@ -29,12 +34,13 @@ type MM = {
* @param param0
* @returns
*/
-export function AccountForm<T extends keyof MM>({
+export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
template,
username,
purpose,
onChange,
focus,
+ admin,
noCashout,
children,
}: {
@@ -44,31 +50,27 @@ export function AccountForm<T extends keyof MM>({
noCashout?: boolean,
admin?: boolean,
template: TalerCorebankApi.AccountData | undefined;
- onChange: MM[T];
- purpose: T;
+ onChange: ChangeByPurposeType[PurposeType];
+ purpose: PurposeType;
}): VNode {
const initial = initializeFromTemplate(username, template);
const [form, setForm] = useState(initial);
const [errors, setErrors] = useState<
- RecursivePartial<typeof initial> | undefined
+ ErrorMessageMappingFor<typeof initial> | undefined
>(undefined);
const { i18n } = useTranslationContext();
const { config } = useBankCoreApiContext()
- const [debitAmount, setDebitAmount] = useState<string>()
-
- const [isExchange, setIsExchange] = useState<boolean>();
- const [isPublic, setIsPublic] = useState<boolean>();
function updateForm(newForm: typeof initial): void {
const parsed = !newForm.cashout_payto_uri
? undefined
: buildPayto("iban", newForm.cashout_payto_uri, undefined);;
- const trimmedAmountStr = debitAmount?.trim();
+ const trimmedAmountStr = newForm.debitAmount?.trim();
const parsedAmount = Amounts.parse(`${config.currency}:${trimmedAmountStr}`);
- const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
+ const errors = undefinedIfEmpty<ErrorMessageMappingFor<typeof initial>>({
cashout_payto_uri: (!newForm.cashout_payto_uri
? undefined
: !parsed
@@ -77,7 +79,7 @@ export function AccountForm<T extends keyof MM>({
? i18n.str`only "IBAN" target are supported`
: !IBAN_REGEX.test(parsed.iban)
? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(parsed.iban, i18n)) as PaytoString,
+ : validateIBAN(parsed.iban, i18n)),
contact_data: undefinedIfEmpty({
email: !newForm.contact_data?.email
? undefined
@@ -93,7 +95,7 @@ export function AccountForm<T extends keyof MM>({
: undefined,
}),
debit_threshold: !trimmedAmountStr
- ? i18n.str`required`
+ ? (purpose === "create" ? i18n.str`required` : undefined)
: !parsedAmount
? i18n.str`not valid`
: Amounts.isZero(parsedAmount)
@@ -111,8 +113,14 @@ export function AccountForm<T extends keyof MM>({
} else {
const cashout = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined)
const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout)
+
+ const internal = !newForm.payto_uri ? undefined : buildPayto("iban", newForm.payto_uri, undefined);
+ const internalURI = !internal ? undefined : stringifyPaytoUri(internal)
+
switch (purpose) {
case "create": {
+ //typescript doesn't correctly narrow a generic type
+ const callback = onChange as ChangeByPurposeType["create"]
const result: TalerCorebankApi.RegisterAccountRequest = {
cashout_payto_uri: cashoutURI,
name: newForm.name!,
@@ -123,20 +131,36 @@ export function AccountForm<T extends keyof MM>({
phone: newForm.contact_data?.phone,
}),
debit_threshold: newForm.debit_threshold as AmountString,
- // ,
- // internal_payto_uri
+ internal_payto_uri: internalURI,
+ is_public: newForm.isPublic,
+ is_taler_exchange: newForm.isExchange,
}
- onChange(result)
+ callback(result)
return;
}
case "update": {
+ //typescript doesn't correctly narrow a generic type
+ const callback = onChange as ChangeByPurposeType["update"]
const result: TalerCorebankApi.AccountReconfiguration = {
- cashout_payto_uri: cashoutURI
+ cashout_payto_uri: cashoutURI,
+ challenge_contact_data: undefinedIfEmpty({
+ email: newForm.contact_data?.email,
+ phone: newForm.contact_data?.phone,
+ }),
+ debit_threshold: newForm.debit_threshold as AmountString,
+ is_taler_exchange: newForm.isExchange,
+ name: newForm.name
+ // is_public: newForm.isPublic
}
- onChange(result as any)
+ callback(result)
return;
}
- case "show":
+ case "show": {
+ return;
+ }
+ default: {
+ assertUnreachable(purpose)
+ }
}
}
}
@@ -203,7 +227,7 @@ export function AccountForm<T extends keyof MM>({
name="name"
data-error={!!errors?.name && form.name !== undefined}
id="name"
- disabled={purpose === "show"}
+ disabled={purpose !== "create"}
value={form.name ?? ""}
onChange={(e) => {
form.name = e.currentTarget.value;
@@ -332,53 +356,70 @@ export function AccountForm<T extends keyof MM>({
</div>
}
- <div class="sm:col-span-5">
- <label for="debit" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Max debt`}</label>
- <InputAmount
- name="debit"
- left
- currency={config.currency}
- value={debitAmount ?? ""}
- onChange={(e) => {
- setDebitAmount(e);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.debit_threshold ? String(errors?.debit_threshold) : undefined}
- isDirty={form.debit_threshold !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500" >allow user debt</p>
- </div>
+ {admin ? <Fragment>
+ <div class="sm:col-span-5">
+ <label for="debit" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Max debt`}</label>
+ <InputAmount
+ name="debit"
+ left
+ currency={config.currency}
+ value={form.debitAmount ?? ""}
+ onChange={(e) => {
+ form.debitAmount = e
+ updateForm(structuredClone(form))
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.debit_threshold ? String(errors?.debit_threshold) : undefined}
+ isDirty={form.debit_threshold !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500" >how much is user able to transfer </p>
+ </div>
- <div class="sm:col-span-5">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
- <i18n.Translate>Is an exchange</i18n.Translate>
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <i18n.Translate>Is an exchange</i18n.Translate>
+ </span>
</span>
- </span>
- <button type="button" data-enabled={!!isExchange} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
+ <button type="button" data-enabled={!!form.isExchange} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
- onClick={() => setIsExchange(!isExchange)}>
- <span aria-hidden="true" data-enabled={!!isExchange} 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>
+ onClick={() => {
+ form.isExchange = !form.isExchange
+ updateForm(structuredClone(form))
+ }}>
+ <span aria-hidden="true" data-enabled={!!form.isExchange} 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>
</div>
- </div>
+ </Fragment> :
+ undefined
+ }
- <div class="sm:col-span-5">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
- <i18n.Translate>Is public</i18n.Translate>
+ {purpose === "create" ?
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <i18n.Translate>Is public</i18n.Translate>
+ </span>
</span>
- </span>
- <button type="button" data-enabled={!!isPublic} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
+ <button type="button" data-enabled={!!form.isPublic} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
- onClick={() => setIsPublic(!isPublic)}>
- <span aria-hidden="true" data-enabled={!!isPublic} 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>
+ onClick={() => {
+ form.isPublic = !form.isPublic
+ updateForm(structuredClone(form))
+ }}>
+ <span aria-hidden="true" data-enabled={!!form.isPublic} 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>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>public accounts have their balance publicly accesible</i18n.Translate>
+ </p>
</div>
- </div>
+ : undefined
+ }
</div>
</div>
@@ -386,7 +427,7 @@ export function AccountForm<T extends keyof MM>({
</form>
);
}
-
+// JNTMECG7RM3AAQB6SRAZNWDSM8
function initializeFromTemplate(
username: string | undefined,
account: TalerCorebankApi.AccountData | undefined,
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 5ee887128..57144177c 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -39,8 +39,8 @@ export function RemoveAccount({
}
if (result.type === "fail") {
switch (result.case) {
- case "unauthorized": return <LoginForm reason="forbidden" />
- case "not-found": return <LoginForm reason="not-found" />
+ case "unauthorized": return <LoginForm currentUser={account} />
+ case "not-found": return <LoginForm currentUser={account} />
default: assertUnreachable(result)
}
}
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index b2ff41e63..ce1a6cf49 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -102,8 +102,8 @@ export function CreateCashout({
}
if (resultAccount.type === "fail") {
switch (resultAccount.case) {
- case "unauthorized": return <LoginForm reason="forbidden" />
- case "not-found": return <LoginForm reason="not-found" />
+ case "unauthorized": return <LoginForm currentUser={accountName} />
+ case "not-found": return <LoginForm currentUser={accountName} />
default: assertUnreachable(resultAccount)
}
}
diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts
index 805d68660..7cdd8a861 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
+import { AmountString, HttpStatusCode, PaytoString, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
import {
ErrorNotification,
ErrorType,
@@ -62,17 +62,36 @@ export type PartialButDefined<T> = {
[P in keyof T]: T[P] | undefined;
};
-export type WithIntermediate<Type extends object> = {
- [prop in keyof Type]: Type[prop] extends object
+/**
+ * every non-map field can be undefined
+ */
+export type WithIntermediate<Type> = {
+ [prop in keyof Type]:
+ Type[prop] extends PaytoString ? Type[prop] | undefined :
+ Type[prop] extends AmountString ? Type[prop] | undefined :
+ Type[prop] extends TranslatedString ? Type[prop] | undefined :
+ Type[prop] extends object
? WithIntermediate<Type[prop]>
: Type[prop] | undefined;
};
-export type RecursivePartial<T> = {
- [P in keyof T]?: T[P] extends (infer U)[]
+export type RecursivePartial<Type> = {
+ [P in keyof Type]?: Type[P] extends (infer U)[]
? RecursivePartial<U>[]
- : T[P] extends object
- ? RecursivePartial<T[P]>
- : T[P];
+ : Type[P] extends object
+ ? RecursivePartial<Type[P]>
+ : Type[P];
+};
+export type ErrorMessageMappingFor<Type> = {
+ [prop in keyof Type]+?:
+ //enumerate known object
+ Exclude<Type[prop],undefined> extends PaytoString ? TranslatedString :
+ Exclude<Type[prop],undefined> extends AmountString ? TranslatedString :
+ Exclude<Type[prop],undefined> extends TranslatedString ? TranslatedString :
+ // arrays: every element
+ Exclude<Type[prop],undefined> extends (infer U)[] ? ErrorMessageMappingFor<U>[] :
+ // map: every field
+ Exclude<Type[prop],undefined> extends object ? ErrorMessageMappingFor<Type[prop]>
+ : TranslatedString;
};
export enum TanChannel {
@@ -337,7 +356,7 @@ export const COUNTRY_TABLE = {
export function validateIBAN(
iban: string,
i18n: ReturnType<typeof useTranslationContext>["i18n"],
-): string | undefined {
+): TranslatedString | undefined {
// Check total length
if (iban.length < 4)
return i18n.str`IBAN numbers usually have more that 4 digits`;