diff options
Diffstat (limited to 'packages/bank-ui/src/pages/WalletWithdrawForm.tsx')
-rw-r--r-- | packages/bank-ui/src/pages/WalletWithdrawForm.tsx | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx new file mode 100644 index 000000000..a9c652643 --- /dev/null +++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx @@ -0,0 +1,404 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + AbsoluteTime, + AmountJson, + Amounts, + HttpStatusCode, + TranslatedString, + assertUnreachable, + parseWithdrawUri, +} from "@gnu-taler/taler-util"; +import { + Attention, + LocalNotificationBanner, + notifyError, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { forwardRef } from "preact/compat"; +import { useState } from "preact/hooks"; +import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; +import { useSessionState } from "../hooks/session.js"; +import { useBankState } from "../hooks/bank-state.js"; +import { usePreferences } from "../hooks/preferences.js"; +import { RouteDefinition } from "@gnu-taler/web-util/browser"; +import { undefinedIfEmpty } from "../utils.js"; +import { OperationState } from "./OperationState/index.js"; +import { + InputAmount, + RenderAmount, + doAutoFocus, +} from "./PaytoWireTransferForm.js"; + +const RefAmount = forwardRef(InputAmount); + +function OldWithdrawalForm({ + onOperationCreated, + limit, + balance, + routeCancel, + focus, + routeOperationDetails, +}: { + limit: AmountJson; + balance: AmountJson; + focus?: boolean; + routeOperationDetails: RouteDefinition<{ wopid: string }>; + onOperationCreated: (wopid: string) => void; + routeCancel: RouteDefinition; +}): VNode { + const { i18n } = useTranslationContext(); + const [settings] = usePreferences(); + + // const walletInegrationApi = useTalerWalletIntegrationAPI() + // const { navigateTo } = useNavigationContext(); + + const [bankState, updateBankState] = useBankState(); + const { + lib: { bank: api }, + config, + } = useBankCoreApiContext(); + + const { state: credentials } = useSessionState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials; + + const [amountStr, setAmountStr] = useState<string | undefined>( + `${settings.maxWithdrawalAmount}`, + ); + const [notification, notify, handleError] = useLocalNotification(); + + if (bankState.currentWithdrawalOperationId) { + // FIXME: doing the preventDefault is not optimal + + // const suri = stringifyWithdrawUri({ + // bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl, + // withdrawalOperationId: bankState.currentWithdrawalOperationId, + // }); + // const uri = parseWithdrawUri(suri)! + const url = routeOperationDetails.url({ + wopid: bankState.currentWithdrawalOperationId, + }); + return ( + <Attention + type="warning" + title={i18n.str`There is an operation already`} + onClose={() => { + updateBankState("currentWithdrawalOperationId", undefined); + }} + > + <span ref={focus ? doAutoFocus : undefined} /> + <i18n.Translate>Complete the operation in</i18n.Translate>{" "} + <a + class="font-semibold text-yellow-700 hover:text-yellow-600" + name="complete operation" + href={url} + // onClick={(e) => { + // e.preventDefault() + // walletInegrationApi.publishTalerAction(uri, () => { + // navigateTo(url) + // }) + // }} + > + <i18n.Translate>this page</i18n.Translate> + </a> + </Attention> + ); + } + + const trimmedAmountStr = amountStr?.trim(); + + const parsedAmount = trimmedAmountStr + ? Amounts.parse(`${limit.currency}:${trimmedAmountStr}`) + : undefined; + + const errors = undefinedIfEmpty({ + amount: + trimmedAmountStr == null + ? i18n.str`Required` + : !parsedAmount + ? i18n.str`Invalid` + : Amounts.cmp(limit, parsedAmount) === -1 + ? i18n.str`Balance is not enough` + : undefined, + }); + + async function doStart() { + if (!parsedAmount || !creds) return; + await handleError(async () => { + const resp = await api.createWithdrawal(creds, { + amount: Amounts.stringify(parsedAmount), + }); + if (resp.type === "ok") { + const uri = parseWithdrawUri(resp.body.taler_withdraw_uri); + if (!uri) { + return notifyError( + i18n.str`Server responded with an invalid withdraw URI`, + i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`, + ); + } else { + updateBankState( + "currentWithdrawalOperationId", + uri.withdrawalOperationId, + ); + onOperationCreated(uri.withdrawalOperationId); + } + } else { + switch (resp.case) { + case HttpStatusCode.Conflict: { + notify({ + type: "error", + title: i18n.str`The operation was rejected due to insufficient funds`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + break; + } + case HttpStatusCode.Unauthorized: { + notify({ + type: "error", + title: i18n.str`The operation was rejected due to insufficient funds`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + break; + } + case HttpStatusCode.NotFound: { + notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + break; + } + default: + assertUnreachable(resp); + } + } + }); + } + + return ( + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mt-4" + autoCapitalize="none" + autoCorrect="off" + onSubmit={(e) => { + e.preventDefault(); + }} + > + <LocalNotificationBanner notification={notification} /> + + <div class="px-4 py-6 "> + <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label for="withdraw-amount">{i18n.str`Amount`}</label> + <RefAmount + currency={limit.currency} + value={amountStr} + name="withdraw-amount" + onChange={(v) => { + setAmountStr(v); + }} + error={errors?.amount} + ref={focus ? doAutoFocus : undefined} + /> + </div> + </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Current balance is{" "} + <RenderAmount + value={balance} + spec={config.currency_specification} + /> + </i18n.Translate> + </p> + {Amounts.cmp(limit, balance) > 0 ? ( + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Your account allows you to withdraw{" "} + <RenderAmount + value={limit} + spec={config.currency_specification} + /> + </i18n.Translate> + </p> + ) : undefined} + <div class="mt-4"> + <div class="sm:inline"> + <button + type="button" + name="set 50" + class=" inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("50.00"); + }} + > + 50.00 + </button> + <button + type="button" + name="set 25" + class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-r-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("25.00"); + }} + > + 25.00 + </button> + </div> + <div class="mt-4 sm:inline"> + <button + type="button" + name="set 10" + class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-l-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("10.00"); + }} + > + 10.00 + </button> + <button + type="button" + name="set 5" + class=" inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("5.00"); + }} + > + 5.00 + </button> + </div> + </div> + </div> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <a + href={routeCancel.url({})} + name="cancel" + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </a> + <button + type="submit" + name="continue" + 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={isRawPayto ? !!errorsPayto : !!errorsWire} + onClick={(e) => { + e.preventDefault(); + doStart(); + }} + > + <i18n.Translate>Continue</i18n.Translate> + </button> + </div> + </form> + ); +} + +export function WalletWithdrawForm({ + focus, + limit, + balance, + routeCancel, + onAuthorizationRequired, + onOperationCreated, + onOperationAborted, + routeOperationDetails, +}: { + limit: AmountJson; + balance: AmountJson; + focus?: boolean; + routeOperationDetails: RouteDefinition<{ wopid: string }>; + onAuthorizationRequired: () => void; + onOperationCreated: (wopid: string) => void; + onOperationAborted: () => void; + routeCancel: RouteDefinition; +}): VNode { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = usePreferences(); + + return ( + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <i18n.Translate>Prepare your Taler wallet</i18n.Translate> + </h2> + <p class="mt-1 text-sm text-gray-500"> + <i18n.Translate> + After using your wallet you will need to confirm or cancel the + operation on this site. + </i18n.Translate> + </p> + </div> + + <div class="col-span-2"> + {settings.showInstallWallet && ( + <Attention + title={i18n.str`You need a Taler wallet`} + onClose={() => { + updateSettings("showInstallWallet", false); + }} + > + <i18n.Translate> + If you don't have one yet you can follow the instruction in + </i18n.Translate>{" "} + <a + target="_blank" + name="wallet page" + rel="noreferrer noopener" + class="font-semibold text-blue-700 hover:text-blue-600" + href="https://taler.net/en/wallet.html" + > + <i18n.Translate>this page</i18n.Translate> + </a> + </Attention> + )} + + {!settings.fastWithdrawal ? ( + <OldWithdrawalForm + focus={focus} + routeOperationDetails={routeOperationDetails} + limit={limit} + balance={balance} + routeCancel={routeCancel} + onOperationCreated={onOperationCreated} + /> + ) : ( + <OperationState + currency={limit.currency} + onAuthorizationRequired={onAuthorizationRequired} + routeClose={routeCancel} + routeHere={routeOperationDetails} + onAbort={onOperationAborted} + // route={routeCancel} + /> + )} + </div> + </div> + ); +} |