summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-11-22 15:20:06 -0300
committerSebastian <sebasjm@gmail.com>2023-11-22 15:20:29 -0300
commit305c513c2bcc2b25fe57cf0ed9723781944f9f3f (patch)
tree2022b093f6bed0bfd74c257168033b206850e2b0
parent33c0267b37eecf44dc9f04e124eb44d27cba700c (diff)
downloadwallet-core-305c513c2bcc2b25fe57cf0ed9723781944f9f3f.tar.gz
wallet-core-305c513c2bcc2b25fe57cf0ed9723781944f9f3f.tar.bz2
wallet-core-305c513c2bcc2b25fe57cf0ed9723781944f9f3f.zip
more cashout ui
-rw-r--r--packages/demobank-ui/src/components/Cashouts/index.ts13
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx21
-rw-r--r--packages/demobank-ui/src/components/Transactions/views.tsx6
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts22
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx11
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx8
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx4
-rw-r--r--packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx4
-rw-r--r--packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx5
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx14
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx2
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx7
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx174
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx12
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts8
-rw-r--r--packages/taler-util/src/http-client/types.ts14
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts2
17 files changed, 245 insertions, 82 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts b/packages/demobank-ui/src/components/Cashouts/index.ts
index 6cbb1247d..ca58de98f 100644
--- a/packages/demobank-ui/src/components/Cashouts/index.ts
+++ b/packages/demobank-ui/src/components/Cashouts/index.ts
@@ -14,17 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpError, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoading, HttpError, utils } from "@gnu-taler/web-util/browser";
import { Loading } from "@gnu-taler/web-util/browser";
// import { compose, StateViewMap } from "../../utils/index.js";
// import { wxApi } from "../../wxApi.js";
import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util";
import { useComponentState } from "./state.js";
import { FailedView, LoadingUriView, ReadyView } from "./views.js";
+import { h } from "preact";
export interface Props {
account: string;
- onSelected: (id: string) => void;
+ onSelected: (id: number) => void;
}
export type State = State.Loading | State.Failed | State.LoadingUriError | State.Ready;
@@ -51,8 +52,8 @@ export namespace State {
export interface Ready extends BaseInfo {
status: "ready";
error: undefined;
- cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: string })[];
- onSelected: (id: string) => void;
+ cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: number })[];
+ onSelected: (id: number) => void;
}
}
@@ -66,7 +67,9 @@ export interface Transaction {
const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingUriView,
+ "loading-error": ({error}) => {
+ return h(ErrorLoading, {error, showDetail:true});
+ },
"failed": FailedView,
ready: ReadyView,
};
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 76a3a90df..651a7a034 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -14,13 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Fragment, h, VNode } from "preact";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { State } from "./index.js";
+import { Amounts, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { Attention, ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { Amounts, assertUnreachable } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
-import { Attention } from "@gnu-taler/web-util/browser";
+import { State } from "./index.js";
+import { useConversionInfo } from "../../hooks/circuit.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -52,6 +52,13 @@ export function FailedView({ error }: State.Failed) {
export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
const { i18n } = useTranslationContext();
+ const resp = useConversionInfo();
+ if (!resp) {
+ return <Loading />
+ }
+ if (resp instanceof TalerError) {
+ return <ErrorLoading error={resp} />
+ }
if (!cashouts.length) return <div />
const txByDate = cashouts.reduce((prev, cur) => {
const d = cur.creation_time.t_s === "never"
@@ -122,8 +129,8 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
</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-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} spec={resp.body.regional_currency_specification} /></td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} spec={resp.body.fiat_currency_specification} /></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">
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx
index 72dd43415..1613cb06a 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -19,6 +19,7 @@ import { format } from "date-fns";
import { Fragment, h, VNode } from "preact";
import { doAutoFocus, RenderAmount } from "../../pages/PaytoWireTransferForm.js";
import { State } from "./index.js";
+import { useBankCoreApiContext } from "../../context/config.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -32,6 +33,7 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {
const { i18n } = useTranslationContext();
+ const {config} = useBankCoreApiContext();
if (!transactions.length) return <div />
const txByDate = transactions.reduce((prev, cur) => {
const d = cur.when.t_ms === "never"
@@ -78,7 +80,7 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
<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} />
+ <RenderAmount value={item.amount} spec={config.currency_specification}/>
</span>
) : (
<span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
@@ -99,7 +101,7 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
</td>
<td data-negative={item.negative ? "true" : "false"}
class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 ">
- {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} withColor />
+ {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} withColor spec={config.currency_specification}/>
) : (
<span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
)}
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index c0164d60a..01c62c409 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -83,7 +83,7 @@ export function useEstimator(): CashoutEstimators {
}
const credit = Amounts.parseOrThrow(resp.body.amount_credit);
const debit = Amounts.parseOrThrow(resp.body.amount_debit);
- const beforeFee = Amounts.add(credit, fee).amount;
+ const beforeFee = Amounts.sub(credit, fee).amount;
return {
debit,
@@ -92,8 +92,8 @@ export function useEstimator(): CashoutEstimators {
};
},
estimateByDebit: async (regionalAmount, fee) => {
- const resp = await api.getConversionInfoAPI().getCashoutRate({
- debit: regionalAmount
+ const resp = await api.getConversionInfoAPI().getCashoutRate({
+ debit: regionalAmount
});
if (resp.type === "fail") {
// can't happen
@@ -170,7 +170,7 @@ export function useBusinessAccounts() {
return undefined;
}
-type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: string }
+type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number }
function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
return c !== undefined
}
@@ -182,11 +182,15 @@ export function useCashouts(account: string) {
async function fetcher([username, token]: [string, AccessToken]) {
const list = await api.getAccountCashouts({ username, token })
if (list.type !== "ok") {
+ console.error(list)
return list;
}
const all: Array<CashoutWithId | undefined> = await Promise.all(list.body.cashouts.map(c => {
return api.getCashoutById({ username, token }, c.cashout_id).then(r => {
- if (r.type === "fail") return undefined
+ if (r.type === "fail") {
+ console.error("failed", r)
+ return undefined
+ }
return { ...r.body, id: c.cashout_id }
})
}))
@@ -196,7 +200,7 @@ export function useCashouts(account: string) {
}
const { data, error } = useSWR<OperationOk<{ cashouts: CashoutWithId[] }> | TalerCoreBankErrorsByMethod<"getAccountCashouts">, TalerHttpError>(
- !config.allow_conversion ? false : [account, token, "getAccountCashouts"], fetcher, {
+ !config.allow_conversion ? undefined : [account, token, "getAccountCashouts"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -213,17 +217,17 @@ export function useCashouts(account: string) {
return undefined;
}
-export function useCashoutDetails(cashoutId: string) {
+export function useCashoutDetails(cashoutId: number | undefined) {
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const { api } = useBankCoreApiContext();
- async function fetcher([username, token, id]: [string, AccessToken, string]) {
+ async function fetcher([username, token, id]: [string, AccessToken, number]) {
return api.getCashoutById({ username, token }, id)
}
const { data, error } = useSWR<TalerCoreBankResultByMethod<"getCashoutById">, TalerHttpError>(
- [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, {
+ cashoutId === undefined ? undefined : [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index 5fef04b66..34c39e9d3 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -23,6 +23,7 @@ import { useBackendState } from "../hooks/backend.js";
import { getAllBooleanPreferences, getLabelForPreferences, usePreferences } from "../hooks/preferences.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
import { useSettingsContext } from "../context/settings.js";
+import { useBankCoreApiContext } from "../context/config.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -45,8 +46,8 @@ export function BankFrame({
useEffect(() => {
if (error) {
const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString
+ console.log(error)
if (error instanceof Error) {
- console.log(error)
notifyException(i18n.str`Internal error, please report.`, error)
} else {
notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
@@ -118,9 +119,9 @@ export function BankFrame({
<Footer
testingUrl={
- (typeof localStorage !== "undefined") && localStorage.getItem("bank-base-url") ?
- localStorage.getItem("bank-base-url") ?? undefined :
- undefined}
+ (typeof localStorage !== "undefined") && localStorage.getItem("bank-base-url") ?
+ localStorage.getItem("bank-base-url") ?? undefined :
+ undefined}
GIT_HASH={GIT_HASH}
VERSION={VERSION}
/>
@@ -172,6 +173,7 @@ function WelcomeAccount({ account: accountName }: { account: string }): VNode {
function AccountBalance({ account }: { account: string }): VNode {
const result = useAccountDetails(account);
+ const { config } = useBankCoreApiContext();
if (!result) {
return <Loading />
}
@@ -183,5 +185,6 @@ function AccountBalance({ account }: { account: string }): VNode {
return <RenderAmount
value={Amounts.parseOrThrow(result.body.balance.amount)}
negative={result.body.balance.credit_debit_indicator === "debit"}
+ spec={config.currency_specification}
/>
}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index a6282c947..e035c7fed 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -18,6 +18,7 @@ import {
AmountJson,
AmountString,
Amounts,
+ CurrencySpecification,
Logger,
PaytoString,
TranslatedString,
@@ -470,13 +471,12 @@ export function InputAmount(
);
}
-export function RenderAmount({ value, negative, withColor }: { value: AmountJson, negative?: boolean, withColor?: boolean }): VNode {
- const { config } = useBankCoreApiContext()
+export function RenderAmount({ value, spec, negative, withColor }: { spec: CurrencySpecification; value: AmountJson, negative?: boolean, withColor?: boolean }): VNode {
const neg = !!negative //convert to true or false
const str = Amounts.stringifyValue(value)
const sep_pos = str.indexOf(FRAC_SEPARATOR)
- if (sep_pos !== -1 && str.length - sep_pos - 1 > config.currency_specification.num_fractional_normal_digits) {
- const limit = sep_pos + config.currency_specification.num_fractional_normal_digits + 1
+ if (sep_pos !== -1 && str.length - sep_pos - 1 > spec.num_fractional_normal_digits) {
+ const limit = sep_pos + spec.num_fractional_normal_digits + 1
const normal = str.substring(0, limit)
const small = str.substring(limit)
return <span data-negative={withColor ? neg : undefined} class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index be8ff8b58..bfb118c6c 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -70,7 +70,7 @@ export function WithdrawalConfirmationQuestion({
}, []);
const [notification, notify, handleError] = useLocalNotification()
- const { api } = useBankCoreApiContext()
+ const { config, api } = useBankCoreApiContext()
const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
const answer = parseInt(captchaAnswer ?? "", 10);
const [busy, setBusy] = useState<Record<string, undefined>>()
@@ -289,7 +289,7 @@ export function WithdrawalConfirmationQuestion({
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={details.amount} />
+ <RenderAmount value={details.amount} spec={config.currency_specification} />
</dd>
</div>
</dl>
diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
index 293b821e2..f2972ed65 100644
--- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -9,7 +9,7 @@ import { CreateCashout } from "../business/CreateCashout.js";
interface Props {
account: string,
onClose: () => void,
- onSelected: (cid: string) => void
+ onSelected: (cid: number) => void
}
export function CashoutListForAccount({ account, onSelected, onClose }: Props): VNode {
@@ -29,7 +29,7 @@ export function CashoutListForAccount({ account, onSelected, onClose }: Props):
</h1>
}
- <CreateCashout onCancel={() => { }} onComplete={() => { }} account={account} />
+ <CreateCashout focus onCancel={onClose} onComplete={() => { }} account={account} />
<Cashouts
account={account}
diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
index 7a4fbddf5..ab5ceb8d5 100644
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -52,7 +52,10 @@ export function ShowAccountDetails({
async function doUpdate() {
if (!update || !submitAccount || !creds) return;
await handleError(async () => {
- const resp = await api.updateAccount(creds, {
+ const resp = await api.updateAccount({
+ token: creds.token,
+ username: account,
+ }, {
cashout_address: submitAccount.cashout_payto_uri,
challenge_contact_data: undefinedIfEmpty({
email: submitAccount.contact_data?.email,
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 5d8e3797a..b38d40012 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -61,12 +61,12 @@ export function AccountForm({
: validateIBAN(parsed.iban, i18n)) as PaytoString,
contact_data: undefinedIfEmpty({
email: !newForm.contact_data?.email
- ? i18n.str`required`
+ ? undefined
: !EMAIL_REGEX.test(newForm.contact_data.email)
? i18n.str`it should be an email`
: undefined,
phone: !newForm.contact_data?.phone
- ? i18n.str`required`
+ ? undefined
: !newForm.contact_data.phone.startsWith("+")
? i18n.str`should start with +`
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
@@ -152,7 +152,7 @@ export function AccountForm({
name="name"
data-error={!!errors?.name && form.name !== undefined}
id="name"
- disabled={purpose !== "create"}
+ disabled={purpose === "show"}
value={form.name ?? ""}
onChange={(e) => {
form.name = e.currentTarget.value;
@@ -189,7 +189,7 @@ export function AccountForm({
name="email"
id="email"
data-error={!!errors?.contact_data?.email && form.contact_data?.email !== undefined}
- disabled={purpose !== "create"}
+ disabled={purpose === "show"}
value={form.contact_data?.email ?? ""}
onChange={(e) => {
if (form.contact_data) {
@@ -273,7 +273,11 @@ export function AccountForm({
<i18n.Translate>account number where the money is going to be sent when doing cashouts</i18n.Translate>
</p>
</div>
-
+ <div class="sm:col-span-5">
+ <pre>
+ {JSON.stringify(errors, undefined, 2)}
+ </pre>
+ </div>
</div>
</div>
{children}
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 8c018120d..7d6cfaf7d 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -105,7 +105,7 @@ export function AccountList({ onRemoveAccount, onShowAccountDetails, onUpdateAcc
i18n.str`unknown`
) : (
<span class="amount">
- <RenderAmount value={balance} negative={balanceIsDebit} />
+ <RenderAmount value={balance} negative={balanceIsDebit} spec={config.currency_specification} />
</span>
)}
</td>
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index 795a2c6d0..e9fa1dc47 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -7,6 +7,7 @@ import { useLastMonitorInfo } from "../../hooks/circuit.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
import { WireTransfer } from "../WireTransfer.js";
import { AccountList } from "./AccountList.js";
+import { useBankCoreApiContext } from "../../context/config.js";
/**
* Query account information and show QR code if there is pending withdrawal
@@ -42,7 +43,6 @@ function Metrics(): VNode {
const [metricType, setMetricType] = useState<TalerCorebankApi.MonitorTimeframeParam>(TalerCorebankApi.MonitorTimeframeParam.day);
const resp = useLastMonitorInfo(new Date(), metricType);
- console.log(resp)
if (!resp) return <Fragment />;
if (resp instanceof TalerError) {
return <ErrorLoading error={resp} />
@@ -162,6 +162,7 @@ function Metrics(): VNode {
function MetricValue({ current, previous }: { current: AmountString | undefined, previous: AmountString | undefined }): VNode {
const { i18n } = useTranslationContext()
+ const {config} = useBankCoreApiContext();
const cmp = current && previous ? Amounts.cmp(current, previous) : 0;
const currAmount = !current ? undefined : Number.parseFloat(Amounts.stringifyValue(current))
const prevAmount = !previous ? undefined : Number.parseFloat(Amounts.stringifyValue(previous))
@@ -173,11 +174,11 @@ function MetricValue({ current, previous }: { current: AmountString | undefined,
const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`
return <dd class="mt-1 flex justify-between md:block lg:flex">
<div class="flex justify-start items-baseline text-2xl font-semibold text-indigo-600">
- {!current ? "-" : <RenderAmount value={Amounts.parseOrThrow(current)} />}
+ {!current ? "-" : <RenderAmount value={Amounts.parseOrThrow(current)} spec={config.currency_specification} />}
</div>
<div class="flex justify-end items-baseline text-2xl font-semibold text-indigo-600">
<small class="ml-2 text-sm font-medium text-gray-500">
- <i18n.Translate>from</i18n.Translate> {!previous ? "-" : <RenderAmount value={Amounts.parseOrThrow(previous)} />}
+ <i18n.Translate>from</i18n.Translate> {!previous ? "-" : <RenderAmount value={Amounts.parseOrThrow(previous)} spec={config.currency_specification}/>}
</small>
</div>
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index c5f4ebc4e..2f77f3960 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -26,6 +26,7 @@ import {
Loading,
LocalNotificationBanner,
ShowInputErrorLabel,
+ notifyInfo,
useLocalNotification,
useTranslationContext
} from "@gnu-taler/web-util/browser";
@@ -46,12 +47,13 @@ import {
import { LoginForm } from "../LoginForm.js";
import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
+import { getRandomPassword, getRandomUsername } from "../rnd.js";
interface Props {
account: string;
focus?: boolean,
onComplete: (id: string) => void;
- onCancel: () => void;
+ onCancel?: () => void;
}
type FormType = {
@@ -77,7 +79,10 @@ export function CreateCashout({
estimateByCredit: calculateFromCredit,
estimateByDebit: calculateFromDebit,
} = useEstimator();
- const { config } = useBankCoreApiContext()
+ const { state: credentials } = useBackendState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials
+
+ const { api, config } = useBankCoreApiContext()
const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, amount: "2" });
const [notification, notify, handleError] = useLocalNotification()
const info = useConversionInfo();
@@ -119,7 +124,7 @@ export function CreateCashout({
debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold)
}
- const {fiat_currency, regional_currency} = info.body
+ const { fiat_currency, regional_currency, fiat_currency_specification, regional_currency_specification } = info.body
const regionalZero = Amounts.zeroOfCurrency(regional_currency);
const fiatZero = Amounts.zeroOfCurrency(fiat_currency);
const limit = account.balanceIsDebit
@@ -128,7 +133,6 @@ export function CreateCashout({
const zeroCalc = { debit: regionalZero, credit: fiatZero, beforeFee: fiatZero };
const [calc, setCalc] = useState(zeroCalc);
- console.log(calc)
const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
const sellRate = conversionInfo.cashout_ratio
/**
@@ -159,6 +163,7 @@ export function CreateCashout({
setForm(newForm);
}
const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
+ subject: !form.subject ? i18n.str`required` : undefined,
amount: !form.amount
? i18n.str`required`
: !inputAmount
@@ -174,6 +179,70 @@ export function CreateCashout({
});
const trimmedAmountStr = form.amount?.trim();
+ async function createCashout() {
+ const request_uid = encodeCrock(getRandomBytes(32))
+ await handleError(async () => {
+ if (!creds || !form.subject || !form.channel) return;
+
+ const resp = await api.createCashout(creds, {
+ request_uid,
+ amount_credit: Amounts.stringify(calc.credit),
+ amount_debit: Amounts.stringify(calc.debit),
+ subject: form.subject,
+ tan_channel: form.channel,
+ })
+ if (resp.type === "ok") {
+ notifyInfo(i18n.str`Cashout created`)
+ } else {
+ switch (resp.case) {
+ case "account-not-found": return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "request-already-used": return notify({
+ type: "error",
+ title: i18n.str`Duplicated request detected, check if the operation succeded or try again.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "incorrect-exchange-rate": return notify({
+ type: "error",
+ title: i18n.str`The exchange rate was incorrectly applied`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "no-contact-info": return notify({
+ type: "error",
+ title: i18n.str`Missing contact info before to create the cashout`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "no-enough-balance": return notify({
+ type: "error",
+ title: i18n.str`The account does not have sufficient funds`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "cashout-not-supported": return notify({
+ type: "error",
+ title: i18n.str`Cashouts are not supported`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "tan-failed": return notify({
+ type: "error",
+ title: i18n.str`Sending the confirmation code failed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ }
+ assertUnreachable(resp)
+ }
+ })
+ }
+
return (
<div>
<LocalNotificationBanner notification={notification} />
@@ -192,18 +261,18 @@ export function CreateCashout({
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
- <span><i18n.Translate>Current balance</i18n.Translate></span>
+ <span><i18n.Translate>Balance</i18n.Translate></span>
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={account.balance} />
+ <RenderAmount value={account.balance} spec={regional_currency_specification} />
</dd>
</div>
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
- <span><i18n.Translate>Cashout fee</i18n.Translate></span>
+ <span><i18n.Translate>Fee</i18n.Translate></span>
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={sellFee} />
+ <RenderAmount value={sellFee} spec={fiat_currency_specification} />
</dd>
</div>
</dl>
@@ -226,7 +295,7 @@ export function CreateCashout({
class="block text-sm font-medium leading-6 text-gray-900"
for="subject"
>
- {i18n.str`Subject`}
+ {i18n.str`Transfer subject`}
</label>
<div class="mt-2">
<input
@@ -253,14 +322,24 @@ export function CreateCashout({
{/* amount */}
<div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="amount"
- >
- {form.isDebit
- ? i18n.str`Amount to send`
- : i18n.str`Amount to receive`}
- </label>
+ <div class="flex justify-between">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="amount"
+ >
+ {form.isDebit
+ ? i18n.str`Amount to send`
+ : i18n.str`Amount to receive`}
+ </label>
+ <button type="button" data-enabled={form.isDebit} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
+ onClick={() => {
+ form.isDebit = !form.isDebit
+ updateForm(structuredClone(form))
+ }}>
+ <span aria-hidden="true" data-enabled={form.isDebit} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ </button>
+
+ </div>
<div class="mt-2">
<InputAmount
name="amount"
@@ -287,7 +366,7 @@ export function CreateCashout({
<div class="justify-between items-center flex ">
<dt class="text-sm text-gray-600"><i18n.Translate>Total cost</i18n.Translate></dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={calc.debit} negative withColor />
+ <RenderAmount value={calc.debit} negative withColor spec={regional_currency_specification} />
</dd>
</div>
@@ -302,7 +381,7 @@ export function CreateCashout({
</a> */}
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={balanceAfter} />
+ <RenderAmount value={balanceAfter} spec={regional_currency_specification} />
</dd>
</div>
{Amounts.isZero(sellFee) || Amounts.isZero(calc.beforeFee) ? undefined : (
@@ -316,14 +395,14 @@ export function CreateCashout({
</a> */}
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={calc.beforeFee} />
+ <RenderAmount value={calc.beforeFee} spec={fiat_currency_specification} />
</dd>
</div>
)}
<div class="flex justify-between items-center border-t-2 afu pt-4">
<dt class="text-lg text-gray-900 font-medium"><i18n.Translate>Total cashout transfer</i18n.Translate></dt>
<dd class="text-lg text-gray-900 font-medium">
- <RenderAmount value={calc.credit} withColor />
+ <RenderAmount value={calc.credit} withColor spec={fiat_currency_specification} />
</dd>
</div>
</dl>
@@ -331,6 +410,55 @@ export function CreateCashout({
)}
{/* channel */}
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="channel"
+ >
+ {i18n.str`Confirmation the operation using`}
+ </label>
+
+ <div class="mt-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
+
+ <label onClick={()=>{
+ form.channel = TanChannel.EMAIL
+ updateForm(structuredClone(form))
+ }} data-selected={form.channel === TanChannel.EMAIL} class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
+ <input type="radio" name="channel" value="Newsletter" class="sr-only" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Email</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg data-selected={form.channel === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]: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" />
+ </svg>
+ </label>
+
+ <label onClick={()=>{
+ form.channel = TanChannel.SMS
+ updateForm(structuredClone(form))
+ }} data-selected={form.channel === TanChannel.SMS} class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
+ <input type="radio" name="channel" value="Existing Customers" class="sr-only" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>SMS</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg data-selected={form.channel === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]: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" />
+ </svg>
+ </label>
+
+ </div>
+ </div>
+
+ </div>
</div>
</div>
@@ -348,10 +476,10 @@ export function CreateCashout({
disabled={!!errors}
onClick={(e) => {
e.preventDefault()
- // doChangePassword()
+ createCashout()
}}
>
- <i18n.Translate>Change</i18n.Translate>
+ <i18n.Translate>Cashout</i18n.Translate>
</button>
</div>
</form>
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index ddfc18a0c..52ff713e2 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -53,7 +53,9 @@ export function ShowCashoutDetails({
const { state } = useBackendState();
const creds = state.status !== "loggedIn" ? undefined : state
const { api } = useBankCoreApiContext()
- const result = useCashoutDetails(id);
+ const cid = Number.parseInt(id, 10)
+
+ const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid);
const [code, setCode] = useState<string | undefined>(undefined);
const [notification, notify, handleError] = useLocalNotification()
@@ -72,6 +74,10 @@ export function ShowCashoutDetails({
default: assertUnreachable(result)
}
}
+ if (Number.isNaN(cid)) {
+ //TODO: better error message
+ return <div>cashout id should be a number</div>
+ }
const errors = undefinedIfEmpty({
code: !code ? i18n.str`required` : undefined,
});
@@ -165,7 +171,7 @@ export function ShowCashoutDetails({
e.preventDefault();
if (!creds) return;
await handleError(async () => {
- const resp = await api.abortCashoutById(creds, id);
+ const resp = await api.abortCashoutById(creds, cid);
if (resp.type === "ok") {
onCancel();
} else {
@@ -207,7 +213,7 @@ export function ShowCashoutDetails({
e.preventDefault();
if (!creds || !code) return;
await handleError(async () => {
- const resp = await api.confirmCashoutById(creds, id, {
+ const resp = await api.confirmCashoutById(creds, cid, {
tan: code,
});
if (resp.type === "ok") {
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
index d7bf6be29..273fb97c6 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -442,7 +442,7 @@ export class TalerCoreBankHttpClient {
body,
});
switch (resp.status) {
- case HttpStatusCode.Accepted: return opSuccess(resp, codecForCashoutPending())
+ case HttpStatusCode.Ok: return opSuccess(resp, codecForCashoutPending())
case HttpStatusCode.NotFound: return opKnownFailure("account-not-found", resp)
case HttpStatusCode.Conflict: {
const body = await resp.json()
@@ -465,7 +465,7 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-abort
*
*/
- async abortCashoutById(auth: UserAndToken, cid: string) {
+ async abortCashoutById(auth: UserAndToken, cid: number) {
const url = new URL(`accounts/${auth.username}/cashouts/${cid}/abort`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -487,7 +487,7 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm
*
*/
- async confirmCashoutById(auth: UserAndToken, cid: string, body: TalerCorebankApi.CashoutConfirmRequest) {
+ async confirmCashoutById(auth: UserAndToken, cid: number, body: TalerCorebankApi.CashoutConfirmRequest) {
const url = new URL(`accounts/${auth.username}/cashouts/${cid}/confirm`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -522,7 +522,7 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID
*
*/
- async getCashoutById(auth: UserAndToken, cid: string) {
+ async getCashoutById(auth: UserAndToken, cid: number) {
const url = new URL(`accounts/${auth.username}/cashouts/${cid}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 0ecc08b33..4c8a146a6 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -404,7 +404,7 @@ export const codecForBankAccountGetWithdrawalResponse =
export const codecForCashoutPending =
(): Codec<TalerCorebankApi.CashoutPending> =>
buildCodecForObject<TalerCorebankApi.CashoutPending>()
- .property("cashout_id", codecForString())
+ .property("cashout_id", codecForNumber())
.build("TalerCorebankApi.CashoutPending");
export const codecForCashoutConversionResponse =
@@ -428,7 +428,7 @@ export const codecForCashouts = (): Codec<TalerCorebankApi.Cashouts> =>
export const codecForCashoutInfo = (): Codec<TalerCorebankApi.CashoutInfo> =>
buildCodecForObject<TalerCorebankApi.CashoutInfo>()
- .property("cashout_id", codecForString())
+ .property("cashout_id", codecForNumber())
.property(
"status",
codecForEither(
@@ -448,7 +448,7 @@ export const codecForGlobalCashouts =
export const codecForGlobalCashoutInfo =
(): Codec<TalerCorebankApi.GlobalCashoutInfo> =>
buildCodecForObject<TalerCorebankApi.GlobalCashoutInfo>()
- .property("cashout_id", codecForString())
+ .property("cashout_id", codecForNumber())
.property("username", codecForString())
.property(
"status",
@@ -465,7 +465,7 @@ export const codecForCashoutStatusResponse =
buildCodecForObject<TalerCorebankApi.CashoutStatusResponse>()
.property("amount_credit", codecForAmountString())
.property("amount_debit", codecForAmountString())
- .property("confirmation_time", codecForTimestamp)
+ .property("confirmation_time", codecOptional(codecForTimestamp))
.property("creation_time", codecForTimestamp)
// .property("credit_payto_uri", codecForPaytoString())
.property(
@@ -1462,7 +1462,7 @@ export namespace TalerCorebankApi {
export interface CashoutPending {
// ID identifying the operation being created
// and now waiting for the TAN confirmation.
- cashout_id: string;
+ cashout_id: number;
}
export interface CashoutConfirmRequest {
@@ -1476,7 +1476,7 @@ export namespace TalerCorebankApi {
}
export interface CashoutInfo {
- cashout_id: string;
+ cashout_id: number;
status: "pending" | "aborted" | "confirmed";
}
export interface GlobalCashouts {
@@ -1484,7 +1484,7 @@ export namespace TalerCorebankApi {
cashouts: GlobalCashoutInfo[];
}
export interface GlobalCashoutInfo {
- cashout_id: string;
+ cashout_id: number;
username: string;
status: "pending" | "aborted" | "confirmed";
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index e5eaa3c14..108f5d005 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -159,6 +159,7 @@ describe("Withdraw CTA states", () => {
amountEffective: "ARS:2" as AmountString,
paytoUris: ["payto://"],
tosAccepted: true,
+ withdrawalAccountList: [],
ageRestrictionOptions: [],
numCoins: 42,
},
@@ -223,6 +224,7 @@ describe("Withdraw CTA states", () => {
amountEffective: "ARS:2" as AmountString,
paytoUris: ["payto://"],
tosAccepted: false,
+ withdrawalAccountList: [],
ageRestrictionOptions: [],
numCoins: 42,
},