aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/business/CreateCashout.tsx
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-11-21 17:10:07 -0300
committerSebastian <sebasjm@gmail.com>2023-11-21 17:10:31 -0300
commit32182fb1b912e1136ba933c4a4f204e6e2f33de2 (patch)
tree9de9caf82632994c233fbbd4366b086818217c7d /packages/demobank-ui/src/pages/business/CreateCashout.tsx
parent6000a55d583832a71335310514688f1f6faed722 (diff)
downloadwallet-core-32182fb1b912e1136ba933c4a4f204e6e2f33de2.tar.xz
cashout creation
Diffstat (limited to 'packages/demobank-ui/src/pages/business/CreateCashout.tsx')
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx485
1 files changed, 218 insertions, 267 deletions
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index 525a170bc..771004ec6 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -36,6 +36,7 @@ import { useBankCoreApiContext } from "../../context/config.js";
import { useAccountDetails } from "../../hooks/access.js";
import { useBackendState } from "../../hooks/backend.js";
import {
+ useConversionInfo,
useEstimator
} from "../../hooks/circuit.js";
import {
@@ -43,11 +44,12 @@ import {
undefinedIfEmpty
} from "../../utils.js";
import { LoginForm } from "../LoginForm.js";
-import { InputAmount } from "../PaytoWireTransferForm.js";
+import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
interface Props {
account: string;
+ focus?: boolean,
onComplete: (id: string) => void;
onCancel: () => void;
}
@@ -66,6 +68,7 @@ type ErrorFrom<T> = {
export function CreateCashout({
account: accountName,
onComplete,
+ focus,
onCancel,
}: Props): VNode {
const { i18n } = useTranslationContext();
@@ -77,20 +80,15 @@ export function CreateCashout({
const { state } = useBackendState()
const creds = state.status !== "loggedIn" ? undefined : state
const { api, config } = useBankCoreApiContext()
- const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
+ const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, amount:"2" });
const [notification, notify, handleError] = useLocalNotification()
+ const info = useConversionInfo();
- if (!config.have_cashout) {
+ if (!config.allow_conversion) {
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) {
return <Loading />
}
@@ -104,15 +102,13 @@ export function CreateCashout({
default: assertUnreachable(resultAccount)
}
}
+ if (!info) {
+ return <Loading />
+ }
- // if (resultRatios.type === "fail") {
- // switch (resultRatios.case) {
- // case "not-supported": return <div>cashout operations are not supported</div>
- // default: assertUnreachable(resultRatios.case)
- // }
- // }
-
- // const ratio = resultRatios.body
+ if (info instanceof TalerError) {
+ return <ErrorLoading error={info} />
+ }
const account = {
balance: Amounts.parseOrThrow(resultAccount.body.balance.amount),
@@ -120,37 +116,46 @@ export function CreateCashout({
debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold)
}
- const zero = Amounts.zeroOfCurrency(account.balance.currency);
+ const {fiat_currency, regional_currency, cashout_ratio, cashout_fee} = info.body
+ const regionalZero = Amounts.zeroOfCurrency(regional_currency);
+ const fiatZero = Amounts.zeroOfCurrency(fiat_currency);
const limit = account.balanceIsDebit
? Amounts.sub(account.debitThreshold, account.balance).amount
: Amounts.add(account.balance, account.debitThreshold).amount;
- const zeroCalc = { debit: zero, credit: zero, beforeFee: zero };
+ const zeroCalc = { debit: regionalZero, credit: fiatZero, beforeFee: regionalZero };
const [calc, setCalc] = useState(zeroCalc);
- const sellRate = config.conversion_info?.sell_at_ratio;
- const sellFee = !config.conversion_info?.sell_out_fee
- ? zero
- : Amounts.parseOrThrow(
- `${account.balance.currency}:${config.conversion_info.sell_out_fee}`,
- );
+ const sellRate = Number.parseFloat(cashout_ratio);
+ const sellFee = !cashout_fee
+ ? fiatZero
+ : Amounts.parseOrThrow(cashout_fee);
- if (sellRate === undefined || sellRate < 0) return <div>error rate</div>;
+ if (sellRate === undefined || sellRate < 0) return <div>error rate d
+ <pre>
+ {JSON.stringify(info.body, undefined, 2)}
+ </pre>
+ </div>;
const safeSellRate = sellRate
- const amount = Amounts.parseOrThrow(
- `${!form.isDebit ? config.fiat_currency.name : account.balance.currency}:${!form.amount ? "0" : form.amount
- }`,
+ /**
+ * can be in regional currency or fiat currency
+ * depending on the isDebit flag
+ */
+ const inputAmount = Amounts.parseOrThrow(
+ `${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" : form.amount}`,
);
useEffect(() => {
async function doAsync() {
await handleError(async () => {
- const resp = await (form.isDebit ?
- calculateFromDebit(amount, sellFee, safeSellRate) :
- calculateFromCredit(amount, sellFee, safeSellRate));
- setCalc(resp)
+ if (Amounts.isNonZero(inputAmount)) {
+ const resp = await (form.isDebit ?
+ calculateFromDebit(inputAmount, fiat_currency, sellFee, safeSellRate) :
+ calculateFromCredit(inputAmount, regional_currency, sellFee, safeSellRate));
+ setCalc(resp)
+ }
})
}
doAsync()
@@ -164,256 +169,202 @@ export function CreateCashout({
const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
amount: !form.amount
? i18n.str`required`
- : !amount
+ : !inputAmount
? i18n.str`could not be parsed`
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`balance is not enough`
- : Amounts.cmp(calc.beforeFee, sellFee) === -1
+ : Amounts.cmp(calc.credit, sellFee) === -1
? i18n.str`the total amount to transfer does not cover the fees`
: Amounts.isZero(calc.credit)
? i18n.str`the total transfer at destination will be zero`
: undefined,
channel: !form.channel ? i18n.str`required` : undefined,
});
+ const trimmedAmountStr = form.amount?.trim();
return (
<div>
<LocalNotificationBanner notification={notification} />
- <h1>New cashout</h1>
- <form class="pure-form">
- <fieldset>
- <label>{i18n.str`Subject`}</label>
- <input
- value={form.subject ?? ""}
- onChange={(e) => {
- form.subject = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.subject}
- isDirty={form.subject !== undefined}
- />
- </fieldset>
- <fieldset>
- <label for="amount">
- {form.isDebit
- ? i18n.str`Amount to send`
- : i18n.str`Amount to receive`}
-
- </label>
- <div style={{ display: "flex" }}>
- <InputAmount
- name="amount"
- currency={amount.currency}
- value={form.amount}
- onChange={(v) => {
- form.amount = v;
- updateForm(structuredClone(form));
- }}
- error={errors?.amount}
- />
- <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
- <input
- class="toggle-checkbox"
- type="checkbox"
- name="asd"
- onChange={(e): void => {
- form.isDebit = !form.isDebit;
- updateForm(structuredClone(form));
- }}
- />
- <div class="toggle-switch"></div>
- </label>
+
+ <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">
+
+ <section class="mt-4 rounded-sm px-4 py-6 p-8 ">
+ <h2 id="summary-heading" class="font-medium text-lg">Cashout</h2>
+
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex">
+ <dt class="text-sm text-gray-600">Convertion rate</dt>
+ <dd class="text-sm text-gray-900">{sellRate}</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>Current balance</span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount value={account.balance} />
+ </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>Cashout fee</span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount value={sellFee} />
+ </dd>
+ </div>
+ </dl>
+
+ </section>
+ <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">
+ {/* subject */}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="subject"
+ >
+ {i18n.str`Subject`}
+ </label>
+ <div class="mt-2">
+ <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 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="subject"
+ id="subject"
+ data-error={!!errors?.subject && form.subject !== undefined}
+ value={form.subject ?? ""}
+ onChange={(e) => {
+ form.subject = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.subject}
+ isDirty={form.subject !== undefined}
+ />
+ </div>
+
+ </div>
+
+ {/* 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="mt-2">
+ <InputAmount
+ name="amount"
+ left
+ currency={limit.currency}
+ value={trimmedAmountStr}
+ onChange={(value) => {
+ form.amount = value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.amount}
+ isDirty={form.amount !== undefined}
+ />
+ </div>
+
+ </div>
+
+ {Amounts.isZero(calc.credit) ? undefined : (
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+
+ <div class="justify-between items-center flex">
+ <dt class="text-sm text-gray-600">Total cost</dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount value={calc.debit} negative />
+ </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>Balance after</span>
+ {/* <a href="#" class="ml-2 shrink-0 text-gray-400 bkx">
+ <span class="sr-only">Learn more about how shipping is calculated</span>
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"
+ class="w-5 h-5"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM8.94 6.94a.75.75 0 11-1.061-1.061 3 3 0 112.871 5.026v.345a.75.75 0 01-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 108.94 6.94zM10 15a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"></path></svg>
+ </a> */}
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount value={balanceAfter} />
+ </dd>
+ </div>
+ {Amounts.isZero(sellFee) || Amounts.isZero(calc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>Amount after conversion</span>
+ {/* <a href="#" class="ml-2 shrink-0 text-gray-400 bkx">
+ <span class="sr-only">Learn more about how shipping is calculated</span>
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"
+ class="w-5 h-5"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM8.94 6.94a.75.75 0 11-1.061-1.061 3 3 0 112.871 5.026v.345a.75.75 0 01-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 108.94 6.94zM10 15a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"></path></svg>
+ </a> */}
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount value={calc.beforeFee} />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">Total cashout transfer</dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount value={calc.credit} />
+ </dd>
+ </div>
+ </dl>
+ </div>
+ )}
+
+ {/* channel */}
+ </div>
</div>
- </fieldset>
- <fieldset>
- <label>{i18n.str`Conversion rate`}</label>
- <input value={sellRate} disabled />
- </fieldset>
- <fieldset>
- <label for="balance-now">{i18n.str`Balance now`}</label>
- <InputAmount
- name="banace-now"
- currency={account.balance.currency}
- value={Amounts.stringifyValue(account.balance)}
- />
- </fieldset>
- <fieldset>
- <label for="total-cost"
- style={{ fontWeight: "bold", color: "red" }}
- >{i18n.str`Total cost`}</label>
- <InputAmount
- name="total-cost"
- currency={account.balance.currency}
- value={Amounts.stringifyValue(calc.debit)}
- />
- </fieldset>
- <fieldset>
- <label for="balance-after">{i18n.str`Balance after`}</label>
- <InputAmount
- name="balance-after"
- currency={account.balance.currency}
- value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
- />
- </fieldset>{" "}
- {Amounts.isZero(sellFee) ? undefined : (
- <Fragment>
- <fieldset>
- <label for="amount-conversiojn">{i18n.str`Amount after conversion`}</label>
- <InputAmount
- name="amount-conversion"
- currency={config.fiat_currency.name}
- value={Amounts.stringifyValue(calc.beforeFee)}
- />
- </fieldset>
-
- <fieldset>
- <label form="cashout-fee">{i18n.str`Cashout fee`}</label>
- <InputAmount
- name="cashout-fee"
- currency={config.fiat_currency.name}
- value={Amounts.stringifyValue(sellFee)}
- />
- </fieldset>
- </Fragment>
- )}
- <fieldset>
- <label for="total"
- style={{ fontWeight: "bold", color: "green" }}
- >{i18n.str`Total cashout transfer`}</label>
- <InputAmount
- name="total"
- currency={config.fiat_currency.name}
- value={Amounts.stringifyValue(calc.credit)}
- />
- </fieldset>
- <fieldset>
- <label>{i18n.str`Confirmation channel`}</label>
-
- <div class="channel">
- <input
- class={
- "pure-button content " +
- (form.channel === TanChannel.EMAIL
- ? "pure-button-primary"
- : "pure-button-secondary")
- }
- type="submit"
- value={i18n.str`Email`}
- onClick={async (e) => {
- e.preventDefault();
- form.channel = TanChannel.EMAIL;
- updateForm(structuredClone(form));
- }}
- />
- <input
- class={
- "pure-button content " +
- (form.channel === TanChannel.SMS
- ? "pure-button-primary"
- : "pure-button-secondary")
- }
- type="submit"
- value={i18n.str`SMS`}
- onClick={async (e) => {
- e.preventDefault();
- form.channel = TanChannel.SMS;
- updateForm(structuredClone(form));
+
+ <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>Change</i18n.Translate>
+ </button>
</div>
- <ShowInputErrorLabel
- message={errors?.channel}
- isDirty={form.channel !== undefined}
- />
- </fieldset>
- <br />
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={(e) => {
- e.preventDefault();
- onCancel();
- }}
- >
- {i18n.str`Cancel`}
- </button>
-
- <button
- class="pure-button pure-button-primary btn-register"
- type="submit"
- disabled={!!errors}
- onClick={async (e) => {
- e.preventDefault();
-
- if (errors || !creds) return;
- await handleError(async () => {
- const request_uid = encodeCrock(getRandomBytes(16))
- 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") {
- mutate(() => true)// clean cashout list
- onComplete(resp.body.cashout_id);
- } else {
- switch (resp.case) {
- 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`Need a contact data where to send the TAN`,
- 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 "account-not-found": return notify({
- type: "error",
- title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case "cashout-not-supported": return notify({
- type: "error",
- title: i18n.str`The bank does not support cashout`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case "request-already-used": return notify({
- type: "error",
- title: i18n.str`Duplicated request found, try again.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case "tan-failed": return notify({
- type: "error",
- title: i18n.str`Server couldn't send the confirmation request.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- default: assertUnreachable(resp)
- }
- }
- })
- }}
- >
- {i18n.str`Create`}
- </button>
- </div>
- </form>
+ </form>
+ </div>
+
</div>
);
}