aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-10-21 20:25:38 -0300
committerSebastian <sebasjm@gmail.com>2023-10-21 20:25:50 -0300
commit2ac73949e7cb8de44e56f2fecae617efab15671e (patch)
tree144a97d71bc9fa964675ef0cc764087ceb14e8eb /packages/demobank-ui
parent4b98b693d696d90f30f0a6546b0e1f4bc181a5f2 (diff)
more ui
Diffstat (limited to 'packages/demobank-ui')
-rw-r--r--packages/demobank-ui/src/assets/lang.svg48
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx159
-rw-r--r--packages/demobank-ui/src/components/CopyButton.tsx32
-rw-r--r--packages/demobank-ui/src/components/LangSelector.tsx3
-rw-r--r--packages/demobank-ui/src/components/Routing.tsx208
-rw-r--r--packages/demobank-ui/src/components/Transactions/views.tsx12
-rw-r--r--packages/demobank-ui/src/components/app.tsx3
-rw-r--r--packages/demobank-ui/src/context/config.ts10
-rw-r--r--packages/demobank-ui/src/hooks/access.ts24
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts14
-rw-r--r--packages/demobank-ui/src/pages.ts44
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx29
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx122
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx12
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts15
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx24
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx65
-rw-r--r--packages/demobank-ui/src/pages/ProfileNavigation.tsx56
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx16
-rw-r--r--packages/demobank-ui/src/pages/ShowAccountDetails.tsx123
-rw-r--r--packages/demobank-ui/src/pages/UpdateAccountPassword.tsx259
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx1
-rw-r--r--packages/demobank-ui/src/pages/admin/Account.tsx10
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx182
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx58
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx32
-rw-r--r--packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx47
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx24
-rw-r--r--packages/demobank-ui/src/pages/admin/Home.tsx143
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx3
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx (renamed from packages/demobank-ui/src/pages/business/Home.tsx)346
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx237
-rw-r--r--packages/demobank-ui/src/route.ts167
33 files changed, 1526 insertions, 1002 deletions
diff --git a/packages/demobank-ui/src/assets/lang.svg b/packages/demobank-ui/src/assets/lang.svg
new file mode 100644
index 000000000..dd72ce65e
--- /dev/null
+++ b/packages/demobank-ui/src/assets/lang.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 2794;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;}
+ .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g id="Layer_2">
+</g>
+<g id="Layer_x5F_1_x5F_1">
+ <g>
+ <polygon points="1204.6,359.2 271.8,30 271.8,2060.1 1204.6,1758.3 "/>
+ <polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 1182.2,1757.3 "/>
+ <polygon class="st0" points="30,2415.4 1182.2,2031.4 1182.2,357.9 30,742 "/>
+ <polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8 "/>
+ <g>
+ <path d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
+ c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
+ c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
+ <path d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
+ c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
+ c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
+ c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
+ c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
+ c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
+ c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
+ C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
+ </g>
+ <path class="st1" d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
+ c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
+ c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
+ c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
+ c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
+ <path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
+ c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
+ <path class="st1" d="M2120,2017l-907-282V380l907-308V2017z M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
+ c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
+ c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
+ <polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 2411.2,757.2 "/>
+ <g>
+ <path class="st2" d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
+ l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 0602f507e..32fe0aa9e 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
import { format } from "date-fns";
@@ -33,55 +33,118 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
const { i18n } = useTranslationContext();
- if (!cashouts.length) {
- return (
- <div>
- <i18n.Translate>No cashout at the moment</i18n.Translate>
- </div>
- );
- }
+ if (!cashouts.length) return <div />
+ const txByDate = cashouts.reduce((prev, cur) => {
+ const d = cur.creation_time.t_s === "never"
+ ? ""
+ : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy")
+ if (!prev[d]) {
+ prev[d] = []
+ }
+ prev[d].push(cur)
+ return prev
+ }, {} as Record<string, typeof cashouts>)
return (
- <div class="results">
- <table class="pure-table pure-table-striped">
- <thead>
- <tr>
- <th>{i18n.str`Created`}</th>
- <th>{i18n.str`Confirmed`}</th>
- <th>{i18n.str`Total debit`}</th>
- <th>{i18n.str`Total credit`}</th>
- <th>{i18n.str`Status`}</th>
- <th>{i18n.str`Subject`}</th>
- </tr>
- </thead>
- <tbody>
- {cashouts.map((item, idx) => {
- return (
- <tr key={idx}>
- <td>{item.creation_time.t_s === "never" ? i18n.str`never` : format(item.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")}</td>
- <td>
- {item.confirmation_time
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900"><i18n.Translate>Latest cashouts</i18n.Translate></h1>
+ </div>
+ </div>
+ <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th scope="col" class=" pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Created`}</th>
+ <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Confirmed`}</th>
+ <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Total debit`}</th>
+ <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Total credit`}</th>
+ <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Status`}</th>
+ <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return <Fragment key={idx}>
+ <tr class="border-t border-gray-200">
+ <th colSpan={4} scope="colgroup" class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3">
+ {date}
+ </th>
+ </tr>
+ {txs.map(item => {
+ const creationTime = item.creation_time.t_s === "never" ? "" : format(item.creation_time.t_s * 1000, "HH:mm:ss")
+ const confirmationTime = item.confirmation_time
? item.confirmation_time.t_s === "never" ? i18n.str`never` : format(item.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss")
- : "-"}
- </td>
- <td><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td>
- <td><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td>
- <td>{item.status}</td>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- onSelected(item.id);
- }}
- >
- {item.subject}
- </a>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ : "-"
+ return (<tr key={idx} class="border-b border-gray-200 last:border-none">
+
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">{creationTime}</div>
+ {/* <dl class="font-normal sm:hidden">
+ <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-700">
+ {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? (
+ <span data-negative={item.negative ? "true" : "false"} class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
+ <RenderAmount value={item.amount} />
+ </span>
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
+ )}</dd>
+
+ <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-500 sm:hidden">
+ {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
+ </dd>
+ <dd class="mt-1 text-gray-500 sm:hidden" >
+ <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
+ {item.subject}
+ </pre>
+ </dd>
+ </dl> */}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{confirmationTime}</td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td>
+
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.status}</td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+ <a href="#" onClick={(e) => {
+ e.preventDefault();
+ onSelected(item.id);
+ }}>
+ {item.subject}
+ </a>
+ </td>
+ </tr>)
+ })}
+ </Fragment>
+
+ })}
+ </tbody>
+
+ </table>
+
+ {/* <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination">
+ <div class="flex flex-1 justify-between sm:justify-end">
+ <button
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ disabled={!onPrev}
+ onClick={onPrev}
+ >
+ <i18n.Translate>First page</i18n.Translate>
+ </button>
+ <button
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ disabled={!onNext}
+ onClick={onNext}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </button>
+ </div>
+ </nav> */}
+ </div>
</div>
);
+ // }
+
}
diff --git a/packages/demobank-ui/src/components/CopyButton.tsx b/packages/demobank-ui/src/components/CopyButton.tsx
index b36de770e..ca1ceaa8a 100644
--- a/packages/demobank-ui/src/components/CopyButton.tsx
+++ b/packages/demobank-ui/src/components/CopyButton.tsx
@@ -5,31 +5,21 @@ import { useEffect, useState } from "preact/hooks";
export function CopyIcon(): VNode {
return (
- <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5">
- <path
- fill-rule="evenodd"
- d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
- />
- <path
- fill-rule="evenodd"
- d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
- />
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg>
)
};
export function CopiedIcon(): VNode {
return (
- <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5">
- <path
- fill-rule="evenodd"
- d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
- />
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)
};
-export function CopyButton({ getContent }: { getContent: () => string }): VNode {
+export function CopyButton({ class: clazz, getContent }: { class: string, getContent: () => string }): VNode {
const [copied, setCopied] = useState(false);
function copyText(): void {
navigator.clipboard.writeText(getContent() || "");
@@ -45,16 +35,14 @@ export function CopyButton({ getContent }: { getContent: () => string }): VNode
if (!copied) {
return (
- <button class="text-white" onClick={copyText} style={{ width: 16, height: 16, fontSize: "initial" }}>
+ <button class={clazz} onClick={copyText} >
<CopyIcon />
</button>
);
}
return (
- <div class="text-white" content="Copied" style={{ display: "inline-block" }}>
- <button disabled style={{ width: 16, height: 16, fontSize: "initial" }}>
- <CopiedIcon />
- </button>
- </div>
+ <button class={clazz} disabled>
+ <CopiedIcon />
+ </button>
);
-} \ No newline at end of file
+}
diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx
index c1d0f64ef..7cf0300df 100644
--- a/packages/demobank-ui/src/components/LangSelector.tsx
+++ b/packages/demobank-ui/src/components/LangSelector.tsx
@@ -23,6 +23,7 @@ import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { strings as messages } from "../i18n/strings.js";
+import langIcon from "../assets/lang.svg";
type LangsNames = {
[P in keyof typeof messages]: string;
@@ -69,7 +70,7 @@ export function LangSelector(): VNode {
setHidden((h) => !h);
}}>
<span class="flex items-center">
- <img src="https://taler.net/images/languageicon.svg" alt="" class="h-5 w-5 flex-shrink-0 rounded-full" />
+ <img src={langIcon} alt="" class="h-5 w-5 flex-shrink-0 rounded-full" />
<span class="ml-3 block truncate">{getLangName(lang)}</span>
</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx
index 04cf96190..1d587fe32 100644
--- a/packages/demobank-ui/src/components/Routing.tsx
+++ b/packages/demobank-ui/src/components/Routing.tsx
@@ -19,19 +19,26 @@ import { createHashHistory } from "history";
import { Fragment, VNode, h } from "preact";
import { Route, Router, route } from "preact-router";
import { useEffect } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
+import { useBackendState } from "../hooks/backend.js";
import { BankFrame } from "../pages/BankFrame.js";
import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";
import { LoginForm } from "../pages/LoginForm.js";
import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js";
import { RegistrationPage } from "../pages/RegistrationPage.js";
-import { AdminHome } from "../pages/admin/Home.js";
-import { BusinessAccount } from "../pages/business/Home.js";
+import { AdminHome } from "../pages/admin/AdminHome.js";
+import { CreateCashout } from "../pages/business/CreateCashout.js";
import { bankUiSettings } from "../settings.js";
+import { ShowAccountDetails } from "../pages/ShowAccountDetails.js";
+import { UpdateAccountPassword } from "../pages/UpdateAccountPassword.js";
+import { RemoveAccount } from "../pages/admin/RemoveAccount.js";
+import { CreateNewAccount } from "../pages/admin/CreateNewAccount.js";
+import { CashoutListForAccount } from "../pages/admin/CashoutListForAccount.js";
+import { ShowCashoutDetails } from "../pages/business/ShowCashoutDetails.js";
+import { WireTransfer } from "../pages/admin/Account.js";
export function Routing(): VNode {
const history = createHashHistory();
- const backend = useBackendContext();
+ const backend = useBackendState();
const { i18n } = useTranslationContext();
if (backend.state.status === "loggedOut") {
@@ -90,7 +97,7 @@ export function Routing(): VNode {
const { isUserAdministrator, username } = backend.state
return (
- <BankFrame account={backend.state.username}>
+ <BankFrame account={username}>
<Router history={history}>
<Route
path="/operation/:wopid"
@@ -107,6 +114,167 @@ export function Routing(): VNode {
path="/public-accounts"
component={() => <PublicHistoriesPage />}
/>
+
+ <Route
+ path="/new-account"
+ component={() => <CreateNewAccount
+ onCancel={() => {
+ route("/account")
+ }}
+ onCreateSuccess={() => {
+ route("/account")
+ }}
+ />}
+ />
+
+ <Route
+ path="/profile/:account/details"
+ component={({ account }: { account: string }) => (
+ <ShowAccountDetails
+ account={account}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onClear={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/profile/:account/change-password"
+ component={({ account }: { account: string }) => (
+ <UpdateAccountPassword
+ focus
+ account={account}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onCancel={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/profile/:account/delete"
+ component={({ account }: { account: string }) => (
+ <RemoveAccount
+ account={account}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onCancel={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/profile/:account/cashouts"
+ component={({ account }: { account: string }) => (
+ <CashoutListForAccount
+ account={account}
+ onSelected={(cid) => {
+ route(`/cashout/${cid}`)
+ }}
+ onClose={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/my-profile"
+ component={() => (
+ <ShowAccountDetails
+ account={username}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onClear={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/my-password"
+ component={() => (
+ <UpdateAccountPassword
+ focus
+ account={username}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onCancel={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/my-cashouts"
+ component={() => (
+ <CashoutListForAccount
+ account={username}
+ onSelected={(cid) => {
+ route(`/cashout/${cid}`)
+ }}
+ onClose={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/new-cashout"
+ component={() => (
+ <CreateCashout
+ account={username}
+ onComplete={(cid) => {
+ route(`/cashout/${cid}`);
+ }}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/cashout/:cid"
+ component={({ cid }: { cid: string }) => (
+ <ShowCashoutDetails
+ id={cid}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+
+
+ <Route
+ path="/wire-transfer/:dest"
+ component={({ dest }: { dest: string }) => (
+ <WireTransfer
+ toAccount={dest}
+ onCancel={() => {
+ route("/account")
+ }}
+ onSuccess={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
<Route
path="/account"
component={() => {
@@ -115,6 +283,22 @@ export function Routing(): VNode {
onRegister={() => {
route("/register");
}}
+ onCreateAccount={() => {
+ route("/new-account")
+ }}
+ onShowAccountDetails={(aid) => {
+ route(`/profile/${aid}/details`)
+ }}
+ onRemoveAccount={(aid) => {
+ route(`/profile/${aid}/delete`)
+ }}
+ onShowCashoutForAccount={(aid) => {
+ route(`/profile/${aid}/cashouts`)
+ }}
+ onUpdateAccountPassword={(aid) => {
+ route(`/profile/${aid}/change-password`)
+
+ }}
/>;
} else {
return <HomePage
@@ -132,20 +316,6 @@ export function Routing(): VNode {
}
}}
/>
- <Route
- path="/business"
- component={() => (
- <BusinessAccount
- account={username}
- onClose={() => {
- route("/account");
- }}
- onRegister={() => {
- route("/register");
- }}
- />
- )}
- />
<Route default component={Redirect} to="/account" />
</Router>
</BankFrame>
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx
index 5cdb47a0c..47daf8963 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -86,11 +86,13 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
<dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt>
<dd class="mt-1 truncate text-gray-500 sm:hidden">
- {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
+ {item.negative ? i18n.str`to` : i18n.str`from`} <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900">
+ {item.counterpart}
+ </a>
</dd>
<dd class="mt-1 text-gray-500 sm:hidden" >
<pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
- {item.subject}
+ {item.subject}
</pre>
</dd>
</dl>
@@ -102,7 +104,11 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
<span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
)}
</td>
- <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">
+ <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900">
+ {item.counterpart}
+ </a>
+ </td>
<td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td>
</tr>)
})}
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
index beb24da57..55e1178fe 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -27,6 +27,7 @@ import { BankCoreApiProvider } from "../context/config.js";
import { strings } from "../i18n/strings.js";
import { bankUiSettings } from "../settings.js";
import { Routing } from "./Routing.js";
+import { BankFrame } from "../pages/BankFrame.js";
const WITH_LOCAL_STORAGE_CACHE = false;
const App: FunctionalComponent = () => {
@@ -34,7 +35,7 @@ const App: FunctionalComponent = () => {
return (
<TranslationProvider source={strings}>
<BackendStateProvider>
- <BankCoreApiProvider baseUrl={baseUrl}>
+ <BankCoreApiProvider baseUrl={baseUrl} frameOnError={BankFrame}>
<SWRConfig
value={{
provider: WITH_LOCAL_STORAGE_CACHE
diff --git a/packages/demobank-ui/src/context/config.ts b/packages/demobank-ui/src/context/config.ts
index 013d8922e..a31d914b8 100644
--- a/packages/demobank-ui/src/context/config.ts
+++ b/packages/demobank-ui/src/context/config.ts
@@ -16,7 +16,7 @@
import { TalerCorebankApi, TalerCoreBankHttpClient, TalerError } from "@gnu-taler/taler-util";
import { BrowserHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, createContext, h, VNode } from "preact";
+import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
import { ErrorLoading } from "../components/ErrorLoading.js";
@@ -43,9 +43,11 @@ export type ConfigResult = undefined
export const BankCoreApiProvider = ({
baseUrl,
children,
+ frameOnError,
}: {
baseUrl: string,
children: ComponentChildren;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>,
}): VNode => {
const [checked, setChecked] = useState<ConfigResult>()
const { i18n } = useTranslationContext();
@@ -68,13 +70,13 @@ export const BankCoreApiProvider = ({
}, []);
if (checked === undefined) {
- return h("div", {}, "loading...")
+ return h(frameOnError, { children: h("div", {}, "loading...") })
}
if (checked.type === "error") {
- return h(ErrorLoading, { error: checked.error, showDetail: true })
+ return h(frameOnError, { children: h(ErrorLoading, { error: checked.error, showDetail: true }) })
}
if (checked.type === "incompatible") {
- return h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`)
+ return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) })
}
const value: Type = {
url, config: checked.config, api
diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts
index 7023b8803..da9812ffa 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -61,7 +61,7 @@ export function useWithdrawalDetails(wid: string) {
// const { state: credentials } = useBackendState();
const { api } = useBankCoreApiContext();
- async function fetcher(wid: string) {
+ async function fetcher([wid]: [string]) {
return await api.getWithdrawalById(wid)
}
@@ -114,7 +114,7 @@ export function usePublicAccounts(initial?: number) {
const [offset, setOffset] = useState<number | undefined>(initial);
const { api } = useBankCoreApiContext();
- async function fetcher(txid: number | undefined) {
+ async function fetcher([txid]: [number | undefined]) {
return await api.getPublicAccounts({
limit: MAX_RESULT_SIZE,
offset: txid ? String(txid) : undefined,
@@ -124,16 +124,16 @@ export function usePublicAccounts(initial?: number) {
const { data, error } = useSWR<TalerCoreBankResultByMethod<"getPublicAccounts">, TalerHttpError>(
[offset, "getPublicAccounts"], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- keepPreviousData: true,
- });
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
const isLastPage =
data && data.body.public_accounts.length < PAGE_SIZE;
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index 0f7af5fe5..06e068d6d 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -15,16 +15,15 @@
*/
import { useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
import { useBackendState } from "./backend.js";
-// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import { AccessToken, AmountJson, Amounts, OperationOk, TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
import { useBankCoreApiContext } from "../context/config.js";
import { assertUnreachable } from "../pages/HomePage.js";
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
const useSWR = _useSWR as unknown as SWRHook;
export type TransferCalculation = {
@@ -44,12 +43,8 @@ type CashoutEstimators = {
};
export function useEstimator(): CashoutEstimators {
- const { state } = useBackendContext();
+ const { state } = useBackendState();
const { api } = useBankCoreApiContext();
- const creds =
- state.status !== "loggedIn"
- ? undefined
- : state.token;
return {
estimateByCredit: async (amount, fee, rate) => {
const resp = await api.getCashoutRate({
@@ -101,13 +96,14 @@ export function useEstimator(): CashoutEstimators {
}
export function useRatiosAndFeeConfig() {
- const { api } = useBankCoreApiContext();
+ const { api, config } = useBankCoreApiContext();
+
function fetcher() {
return api.getConversionRates()
}
const { data, error } = useSWR<TalerCoreBankResultByMethod<"getConversionRates">, TalerHttpError>(
- [, "getConversionRates"], fetcher, {
+ !config.have_cashout || !config.fiat_currency ? false : [, "getConversionRates"], fetcher, {
refreshInterval: 60 * 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
diff --git a/packages/demobank-ui/src/pages.ts b/packages/demobank-ui/src/pages.ts
new file mode 100644
index 000000000..c78240a02
--- /dev/null
+++ b/packages/demobank-ui/src/pages.ts
@@ -0,0 +1,44 @@
+import { WithdrawalOperationPage } from "./pages/HomePage.js";
+import { PageEntry, pageDefinition } from "./route.js";
+
+// const operationById: PageEntry<{ operationId: string }> = {
+// url: pageDefinition("#/operation/:operationId"),
+// view: WithdrawalOperationPage,
+// };
+
+
+// const home: PageEntry = {
+// url: "#/",
+// view: Home,
+// };
+// const cases: PageEntry = {
+// url: "#/cases",
+// view: Cases,
+// };
+
+// const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
+// url: pageDefinition("#/account/:account/new/:type?"),
+// view: NewFormEntry,
+// };
+
+// const settings: PageEntry = {
+// url: "#/settings",
+// view: Settings,
+// };
+// const officer: PageEntry = {
+// url: "#/officer",
+// view: Officer,
+// };
+// const welcome: PageEntry<{ asd?: string; name?: string }> = {
+// url: pageDefinition("#/welcome/:name?"),
+// view: Welcome,
+// };
+// const form: PageEntry<{ number?: string }> = {
+// url: pageDefinition("#/form/:number?"),
+// view: AntiMoneyLaunderingForm,
+// };
+
+export const Pages = {
+ // operationById,
+
+};
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index 0604001e3..00643ec3e 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -54,40 +54,11 @@ function ShowDemoInfo(): VNode {
}
export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> {
- const { i18n } = useTranslationContext();
return <Fragment>
- <MaybeBusinessButton account={account} onClick={goToBusinessAccount} />
-
<ShowDemoInfo />
-
<PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} />
<Transactions account={account} />
</Fragment>;
}
-function MaybeBusinessButton({
- account,
- onClick,
-}: {
- account: string;
- onClick: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- return <Fragment />
- // const result = useBusinessAccountDetails(account);
- // if (!result.ok) return <Fragment />;
- // return (
- // <div class="w-full flex justify-end">
- // <button
- // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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"
- // onClick={(e) => {
- // e.preventDefault()
- // onClick()
- // }}
- // >
- // <i18n.Translate>Business Profile</i18n.Translate>
- // </button>
- // </div>
- // );
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index 96ce9c317..c0babd0c9 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -23,8 +23,8 @@ import { Attention } from "../components/Attention.js";
import { CopyButton } from "../components/CopyButton.js";
import { LangSelector } from "../components/LangSelector.js";
import { Loading } from "../components/Loading.js";
-import { useBackendContext } from "../context/backend.js";
import { useAccountDetails } from "../hooks/access.js";
+import { useBackendState } from "../hooks/backend.js";
import { getAllBooleanSettings, getLabelForSetting, useSettings } from "../hooks/settings.js";
import { bankUiSettings } from "../settings.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
@@ -49,7 +49,7 @@ export function BankFrame({
children: ComponentChildren;
}): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
+ const backend = useBackendState();
const [settings, updateSettings] = useSettings();
const [open, setOpen] = useState(false)
@@ -80,9 +80,9 @@ export function BankFrame({
return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;">
<div class="bg-indigo-600 pb-32">
<nav class="">
- <div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
+ <div class="mx-auto max-w-7xl px-2 ">
<div class="relative flex h-16 items-center justify-between ">
- <div class="flex items-center px-2 lg:px-0">
+ <div class="flex items-center px-2">
<div class="flex-shrink-0 bg-white rounded-lg">
<a href={bankUiSettings.iconLinkURL ?? "#"}>
<img
@@ -94,7 +94,7 @@ export function BankFrame({
</a>
</div>
{bankUiSettings.demoSites &&
- <div class="hidden sm:block lg:ml-10 ">
+ <div class="hidden sm:block ml-6 ">
<div class="flex space-x-4">
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
{bankUiSettings.demoSites.map(([name, url]) => {
@@ -161,41 +161,26 @@ export function BankFrame({
<div class="relative mt-6 flex-1 px-4 sm:px-6">
<nav class="flex flex-1 flex-col" aria-label="Sidebar">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
- <li>
- <a href="#"
- class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
- onClick={() => {
- backend.logOut();
- setOpen(false)
- updateSettings("currentWithdrawalOperationId", undefined);
- }}
- >
- <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
- </svg>
- <i18n.Translate>Log out</i18n.Translate>
- </a>
- </li>
+ {backend.state.status === "loggedIn" ?
+ <li>
+ <a href="#"
+ class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
+ onClick={() => {
+ backend.logOut();
+ setOpen(false)
+ updateSettings("currentWithdrawalOperationId", undefined);
+ }}
+ >
+ <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+ </svg>
+ <i18n.Translate>Log out</i18n.Translate>
+ </a>
+ </li>
+ : undefined}
<li>
<LangSelector />
</li>
- {bankUiSettings.demoSites &&
- <li class="sm:hidden">
- <div class="text-xs font-semibold leading-6 text-gray-400">
- <i18n.Translate>Sites</i18n.Translate>
- </div>
- <ul role="list" class="space-y-1">
- {bankUiSettings.demoSites.map(([name, url]) => {
- return <li>
- <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
- <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
- <span class="truncate">{name}</span>
- </a>
- </li>
- })}
- </ul>
- </li>
- }
<li>
<div class="text-xs font-semibold leading-6 text-gray-400">
<i18n.Translate>Preferences</i18n.Translate>
@@ -220,6 +205,23 @@ export function BankFrame({
})}
</ul>
</li>
+ {bankUiSettings.demoSites &&
+ <li class="sm:hidden">
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Sites</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-1">
+ {bankUiSettings.demoSites.map(([name, url]) => {
+ return <li>
+ <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+ <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
+ <span class="truncate">{name}</span>
+ </a>
+ </li>
+ })}
+ </ul>
+ </li>
+ }
</ul>
</nav>
</div>
@@ -342,28 +344,30 @@ function Footer() {
function WelcomeAccount({ account: accountName }: { account: string }): VNode {
const { i18n } = useTranslationContext();
-
- const result = useAccountDetails(accountName);
- if (!result) {
- return <Loading />
- }
- if (result instanceof TalerError) {
- return <div />
- }
-
- const payto = result.type === "fail" ? undefined : parsePaytoUri(result.body.payto_uri)
- const info = !payto || !payto.isKnown ? undefined
- : payto.targetType === "iban" ? { account: payto.iban, uri: stringifyPaytoUri(payto) }
- : payto.targetType === "x-taler-bank" ? { account: payto.account, uri: stringifyPaytoUri(payto) }
- : undefined;
-
- return <i18n.Translate>
- Welcome, <span class="whitespace-nowrap">{accountName}</span> {info !== undefined ?
- <small class="whitespace-nowrap">
- (<a href={info.uri}>{info.account}</a> <CopyButton getContent={() => info.uri} />)
- </small>
- : <Fragment />}!
- </i18n.Translate>
+ return <a href="#/my-profile" class="underline underline-offset-2">
+ <i18n.Translate>Welcome, <span class="whitespace-nowrap">{accountName}</span></i18n.Translate>
+ </a>
+ // const result = useAccountDetails(accountName);
+ // if (!result) {
+ // return <Loading />
+ // }
+ // if (result instanceof TalerError) {
+ // return <div />
+ // }
+
+ // const payto = result.type === "fail" ? undefined : parsePaytoUri(result.body.payto_uri)
+ // const info = !payto || !payto.isKnown ? undefined
+ // : payto.targetType === "iban" ? { account: payto.iban, uri: stringifyPaytoUri(payto) }
+ // : payto.targetType === "x-taler-bank" ? { account: payto.account, uri: stringifyPaytoUri(payto) }
+ // : undefined;
+
+ // return <i18n.Translate>
+ // Welcome, <span class="whitespace-nowrap">{accountName}</span> {info !== undefined ?
+ // <small class="whitespace-nowrap">
+ // (<a href={info.uri}>{info.account}</a> <CopyButton getContent={() => info.uri} />)
+ // </small>
+ // : <Fragment />}!
+ // </i18n.Translate>
}
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index 981b0f880..6d1d35288 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,24 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode, TalerAuthentication, TranslatedString } from "@gnu-taler/taler-util";
-import { ErrorType, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { notify, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { useBackendContext } from "../context/backend.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useBackendState } from "../hooks/backend.js";
import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
-import { doAutoFocus } from "./PaytoWireTransferForm.js";
-import { useBankCoreApiContext } from "../context/config.js";
import { assertUnreachable } from "./HomePage.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
/**
* Collect and submit login data.
*/
export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forbidden", onRegister?: () => void }): VNode {
- const backend = useBackendContext();
+ 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>();
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
index c9c1fa238..9e34a846b 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -86,10 +86,10 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
const wid = withdrawalOperationId
async function doAbort() {
- setBusy({})
await withRuntimeErrorHandling(i18n, async () => {
const resp = await api.abortWithdrawalById(wid);
if (resp.type === "ok") {
+ updateSettings("currentWithdrawalOperationId", undefined)
onClose();
} else {
switch (resp.case) {
@@ -103,7 +103,6 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
}
}
})
- setBusy(undefined)
}
async function doConfirm() {
@@ -220,11 +219,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
status: "ready",
error: undefined,
uri: parsedUri,
- onClose: async () => {
- await doAbort()
- updateSettings("currentWithdrawalOperationId", undefined)
- onClose()
- },
+ onClose: doAbort,
onAbort: doAbort,
}
}
@@ -252,11 +247,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
return {
status: "need-confirmation",
error: undefined,
- onAbort: async () => {
- await doAbort()
- updateSettings("currentWithdrawalOperationId", undefined)
- onClose()
- },
+ onAbort: doAbort,
busy: !!busy,
onConfirm: doConfirm
}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index c2d87d0e6..d0ee0243d 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -30,8 +30,8 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
const { i18n } = useTranslationContext();
const [settings] = useSettings();
- const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer");
-
+ const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
+ console.log("patment", tab)
return (
<div class="mt-2">
@@ -48,9 +48,9 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
}} />
<div class="flex flex-col">
<span class="flex">
- <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
+ <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
<span class="grow self-center text-lg text-gray-900 align-middle text-center">
- <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
+ <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
</span>
<svg class="self-center flex-none h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
@@ -59,14 +59,14 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
<div class="mt-1 flex items-center text-sm text-gray-500">
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
</div>
- {!!settings.currentWithdrawalOperationId &&
- <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
- <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
- <circle cx="3" cy="3" r="3" />
- </svg>
- <i18n.Translate>operation ready</i18n.Translate>
- </span>
- }
+ {!!settings.currentWithdrawalOperationId &&
+ <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
+ <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
+ <circle cx="3" cy="3" r="3" />
+ </svg>
+ <i18n.Translate>operation ready</i18n.Translate>
+ </span>
+ }
</div>
</label>
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index e713324c5..d859c10d7 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -51,20 +51,25 @@ const logger = new Logger("PaytoWireTransferForm");
export function PaytoWireTransferForm({
focus,
title,
+ toAccount,
onSuccess,
onCancel,
limit,
}: {
title: TranslatedString,
focus?: boolean;
+ toAccount?: string,
onSuccess: () => void;
onCancel: (() => void) | undefined;
limit: AmountJson;
}): VNode {
- const [isRawPayto, setIsRawPayto] = useState(true);
+ const [isRawPayto, setIsRawPayto] = useState(false);
const { state: credentials } = useBackendState()
const { api } = useBankCoreApiContext();
- const [iban, setIban] = useState<string | undefined>();
+
+ const sendingToFixedAccount = toAccount !== undefined
+ //FIXME: support other destination that just IBAN
+ const [iban, setIban] = useState<string | undefined>(toAccount);
const [subject, setSubject] = useState<string | undefined>();
const [amount, setAmount] = useState<string | undefined>();
@@ -163,7 +168,7 @@ export function PaytoWireTransferForm({
setAmount(undefined);
setIban(undefined);
setSubject(undefined);
- rawPaytoInputSetter(undefined)
+ rawPaytoInputSetter(undefined)
})
}
@@ -181,9 +186,12 @@ export function PaytoWireTransferForm({
<input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => {
if (parsed && parsed.isKnown && parsed.targetType === "iban") {
setIban(parsed.iban)
- const amount = Amounts.parse(parsed.params["amount"])
- if (amount) {
- setAmount(Amounts.stringifyValue(amount))
+ const amountStr = parsed.params["amount"]
+ if (amountStr) {
+ const amount = Amounts.parse(parsed.params["amount"])
+ if (amount) {
+ setAmount(Amounts.stringifyValue(amount))
+ }
}
const subject = parsed.params["message"]
if (subject) {
@@ -201,28 +209,30 @@ export function PaytoWireTransferForm({
</span>
</label>
- <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
- <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => {
- if (iban) {
- const payto = buildPayto("iban", iban, undefined)
- if (parsedAmount) {
- payto.params["amount"] = Amounts.stringify(parsedAmount)
- }
- if (subject) {
- payto.params["message"] = subject
+ {sendingToFixedAccount ? undefined :
+ <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
+ <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => {
+ if (iban) {
+ const payto = buildPayto("iban", iban, undefined)
+ if (parsedAmount) {
+ payto.params["amount"] = Amounts.stringify(parsedAmount)
+ }
+ if (subject) {
+ payto.params["message"] = subject
+ }
+ rawPaytoInputSetter(stringifyPaytoUri(payto))
}
- rawPaytoInputSetter(stringifyPaytoUri(payto))
- }
- setIsRawPayto(true)
- }} />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Import payto:// URI</i18n.Translate>
+ setIsRawPayto(true)
+ }} />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Import payto:// URI</i18n.Translate>
+ </span>
</span>
</span>
- </span>
- </label>
+ </label>
+ }
</div>
</div>
</div>
@@ -244,9 +254,10 @@ export function PaytoWireTransferForm({
<input
ref={focus ? doAutoFocus : undefined}
type="text"
- 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"
+ 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"
name="iban"
id="iban"
+ disabled={sendingToFixedAccount}
value={iban ?? ""}
placeholder="CC0123456789"
autocomplete="off"
@@ -369,7 +380,7 @@ export function PaytoWireTransferForm({
export function doAutoFocus(element: HTMLElement | null) {
if (element) {
setTimeout(() => {
- element.focus()
+ element.focus({ preventScroll: true })
element.scrollIntoView({
behavior: "smooth",
block: "center",
diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
new file mode 100644
index 000000000..c061c9742
--- /dev/null
+++ b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
@@ -0,0 +1,56 @@
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useBankCoreApiContext } from "../context/config.js";
+import { assertUnreachable } from "./HomePage.js";
+
+export function ProfileNavigation({ current }: { current: "details" | "credentials" | "cashouts" }): VNode {
+ const { i18n } = useTranslationContext()
+ const { config } = useBankCoreApiContext()
+ return <div>
+ <div class="sm:hidden">
+ <label for="tabs" class="sr-only"><i18n.Translate>Select a section</i18n.Translate></label>
+ <select id="tabs" name="tabs" class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" onChange={(e) => {
+ const op = e.currentTarget.value as typeof current
+ switch (op) {
+ case "details": {
+ window.location.href = "#/my-profile";
+ return;
+ }
+ case "credentials": {
+ window.location.href = "#/my-password";
+ return;
+ }
+ case "cashouts": {
+ window.location.href = "#/my-cashouts";
+ return;
+ }
+ default: assertUnreachable(op)
+ }
+ }}>
+ <option value="details" selected={current == "details"}><i18n.Translate>Details</i18n.Translate></option>
+ <option value="credentials" selected={current == "credentials"}><i18n.Translate>Credentials</i18n.Translate></option>
+ {config.have_cashout ?
+ <option value="cashouts" selected={current == "cashouts"}><i18n.Translate>Cashouts</i18n.Translate></option>
+ : undefined}
+ </select>
+ </div>
+ <div class="hidden sm:block">
+ <nav class="isolate flex divide-x divide-gray-200 rounded-lg shadow" aria-label="Tabs">
+ <a href="#/my-profile" data-selected={current == "details"} class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" >
+ <span><i18n.Translate>Details</i18n.Translate></span>
+ <span aria-hidden="true" data-selected={current == "details"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ </a>
+ <a href="#/my-password" data-selected={current == "credentials"} aria-current="page" class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
+ <span><i18n.Translate>Credentials</i18n.Translate></span>
+ <span aria-hidden="true" data-selected={current == "credentials"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ </a>
+ {config.have_cashout ?
+ <a href="#/my-cashouts" data-selected={current == "cashouts"} class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
+ <span>Cashouts</span>
+ <span aria-hidden="true" data-selected={current == "cashouts"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ </a>
+ : undefined}
+ </nav>
+ </div>
+ </div>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index ce38a9fb8..3520405c5 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,21 +13,19 @@
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 { AccessToken, HttpStatusCode, Logger, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import { AccessToken, Logger, TranslatedString } from "@gnu-taler/taler-util";
import {
- RequestError,
notify,
- notifyError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { bankUiSettings } from "../settings.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { getRandomPassword, getRandomUsername } from "./rnd.js";
import { useBankCoreApiContext } from "../context/config.js";
+import { useBackendState } from "../hooks/backend.js";
+import { bankUiSettings } from "../settings.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
+import { getRandomPassword, getRandomUsername } from "./rnd.js";
const logger = new Logger("RegistrationPage");
@@ -55,7 +53,7 @@ export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
* Collect and submit registration data.
*/
function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode {
- const backend = useBackendContext();
+ const backend = useBackendState();
const [username, setUsername] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
index c65b90503..21724474a 100644
--- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -1,25 +1,24 @@
-import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import { notify, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoading } from "../components/ErrorLoading.js";
import { Loading } from "../components/Loading.js";
import { useBankCoreApiContext } from "../context/config.js";
import { useAccountDetails } from "../hooks/access.js";
import { useBackendState } from "../hooks/backend.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
import { assertUnreachable } from "./HomePage.js";
import { LoginForm } from "./LoginForm.js";
import { AccountForm } from "./admin/AccountForm.js";
+import { ProfileNavigation } from "./ProfileNavigation.js";
export function ShowAccountDetails({
account,
onClear,
onUpdateSuccess,
- onChangePassword,
}: {
onClear?: () => void;
- onChangePassword: () => void;
onUpdateSuccess: () => void;
account: string;
}): VNode {
@@ -27,6 +26,8 @@ export function ShowAccountDetails({
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const { api } = useBankCoreApiContext()
+ const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
+ credentials.username === account : false
const [update, setUpdate] = useState(false);
const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountData | undefined>();
@@ -47,11 +48,7 @@ export function ShowAccountDetails({
}
async function doUpdate() {
- if (!update) {
- setUpdate(true);
- return;
- }
- if (!submitAccount || !creds) return;
+ if (!update || !submitAccount || !creds) return;
await withRuntimeErrorHandling(i18n, async () => {
const resp = await api.updateAccount(creds, {
cashout_address: submitAccount.cashout_payto_uri,
@@ -62,8 +59,9 @@ export function ShowAccountDetails({
is_exchange: false,
name: submitAccount.name,
});
-
+
if (resp.type === "ok") {
+ notifyInfo(i18n.str`Account updated`);
onUpdateSuccess();
} else {
switch (resp.case) {
@@ -87,21 +85,23 @@ export function ShowAccountDetails({
}
return (
- <div>
+ <Fragment>
+ {accountIsTheCurrentUser ?
+ <ProfileNavigation current="details" />
+ :
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account "{account}"</i18n.Translate>
+ </h1>
+
+ }
+
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
- {update ?
- <i18n.Translate>Update account</i18n.Translate>
- :
- <i18n.Translate>Account details</i18n.Translate>
- }
- </h2>
- <div class="mt-4">
<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>change the account details</i18n.Translate>
+ <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
+ <i18n.Translate>Change details</i18n.Translate>
</span>
</span>
<button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 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"
@@ -111,69 +111,36 @@ export function ShowAccountDetails({
<span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]: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>
-
+ </h2>
</div>
+
<AccountForm
+ focus={update}
+ username={account}
template={result.body}
purpose={update ? "update" : "show"}
onChange={(a) => setSubmitAccount(a)}
>
-
- </AccountForm>
-
- <p class="buttons-account">
- <div
- style={{
- display: "flex",
- justifyContent: "space-between",
- flexFlow: "wrap-reverse",
- }}
- >
- <div>
- {onClear ? (
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- ) : undefined}
- </div>
- <div style={{ display: "flex" }}>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={update && !submitAccount}
- type="submit"
- value={i18n.str`Change password`}
- onClick={async (e) => {
- e.preventDefault();
- onChangePassword();
- }}
- />
- </div>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={update && !submitAccount}
- type="submit"
- value={update ? i18n.str`Confirm` : i18n.str`Update`}
- onClick={async (e) => {
- e.preventDefault();
- doUpdate()
- }}
- />
- </div>
- </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ {onClear ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onClear}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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={!update || !submitAccount}
+ onClick={doUpdate}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
</div>
- </p>
+ </AccountForm>
</div>
- </div>
+ </Fragment>
);
}
+
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
index d82dac4b1..e3f0de8cc 100644
--- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -1,16 +1,16 @@
-import { TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { notify, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
-import { doAutoFocus } from "./PaytoWireTransferForm.js";
import { useBankCoreApiContext } from "../context/config.js";
-import { assertUnreachable } from "./HomePage.js";
import { useBackendState } from "../hooks/backend.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
+import { assertUnreachable } from "./HomePage.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { ProfileNavigation } from "./ProfileNavigation.js";
export function UpdateAccountPassword({
- account,
+ account: accountName,
onCancel,
onUpdateSuccess,
focus,
@@ -22,13 +22,18 @@ export function UpdateAccountPassword({
}): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
+ const token = credentials.status !== "loggedIn" ? undefined : credentials.token
const { api } = useBankCoreApiContext();
+ const [current, setCurrent] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [repeat, setRepeat] = useState<string | undefined>();
+ const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
+ credentials.username === accountName : false
+
const errors = undefinedIfEmpty({
+ current: !accountIsTheCurrentUser ? undefined : !current ? i18n.str`required` : undefined,
password: !password ? i18n.str`required` : undefined,
repeat: !repeat
? i18n.str`required`
@@ -37,13 +42,16 @@ export function UpdateAccountPassword({
: undefined,
});
+
async function doChangePassword() {
- if (!!errors || !password || !creds) return;
+ if (!!errors || !password || !token) return;
await withRuntimeErrorHandling(i18n, async () => {
- const resp = await api.updatePassword(creds, {
+ const resp = await api.updatePassword({ username: accountName, token }, {
+ // old_password: current,
new_password: password,
});
if (resp.type === "ok") {
+ notifyInfo(i18n.str`Password changed`);
onUpdateSuccess();
} else {
switch (resp.case) {
@@ -51,6 +59,10 @@ export function UpdateAccountPassword({
type: "error",
title: i18n.str`Not authorized to change the password, maybe the session is invalid.`
})
+ case "no-rights": return notify({
+ type: "error",
+ title: i18n.str`This user have no right on to change the password.`
+ })
case "not-found": return notify({
type: "error",
title: i18n.str`Account not found`
@@ -62,112 +74,147 @@ export function UpdateAccountPassword({
}
return (
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>Update password for account "{account}"</i18n.Translate>
- </h2>
- </div>
- <form
- class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
- autoCapitalize="none"
- autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
- }}
- >
- <div class="px-4 py-6 sm:p-8">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <Fragment>
+ {accountIsTheCurrentUser ?
+ <ProfileNavigation current="credentials" /> :
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account "{accountName}"</i18n.Translate>
+ </h1>
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="password"
- >
- {i18n.str`New password`}
- </label>
- <div class="mt-2">
- <input
- ref={focus ? doAutoFocus : undefined}
- type="password"
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="password"
- id="password"
- data-error={!!errors?.password && password !== undefined}
- value={password ?? ""}
- onChange={(e) => {
- setPassword(e.currentTarget.value)
- }}
- // placeholder=""
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- </div>
- {/* <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>user </i18n.Translate>
- </p> */}
- </div>
+ }
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="repeat"
- >
- {i18n.str`Type it again`}
- </label>
- <div class="mt-2">
- <input
- type="password"
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="repeat"
- id="repeat"
- data-error={!!errors?.repeat && repeat !== undefined}
- value={repeat ?? ""}
- onChange={(e) => {
- setRepeat(e.currentTarget.value)
- }}
- // placeholder=""
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.repeat}
- isDirty={repeat !== undefined}
- />
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Update password</i18n.Translate>
+ </h2>
+ </div>
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={e => {
+ e.preventDefault()
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="password"
+ >
+ {i18n.str`New password`}
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="password"
+ id="password"
+ data-error={!!errors?.password && password !== undefined}
+ value={password ?? ""}
+ onChange={(e) => {
+ setPassword(e.currentTarget.value)
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>repeat the same password</i18n.Translate>
- </p>
- </div>
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="repeat"
+ >
+ {i18n.str`Type it again`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="repeat"
+ id="repeat"
+ data-error={!!errors?.repeat && repeat !== undefined}
+ value={repeat ?? ""}
+ onChange={(e) => {
+ setRepeat(e.currentTarget.value)
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>repeat the same password</i18n.Translate>
+ </p>
+ </div>
+ {accountIsTheCurrentUser ?
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="password"
+ >
+ {i18n.str`Current password`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="current"
+ id="current-password"
+ data-error={!!errors?.current && current !== undefined}
+ value={current ?? ""}
+ onChange={(e) => {
+ setCurrent(e.currentTarget.value)
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.current}
+ isDirty={current !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>your current password, for security</i18n.Translate>
+ </p>
+ </div>
+ : undefined}
+ </div>
</div>
- </div>
- <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- {onCancel ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ {onCancel ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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={(e) => {
+ e.preventDefault()
+ doChangePassword()
+ }}
>
- <i18n.Translate>Cancel</i18n.Translate>
+ <i18n.Translate>Change</i18n.Translate>
</button>
- : <div />
- }
- <button type="submit"
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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={(e) => {
- e.preventDefault()
- doChangePassword()
- }}
- >
- <i18n.Translate>Change</i18n.Translate>
- </button>
- </div>
- </form>
- </div>
+ </div>
+ </form>
+ </div>
+ </Fragment>
);
} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 7266e4de4..51edbc95f 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -30,6 +30,7 @@ import { assertUnreachable } from "./HomePage.js";
import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
import { Attention } from "../components/Attention.js";
+import { Pages } from "../pages.js";
const logger = new Logger("WithdrawalQRCode");
diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx
index bf2fa86f0..103747414 100644
--- a/packages/demobank-ui/src/pages/admin/Account.tsx
+++ b/packages/demobank-ui/src/pages/admin/Account.tsx
@@ -3,15 +3,15 @@ import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { ErrorLoading } from "../../components/ErrorLoading.js";
import { Loading } from "../../components/Loading.js";
-import { useBackendContext } from "../../context/backend.js";
import { useAccountDetails } from "../../hooks/access.js";
import { assertUnreachable } from "../HomePage.js";
import { LoginForm } from "../LoginForm.js";
import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js";
+import { useBackendState } from "../../hooks/backend.js";
-export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
+export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { onSuccess?: () => void; toAccount?: string, onCancel?: () => void, onRegister?: () => void }): VNode {
const { i18n } = useTranslationContext();
- const r = useBackendContext();
+ const r = useBackendState();
const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
const result = useAccountDetails(account);
@@ -41,11 +41,13 @@ export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode
return (
<PaytoWireTransferForm
title={i18n.str`Make a wire transfer`}
+ toAccount={toAccount}
limit={limit}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
+ if (onSuccess) onSuccess()
}}
- onCancel={undefined}
+ onCancel={onCancel}
/>
);
}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 8470930bf..bce089560 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -1,16 +1,20 @@
-import { ComponentChildren, VNode, h } from "preact";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
import { useEffect, useRef, useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { TalerCorebankApi, buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
import { doAutoFocus } from "../PaytoWireTransferForm.js";
+import { CopyButton } from "../../components/CopyButton.js";
+import { assertUnreachable } from "../HomePage.js";
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
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 }
+
/**
* Create valid account object to update or create
* Take template as initial values for the form
@@ -21,6 +25,7 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
*/
export function AccountForm({
template,
+ username,
purpose,
onChange,
focus,
@@ -28,11 +33,12 @@ export function AccountForm({
}: {
focus?: boolean,
children: ComponentChildren,
+ username?: string,
template: TalerCorebankApi.AccountData | undefined;
- onChange: (a: TalerCorebankApi.AccountData | undefined) => void;
+ onChange: (a: AccountFormData | undefined) => void;
purpose: "create" | "update" | "show";
}): VNode {
- const initial = initializeFromTemplate(template);
+ const initial = initializeFromTemplate(username, template);
const [form, setForm] = useState(initial);
const [errors, setErrors] = useState<
RecursivePartial<typeof initial> | undefined
@@ -69,14 +75,8 @@ export function AccountForm({
? i18n.str`phone number can't have other than numbers`
: undefined,
}),
- // iban: !newForm.iban
- // ? undefined //optional field
- // : !IBAN_REGEX.test(newForm.iban)
- // ? i18n.str`IBAN should have just uppercased letters and numbers`
- // : validateIBAN(newForm.iban, i18n),
name: !newForm.name ? i18n.str`required` : undefined,
-
- // username: !newForm.username ? i18n.str`required` : undefined,
+ username: !newForm.username ? i18n.str`required` : undefined,
});
setErrors(errors);
setForm(newForm);
@@ -95,7 +95,7 @@ export function AccountForm({
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- {/* <div class="sm:col-span-5">
+ <div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="username"
@@ -105,7 +105,7 @@ export function AccountForm({
</label>
<div class="mt-2">
<input
- ref={focus ? doAutoFocus : undefined}
+ ref={focus && purpose === "create" ? doAutoFocus : undefined}
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="username"
@@ -128,7 +128,7 @@ export function AccountForm({
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>account identification in the bank</i18n.Translate>
</p>
- </div> */}
+ </div>
<div class="sm:col-span-5">
<label
@@ -165,27 +165,7 @@ export function AccountForm({
</div>
- {purpose !== "create" && (<div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="internal-iban"
- >
- {i18n.str`Internal IBAN`}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="internal-iban"
- id="internal-iban"
- disabled={true}
- value={form.payto_uri ?? ""}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>international bank account number</i18n.Translate>
- </p>
- </div>)}
+ {purpose !== "create" && (<RenderPaytoDisabledField paytoURI={form.payto_uri} />)}
<div class="sm:col-span-5">
<label
@@ -264,6 +244,7 @@ export function AccountForm({
<div class="mt-2">
<input
type="text"
+ ref={focus && purpose === "update" ? doAutoFocus : undefined}
data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="cashout"
@@ -294,8 +275,9 @@ export function AccountForm({
}
function initializeFromTemplate(
+ username: string | undefined,
account: TalerCorebankApi.AccountData | undefined,
-): WithIntermediate<TalerCorebankApi.AccountData> {
+): WithIntermediate<AccountFormData> {
const emptyAccount = {
cashout_payto_uri: undefined,
contact_data: undefined,
@@ -314,8 +296,136 @@ function initializeFromTemplate(
if (typeof initial.contact_data === "undefined") {
initial.contact_data = emptyContact;
}
- // initial.contact_data.email;
+ const result: WithIntermediate<AccountFormData> = initial as any // FIXME: check types
+ result.username = username
+
return initial as any;
}
+function RenderPaytoDisabledField({ paytoURI }: { paytoURI: string | undefined }): VNode {
+ const { i18n } = useTranslationContext()
+ const payto = parsePaytoUri(paytoURI ?? "");
+ if (payto?.isKnown) {
+ if (payto.targetType === "iban") {
+ const value = payto.iban;
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="internal-iban"
+ >
+ {i18n.str`Internal IBAN`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="internal-iban"
+ id="internal-iban"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>international bank account number</i18n.Translate>
+ </p>
+ </div>
+ }
+ if (payto.targetType === "x-taler-bank") {
+ const value = payto.account;
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-id"
+ >
+ {i18n.str`Account ID`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="account-id"
+ id="account-id"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>internal account id</i18n.Translate>
+ </p>
+ </div>
+ }
+ if (payto.targetType === "bitcoin") {
+ const value = payto.targetPath;
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-id"
+ >
+ {i18n.str`Bitcoin address`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="account-id"
+ id="account-id"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>bitcoin address</i18n.Translate>
+ </p>
+ </div>
+ }
+ assertUnreachable(payto)
+ }
+
+ const value = paytoURI ?? ""
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="internal-payto"
+ >
+ {i18n.str`Internal account`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="internal-payto"
+ id="internal-payto"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>generic payto URI</i18n.Translate>
+ </p>
+ </div>
+}
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 8a1e8294a..39b43b9b1 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -1,22 +1,26 @@
import { Amounts, TalerError } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { ErrorLoading } from "../../components/ErrorLoading.js";
import { Loading } from "../../components/Loading.js";
import { useBusinessAccounts } from "../../hooks/circuit.js";
import { assertUnreachable } from "../HomePage.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { AccountAction } from "./Home.js";
+import { useBankCoreApiContext } from "../../context/config.js";
interface Props {
- onAction: (type: AccountAction, account: string) => void;
- account: string | undefined;
onCreateAccount: () => void;
+
+ onShowAccountDetails: (aid: string) => void;
+ onRemoveAccount: (aid: string) => void;
+ onUpdateAccountPassword: (aid: string) => void;
+ onShowCashoutForAccount: (aid: string) => void;
}
-export function AccountList({ account, onAction, onCreateAccount }: Props): VNode {
+export function AccountList({ onRemoveAccount, onShowAccountDetails, onUpdateAccountPassword, onShowCashoutForAccount, onCreateAccount }: Props): VNode {
const result = useBusinessAccounts();
const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext()
if (!result) {
return <Loading />
@@ -74,6 +78,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
const balance = !item.balance
? undefined
: Amounts.parse(item.balance.amount);
+ const noBalance = Amounts.isZero(item.balance.amount)
const balanceIsDebit =
item.balance &&
item.balance.credit_debit_indicator == "debit";
@@ -83,7 +88,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
<a href="#" class="text-indigo-600 hover:text-indigo-900"
onClick={(e) => {
e.preventDefault();
- onAction("show-details", item.username)
+ onShowAccountDetails(item.username)
}}
>
{item.username}
@@ -94,7 +99,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{item.name}
</td>
- <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ <td data-negative={noBalance ? undefined : balanceIsDebit ? "true" : "false"} class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600 ">
{!balance ? (
i18n.str`unknown`
) : (
@@ -107,27 +112,34 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
<a href="#" class="text-indigo-600 hover:text-indigo-900"
onClick={(e) => {
e.preventDefault();
- onAction("update-password", item.username)
+ onUpdateAccountPassword(item.username)
}}
>
change password
</a>
<br />
- <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
- e.preventDefault();
- onAction("show-cashout", item.username)
- }}
- >
- cashouts
- </a>
- <br />
- <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
- e.preventDefault();
- onAction("remove-account", item.username)
- }}
- >
- remove
- </a>
+ {config.have_cashout ?
+ <Fragment>
+
+ <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
+ e.preventDefault();
+ onShowCashoutForAccount(item.username)
+ }}
+ >
+ cashouts
+ </a>
+ <br />
+ </Fragment>
+ : undefined}
+ {noBalance ?
+ <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
+ e.preventDefault();
+ onRemoveAccount(item.username)
+ }}
+ >
+ remove
+ </a>
+ : undefined}
</td>
</tr>
})}
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
new file mode 100644
index 000000000..01f9f6dbd
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -0,0 +1,32 @@
+import { Fragment, VNode, h } from "preact";
+import { Transactions } from "../../components/Transactions/index.js";
+import { WireTransfer } from "./Account.js";
+import { AccountList } from "./AccountList.js";
+
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+interface Props {
+ onRegister: () => void;
+
+ onCreateAccount: () => void;
+ onShowAccountDetails: (aid: string) => void;
+ onRemoveAccount: (aid: string) => void;
+ onUpdateAccountPassword: (aid: string) => void;
+ onShowCashoutForAccount: (aid: string) => void;
+}
+export function AdminHome({ onCreateAccount, onRegister, onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: Props): VNode {
+ return <Fragment>
+ <AccountList
+ onCreateAccount={onCreateAccount}
+ onRemoveAccount={onRemoveAccount}
+ onShowCashoutForAccount={onShowCashoutForAccount}
+ onShowAccountDetails={onShowAccountDetails}
+ onUpdateAccountPassword={onUpdateAccountPassword}
+ />
+
+ <WireTransfer onRegister={onRegister} />
+
+ <Transactions account="admin" />
+ </Fragment>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx
new file mode 100644
index 000000000..466dc1a4b
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx
@@ -0,0 +1,47 @@
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Cashouts } from "../../components/Cashouts/index.js";
+import { useBackendState } from "../../hooks/backend.js";
+import { ProfileNavigation } from "../ProfileNavigation.js";
+
+interface Props {
+ account: string,
+ onClose: () => void,
+ onSelected: (cid: string) => void
+}
+
+export function CashoutListForAccount({ account, onSelected, onClose }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const { state: credentials } = useBackendState();
+ const token = credentials.status !== "loggedIn" ? undefined : credentials.token
+
+ const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
+ credentials.username === account : false
+
+ return <Fragment>
+ {accountIsTheCurrentUser ?
+ <ProfileNavigation current="cashouts" />
+ :
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cashout for account {account}</i18n.Translate>
+ </h1>
+ }
+ <Cashouts
+ account={account}
+ onSelected={onSelected}
+ />
+ <p>
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ onClose();
+ }}
+ />
+ </p>
+ </Fragment>
+}
+
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index e10c3ad41..772ea6e84 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -1,21 +1,22 @@
import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { buildRequestErrorMessage, withRuntimeErrorHandling } from "../../utils.js";
import { getRandomPassword } from "../rnd.js";
-import { AccountForm } from "./AccountForm.js";
+import { AccountForm, AccountFormData } from "./AccountForm.js";
import { useBackendState } from "../../hooks/backend.js";
import { useBankCoreApiContext } from "../../context/config.js";
import { assertUnreachable } from "../HomePage.js";
import { mutate } from "swr";
+import { Attention } from "../../components/Attention.js";
export function CreateNewAccount({
onCancel,
onCreateSuccess,
}: {
onCancel: () => void;
- onCreateSuccess: (password: string) => void;
+ onCreateSuccess: () => void;
}): VNode {
const { i18n } = useTranslationContext();
// const { createAccount } = useAdminAccountAPI();
@@ -23,9 +24,7 @@ export function CreateNewAccount({
const token = credentials.status !== "loggedIn" ? undefined : credentials.token
const { api } = useBankCoreApiContext();
- const [submitAccount, setSubmitAccount] = useState<
- TalerCorebankApi.AccountData | undefined
- >();
+ const [submitAccount, setSubmitAccount] = useState<AccountFormData | undefined>();
async function doCreate() {
if (!submitAccount || !token) return;
@@ -35,14 +34,17 @@ export function CreateNewAccount({
challenge_contact_data: submitAccount.contact_data,
internal_payto_uri: submitAccount.payto_uri,
name: submitAccount.name,
- username: "",//FIXME: not in account data
+ username: submitAccount.username,//FIXME: not in account data
password: getRandomPassword(),
};
const resp = await api.createAccount(token, account);
if (resp.type === "ok") {
mutate(() => true)// clean account list
- onCreateSuccess(account.password);
+ notifyInfo(
+ i18n.str`Account created with password "${account.password}". The user must change the password on the next login.`,
+ );
+ onCreateSuccess();
} else {
switch (resp.case) {
case "invalid-input": return notify({
@@ -75,6 +77,12 @@ export function CreateNewAccount({
})
}
+ if (!(credentials.status === "loggedIn" && credentials.isUserAdministrator)) {
+ return <Attention type="warning" title={i18n.str`Can't create accounts`} onClose={onCancel}>
+ <i18n.Translate>Only system admin can create accounts.</i18n.Translate>
+ </Attention>
+ }
+
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx
deleted file mode 100644
index 71ea8ce1b..000000000
--- a/packages/demobank-ui/src/pages/admin/Home.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { Cashouts } from "../../components/Cashouts/index.js";
-import { Transactions } from "../../components/Transactions/index.js";
-import { ShowAccountDetails } from "../ShowAccountDetails.js";
-import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
-import { ShowCashoutDetails } from "../business/Home.js";
-import { AdminAccount } from "./Account.js";
-import { AccountList } from "./AccountList.js";
-import { CreateNewAccount } from "./CreateNewAccount.js";
-import { RemoveAccount } from "./RemoveAccount.js";
-
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-interface Props {
- onRegister: () => void;
-}
-export type AccountAction = "show-details" |
- "show-cashout" |
- "update-password" |
- "remove-account" |
- "show-cashouts-details";
-
-export function AdminHome({ onRegister }: Props): VNode {
- const [action, setAction] = useState<{
- type: AccountAction,
- account: string
- } | undefined>()
-
- const [createAccount, setCreateAccount] = useState(false);
-
- const { i18n } = useTranslationContext();
-
- if (action) {
- switch (action.type) {
- case "show-cashouts-details": return <ShowCashoutDetails
- id={action.account}
- onCancel={() => {
- setAction(undefined);
- }}
- />
- case "show-cashout": return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Cashout for account {action.account}</i18n.Translate>
- </h1>
- </div>
- <Cashouts
- account={action.account}
- onSelected={(id) => {
- setAction({
- type: "show-cashouts-details",
- account: action.account
- });
- }}
- />
- <p>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- setAction(undefined);
- }}
- />
- </p>
- </div>
- )
- case "update-password": return <UpdateAccountPassword
- account={action.account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Password changed`);
- setAction(undefined);
- }}
- onCancel={() => {
- setAction(undefined);
- }}
- />
- case "remove-account": return <RemoveAccount
- account={action.account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account removed`);
- setAction(undefined);
- }}
- onCancel={() => {
- setAction(undefined);
- }}
- />
- case "show-details": return <ShowAccountDetails
- account={action.account}
- onChangePassword={() => {
- setAction({
- type: "update-password",
- account: action.account,
- })
- }}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- setAction(undefined);
- }}
- onClear={() => {
- setAction(undefined);
- }}
- />
- }
- }
-
- if (createAccount) {
- return (
- <CreateNewAccount
- onCancel={() => setCreateAccount(false)}
- onCreateSuccess={(password) => {
- notifyInfo(
- i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
- );
- setCreateAccount(false);
- }}
- />
- );
- }
-
- return (
- <Fragment>
-
- <AccountList
- onCreateAccount={() => {
- setCreateAccount(true);
- }}
- account={undefined}
- onAction={(type, account) => setAction({ account, type })}
-
- />
-
- <AdminAccount onRegister={onRegister} />
-
- <Transactions account="admin" />
- </Fragment>
- );
-} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 9a212ebd0..88961c2cb 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -1,5 +1,5 @@
import { Amounts, HttpStatusCode, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpResponsePaginated, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Attention } from "../../components/Attention.js";
@@ -63,6 +63,7 @@ export function RemoveAccount({
await withRuntimeErrorHandling(i18n, async () => {
const resp = await api.deleteAccount({ username: account, token });
if (resp.type === "ok") {
+ notifyInfo(i18n.str`Account removed`);
onUpdateSuccess();
} else {
switch (resp.case) {
diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index d7beda01d..4696c899e 100644
--- a/packages/demobank-ui/src/pages/business/Home.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -21,15 +21,12 @@ import {
} from "@gnu-taler/taler-util";
import {
notify,
- notifyError,
- notifyInfo,
useTranslationContext
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { mutate } from "swr";
-import { Cashouts } from "../../components/Cashouts/index.js";
import { ErrorLoading } from "../../components/ErrorLoading.js";
import { Loading } from "../../components/Loading.js";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
@@ -43,114 +40,15 @@ import {
} from "../../hooks/circuit.js";
import {
TanChannel,
- buildRequestErrorMessage,
undefinedIfEmpty,
- withRuntimeErrorHandling,
+ withRuntimeErrorHandling
} from "../../utils.js";
import { LoginForm } from "../LoginForm.js";
import { InputAmount } from "../PaytoWireTransferForm.js";
-import { ShowAccountDetails } from "../ShowAccountDetails.js";
-import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
+import { assertUnreachable } from "../HomePage.js";
+import { Attention } from "../../components/Attention.js";
interface Props {
- account: string,
- onClose: () => void;
- onRegister: () => void;
-}
-export function BusinessAccount({
- onClose,
- account,
- onRegister,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- const [updatePassword, setUpdatePassword] = useState(false);
- const [newCashout, setNewcashout] = useState(false);
- const [showCashoutDetails, setShowCashoutDetails] = useState<
- string | undefined
- >();
-
- if (newCashout) {
- return (
- <CreateCashout
- account={account}
- onCancel={() => {
- setNewcashout(false);
- }}
- onComplete={(id) => {
- notifyInfo(
- i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`,
- );
- setNewcashout(false);
- setShowCashoutDetails(id);
- }}
- />
- );
- }
- if (showCashoutDetails) {
- return (
- <ShowCashoutDetails
- id={showCashoutDetails}
- onCancel={() => {
- setShowCashoutDetails(undefined);
- }}
- />
- );
- }
- if (updatePassword) {
- return (
- <UpdateAccountPassword
- account={account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Password changed`);
- setUpdatePassword(false);
- }}
- onCancel={() => {
- setUpdatePassword(false);
- }}
- />
- );
- }
- return (
- <div>
- <ShowAccountDetails
- account={account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- }}
- onChangePassword={() => {
- setUpdatePassword(true);
- }}
- onClear={onClose}
- />
- <section style={{ marginTop: "2em" }}>
- <div class="active">
- <h3>{i18n.str`Latest cashouts`}</h3>
- <Cashouts
- account={account}
- onSelected={(id) => {
- setShowCashoutDetails(id);
- }}
- />
- </div>
- <br />
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div />
- <input
- class="pure-button pure-button-primary content"
- type="submit"
- value={i18n.str`New cashout`}
- onClick={async (e) => {
- e.preventDefault();
- setNewcashout(true);
- }}
- />
- </div>
- </section>
- </div>
- );
-}
-
-interface PropsCashout {
account: string;
onComplete: (id: string) => void;
onCancel: () => void;
@@ -167,11 +65,11 @@ type ErrorFrom<T> = {
};
-function CreateCashout({
+export function CreateCashout({
account: accountName,
onComplete,
onCancel,
-}: PropsCashout): VNode {
+}: Props): VNode {
const { i18n } = useTranslationContext();
const resultRatios = useRatiosAndFeeConfig();
const resultAccount = useAccountDetails(accountName);
@@ -184,6 +82,17 @@ function CreateCashout({
const { api, config } = useBankCoreApiContext()
const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
+ if (!config.have_cashout) {
+ return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}>
+ <i18n.Translate>The bank configuration does not support cashout operations.</i18n.Translate>
+ </Attention>
+ }
+ if (!config.fiat_currency) {
+ return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}>
+ <i18n.Translate>The bank configuration support cashout operations but there is no fiat currency.</i18n.Translate>
+ </Attention>
+ }
+
if (!resultAccount || !resultRatios) {
return <Loading />
}
@@ -207,9 +116,6 @@ function CreateCashout({
default: assertUnreachable(resultRatios.case)
}
}
- if (!config.fiat_currency) {
- return <div>cashout operations are not supported</div>
- }
const ratio = resultRatios.body
@@ -514,223 +420,3 @@ function CreateCashout({
</div>
);
}
-
-interface ShowCashoutProps {
- id: string;
- onCancel: () => void;
-}
-export function ShowCashoutDetails({
- id,
- onCancel,
-}: ShowCashoutProps): VNode {
- const { i18n } = useTranslationContext();
- const { state } = useBackendState();
- const creds = state.status !== "loggedIn" ? undefined : state
- const { api } = useBankCoreApiContext()
- const result = useCashoutDetails(id);
- const [code, setCode] = useState<string | undefined>(undefined);
-
- if (!result) {
- return <Loading />
- }
- if (result instanceof TalerError) {
- return <ErrorLoading error={result} />
- }
- if (result.type === "fail") {
- switch (result.case) {
- case "already-aborted": return <div>this cashout is already aborted</div>
- default: assertUnreachable(result.case)
- }
- }
- const errors = undefinedIfEmpty({
- code: !code ? i18n.str`required` : undefined,
- });
- const isPending = String(result.body.status).toUpperCase() === "PENDING";
- return (
- <div>
- <h1>Cashout details {id}</h1>
- <form class="pure-form">
- <fieldset>
- <label>
- <i18n.Translate>Subject</i18n.Translate>
- </label>
- <input readOnly value={result.body.subject} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Created</i18n.Translate>
- </label>
- <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Confirmed</i18n.Translate>
- </label>
- <input readOnly value={result.body.confirmation_time === undefined ? "-" :
- (result.body.confirmation_time.t_s === "never" ?
- i18n.str`never` :
- format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss"))
- } />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Debited</i18n.Translate>
- </label>
- <input readOnly value={result.body.amount_debit} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Credit</i18n.Translate>
- </label>
- <input readOnly value={result.body.amount_credit} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Status</i18n.Translate>
- </label>
- <input readOnly value={result.body.status} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Destination</i18n.Translate>
- </label>
- <input readOnly value={result.body.credit_payto_uri} />
- </fieldset>
- {isPending ? (
- <fieldset>
- <label>
- <i18n.Translate>Code</i18n.Translate>
- </label>
- <input
- value={code ?? ""}
- onChange={(e) => {
- setCode(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.code}
- isDirty={code !== undefined}
- />
- </fieldset>
- ) : undefined}
- </form>
- <br />
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={(e) => {
- e.preventDefault();
- onCancel();
- }}
- >
- {i18n.str`Back`}
- </button>
- {isPending ? (
- <div>
- <button
- type="submit"
- class="pure-button pure-button-primary button-error"
- onClick={async (e) => {
- e.preventDefault();
- if (!creds) return;
- await withRuntimeErrorHandling(i18n, async () => {
- const resp = await api.abortCashoutById(creds, id);
- if (resp.type === "ok") {
- onCancel();
- } else {
- switch (resp.case) {
- case "not-found": return notify({
- type: "error",
- title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "already-confirmed": return notify({
- type: "error",
- title: i18n.str`Cashout was already confimed.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: {
- assertUnreachable(resp)
- }
- }
- }
- })
- }}
- >
- {i18n.str`Abort`}
- </button>
- &nbsp;
- <button
- type="submit"
- disabled={!code}
- class="pure-button pure-button-primary "
- onClick={async (e) => {
- e.preventDefault();
- if (!creds || !code) return;
- await withRuntimeErrorHandling(i18n, async () => {
- const resp = await api.confirmCashoutById(creds, id, {
- tan: code,
- });
- if (resp.type === "ok") {
- mutate(() => true)//clean cashout state
- } else {
- switch (resp.case) {
- case "not-found": return notify({
- type: "error",
- title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "wrong-tan-or-credential": return notify({
- type: "error",
- title: i18n.str`Invalid code or credentials.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "cashout-address-changed": return notify({
- type: "error",
- title: i18n.str`The cash-out address between the creation and the confirmation changed.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: assertUnreachable(resp)
- }
- }
- })
- }}
- >
- {i18n.str`Confirm`}
- </button>
- </div>
- ) : (
- <div />
- )}
- </div>
- </div>
- );
-}
-
-const MAX_AMOUNT_DIGIT = 2;
-/**
- * Truncate the amount of digits to display
- * in the form based on the fee calculations
- *
- * Backend must have the same truncation
- * @param a
- * @returns
- */
-function truncate(a: AmountJson): AmountJson {
- const str = Amounts.stringify(a);
- const idx = str.indexOf(".");
- if (idx === -1) {
- return a;
- }
- const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT);
- return Amounts.parseOrThrow(truncated);
-}
-
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
new file mode 100644
index 000000000..a8e34e4b9
--- /dev/null
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -0,0 +1,237 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 {
+ TalerError,
+ TranslatedString
+} from "@gnu-taler/taler-util";
+import {
+ notify,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { mutate } from "swr";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+import { Loading } from "../../components/Loading.js";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useBankCoreApiContext } from "../../context/config.js";
+import { useBackendState } from "../../hooks/backend.js";
+import {
+ useCashoutDetails
+} from "../../hooks/circuit.js";
+import {
+ undefinedIfEmpty,
+ withRuntimeErrorHandling
+} from "../../utils.js";
+import { assertUnreachable } from "../HomePage.js";
+
+interface Props {
+ id: string;
+ onCancel: () => void;
+}
+export function ShowCashoutDetails({
+ id,
+ onCancel,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { state } = useBackendState();
+ const creds = state.status !== "loggedIn" ? undefined : state
+ const { api } = useBankCoreApiContext()
+ const result = useCashoutDetails(id);
+ const [code, setCode] = useState<string | undefined>(undefined);
+
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "already-aborted": return <div>this cashout is already aborted</div>
+ default: assertUnreachable(result.case)
+ }
+ }
+ const errors = undefinedIfEmpty({
+ code: !code ? i18n.str`required` : undefined,
+ });
+ const isPending = String(result.body.status).toUpperCase() === "PENDING";
+ return (
+ <div>
+ <h1>Cashout details {id}</h1>
+ <form class="pure-form">
+ <fieldset>
+ <label>
+ <i18n.Translate>Subject</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.subject} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Created</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Confirmed</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.confirmation_time === undefined ? "-" :
+ (result.body.confirmation_time.t_s === "never" ?
+ i18n.str`never` :
+ format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss"))
+ } />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Debited</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.amount_debit} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Credit</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.amount_credit} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Status</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.status} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Destination</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.credit_payto_uri} />
+ </fieldset>
+ {isPending ? (
+ <fieldset>
+ <label>
+ <i18n.Translate>Code</i18n.Translate>
+ </label>
+ <input
+ value={code ?? ""}
+ onChange={(e) => {
+ setCode(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.code}
+ isDirty={code !== undefined}
+ />
+ </fieldset>
+ ) : undefined}
+ </form>
+ <br />
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <button
+ class="pure-button pure-button-secondary btn-cancel"
+ onClick={(e) => {
+ e.preventDefault();
+ onCancel();
+ }}
+ >
+ {i18n.str`Back`}
+ </button>
+ {isPending ? (
+ <div>
+ <button
+ type="submit"
+ class="pure-button pure-button-primary button-error"
+ onClick={async (e) => {
+ e.preventDefault();
+ if (!creds) return;
+ await withRuntimeErrorHandling(i18n, async () => {
+ const resp = await api.abortCashoutById(creds, id);
+ if (resp.type === "ok") {
+ onCancel();
+ } else {
+ switch (resp.case) {
+ case "not-found": return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "already-confirmed": return notify({
+ type: "error",
+ title: i18n.str`Cashout was already confimed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: {
+ assertUnreachable(resp)
+ }
+ }
+ }
+ })
+ }}
+ >
+ {i18n.str`Abort`}
+ </button>
+ &nbsp;
+ <button
+ type="submit"
+ disabled={!code}
+ class="pure-button pure-button-primary "
+ onClick={async (e) => {
+ e.preventDefault();
+ if (!creds || !code) return;
+ await withRuntimeErrorHandling(i18n, async () => {
+ const resp = await api.confirmCashoutById(creds, id, {
+ tan: code,
+ });
+ if (resp.type === "ok") {
+ mutate(() => true)//clean cashout state
+ } else {
+ switch (resp.case) {
+ case "not-found": return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "wrong-tan-or-credential": return notify({
+ type: "error",
+ title: i18n.str`Invalid code or credentials.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "cashout-address-changed": return notify({
+ type: "error",
+ title: i18n.str`The cash-out address between the creation and the confirmation changed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: assertUnreachable(resp)
+ }
+ }
+ })
+ }}
+ >
+ {i18n.str`Confirm`}
+ </button>
+ </div>
+ ) : (
+ <div />
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/demobank-ui/src/route.ts b/packages/demobank-ui/src/route.ts
new file mode 100644
index 000000000..d54f9be83
--- /dev/null
+++ b/packages/demobank-ui/src/route.ts
@@ -0,0 +1,167 @@
+import { createHashHistory } from "history";
+import { h as create, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+const history = createHashHistory();
+
+type PageDefinition<DynamicPart extends Record<string, string>> = {
+ pattern: string;
+ (params: DynamicPart): string;
+};
+
+function replaceAll(
+ pattern: string,
+ vars: Record<string, string>,
+ values: Record<string, string>,
+): string {
+ let result = pattern;
+ for (const v in vars) {
+ result = result.replace(vars[v], !values[v] ? "" : values[v]);
+ }
+ return result;
+}
+
+export function pageDefinition<T extends Record<string, string>>(
+ pattern: string,
+): PageDefinition<T> {
+ const patternParams = pattern.match(/(:[\w?]*)/g);
+ if (!patternParams)
+ throw Error(
+ `page definition pattern ${pattern} doesn't have any parameter`,
+ );
+
+ const vars = patternParams.reduce((prev, cur) => {
+ const pName = cur.match(/(\w+)/g);
+
+ //skip things like :? in the path pattern
+ if (!pName || !pName[0]) return prev;
+ const name = pName[0];
+ return { ...prev, [name]: cur };
+ }, {} as Record<string, string>);
+
+ const f = (values: T): string => replaceAll(pattern, vars, values);
+ f.pattern = pattern;
+ return f;
+}
+
+export type PageEntry<T = unknown> = T extends Record<string, string>
+ ? {
+ url: PageDefinition<T>;
+ view: (props: T) => VNode;
+ }
+ : T extends unknown
+ ? {
+ url: string;
+ view: (props: {}) => VNode;
+ }
+ : never;
+
+export function Router({
+ pageList,
+ onNotFound,
+}: {
+ pageList: Array<PageEntry<any>>;
+ onNotFound: () => VNode;
+}): VNode {
+ const current = useCurrentLocation(pageList);
+ if (current !== undefined) {
+ return create(current.page.view, current.values);
+ }
+ return onNotFound();
+}
+
+type Location = {
+ page: PageEntry<any>;
+ path: string;
+ values: Record<string, string>;
+};
+export function useCurrentLocation(pageList: Array<PageEntry<any>>) {
+ const [currentLocation, setCurrentLocation] = useState<Location>();
+ /**
+ * Search path in the pageList
+ * get the values from the path found
+ * add params from searchParams
+ *
+ * @param path
+ * @param params
+ */
+ function doSync(path: string, params: URLSearchParams) {
+ let result: typeof currentLocation;
+ for (let idx = 0; idx < pageList.length; idx++) {
+ const page = pageList[idx];
+ if (typeof page.url === "string") {
+ if (page.url === path) {
+ const values: Record<string, string> = {};
+ params.forEach((v, k) => {
+ values[k] = v;
+ });
+ result = { page, values, path };
+ break;
+ }
+ } else {
+ const values = doestUrlMatchToRoute(path, page.url.pattern);
+ if (values !== undefined) {
+ params.forEach((v, k) => {
+ values[k] = v;
+ });
+ result = { page, values, path };
+ break;
+ }
+ }
+ }
+ setCurrentLocation(result);
+ }
+ useEffect(() => {
+ doSync(window.location.hash, new URLSearchParams(window.location.search));
+ return history.listen(() => {
+ doSync(window.location.hash, new URLSearchParams(window.location.search));
+ });
+ }, []);
+ return currentLocation;
+}
+
+function doestUrlMatchToRoute(
+ url: string,
+ route: string,
+): undefined | Record<string, string> {
+ const paramsPattern = /(?:\?([^#]*))?$/;
+ // const paramsPattern = /(?:\?([^#]*))?(#.*)?$/;
+ const params = url.match(paramsPattern);
+ const urlWithoutParams = url.replace(paramsPattern, "");
+
+ const result: Record<string, string> = {};
+ if (params && params[1]) {
+ const paramList = params[1].split("&");
+ for (let i = 0; i < paramList.length; i++) {
+ const idx = paramList[i].indexOf("=");
+ const name = paramList[i].substring(0, idx);
+ const value = paramList[i].substring(idx + 1);
+ result[decodeURIComponent(name)] = decodeURIComponent(value);
+ }
+ }
+ const urlSeg = urlWithoutParams.split("/");
+ const routeSeg = route.split("/");
+ let max = Math.max(urlSeg.length, routeSeg.length);
+ for (let i = 0; i < max; i++) {
+ if (routeSeg[i] && routeSeg[i].charAt(0) === ":") {
+ const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, "");
+
+ const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || "";
+ const plus = ~flags.indexOf("+");
+ const star = ~flags.indexOf("*");
+ const val = urlSeg[i] || "";
+
+ if (!val && !star && (flags.indexOf("?") < 0 || plus)) {
+ return undefined;
+ }
+ result[param] = decodeURIComponent(val);
+ if (plus || star) {
+ result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/");
+ break;
+ }
+ } else if (routeSeg[i] !== urlSeg[i]) {
+ return undefined;
+ }
+ }
+ return result;
+}
+const EMPTY: Record<string, string> = {};