diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/business')
-rw-r--r-- | packages/demobank-ui/src/pages/business/CreateCashout.tsx | 174 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx | 12 |
2 files changed, 160 insertions, 26 deletions
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") { |