gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated (62cec6798 -> 69b66e715)


From: gnunet
Subject: [taler-wallet-core] branch master updated (62cec6798 -> 69b66e715)
Date: Fri, 26 May 2023 21:52:40 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a change to branch master
in repository wallet-core.

    from 62cec6798 using taler crypto
     new be27647ff use buildKeyStorage to prevent different type for same key
     new 69b66e715 account as hook

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/exchange-backoffice-ui/src/account.ts     |  14 +-
 .../exchange-backoffice-ui/src/forms/902_1e.ts     |   2 +-
 .../exchange-backoffice-ui/src/forms/902_4e.ts     |   2 +-
 .../exchange-backoffice-ui/src/forms/simplest.ts   |   2 +-
 .../exchange-backoffice-ui/src/hooks/useOfficer.ts | 100 +++++++++
 packages/exchange-backoffice-ui/src/pages.ts       |   4 +-
 .../pages/{AccountDetails.tsx => CaseDetails.tsx}  |   2 +-
 .../exchange-backoffice-ui/src/pages/Cases.tsx     |   8 +-
 .../src/pages/CreateAccount.tsx                    |  89 ++++++++
 .../src/pages/HandleAccountNotReady.tsx            |  31 +++
 .../exchange-backoffice-ui/src/pages/Officer.tsx   | 247 +--------------------
 .../src/pages/UnlockAccount.tsx                    |  70 ++++++
 .../src/hooks/useSettings.ts                       |  30 ++-
 packages/web-util/src/hooks/index.ts               |   2 +-
 packages/web-util/src/hooks/useLang.ts             |  12 +-
 packages/web-util/src/hooks/useLocalStorage.ts     |  70 +++---
 packages/web-util/src/hooks/useMemoryStorage.ts    |  60 +++--
 17 files changed, 422 insertions(+), 323 deletions(-)
 create mode 100644 packages/exchange-backoffice-ui/src/hooks/useOfficer.ts
 rename packages/exchange-backoffice-ui/src/pages/{AccountDetails.tsx => 
CaseDetails.tsx} (99%)
 create mode 100644 packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx
 create mode 100644 
packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx
 create mode 100644 packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx

diff --git a/packages/exchange-backoffice-ui/src/account.ts 
b/packages/exchange-backoffice-ui/src/account.ts
index 05f0f8984..bd3c2003e 100644
--- a/packages/exchange-backoffice-ui/src/account.ts
+++ b/packages/exchange-backoffice-ui/src/account.ts
@@ -16,7 +16,7 @@ export interface Account {
 }
 
 /**
- * Restore previous session and unlock account
+ * Restore previous session and unlock account with password
  *
  * @param salt string from which crypto params will be derived
  * @param key secured private key
@@ -55,7 +55,7 @@ declare const opaque_SigningKey: unique symbol;
 export type SigningKey = Uint8Array & { [opaque_SigningKey]: true };
 
 /**
- * Create new account (secured private key) under session
+ * Create new account (secured private key)
  * secured with the given password
  *
  * @param sessionId
@@ -64,8 +64,8 @@ export type SigningKey = Uint8Array & { [opaque_SigningKey]: 
true };
  */
 export async function createNewAccount(
   password: string,
-): Promise<LockedAccount> {
-  const { eddsaPriv } = createEddsaKeyPair();
+): Promise<Account & { safe: LockedAccount }> {
+  const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
 
   const key = stringToBytes(password);
 
@@ -76,9 +76,11 @@ export async function createNewAccount(
     password,
   );
 
-  const protectedPriv = encodeCrock(protectedPrivKey);
+  const signingKey = eddsaPriv as SigningKey;
+  const accountId = encodeCrock(eddsaPub) as AccountId;
+  const safe = encodeCrock(protectedPrivKey) as LockedAccount;
 
-  return protectedPriv as LockedAccount;
+  return { accountId, signingKey, safe };
 }
 
 export class UnwrapKeyError extends Error {
diff --git a/packages/exchange-backoffice-ui/src/forms/902_1e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_1e.ts
index 04952a985..654085443 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_1e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_1e.ts
@@ -8,7 +8,7 @@ import { FlexibleForm, languageList } from "./index.js";
 import { FormState } from "../handlers/FormProvider.js";
 import { State } from "../pages/AntiMoneyLaunderingForm.js";
 import { AmlState } from "../types.js";
-import { amlStateConverter } from "../pages/AccountDetails.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
 import { Simplest, resolutionSection } from "./simplest.js";
 
 export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
diff --git a/packages/exchange-backoffice-ui/src/forms/902_4e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
index 15ad17144..f77a2f63a 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_4e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
@@ -11,7 +11,7 @@ import { h as create } from "preact";
 import { ChevronRightIcon } from "@heroicons/react/24/solid";
 import { State } from "../pages/AntiMoneyLaunderingForm.js";
 import { AmlState } from "../types.js";
-import { amlStateConverter } from "../pages/AccountDetails.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
 import { Simplest, resolutionSection } from "./simplest.js";
 
 export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
diff --git a/packages/exchange-backoffice-ui/src/forms/simplest.ts 
b/packages/exchange-backoffice-ui/src/forms/simplest.ts
index 5da01961b..7eda03fef 100644
--- a/packages/exchange-backoffice-ui/src/forms/simplest.ts
+++ b/packages/exchange-backoffice-ui/src/forms/simplest.ts
@@ -7,7 +7,7 @@ import {
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
 import { AmlState } from "../types.js";
-import { amlStateConverter } from "../pages/AccountDetails.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
 import { State } from "../pages/AntiMoneyLaunderingForm.js";
 import { DoubleColumnFormSection, UIFormField } from "../handlers/forms.js";
 
diff --git a/packages/exchange-backoffice-ui/src/hooks/useOfficer.ts 
b/packages/exchange-backoffice-ui/src/hooks/useOfficer.ts
new file mode 100644
index 000000000..2ed375846
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/hooks/useOfficer.ts
@@ -0,0 +1,100 @@
+import {
+  AbsoluteTime,
+  Codec,
+  buildCodecForObject,
+  codecForAbsoluteTime,
+  codecForString,
+} from "@gnu-taler/taler-util";
+import {
+  Account,
+  LockedAccount,
+  createNewAccount,
+  unlockAccount,
+} from "../account.js";
+import {
+  buildStorageKey,
+  useLocalStorage,
+  useMemoryStorage,
+} from "@gnu-taler/web-util/browser";
+
+export interface Officer {
+  account: LockedAccount;
+  when: AbsoluteTime;
+}
+
+const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
+
+export const codecForOfficer = (): Codec<Officer> =>
+  buildCodecForObject<Officer>()
+    .property("account", codecForLockedAccount) // FIXME
+    .property("when", codecForAbsoluteTime) // FIXME
+    .build("Officer");
+
+export type OfficerState = OfficerNotReady | OfficerReady;
+export type OfficerNotReady = OfficerNotFound | OfficerLocked;
+interface OfficerNotFound {
+  state: "not-found";
+  create: (password: string) => Promise<void>;
+}
+interface OfficerLocked {
+  state: "locked";
+  forget: () => void;
+  tryUnlock: (password: string) => Promise<void>;
+}
+interface OfficerReady {
+  state: "ready";
+  account: Account;
+  forget: () => void;
+  lock: () => void;
+}
+
+const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
+const ACCOUNT_KEY = buildStorageKey<Account>("account");
+
+export function useOfficer(): OfficerState {
+  const accountStorage = useMemoryStorage(ACCOUNT_KEY);
+  const officerStorage = useLocalStorage(OFFICER_KEY);
+
+  const officer = officerStorage.value;
+  const account = accountStorage.value;
+
+  if (officer === undefined) {
+    return {
+      state: "not-found",
+      create: async (pwd: string) => {
+        const { accountId, safe, signingKey } = await createNewAccount(pwd);
+        officerStorage.update({
+          account: safe,
+          when: AbsoluteTime.now(),
+        });
+
+        accountStorage.update({ accountId, signingKey });
+      },
+    };
+  }
+
+  if (account === undefined) {
+    return {
+      state: "locked",
+      forget: () => {
+        officerStorage.reset();
+      },
+      tryUnlock: async (pwd: string) => {
+        const ac = await unlockAccount(officer.account, pwd);
+        accountStorage.update(ac);
+      },
+    };
+  }
+
+  return {
+    state: "ready",
+    account: account,
+    lock: () => {
+      accountStorage.reset();
+    },
+    forget: () => {
+      officerStorage.reset();
+      accountStorage.reset();
+    },
+  };
+}
diff --git a/packages/exchange-backoffice-ui/src/pages.ts 
b/packages/exchange-backoffice-ui/src/pages.ts
index 2b13ce585..18fb7a158 100644
--- a/packages/exchange-backoffice-ui/src/pages.ts
+++ b/packages/exchange-backoffice-ui/src/pages.ts
@@ -5,7 +5,7 @@ import { Welcome } from "./pages/Welcome.js";
 import { PageEntry, pageDefinition } from "./route.js";
 import { Officer } from "./pages/Officer.js";
 import { Cases } from "./pages/Cases.js";
-import { AccountDetails } from "./pages/AccountDetails.js";
+import { CaseDetails } from "./pages/CaseDetails.js";
 import { NewFormEntry } from "./pages/NewFormEntry.js";
 
 const home: PageEntry = {
@@ -18,7 +18,7 @@ const cases: PageEntry = {
 };
 const account: PageEntry<{ account?: string }> = {
   url: pageDefinition("#/account/:account"),
-  view: AccountDetails,
+  view: CaseDetails,
 };
 
 const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
diff --git a/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx 
b/packages/exchange-backoffice-ui/src/pages/CaseDetails.tsx
similarity index 99%
rename from packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
rename to packages/exchange-backoffice-ui/src/pages/CaseDetails.tsx
index b252d2ab0..e5fb8eaba 100644
--- a/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/CaseDetails.tsx
@@ -141,7 +141,7 @@ function getEventsFromAmlHistory(
   return ae.concat(ke).sort(selectSooner);
 }
 
-export function AccountDetails({ account }: { account?: string }) {
+export function CaseDetails({ account }: { account?: string }) {
   const events = getEventsFromAmlHistory(
     response.aml_history,
     response.kyc_attributes,
diff --git a/packages/exchange-backoffice-ui/src/pages/Cases.tsx 
b/packages/exchange-backoffice-ui/src/pages/Cases.tsx
index 1983769ed..28b9d2a88 100644
--- a/packages/exchange-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/Cases.tsx
@@ -4,8 +4,10 @@ import { AmlRecords, AmlState } from "../types.js";
 import { InputChoiceHorizontal } from "../handlers/InputChoiceHorizontal.js";
 import { createNewForm } from "../handlers/forms.js";
 import { TranslatedString } from "@gnu-taler/taler-util";
-import { amlStateConverter as amlStateConverter } from "./AccountDetails.js";
+import { amlStateConverter as amlStateConverter } from "./CaseDetails.js";
 import { useState } from "preact/hooks";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { useOfficer } from "../hooks/useOfficer.js";
 
 const response: AmlRecords = {
   records: [
@@ -61,6 +63,10 @@ function doFilter(
 }
 
 export function Cases() {
+  const officer = useOfficer();
+  if (officer.state !== "ready") {
+    return <HandleAccountNotReady officer={officer} />;
+  }
   const form = createNewForm<{
     state: AmlState;
   }>();
diff --git a/packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx 
b/packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx
new file mode 100644
index 000000000..41a1d20ff
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx
@@ -0,0 +1,89 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { createNewForm } from "../handlers/forms.js";
+
+export function CreateAccount({
+  onNewAccount,
+}: {
+  onNewAccount: (password: string) => void;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const Form = createNewForm<{
+    password: string;
+    repeat: string;
+  }>();
+
+  return (
+    <div class="flex min-h-full flex-col ">
+      <div class="sm:mx-auto sm:w-full sm:max-w-md">
+        <h2 class="mt-6 text-center text-2xl font-bold leading-9 
tracking-tight text-gray-900">
+          Create account
+        </h2>
+      </div>
+
+      <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+        <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+          <Form.Provider
+            computeFormState={(v) => {
+              return {
+                password: {
+                  error: !v.password
+                    ? i18n.str`required`
+                    : v.password.length < 8
+                    ? i18n.str`should have at least 8 characters`
+                    : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
+                    ? i18n.str`should have lowercase and uppercase characters`
+                    : !v.password.match(/\d/)
+                    ? i18n.str`should have numbers`
+                    : !v.password.match(/[^a-zA-Z\d]/)
+                    ? i18n.str`should have at least one character which is not 
a number or letter`
+                    : undefined,
+                },
+                repeat: {
+                  // error: !v.repeat
+                  //   ? i18n.str`required`
+                  //   // : v.repeat !== v.password
+                  //   // ? i18n.str`doesn't match`
+                  //   : undefined,
+                },
+              };
+            }}
+            onSubmit={async (v) => {
+              onNewAccount(v.password);
+            }}
+          >
+            <div class="mb-4">
+              <Form.InputLine
+                label={"Password" as TranslatedString}
+                name="password"
+                type="password"
+                help={
+                  "lower and upper case letters, number and special character" 
as TranslatedString
+                }
+                required
+              />
+            </div>
+            <div class="mb-4">
+              <Form.InputLine
+                label={"Repeat password" as TranslatedString}
+                name="repeat"
+                type="password"
+                required
+              />
+            </div>
+
+            <div class="mt-8">
+              <button
+                type="submit"
+                class="flex w-full justify-center rounded-md bg-indigo-600 
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"
+              >
+                Create
+              </button>
+            </div>
+          </Form.Provider>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git 
a/packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx 
b/packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx
new file mode 100644
index 000000000..b0c430875
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -0,0 +1,31 @@
+import { VNode, h } from "preact";
+import { OfficerNotReady } from "../hooks/useOfficer.js";
+import { CreateAccount } from "./CreateAccount.js";
+import { UnlockAccount } from "./UnlockAccount.js";
+
+export function HandleAccountNotReady({
+  officer,
+}: {
+  officer: OfficerNotReady;
+}): VNode {
+  if (officer.state === "not-found") {
+    return (
+      <CreateAccount
+        onNewAccount={(password) => {
+          officer.create(password);
+        }}
+      />
+    );
+  }
+
+  if (officer.state === "locked") {
+    return (
+      <UnlockAccount
+        onAccountUnlocked={(pwd) => {
+          officer.tryUnlock(pwd);
+        }}
+      />
+    );
+  }
+  throw Error(`unexpected account state ${(officer as any).state}`);
+}
diff --git a/packages/exchange-backoffice-ui/src/pages/Officer.tsx 
b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
index 40ec33018..5320369e4 100644
--- a/packages/exchange-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
@@ -1,83 +1,11 @@
-import {
-  AbsoluteTime,
-  Codec,
-  TranslatedString,
-  buildCodecForObject,
-  codecForAbsoluteTime,
-  codecForString,
-} from "@gnu-taler/taler-util";
-import {
-  notifyError,
-  notifyInfo,
-  useLocalStorage,
-  useMemoryStorage,
-  useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import {
-  Account,
-  LockedAccount,
-  UnwrapKeyError,
-  createNewAccount,
-  unlockAccount,
-} from "../account.js";
-import { createNewForm } from "../handlers/forms.js";
-
-export interface Officer {
-  account: LockedAccount;
-  when: AbsoluteTime;
-}
-
-const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
-
-export const codecForOfficer = (): Codec<Officer> =>
-  buildCodecForObject<Officer>()
-    .property("account", codecForLockedAccount) // FIXME
-    .property("when", codecForAbsoluteTime) // FIXME
-    .build("Officer");
+import { Fragment, h } from "preact";
+import { useOfficer } from "../hooks/useOfficer.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
 
 export function Officer() {
-  const password = useMemoryStorage("password");
-  const officer = useLocalStorage("officer", {
-    codec: codecForOfficer(),
-  });
-  const [keys, setKeys] = useState<Account>();
-
-  useEffect(() => {
-    if (officer.value === undefined || password.value === undefined) {
-      return;
-    }
-
-    unlockAccount(officer.value.account, password.value)
-      .then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
-      .catch((e) => {
-        if (e instanceof UnwrapKeyError) {
-          console.log(e);
-        }
-      });
-  }, [officer.value, password.value]);
-
-  if (officer.value === undefined || !officer.value.account) {
-    return (
-      <CreateAccount
-        onNewAccount={(account, pwd) => {
-          password.update(pwd);
-          officer.update({ account, when: AbsoluteTime.now() });
-        }}
-      />
-    );
-  }
-
-  if (password.value === undefined) {
-    return (
-      <UnlockAccount
-        lockedAccount={officer.value.account}
-        onAccountUnlocked={(pwd) => {
-          password.update(pwd);
-        }}
-      />
-    );
+  const officer = useOfficer();
+  if (officer.state !== "ready") {
+    return <HandleAccountNotReady officer={officer} />;
   }
 
   return (
@@ -86,12 +14,12 @@ export function Officer() {
         Public key
       </h1>
       <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg">
-        <p class="mt-6 font-mono break-all">{keys?.accountId}</p>
+        <p class="mt-6 font-mono break-all">{officer.account.accountId}</p>
       </div>
       <p>
         <a
           href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent(
-            `I want my AML account\n\n\nPubKey: ${keys?.accountId}`,
+            `I want my AML account\n\n\nPubKey: ${officer.account.accountId}`,
           )}`}
           target="_blank"
           rel="noreferrer"
@@ -104,7 +32,7 @@ export function Officer() {
         <button
           type="button"
           onClick={() => {
-            password.reset();
+            officer.lock();
           }}
           class="m-4 block rounded-md border-0 bg-gray-200 px-3 py-2 
text-center text-sm text-black shadow-sm "
         >
@@ -115,7 +43,7 @@ export function Officer() {
         <button
           type="button"
           onClick={() => {
-            officer.reset();
+            officer.forget();
           }}
           class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm 
 text-white shadow-sm hover:bg-red-500 "
         >
@@ -125,158 +53,3 @@ export function Officer() {
     </div>
   );
 }
-
-function CreateAccount({
-  onNewAccount,
-}: {
-  onNewAccount: (account: LockedAccount, password: string) => void;
-}): VNode {
-  const { i18n } = useTranslationContext();
-  const Form = createNewForm<{
-    password: string;
-    repeat: string;
-  }>();
-
-  return (
-    <div class="flex min-h-full flex-col ">
-      <div class="sm:mx-auto sm:w-full sm:max-w-md">
-        <h2 class="mt-6 text-center text-2xl font-bold leading-9 
tracking-tight text-gray-900">
-          Create account
-        </h2>
-      </div>
-
-      <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
-        <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
-          <Form.Provider
-            computeFormState={(v) => {
-              return {
-                password: {
-                  error: !v.password
-                    ? i18n.str`required`
-                    : v.password.length < 8
-                    ? i18n.str`should have at least 8 characters`
-                    : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
-                    ? i18n.str`should have lowercase and uppercase characters`
-                    : !v.password.match(/\d/)
-                    ? i18n.str`should have numbers`
-                    : !v.password.match(/[^a-zA-Z\d]/)
-                    ? i18n.str`should have at least one character which is not 
a number or letter`
-                    : undefined,
-                },
-                repeat: {
-                  // error: !v.repeat
-                  //   ? i18n.str`required`
-                  //   // : v.repeat !== v.password
-                  //   // ? i18n.str`doesn't match`
-                  //   : undefined,
-                },
-              };
-            }}
-            onSubmit={async (v) => {
-              const account = await createNewAccount(v.password);
-              onNewAccount(account, v.password);
-            }}
-          >
-            <div class="mb-4">
-              <Form.InputLine
-                label={"Password" as TranslatedString}
-                name="password"
-                type="password"
-                help={
-                  "lower and upper case letters, number and special character" 
as TranslatedString
-                }
-                required
-              />
-            </div>
-            <div class="mb-4">
-              <Form.InputLine
-                label={"Repeat password" as TranslatedString}
-                name="repeat"
-                type="password"
-                required
-              />
-            </div>
-
-            <div class="mt-8">
-              <button
-                type="submit"
-                class="flex w-full justify-center rounded-md bg-indigo-600 
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"
-              >
-                Create
-              </button>
-            </div>
-          </Form.Provider>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function UnlockAccount({
-  lockedAccount,
-  onAccountUnlocked,
-}: {
-  lockedAccount: LockedAccount;
-  onAccountUnlocked: (password: string) => void;
-}): VNode {
-  const Form = createNewForm<{
-    password: string;
-  }>();
-
-  return (
-    <div class="flex min-h-full flex-col ">
-      <div class="sm:mx-auto sm:w-full sm:max-w-md">
-        <h2 class="mt-6 text-center text-2xl font-bold leading-9 
tracking-tight text-gray-900">
-          Account locked
-        </h2>
-        <p class="mt-6 text-lg leading-8 text-gray-600">
-          Your account is normally locked anytime you reload. To unlock type
-          your password again.
-        </p>
-      </div>
-
-      <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
-        <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
-          <Form.Provider
-            onSubmit={async (v) => {
-              try {
-                // test login
-                await unlockAccount(lockedAccount, v.password);
-
-                onAccountUnlocked(v.password ?? "");
-                notifyInfo("Account unlocked" as TranslatedString);
-              } catch (e) {
-                if (e instanceof UnwrapKeyError) {
-                  notifyError(
-                    "Could not unlock account" as any,
-                    e.message as any,
-                  );
-                } else {
-                  throw e;
-                }
-              }
-            }}
-          >
-            <div class="mb-4">
-              <Form.InputLine
-                label={"Password" as TranslatedString}
-                name="password"
-                type="password"
-                required
-              />
-            </div>
-
-            <div class="mt-8">
-              <button
-                type="submit"
-                class="flex w-full justify-center rounded-md bg-indigo-600 
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"
-              >
-                Unlock
-              </button>
-            </div>
-          </Form.Provider>
-        </div>
-      </div>
-    </div>
-  );
-}
diff --git a/packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx 
b/packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx
new file mode 100644
index 000000000..941e28627
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -0,0 +1,70 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { UnwrapKeyError } from "../account.js";
+import { createNewForm } from "../handlers/forms.js";
+
+export function UnlockAccount({
+  onAccountUnlocked,
+}: {
+  onAccountUnlocked: (password: string) => void;
+}): VNode {
+  const Form = createNewForm<{
+    password: string;
+  }>();
+
+  return (
+    <div class="flex min-h-full flex-col ">
+      <div class="sm:mx-auto sm:w-full sm:max-w-md">
+        <h2 class="mt-6 text-center text-2xl font-bold leading-9 
tracking-tight text-gray-900">
+          Account locked
+        </h2>
+        <p class="mt-6 text-lg leading-8 text-gray-600">
+          Your account is normally locked anytime you reload. To unlock type
+          your password again.
+        </p>
+      </div>
+
+      <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+        <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+          <Form.Provider
+            onSubmit={async (v) => {
+              try {
+                await onAccountUnlocked(v.password);
+
+                notifyInfo("Account unlocked" as TranslatedString);
+              } catch (e) {
+                if (e instanceof UnwrapKeyError) {
+                  notifyError(
+                    "Could not unlock account" as any,
+                    e.message as any,
+                  );
+                } else {
+                  throw e;
+                }
+              }
+            }}
+          >
+            <div class="mb-4">
+              <Form.InputLine
+                label={"Password" as TranslatedString}
+                name="password"
+                type="password"
+                required
+              />
+            </div>
+
+            <div class="mt-8">
+              <button
+                type="submit"
+                class="flex w-full justify-center rounded-md bg-indigo-600 
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"
+              >
+                Unlock
+              </button>
+            </div>
+          </Form.Provider>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useSettings.ts 
b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
index ebb1e991c..7e7b26a39 100644
--- a/packages/taler-wallet-webextension/src/hooks/useSettings.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
@@ -14,8 +14,13 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { useLocalStorage } from "@gnu-taler/web-util/browser";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
 import { Settings, defaultSettings } from "../platform/api.js";
+import {
+  Codec,
+  buildCodecForObject,
+  codecForBoolean,
+} from "@gnu-taler/taler-util";
 
 function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
   if (str === undefined) return undefined;
@@ -26,17 +31,30 @@ function parse_json_or_undefined<T>(str: string | 
undefined): T | undefined {
   }
 }
 
+export const codecForSettings = (): Codec<Settings> =>
+  buildCodecForObject<Settings>()
+    .property("walletAllowHttp", codecForBoolean())
+    .property("walletBatchWithdrawal", codecForBoolean())
+    .property("injectTalerSupport", codecForBoolean())
+    .property("advanceMode", codecForBoolean())
+    .property("backup", codecForBoolean())
+    .property("langSelector", codecForBoolean())
+    .property("showJsonOnError", codecForBoolean())
+    .property("extendedAccountTypes", codecForBoolean())
+    .property("deleteActiveTransactions", codecForBoolean())
+    .build("Settings");
+
+const SETTINGS_KEY = buildStorageKey("wallet-settings", codecForSettings());
+
 export function useSettings(): [
   Readonly<Settings>,
   <T extends keyof Settings>(key: T, value: Settings[T]) => void,
 ] {
-  const { value, update } = useLocalStorage("wallet-settings");
+  const { value, update } = useLocalStorage(SETTINGS_KEY);
 
-  const parsed: Settings = parse_json_or_undefined(value) ?? defaultSettings;
+  const parsed: Settings = value ?? defaultSettings;
   function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
-    const newValue = { ...parsed, [k]: v };
-    const json = JSON.stringify(newValue);
-    update(json);
+    update({ ...parsed, [k]: v });
   }
   return [parsed, updateField];
 }
diff --git a/packages/web-util/src/hooks/index.ts 
b/packages/web-util/src/hooks/index.ts
index ae8872497..a3a2053e6 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,5 +1,5 @@
 export { useLang } from "./useLang.js";
-export { useLocalStorage } from "./useLocalStorage.js";
+export { useLocalStorage, buildStorageKey } from "./useLocalStorage.js";
 export { useMemoryStorage } from "./useMemoryStorage.js";
 export {
   useNotifications,
diff --git a/packages/web-util/src/hooks/useLang.ts 
b/packages/web-util/src/hooks/useLang.ts
index d64cf6e1a..448cd8aba 100644
--- a/packages/web-util/src/hooks/useLang.ts
+++ b/packages/web-util/src/hooks/useLang.ts
@@ -14,7 +14,11 @@
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
 
-import { LocalStorageState, useLocalStorage } from "./useLocalStorage.js";
+import {
+  StorageState,
+  buildStorageKey,
+  useLocalStorage,
+} from "./useLocalStorage.js";
 
 function getBrowserLang(): string | undefined {
   if (typeof window === "undefined") return undefined;
@@ -23,7 +27,9 @@ function getBrowserLang(): string | undefined {
   return undefined;
 }
 
-export function useLang(initial?: string): Required<LocalStorageState> {
+const langPreferenceKey = buildStorageKey("lang-preference");
+
+export function useLang(initial?: string): Required<StorageState> {
   const defaultValue = (getBrowserLang() || initial || "en").substring(0, 2);
-  return useLocalStorage("lang-preference", { defaultValue: defaultValue });
+  return useLocalStorage(langPreferenceKey, defaultValue);
 }
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts 
b/packages/web-util/src/hooks/useLocalStorage.ts
index 55efd01cb..45b7abd3c 100644
--- a/packages/web-util/src/hooks/useLocalStorage.ts
+++ b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -19,7 +19,7 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { Codec } from "@gnu-taler/taler-util";
+import { Codec, codecForString } from "@gnu-taler/taler-util";
 import { useEffect, useState } from "preact/hooks";
 import {
   ObservableMap,
@@ -28,7 +28,26 @@ import {
   memoryMap,
 } from "../utils/observable.js";
 
-export interface LocalStorageState<Type = string> {
+declare const opaque_StorageKey: unique symbol;
+
+export type StorageKey<Key> = {
+  id: string;
+  [opaque_StorageKey]: true;
+  codec: Codec<Key>;
+};
+
+export function buildStorageKey<Key = string>(
+  name: string,
+  codec?: Codec<Key>,
+): StorageKey<Key> {
+  return {
+    id: name,
+    codec: codec ?? (codecForString() as Codec<Key>),
+    [opaque_StorageKey]: true,
+  };
+}
+
+export interface StorageState<Type = string> {
   value?: Type;
   update: (s: Type) => void;
   reset: () => void;
@@ -50,59 +69,48 @@ const storage: ObservableMap<string, string> = (function 
buildStorage() {
 
 //with initial value
 export function useLocalStorage<Type = string>(
-  key: string,
-  options?: {
-    defaultValue: Type;
-    codec?: Codec<Type>;
-  },
-): Required<LocalStorageState<Type>>;
+  key: StorageKey<Type>,
+  defaultValue: Type,
+): Required<StorageState<Type>>;
 //without initial value
 export function useLocalStorage<Type = string>(
-  key: string,
-  options?: {
-    codec?: Codec<Type>;
-  },
-): LocalStorageState<Type>;
+  key: StorageKey<Type>,
+): StorageState<Type>;
 // impl
 export function useLocalStorage<Type = string>(
-  key: string,
-  options?: {
-    defaultValue?: Type;
-    codec?: Codec<Type>;
-  },
-): LocalStorageState<Type> {
+  key: StorageKey<Type>,
+  defaultValue?: Type,
+): StorageState<Type> {
   function convert(updated: string | undefined): Type | undefined {
-    if (updated === undefined) return options?.defaultValue; //optional
+    if (updated === undefined) return defaultValue; //optional
     try {
-      return !options?.codec
-        ? (updated as Type)
-        : options.codec.decode(JSON.parse(updated));
+      return key.codec.decode(JSON.parse(updated));
     } catch (e) {
       //decode error
-      return options?.defaultValue;
+      return defaultValue;
     }
   }
   const [storedValue, setStoredValue] = useState<Type | undefined>(
     (): Type | undefined => {
-      const prev = storage.get(key);
+      const prev = storage.get(key.id);
       return convert(prev);
     },
   );
 
   useEffect(() => {
-    return storage.onUpdate(key, () => {
-      const newValue = storage.get(key);
+    return storage.onUpdate(key.id, () => {
+      const newValue = storage.get(key.id);
       setStoredValue(convert(newValue));
     });
   }, []);
 
   const setValue = (value?: Type): void => {
     if (value === undefined) {
-      storage.delete(key);
+      storage.delete(key.id);
     } else {
       storage.set(
-        key,
-        options?.codec ? JSON.stringify(value) : (value as string),
+        key.id,
+        key.codec ? JSON.stringify(value) : (value as string),
       );
     }
   };
@@ -111,7 +119,7 @@ export function useLocalStorage<Type = string>(
     value: storedValue,
     update: setValue,
     reset: () => {
-      setValue(options?.defaultValue);
+      setValue(defaultValue);
     },
   };
 }
diff --git a/packages/web-util/src/hooks/useMemoryStorage.ts 
b/packages/web-util/src/hooks/useMemoryStorage.ts
index 7160b035e..d8be24c1e 100644
--- a/packages/web-util/src/hooks/useMemoryStorage.ts
+++ b/packages/web-util/src/hooks/useMemoryStorage.ts
@@ -20,48 +20,44 @@
  */
 
 import { useEffect, useState } from "preact/hooks";
-import {
-  ObservableMap,
-  browserStorageMap,
-  localStorageMap,
-  memoryMap,
-} from "../utils/observable.js";
+import { ObservableMap, memoryMap } from "../utils/observable.js";
+import { StorageKey, StorageState } from "./useLocalStorage.js";
 
-export interface LocalStorageState {
-  value?: string;
-  update: (s: string) => void;
-  reset: () => void;
-}
-
-const storage: ObservableMap<string, string> = memoryMap<string>();
+const storage: ObservableMap<string, any> = memoryMap<any>();
 
-export function useMemoryStorage(
-  key: string,
-  initialValue: string,
-): Required<LocalStorageState>;
-export function useMemoryStorage(key: string): LocalStorageState;
-export function useMemoryStorage(
-  key: string,
-  initialValue?: string,
-): LocalStorageState {
-  const [storedValue, setStoredValue] = useState<string | undefined>(
-    (): string | undefined => {
-      return storage.get(key) ?? initialValue;
+//with initial value
+export function useMemoryStorage<Type = string>(
+  key: StorageKey<Type>,
+  defaultValue: Type,
+): Required<StorageState<Type>>;
+//with initial value
+export function useMemoryStorage<Type = string>(
+  key: StorageKey<Type>,
+): StorageState<Type>;
+// impl
+export function useMemoryStorage<Type = string>(
+  key: StorageKey<Type>,
+  defaultValue?: Type,
+): StorageState<Type> {
+  const [storedValue, setStoredValue] = useState<Type | undefined>(
+    (): Type | undefined => {
+      const prev = storage.get(key.id);
+      return prev;
     },
   );
 
   useEffect(() => {
-    return storage.onUpdate(key, () => {
-      const newValue = storage.get(key);
-      setStoredValue(newValue ?? initialValue);
+    return storage.onUpdate(key.id, () => {
+      const newValue = storage.get(key.id);
+      setStoredValue(newValue);
     });
   }, []);
 
-  const setValue = (value?: string): void => {
+  const setValue = (value?: Type): void => {
     if (value === undefined) {
-      storage.delete(key);
+      storage.delete(key.id);
     } else {
-      storage.set(key, value);
+      storage.set(key.id, value);
     }
   };
 
@@ -69,7 +65,7 @@ export function useMemoryStorage(
     value: storedValue,
     update: setValue,
     reset: () => {
-      setValue(initialValue);
+      setValue(defaultValue);
     },
   };
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]