amount field

This commit is contained in:
Sebastian 2022-11-22 15:43:39 -03:00
parent dc08d7d20e
commit 88618df7b8
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
18 changed files with 298 additions and 815 deletions

View File

@ -0,0 +1,65 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useTranslationContext } from "../context/translation.js";
import { Grid } from "../mui/Grid.js";
import { AmountFieldHandler, TextFieldHandler } from "../mui/handlers.js";
import { AmountField } from "./AmountField.js";
export default {
title: "components/amountField",
};
function RenderAmount(): VNode {
const [value, setValue] = useState<AmountJson | undefined>(undefined);
const error = value === undefined ? undefined : undefined;
const handler: AmountFieldHandler = {
value: value ?? Amounts.zeroOfCurrency("USD"),
onInput: async (e) => {
setValue(e);
},
error,
};
const { i18n } = useTranslationContext();
return (
<Fragment>
<AmountField
required
label={<i18n.Translate>Amount</i18n.Translate>}
currency="USD"
highestDenom={2000000}
lowestDenom={0.01}
handler={handler}
/>
<p>
<pre>{JSON.stringify(value, undefined, 2)}</pre>
</p>
</Fragment>
);
}
export const AmountFieldExample = (): VNode => RenderAmount();

View File

@ -14,51 +14,182 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import {
amountFractionalBase,
amountFractionalLength,
AmountJson,
amountMaxValue,
Amounts,
Result,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { TextFieldHandler } from "../mui/handlers.js"; import { useState } from "preact/hooks";
import { AmountFieldHandler } from "../mui/handlers.js";
import { TextField } from "../mui/TextField.js"; import { TextField } from "../mui/TextField.js";
import { ErrorText } from "./styled/index.js";
const HIGH_DENOM_SYMBOL = ["", "K", "M", "G", "T", "P"];
const LOW_DENOM_SYMBOL = ["", "m", "mm", "n", "p", "f"];
export function AmountField({ export function AmountField({
label, label,
handler, handler,
currency, lowestDenom = 1,
highestDenom = 1,
required, required,
}: { }: {
label: VNode; label: VNode;
lowestDenom?: number;
highestDenom?: number;
required?: boolean; required?: boolean;
currency: string; handler: AmountFieldHandler;
handler: TextFieldHandler;
}): VNode { }): VNode {
const [unit, setUnit] = useState(1);
const [dotAtTheEnd, setDotAtTheEnd] = useState(false);
const currency = handler.value.currency;
let hd = Math.floor(Math.log10(highestDenom || 1) / 3);
let ld = Math.ceil((-1 * Math.log10(lowestDenom || 1)) / 3);
const currencyLabels: Array<{ name: string; unit: number }> = [
{
name: currency,
unit: 1,
},
];
while (hd > 0) {
currencyLabels.push({
name: `${HIGH_DENOM_SYMBOL[hd]}${currency}`,
unit: Math.pow(10, hd * 3),
});
hd--;
}
while (ld > 0) {
currencyLabels.push({
name: `${LOW_DENOM_SYMBOL[ld]}${currency}`,
unit: Math.pow(10, -1 * ld * 3),
});
ld--;
}
const prev = Amounts.stringifyValue(handler.value);
function positiveAmount(value: string): string { function positiveAmount(value: string): string {
if (!value) return ""; setDotAtTheEnd(value.endsWith("."));
try { if (!value) {
const num = Number.parseFloat(value);
if (Number.isNaN(num) || num < 0) return handler.value;
if (handler.onInput) { if (handler.onInput) {
handler.onInput(value); handler.onInput(Amounts.zeroOfCurrency(currency));
} }
return value; return "";
}
try {
//remove all but last dot
const parsed = value.replace(/(\.)(?=.*\1)/g, "");
const real = parseValue(currency, parsed);
if (!real || real.value < 0) {
return prev;
}
const normal = normalize(real, unit);
console.log(real, unit, normal);
if (normal && handler.onInput) {
handler.onInput(normal);
}
return parsed;
} catch (e) { } catch (e) {
// do nothing // do nothing
} }
return handler.value; return prev;
} }
const normal = denormalize(handler.value, unit) ?? handler.value;
const textValue = Amounts.stringifyValue(normal) + (dotAtTheEnd ? "." : "");
return ( return (
<TextField <Fragment>
label={label} <TextField
type="number" label={label}
min="0" type="text"
step="0.1" min="0"
variant="filled" inputmode="decimal"
error={handler.error} step="0.1"
required={required} variant="filled"
startAdornment={ error={handler.error}
<div style={{ padding: "25px 12px 8px 12px" }}>{currency}</div> required={required}
} startAdornment={
value={handler.value} currencyLabels.length === 1 ? (
disabled={!handler.onInput} <div
onInput={positiveAmount} style={{
/> marginTop: 20,
padding: "5px 12px 8px 12px",
}}
>
{currency}
</div>
) : (
<select
disabled={!handler.onInput}
onChange={(e) => {
const unit = Number.parseFloat(e.currentTarget.value);
setUnit(unit);
}}
value={String(unit)}
style={{
marginTop: 20,
padding: "5px 12px 8px 12px",
background: "transparent",
border: 0,
}}
>
{currencyLabels.map((c) => (
<option key={c} value={c.unit}>
<div>{c.name}</div>
</option>
))}
</select>
)
}
value={textValue}
disabled={!handler.onInput}
onInput={positiveAmount}
/>
</Fragment>
); );
} }
function parseValue(currency: string, s: string): AmountJson | undefined {
const [intPart, fractPart] = s.split(".");
const tail = "." + (fractPart || "0");
if (tail.length > amountFractionalLength + 1) {
return undefined;
}
const value = Number.parseInt(intPart, 10);
if (Number.isNaN(value) || value > amountMaxValue) {
return undefined;
}
return {
currency,
fraction: Math.round(amountFractionalBase * Number.parseFloat(tail)),
value,
};
}
function normalize(amount: AmountJson, unit: number): AmountJson | undefined {
if (unit === 1 || Amounts.isZero(amount)) return amount;
const result =
unit < 1
? Amounts.divide(amount, 1 / unit)
: Amounts.mult(amount, unit).amount;
return result;
}
function denormalize(amount: AmountJson, unit: number): AmountJson | undefined {
if (unit === 1 || Amounts.isZero(amount)) return amount;
const result =
unit < 1
? Amounts.mult(amount, 1 / unit).amount
: Amounts.divide(amount, unit);
return result;
}

View File

@ -57,9 +57,9 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
? !tx.withdrawalDetails.confirmed ? !tx.withdrawalDetails.confirmed
? i18n.str`Need approval in the Bank` ? i18n.str`Need approval in the Bank`
: i18n.str`Exchange is waiting the wire transfer` : i18n.str`Exchange is waiting the wire transfer`
: undefined : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer
: tx.withdrawalDetails.type === WithdrawalType.ManualTransfer ? i18n.str`Exchange is waiting the wire transfer`
? i18n.str`Exchange is waiting the wire transfer` : "" //pending but no message
: undefined : undefined
} }
/> />

View File

@ -25,5 +25,6 @@ import * as a3 from "./Amount.stories.js";
import * as a4 from "./ShowFullContractTermPopup.stories.js"; import * as a4 from "./ShowFullContractTermPopup.stories.js";
import * as a5 from "./TermsOfService/stories.js"; import * as a5 from "./TermsOfService/stories.js";
import * as a6 from "./QR.stories"; import * as a6 from "./QR.stories";
import * as a7 from "./AmountField.stories.js";
export default [a1, a2, a3, a4, a5, a6]; export default [a1, a2, a3, a4, a5, a6, a7];

View File

@ -41,6 +41,7 @@ export interface Props {
multiline?: boolean; multiline?: boolean;
onChange?: (s: string) => void; onChange?: (s: string) => void;
onInput?: (s: string) => string; onInput?: (s: string) => string;
inputmode?: string;
min?: string; min?: string;
step?: string; step?: string;
placeholder?: string; placeholder?: string;

View File

@ -13,6 +13,7 @@
You should have received a copy of the GNU General Public License along with 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/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { AmountJson } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
export interface TextFieldHandler { export interface TextFieldHandler {
@ -21,6 +22,12 @@ export interface TextFieldHandler {
error?: string; error?: string;
} }
export interface AmountFieldHandler {
onInput?: (value: AmountJson) => Promise<void>;
value: AmountJson;
error?: string;
}
export interface ButtonHandler { export interface ButtonHandler {
onClick?: () => Promise<void>; onClick?: () => Promise<void>;
error?: TalerError; error?: TalerError;

View File

@ -1,58 +0,0 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from "../test-utils.js";
import { CreateManualWithdraw as TestedComponent } from "./CreateManualWithdraw.js";
export default {
title: "wallet/manual withdraw/creation",
component: TestedComponent,
argTypes: {},
};
// ,
const exchangeUrlWithCurrency = {
"http://exchange.taler:8081": "COL",
"http://exchange.tal": "EUR",
};
export const WithoutAnyExchangeKnown = createExample(TestedComponent, {
exchangeUrlWithCurrency: {},
});
export const InitialState = createExample(TestedComponent, {
exchangeUrlWithCurrency,
});
export const WithAmountInitialized = createExample(TestedComponent, {
initialAmount: "10",
exchangeUrlWithCurrency,
});
export const WithExchangeError = createExample(TestedComponent, {
error: "The exchange url seems invalid",
exchangeUrlWithCurrency,
});
export const WithAmountError = createExample(TestedComponent, {
initialAmount: "e",
exchangeUrlWithCurrency,
});

View File

@ -1,232 +0,0 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { expect } from "chai";
import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js";
import { mountHook } from "../test-utils.js";
import { useComponentState } from "./CreateManualWithdraw.js";
const exchangeListWithARSandUSD = {
url1: "USD",
url2: "ARS",
url3: "ARS",
};
const exchangeListEmpty = {};
describe("CreateManualWithdraw states", () => {
it("should set noExchangeFound when exchange list is empty", () => {
const { pullLastResultOrThrow } = mountHook(() =>
useComponentState(exchangeListEmpty, undefined, undefined),
);
const { noExchangeFound } = pullLastResultOrThrow();
expect(noExchangeFound).equal(true);
});
it("should set noExchangeFound when exchange list doesn't include selected currency", () => {
const { pullLastResultOrThrow } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "COL"),
);
const { noExchangeFound } = pullLastResultOrThrow();
expect(noExchangeFound).equal(true);
});
it("should select the first exchange from the list", () => {
const { pullLastResultOrThrow } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, undefined),
);
const { exchange } = pullLastResultOrThrow();
expect(exchange.value).equal("url1");
});
it("should select the first exchange with the selected currency", () => {
const { pullLastResultOrThrow } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
const { exchange } = pullLastResultOrThrow();
expect(exchange.value).equal("url2");
});
it("should change the exchange when currency change", async () => {
const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
{
const { exchange, currency } = pullLastResultOrThrow();
expect(exchange.value).equal("url2");
if (currency.onChange === undefined) expect.fail();
currency.onChange("USD");
}
expect(await waitForStateUpdate()).true;
{
const { exchange } = pullLastResultOrThrow();
expect(exchange.value).equal("url1");
}
});
it("should change the currency when exchange change", async () => {
const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
{
const { exchange, currency } = pullLastResultOrThrow();
expect(exchange.value).equal("url2");
expect(currency.value).equal("ARS");
if (exchange.onChange === undefined) expect.fail();
exchange.onChange("url1");
}
expect(await waitForStateUpdate()).true;
{
const { exchange, currency } = pullLastResultOrThrow();
expect(exchange.value).equal("url1");
expect(currency.value).equal("USD");
}
});
it("should update parsed amount when amount change", async () => {
const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
{
const { amount, parsedAmount } = pullLastResultOrThrow();
expect(parsedAmount).equal(undefined);
expect(amount.onInput).not.undefined;
if (!amount.onInput) return;
amount.onInput("12");
}
expect(await waitForStateUpdate()).true;
{
const { parsedAmount } = pullLastResultOrThrow();
expect(parsedAmount).deep.equals({
value: 12,
fraction: 0,
currency: "ARS",
});
}
});
it("should have an amount field", async () => {
const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
await defaultTestForInputText(
waitForStateUpdate,
() => pullLastResultOrThrow().amount,
);
});
it("should have an exchange selector ", async () => {
const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
await defaultTestForInputSelect(
waitForStateUpdate,
() => pullLastResultOrThrow().exchange,
);
});
it("should have a currency selector ", async () => {
const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
await defaultTestForInputSelect(
waitForStateUpdate,
() => pullLastResultOrThrow().currency,
);
});
});
async function defaultTestForInputText(
awaiter: () => Promise<boolean>,
getField: () => TextFieldHandler,
): Promise<void> {
let nextValue = "";
{
const field = getField();
const initialValue = field.value;
nextValue = `${initialValue} something else`;
expect(field.onInput).not.undefined;
if (!field.onInput) return;
field.onInput(nextValue);
}
expect(await awaiter()).true;
{
const field = getField();
expect(field.value).equal(nextValue);
}
}
async function defaultTestForInputSelect(
awaiter: () => Promise<boolean>,
getField: () => SelectFieldHandler,
): Promise<void> {
let nextValue = "";
{
const field = getField();
const initialValue = field.value;
const keys = Object.keys(field.list);
const nextIdx = keys.indexOf(initialValue) + 1;
if (keys.length < nextIdx) {
throw new Error("no enough values");
}
nextValue = keys[nextIdx];
if (field.onChange === undefined) expect.fail();
field.onChange(nextValue);
}
expect(await awaiter()).true;
{
const field = getField();
expect(field.value).equal(nextValue);
}
}

View File

@ -1,282 +0,0 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage.js";
import { SelectList } from "../components/SelectList.js";
import {
BoldLight,
Centered,
Input,
InputWithLabel,
LightText,
LinkPrimary,
SubTitle,
} from "../components/styled/index.js";
import { useTranslationContext } from "../context/translation.js";
import { Button } from "../mui/Button.js";
import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js";
export interface Props {
error: string | undefined;
initialAmount?: string;
exchangeUrlWithCurrency: Record<string, string>;
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
initialCurrency?: string;
}
export interface State {
noExchangeFound: boolean;
parsedAmount: AmountJson | undefined;
amount: TextFieldHandler;
currency: SelectFieldHandler;
exchange: SelectFieldHandler;
}
export function useComponentState(
exchangeUrlWithCurrency: Record<string, string>,
initialAmount: string | undefined,
initialCurrency: string | undefined,
): State {
const exchangeSelectList = Object.keys(exchangeUrlWithCurrency);
const currencySelectList = Object.values(exchangeUrlWithCurrency);
const exchangeMap = exchangeSelectList.reduce(
(p, c) => ({ ...p, [c]: `${c} (${exchangeUrlWithCurrency[c]})` }),
{} as Record<string, string>,
);
const currencyMap = currencySelectList.reduce(
(p, c) => ({ ...p, [c]: c }),
{} as Record<string, string>,
);
const foundExchangeForCurrency = exchangeSelectList.findIndex(
(e) => exchangeUrlWithCurrency[e] === initialCurrency,
);
const initialExchange =
foundExchangeForCurrency !== -1
? exchangeSelectList[foundExchangeForCurrency]
: !initialCurrency && exchangeSelectList.length > 0
? exchangeSelectList[0]
: undefined;
const [exchange, setExchange] = useState(initialExchange || "");
const [currency, setCurrency] = useState(
initialExchange ? exchangeUrlWithCurrency[initialExchange] : "",
);
const [amount, setAmount] = useState(initialAmount || "");
const parsedAmount = Amounts.parse(`${currency}:${amount}`);
async function changeExchange(exchange: string): Promise<void> {
setExchange(exchange);
setCurrency(exchangeUrlWithCurrency[exchange]);
}
async function changeCurrency(currency: string): Promise<void> {
setCurrency(currency);
const found = Object.entries(exchangeUrlWithCurrency).find(
(e) => e[1] === currency,
);
if (found) {
setExchange(found[0]);
} else {
setExchange("");
}
}
return {
noExchangeFound: initialExchange === undefined,
currency: {
list: currencyMap,
value: currency,
onChange: changeCurrency,
},
exchange: {
list: exchangeMap,
value: exchange,
onChange: changeExchange,
},
amount: {
value: amount,
onInput: async (e: string) => setAmount(e),
},
parsedAmount,
};
}
export function CreateManualWithdraw({
initialAmount,
exchangeUrlWithCurrency,
error,
initialCurrency,
onCreate,
}: Props): VNode {
const { i18n } = useTranslationContext();
const state = useComponentState(
exchangeUrlWithCurrency,
initialAmount,
initialCurrency,
);
if (state.noExchangeFound) {
if (initialCurrency) {
return (
<section>
<SubTitle>
<i18n.Translate>
Manual Withdrawal for {initialCurrency}
</i18n.Translate>
</SubTitle>
<LightText>
<i18n.Translate>
Choose a exchange from where the coins will be withdrawn. The
exchange will send the coins to this wallet after receiving a wire
transfer with the correct subject.
</i18n.Translate>
</LightText>
<Centered style={{ marginTop: 100 }}>
<BoldLight>
<i18n.Translate>
No exchange found for {initialCurrency}
</i18n.Translate>
</BoldLight>
<LinkPrimary
href={Pages.settingsExchangeAdd({ currency: initialCurrency })}
style={{ marginLeft: "auto" }}
>
<i18n.Translate>Add Exchange</i18n.Translate>
</LinkPrimary>
</Centered>
</section>
);
}
return (
<section>
<SubTitle>
<i18n.Translate>
Manual Withdrawal for {state.currency.value}
</i18n.Translate>
</SubTitle>
<LightText>
<i18n.Translate>
Choose a exchange from where the coins will be withdrawn. The
exchange will send the coins to this wallet after receiving a wire
transfer with the correct subject.
</i18n.Translate>
</LightText>
<Centered style={{ marginTop: 100 }}>
<BoldLight>
<i18n.Translate>No exchange configured</i18n.Translate>
</BoldLight>
<LinkPrimary
href={Pages.settingsExchangeAdd({})}
style={{ marginLeft: "auto" }}
>
<i18n.Translate>Add Exchange</i18n.Translate>
</LinkPrimary>
</Centered>
</section>
);
}
return (
<Fragment>
<section>
{error && (
<ErrorMessage
title={
<i18n.Translate>Can&apos;t create the reserve</i18n.Translate>
}
description={error}
/>
)}
<SubTitle>
<i18n.Translate>
Manual Withdrawal for {state.currency.value}
</i18n.Translate>
</SubTitle>
<LightText>
<i18n.Translate>
Choose a exchange from where the coins will be withdrawn. The
exchange will send the coins to this wallet after receiving a wire
transfer with the correct subject.
</i18n.Translate>
</LightText>
<p>
<Input>
<SelectList
label={<i18n.Translate>Currency</i18n.Translate>}
name="currency"
{...state.currency}
/>
</Input>
<Input>
<SelectList
label={<i18n.Translate>Exchange</i18n.Translate>}
name="exchange"
{...state.exchange}
/>
</Input>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<LinkPrimary
href={Pages.settingsExchangeAdd({})}
style={{ marginLeft: "auto" }}
>
<i18n.Translate>Add Exchange</i18n.Translate>
</LinkPrimary>
</div>
{state.currency.value && (
<InputWithLabel
invalid={!!state.amount.value && !state.parsedAmount}
>
<label>
<i18n.Translate>Amount</i18n.Translate>
</label>
<div>
<span>{state.currency.value}</span>
<input
type="number"
value={state.amount.value}
// onInput={(e) => state.amount.onInput(e.currentTarget.value)}
/>
</div>
</InputWithLabel>
)}
</p>
</section>
<footer>
<div />
<Button
variant="contained"
disabled={!state.parsedAmount || !state.exchange.value}
onClick={() => onCreate(state.exchange.value, state.parsedAmount!)}
>
<i18n.Translate>Start withdrawal</i18n.Translate>
</Button>
</footer>
</Fragment>
);
}

View File

@ -18,6 +18,7 @@ import { AmountJson, PaytoUri } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { import {
AmountFieldHandler,
ButtonHandler, ButtonHandler,
SelectFieldHandler, SelectFieldHandler,
TextFieldHandler, TextFieldHandler,
@ -98,7 +99,7 @@ export namespace State {
totalFee: AmountJson; totalFee: AmountJson;
totalToDeposit: AmountJson; totalToDeposit: AmountJson;
amount: TextFieldHandler; amount: AmountFieldHandler;
account: SelectFieldHandler; account: SelectFieldHandler;
cancelHandler: ButtonHandler; cancelHandler: ButtonHandler;
depositHandler: ButtonHandler; depositHandler: ButtonHandler;

View File

@ -52,9 +52,13 @@ export function useComponentState(
}); });
const initialValue = const initialValue =
parsed !== undefined ? Amounts.stringifyValue(parsed) : "0"; parsed !== undefined
? parsed
: currency !== undefined
? Amounts.zeroOfCurrency(currency)
: undefined;
// const [accountIdx, setAccountIdx] = useState<number>(0); // const [accountIdx, setAccountIdx] = useState<number>(0);
const [amount, setAmount] = useState(initialValue); const [amount, setAmount] = useState<AmountJson>(initialValue ?? ({} as any));
const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); const [selectedAccount, setSelectedAccount] = useState<PaytoUri>();
const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined); const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined);
@ -81,7 +85,7 @@ export function useComponentState(
} }
const { accounts, balances } = hook.response; const { accounts, balances } = hook.response;
const parsedAmount = Amounts.parse(`${currency}:${amount}`); // const parsedAmount = Amounts.parse(`${currency}:${amount}`);
if (addingAccount) { if (addingAccount) {
return { return {
@ -129,8 +133,8 @@ export function useComponentState(
const firstAccount = accounts[0].uri; const firstAccount = accounts[0].uri;
const currentAccount = !selectedAccount ? firstAccount : selectedAccount; const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
if (fee === undefined && parsedAmount) { if (fee === undefined) {
getFeeForAmount(currentAccount, parsedAmount, api).then((initialFee) => { getFeeForAmount(currentAccount, amount, api).then((initialFee) => {
setFee(initialFee); setFee(initialFee);
}); });
return { return {
@ -143,9 +147,9 @@ export function useComponentState(
async function updateAccountFromList(accountStr: string): Promise<void> { async function updateAccountFromList(accountStr: string): Promise<void> {
const uri = !accountStr ? undefined : parsePaytoUri(accountStr); const uri = !accountStr ? undefined : parsePaytoUri(accountStr);
if (uri && parsedAmount) { if (uri) {
try { try {
const result = await getFeeForAmount(uri, parsedAmount, api); const result = await getFeeForAmount(uri, amount, api);
setSelectedAccount(uri); setSelectedAccount(uri);
setFee(result); setFee(result);
} catch (e) { } catch (e) {
@ -155,17 +159,15 @@ export function useComponentState(
} }
} }
async function updateAmount(numStr: string): Promise<void> { async function updateAmount(newAmount: AmountJson): Promise<void> {
const parsed = Amounts.parse(`${currency}:${numStr}`); // const parsed = Amounts.parse(`${currency}:${numStr}`);
if (parsed) { try {
try { const result = await getFeeForAmount(currentAccount, newAmount, api);
const result = await getFeeForAmount(currentAccount, parsed, api); setAmount(newAmount);
setAmount(numStr); setFee(result);
setFee(result); } catch (e) {
} catch (e) { setAmount(newAmount);
setAmount(numStr); setFee(undefined);
setFee(undefined);
}
} }
} }
@ -175,32 +177,29 @@ export function useComponentState(
: Amounts.zeroOfCurrency(currency); : Amounts.zeroOfCurrency(currency);
const totalToDeposit = const totalToDeposit =
parsedAmount && fee !== undefined fee !== undefined
? Amounts.sub(parsedAmount, totalFee).amount ? Amounts.sub(amount, totalFee).amount
: Amounts.zeroOfCurrency(currency); : Amounts.zeroOfCurrency(currency);
const isDirty = amount !== initialValue; const isDirty = amount !== initialValue;
const amountError = !isDirty const amountError = !isDirty
? undefined ? undefined
: !parsedAmount : Amounts.cmp(balance, amount) === -1
? "Invalid amount"
: Amounts.cmp(balance, parsedAmount) === -1
? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
: undefined; : undefined;
const unableToDeposit = const unableToDeposit =
!parsedAmount || //no amount specified
Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee
fee === undefined || //no fee calculated yet fee === undefined || //no fee calculated yet
amountError !== undefined; //amount field may be invalid amountError !== undefined; //amount field may be invalid
async function doSend(): Promise<void> { async function doSend(): Promise<void> {
if (!parsedAmount || !currency) return; if (!currency) return;
const depositPaytoUri = stringifyPaytoUri(currentAccount); const depositPaytoUri = stringifyPaytoUri(currentAccount);
const amount = Amounts.stringify(parsedAmount); const amountStr = Amounts.stringify(amount);
await api.wallet.call(WalletApiOperation.CreateDepositGroup, { await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
amount, amount: amountStr,
depositPaytoUri, depositPaytoUri,
}); });
onSuccess(currency); onSuccess(currency);
@ -211,7 +210,7 @@ export function useComponentState(
error: undefined, error: undefined,
currency, currency,
amount: { amount: {
value: String(amount), value: amount,
onInput: updateAmount, onInput: updateAmount,
error: amountError, error: amountError,
}, },

View File

@ -52,7 +52,7 @@ export const WithNoAccountForIBAN = createExample(ReadyView, {
onInput: async () => { onInput: async () => {
null; null;
}, },
value: "10:USD", value: Amounts.parseOrThrow("USD:10"),
}, },
onAddAccount: {}, onAddAccount: {},
cancelHandler: {}, cancelHandler: {},
@ -87,7 +87,7 @@ export const WithIBANAccountTypeSelected = createExample(ReadyView, {
onInput: async () => { onInput: async () => {
null; null;
}, },
value: "10:USD", value: Amounts.parseOrThrow("USD:10"),
}, },
onAddAccount: {}, onAddAccount: {},
cancelHandler: {}, cancelHandler: {},
@ -123,7 +123,7 @@ export const NewBitcoinAccountTypeSelected = createExample(ReadyView, {
onInput: async () => { onInput: async () => {
null; null;
}, },
value: "10:USD", value: Amounts.parseOrThrow("USD:10"),
}, },
cancelHandler: {}, cancelHandler: {},
depositHandler: { depositHandler: {

View File

@ -194,7 +194,7 @@ describe("DepositPage states", () => {
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri));
expect(r.amount.value).eq("0"); expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
} }
@ -269,7 +269,7 @@ describe("DepositPage states", () => {
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
expect(r.amount.value).eq("0"); expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(r.account.onChange).not.undefined; expect(r.account.onChange).not.undefined;
@ -285,7 +285,7 @@ describe("DepositPage states", () => {
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq(accountSelected); expect(r.account.value).eq(accountSelected);
expect(r.amount.value).eq("0"); expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
} }
@ -423,7 +423,7 @@ describe("DepositPage states", () => {
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
expect(r.amount.value).eq("0"); expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(r.account.onChange).not.undefined; expect(r.account.onChange).not.undefined;
@ -439,13 +439,13 @@ describe("DepositPage states", () => {
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq(accountSelected); expect(r.account.value).eq(accountSelected);
expect(r.amount.value).eq("0"); expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.amount.onInput).not.undefined; expect(r.amount.onInput).not.undefined;
if (!r.amount.onInput) return; if (!r.amount.onInput) return;
r.amount.onInput("10"); r.amount.onInput(Amounts.parseOrThrow("EUR:10"));
} }
expect(await waitForStateUpdate()).true; expect(await waitForStateUpdate()).true;
@ -456,7 +456,7 @@ describe("DepositPage states", () => {
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq(accountSelected); expect(r.account.value).eq(accountSelected);
expect(r.amount.value).eq("10"); expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
expect(r.depositHandler.onClick).not.undefined; expect(r.depositHandler.onClick).not.undefined;

View File

@ -173,25 +173,22 @@ export function ReadyView(state: State.Ready): VNode {
<Grid item xs={1}> <Grid item xs={1}>
<AmountField <AmountField
label={<i18n.Translate>Amount</i18n.Translate>} label={<i18n.Translate>Amount</i18n.Translate>}
currency={state.currency}
handler={state.amount} handler={state.amount}
/> />
</Grid> </Grid>
<Grid item xs={1}> <Grid item xs={1}>
<AmountField <AmountField
label={<i18n.Translate>Deposit fee</i18n.Translate>} label={<i18n.Translate>Deposit fee</i18n.Translate>}
currency={state.currency}
handler={{ handler={{
value: Amounts.stringifyValue(state.totalFee), value: state.totalFee,
}} }}
/> />
</Grid> </Grid>
<Grid item xs={1}> <Grid item xs={1}>
<AmountField <AmountField
label={<i18n.Translate>Total deposit</i18n.Translate>} label={<i18n.Translate>Total deposit</i18n.Translate>}
currency={state.currency}
handler={{ handler={{
value: Amounts.stringifyValue(state.totalToDeposit), value: state.totalToDeposit,
}} }}
/> />
</Grid> </Grid>

View File

@ -33,9 +33,7 @@ import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js"; import { Grid } from "../mui/Grid.js";
import { TextFieldHandler } from "../mui/handlers.js";
import { Paper } from "../mui/Paper.js"; import { Paper } from "../mui/Paper.js";
import { TextField } from "../mui/TextField.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import arrowIcon from "../svg/chevron-down.svg"; import arrowIcon from "../svg/chevron-down.svg";
import bankIcon from "../svg/ri-bank-line.svg"; import bankIcon from "../svg/ri-bank-line.svg";
@ -279,12 +277,13 @@ export function DestinationSelectionGetCash({
const parsedInitialAmount = !initialAmount const parsedInitialAmount = !initialAmount
? undefined ? undefined
: Amounts.parse(initialAmount); : Amounts.parse(initialAmount);
const parsedInitialAmountValue = !parsedInitialAmount
? "0"
: Amounts.stringifyValue(parsedInitialAmount);
const [currency, setCurrency] = useState(parsedInitialAmount?.currency); const [currency, setCurrency] = useState(parsedInitialAmount?.currency);
const [amount, setAmount] = useState(parsedInitialAmountValue); const [amount, setAmount] = useState(
parsedInitialAmount ?? Amounts.zeroOfCurrency(currency ?? "KUDOS"),
);
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const previous1: Contact[] = []; const previous1: Contact[] = [];
const previous2: Contact[] = [ const previous2: Contact[] = [
@ -313,10 +312,8 @@ export function DestinationSelectionGetCash({
</div> </div>
); );
} }
const currencyAndAmount = `${currency}:${amount}`; const currencyAndAmount = Amounts.stringify(amount);
const parsedAmount = Amounts.parse(currencyAndAmount); const invalid = Amounts.isZero(amount);
// const dirty = parsedInitialAmountValue !== amount;
const invalid = !parsedAmount || Amounts.isZero(parsedAmount);
return ( return (
<Container> <Container>
<h1> <h1>
@ -325,7 +322,6 @@ export function DestinationSelectionGetCash({
<Grid container columns={2} justifyContent="space-between"> <Grid container columns={2} justifyContent="space-between">
<AmountField <AmountField
label={<i18n.Translate>Amount</i18n.Translate>} label={<i18n.Translate>Amount</i18n.Translate>}
currency={currency}
required required
handler={{ handler={{
onInput: async (s) => setAmount(s), onInput: async (s) => setAmount(s),
@ -416,12 +412,12 @@ export function DestinationSelectionSendCash({
const parsedInitialAmount = !initialAmount const parsedInitialAmount = !initialAmount
? undefined ? undefined
: Amounts.parse(initialAmount); : Amounts.parse(initialAmount);
const parsedInitialAmountValue = !parsedInitialAmount
? ""
: Amounts.stringifyValue(parsedInitialAmount);
const currency = parsedInitialAmount?.currency; const currency = parsedInitialAmount?.currency;
const [amount, setAmount] = useState(parsedInitialAmountValue); const [amount, setAmount] = useState(
parsedInitialAmount ?? Amounts.zeroOfCurrency(currency ?? "KUDOS"),
);
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const previous1: Contact[] = []; const previous1: Contact[] = [];
const previous2: Contact[] = [ const previous2: Contact[] = [
@ -450,9 +446,9 @@ export function DestinationSelectionSendCash({
</div> </div>
); );
} }
const currencyAndAmount = `${currency}:${amount}`; const currencyAndAmount = Amounts.stringify(amount);
const parsedAmount = Amounts.parse(currencyAndAmount); //const parsedAmount = Amounts.parse(currencyAndAmount);
const invalid = !parsedAmount || Amounts.isZero(parsedAmount); const invalid = Amounts.isZero(amount);
return ( return (
<Container> <Container>
<h1> <h1>
@ -462,7 +458,6 @@ export function DestinationSelectionSendCash({
<div> <div>
<AmountField <AmountField
label={<i18n.Translate>Amount</i18n.Translate>} label={<i18n.Translate>Amount</i18n.Translate>}
currency={currency}
required required
handler={{ handler={{
onInput: async (s) => setAmount(s), onInput: async (s) => setAmount(s),

View File

@ -44,21 +44,22 @@ export function useComponentState(
const comparingExchanges = selectedIdx !== initialValue; const comparingExchanges = selectedIdx !== initialValue;
const initialExchange = const initialExchange = comparingExchanges
comparingExchanges ? exchanges[initialValue] : undefined; ? exchanges[initialValue]
: undefined;
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
const selected = !selectedExchange const selected = !selectedExchange
? undefined ? undefined
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
exchangeBaseUrl: selectedExchange.exchangeBaseUrl, exchangeBaseUrl: selectedExchange.exchangeBaseUrl,
}); });
const original = !initialExchange const original = !initialExchange
? undefined ? undefined
: await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
exchangeBaseUrl: initialExchange.exchangeBaseUrl, exchangeBaseUrl: initialExchange.exchangeBaseUrl,
}); });
return { return {
exchanges, exchanges,

View File

@ -1,141 +0,0 @@
/*
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 {
AcceptManualWithdrawalResult,
AmountJson,
Amounts,
NotificationType,
parsePaytoUri,
PaytoUri,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Loading } from "../components/Loading.js";
import { LoadingError } from "../components/LoadingError.js";
import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { wxApi } from "../wxApi.js";
import { CreateManualWithdraw } from "./CreateManualWithdraw.js";
import { ReserveCreated } from "./ReserveCreated.js";
interface Props {
amount?: string;
onCancel: () => Promise<void>;
}
export function ManualWithdrawPage({ amount, onCancel }: Props): VNode {
const [success, setSuccess] = useState<
| {
response: AcceptManualWithdrawalResult;
exchangeBaseUrl: string;
amount: AmountJson;
paytoURI: PaytoUri | undefined;
payto: string;
}
| undefined
>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const state = useAsyncAsHook(() =>
wxApi.wallet.call(WalletApiOperation.ListExchanges, {}),
);
useEffect(() => {
return wxApi.listener.onUpdateNotification(
[NotificationType.ExchangeAdded],
state?.retry,
);
});
const { i18n } = useTranslationContext();
async function doCreate(
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<void> {
try {
const response = await wxApi.wallet.call(
WalletApiOperation.AcceptManualWithdrawal,
{
exchangeBaseUrl: exchangeBaseUrl,
amount: Amounts.stringify(amount),
},
);
const payto = response.exchangePaytoUris[0];
const paytoURI = parsePaytoUri(payto);
setSuccess({ exchangeBaseUrl, response, amount, paytoURI, payto });
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError("unexpected error");
}
setSuccess(undefined);
}
}
if (success) {
return (
<ReserveCreated
reservePub={success.response.reservePub}
paytoURI={success.paytoURI}
// payto={success.payto}
exchangeBaseUrl={success.exchangeBaseUrl}
amount={success.amount}
onCancel={onCancel}
/>
);
}
if (!state) {
return <Loading />;
}
if (state.hasError) {
return (
<LoadingError
title={
<i18n.Translate>
Could not load the list of known exchanges
</i18n.Translate>
}
error={state}
/>
);
}
const exchangeList = state.response.exchanges.reduce(
(p, c) => ({
...p,
[c.exchangeBaseUrl]: c.currency || "??",
}),
{} as Record<string, string>,
);
const parsedAmount = !amount ? undefined : Amounts.parse(amount);
const currency = parsedAmount?.currency;
const amountValue = !parsedAmount
? undefined
: Amounts.stringifyValue(parsedAmount);
return (
<CreateManualWithdraw
error={error}
exchangeUrlWithCurrency={exchangeList}
onCreate={doCreate}
initialCurrency={currency}
initialAmount={amountValue}
/>
);
}

View File

@ -20,7 +20,6 @@
*/ */
import * as a1 from "./Backup.stories.js"; import * as a1 from "./Backup.stories.js";
import * as a3 from "./CreateManualWithdraw.stories.js";
import * as a4 from "./DepositPage/stories.js"; import * as a4 from "./DepositPage/stories.js";
import * as a5 from "./ExchangeAddConfirm.stories.js"; import * as a5 from "./ExchangeAddConfirm.stories.js";
import * as a6 from "./ExchangeAddSetUrl.stories.js"; import * as a6 from "./ExchangeAddSetUrl.stories.js";
@ -40,7 +39,6 @@ import * as a20 from "./ManageAccount/stories.js";
export default [ export default [
a1, a1,
a3,
a4, a4,
a5, a5,
a6, a6,