summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/pages/RegistrationPage.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/pages/RegistrationPage.tsx')
-rw-r--r--packages/bank-ui/src/pages/RegistrationPage.tsx424
1 files changed, 424 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx
new file mode 100644
index 000000000..5dd19a63f
--- /dev/null
+++ b/packages/bank-ui/src/pages/RegistrationPage.tsx
@@ -0,0 +1,424 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpStatusCode,
+ TalerErrorCode
+} from "@gnu-taler/taler-util";
+import {
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ useBankCoreApiContext,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useSettingsContext } from "../context/settings.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { getRandomPassword, getRandomUsername } from "./rnd.js";
+
+export function RegistrationPage({
+ onRegistrationSuccesful,
+ routeCancel,
+}: {
+ onRegistrationSuccesful: (user: string, password: string) => void;
+ routeCancel: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+ if (!config.allow_registrations) {
+ return (
+ <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
+ );
+ }
+ return (
+ <RegistrationForm
+ onRegistrationSuccesful={onRegistrationSuccesful}
+ routeCancel={routeCancel}
+ />
+ );
+}
+
+// eslint-disable-next-line no-useless-escape
+export const USERNAME_REGEX = /^[a-zA-Z0-9\-\.\_\~]*$/;
+export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/;
+export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+
+/**
+ * Collect and submit registration data.
+ */
+function RegistrationForm({
+ onRegistrationSuccesful,
+ routeCancel,
+}: {
+ onRegistrationSuccesful: (user: string, password: string) => void;
+ routeCancel: RouteDefinition;
+}): VNode {
+ const [username, setUsername] = useState<string | undefined>();
+ const [name, setName] = useState<string | undefined>();
+ const [password, setPassword] = useState<string | undefined>();
+ // const [phone, setPhone] = useState<string | undefined>();
+ // const [email, setEmail] = useState<string | undefined>();
+ const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
+ const [notification, , handleError] = useLocalNotification();
+ const settings = useSettingsContext();
+
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ // const { register } = useTestingAPI();
+ const { i18n } = useTranslationContext();
+
+ const errors = undefinedIfEmpty({
+ name: !name ? i18n.str`Missing name` : undefined,
+ username: !username
+ ? i18n.str`Missing username`
+ : !USERNAME_REGEX.test(username)
+ ? i18n.str`Use letters, numbers or any of these characters: - . _ ~`
+ : undefined,
+ // phone: !phone
+ // ? undefined
+ // : !PHONE_REGEX.test(phone)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : undefined,
+ // email: !email
+ // ? undefined
+ // : !EMAIL_REGEX.test(email)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : undefined,
+ password: !password ? i18n.str`Missing password` : undefined,
+ repeatPassword: !repeatPassword
+ ? i18n.str`Missing password`
+ : repeatPassword !== password
+ ? i18n.str`Passwords don't match`
+ : undefined,
+ });
+
+ async function doRegistrationAndLogin(
+ name: string,
+ username: string,
+ password: string,
+ onComplete: () => void,
+ ) {
+ await handleError(async (onError) => {
+ const resp = await api.createAccount(undefined, {
+ name,
+ username,
+ password,
+ });
+ if (resp.type === "ok") {
+ onComplete();
+ } else {
+ onError(resp, (_case) => {
+ switch (_case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`Server replied with invalid phone or email.`;
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`No enough permission to create that account.`;
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return i18n.str`Registration is disabled because the bank ran out of bonus credit.`;
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return i18n.str`That username can't be used because is reserved.`;
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return i18n.str`That username is already taken.`;
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return i18n.str`That account id is already taken.`;
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return i18n.str`No information for the selected authentication channel.`;
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return i18n.str`Authentication channel is not supported.`;
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return i18n.str`Only admin is allow to set debt limit.`;
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return i18n.str`Only admin can create accounts with second factor authentication.`;
+ }
+ });
+ }
+ });
+ }
+
+ async function doRegistrationStep() {
+ if (!username || !password || !name) return;
+ await doRegistrationAndLogin(name, username, password, () => {
+ setUsername(undefined);
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ onRegistrationSuccesful(username, password);
+ });
+ }
+
+ async function doRandomRegistration() {
+ const user = getRandomUsername();
+
+ const password = settings.simplePasswordForRandomAccounts
+ ? "123"
+ : getRandomPassword();
+ const username = `_${user.first}-${user.second}_`;
+ const name = `${capitalizeFirstLetter(user.first)} ${capitalizeFirstLetter(
+ user.second,
+ )}`;
+ await doRegistrationAndLogin(name, username, password, () => {
+ onRegistrationSuccesful(username, password);
+ });
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="flex min-h-full flex-col justify-center">
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Account registration`}</h2>
+ </div>
+
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div>
+ <label
+ for="username"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Login username</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="username"
+ id="username"
+ class="block w-full 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 ?? ""}
+ enterkeyhint="next"
+ placeholder="account identification to login"
+ autocomplete="username"
+ required
+ onInput={(e): void => {
+ setUsername(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={username !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label
+ for="password"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Password</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="password"
+ id="password"
+ autocomplete="current-password"
+ class="block w-full 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"
+ enterkeyhint="send"
+ value={password ?? ""}
+ placeholder="Password"
+ required
+ onInput={(e): void => {
+ setPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label
+ for="register-repeat"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Repeat password</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="register-repeat"
+ id="register-repeat"
+ autocomplete="current-password"
+ class="block w-full 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"
+ enterkeyhint="send"
+ value={repeatPassword ?? ""}
+ placeholder="Same password"
+ required
+ onInput={(e): void => {
+ setRepeatPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeatPassword}
+ isDirty={repeatPassword !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label
+ for="name"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Full name</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="name"
+ id="name"
+ class="block w-full 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={name ?? ""}
+ enterkeyhint="next"
+ placeholder="John Doe"
+ autocomplete="name"
+ required
+ onInput={(e): void => {
+ setName(e.currentTarget.value);
+ }}
+ />
+ {/* <ShowInputErrorLabel
+ message={errors?.name}
+ isDirty={name !== undefined}
+ /> */}
+ </div>
+ </div>
+
+ {/* <div>
+ <label for="phone" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Phone</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="phone"
+ id="phone"
+ class="block w-full 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={phone ?? ""}
+ enterkeyhint="next"
+ placeholder="your phone"
+ autocomplete="none"
+ onInput={(e): void => {
+ setPhone(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.phone}
+ isDirty={phone !== undefined}
+ />
+ </div>
+ </div>
+ <div>
+ <label for="email" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Email</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="email"
+ id="email"
+ class="block w-full 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={email ?? ""}
+ enterkeyhint="next"
+ placeholder="your email"
+ autocomplete="email"
+ onInput={(e): void => {
+ setEmail(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.email}
+ isDirty={email !== undefined}
+ />
+ </div>
+ </div> */}
+
+ <div class="flex w-full justify-between">
+ <a
+ name="cancel"
+ href={routeCancel.url({})}
+ class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="register"
+ class=" rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!!errors}
+ onClick={async (e) => {
+ e.preventDefault();
+
+ doRegistrationStep();
+ }}
+ >
+ <i18n.Translate>Register</i18n.Translate>
+ </button>
+ </div>
+ </form>
+
+ {settings.allowRandomAccountCreation && (
+ <p class="mt-10 text-center text-sm text-gray-500 border-t">
+ <button
+ type="submit"
+ name="create random"
+ class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
+ onClick={(e) => {
+ e.preventDefault();
+ doRandomRegistration();
+ }}
+ >
+ <i18n.Translate>Create a random temporary user</i18n.Translate>
+ </button>
+ </p>
+ )}
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+function capitalizeFirstLetter(str: string) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}