taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit b69c478e8e7b5d209a563629cb53cc3ccb23fd97
parent 1d633f06390c0de2b9cdb7d7a44fa0ae4f42e5fd
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 31 Oct 2025 15:58:39 -0300

backoffice completed

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 283++++++++++++++++++++++++++++++++-----------------------------------------------
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 39+++++++++++++++++----------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 465++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx | 63---------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 77++++++++++++++++++++---------------------------------------------------------
7 files changed, 389 insertions(+), 546 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -4,6 +4,7 @@ import { Challenge, ChallengeResponse, HttpStatusCode, + opEmptySuccess, TalerErrorCode, TanChannel } from "@gnu-taler/taler-util"; @@ -12,6 +13,7 @@ import { LocalNotificationBannerBulma, SafeHandlerTemplate, undefinedIfEmpty, + useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; @@ -22,7 +24,6 @@ import { datetimeFormatForSettings, usePreference, } from "../hooks/preference.js"; -import { Notification } from "../utils/types.js"; import { FormErrors, FormProvider } from "./form/FormProvider.js"; import { Input } from "./form/Input.js"; @@ -83,61 +84,20 @@ function SolveChallenge({ }; setValue(v); } - - async function doVerificationImpl() { - try { - const resp = await lib.instance.confirmChallenge(challenge.challenge_id, { - tan: value.code!, - }); - if (resp.case === "ok") { - return onSolved(); - } - switch (resp.case) { - case HttpStatusCode.Unauthorized: { - setNotif({ - message: i18n.str`Failed to validate the verification code.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case TalerErrorCode.MERCHANT_TAN_CHALLENGE_FAILED: { - setNotif({ - message: i18n.str`Failed to validate the verification code.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN: { - setNotif({ - message: i18n.str`Failed to validate the verification code.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case TalerErrorCode.MERCHANT_TAN_TOO_MANY_ATTEMPTS: { - setNotif({ - message: i18n.str`Failed to validate the verification code.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - default: { - assertUnreachable(resp); - } - } - } catch (error) { - setNotif({ - message: i18n.str`Failed to verify code`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); + const data = !value.code || !!errors ? undefined : { tan: value.code } + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const verify = safeFunctionHandler(lib.instance.confirmChallenge, !data ? undefined : [challenge.challenge_id, data]) + verify.onSuccess = onSolved + verify.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` + case TalerErrorCode.MERCHANT_TAN_CHALLENGE_FAILED: return i18n.str`Sending the code failed` + case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN: return i18n.str`Challenge expired` + case TalerErrorCode.MERCHANT_TAN_TOO_MANY_ATTEMPTS: return i18n.str`Too many attempts` } } + return ( <Fragment> <LocalNotificationBannerBulma notification={notification} /> @@ -220,8 +180,7 @@ function SolveChallenge({ </button> <ButtonBetterBulma type="is-info" - disabled={errors !== undefined} - onClick={doVerificationImpl} + onClick={verify} > <i18n.Translate>Verify</i18n.Translate> </ButtonBetterBulma> @@ -255,6 +214,7 @@ export function SolveMFAChallenges({ ch: Challenge; expiration: AbsoluteTime; }>(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [settings] = usePreference(); if (selected) { @@ -263,20 +223,25 @@ export function SolveMFAChallenges({ onCancel={() => setSelected(undefined)} challenge={selected.ch} expiration={selected.expiration} - onSolved={() => { - setSelected(undefined); - const newSolved = [...solved, selected.ch.challenge_id]; + // onSolved={() => { + // setSelected(undefined); + // const newSolved = [...solved, selected.ch.challenge_id]; - const done = currentChallenge.combi_and - ? newSolved.length === currentChallenge.challenges.length - : newSolved.length > 0; + // const done = currentChallenge.combi_and + // ? newSolved.length === currentChallenge.challenges.length + // : newSolved.length > 0; - if (done) { - onCompleted(newSolved); - } else { - setSolved(newSolved); - } + // if (done) { + // onCompleted(newSolved); + // } else { + // setSolved(newSolved); + // } + // }} + onSolved={() => { + setSelected(undefined); + setSolved([...solved, selected.ch.challenge_id]); }} + /> ); } @@ -285,86 +250,51 @@ export function SolveMFAChallenges({ ? solved.length === currentChallenge.challenges.length : solved.length > 0; - async function doSendCodeImpl(ch: Challenge) { - try { - const resp = await lib.instance.sendChallenge(ch.challenge_id); - if (resp.case === "ok") { - if (resp.body.earliest_retransmission) { - setRetransmission({ - ...retransmission, - [ch.tan_channel]: AbsoluteTime.fromProtocolTimestamp( - resp.body.earliest_retransmission, - ), - }); - } - return setSelected({ - ch, - expiration: !resp.body.solve_expiration - ? AbsoluteTime.never() - : AbsoluteTime.fromProtocolTimestamp(resp.body.solve_expiration), - }); - } - switch (resp.case) { - case HttpStatusCode.Unauthorized: { - setNotif({ - message: i18n.str`Failed to send the verification code.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case HttpStatusCode.Forbidden: { - setNotif({ - message: i18n.str`The request was valid, but the server is refusing action.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN: { - setNotif({ - message: i18n.str`The backend is not aware of the specified MFA challenge.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case TalerErrorCode.MERCHANT_TAN_MFA_HELPER_EXEC_FAILED: { - setNotif({ - message: i18n.str`The backend failed to launch a helper process required for the multi-factor authentication step.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case TalerErrorCode.MERCHANT_TAN_CHALLENGE_SOLVED: { - setNotif({ - message: i18n.str`The challenge was already solved.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case TalerErrorCode.MERCHANT_TAN_TOO_EARLY: { - setNotif({ - message: i18n.str`It is too early to request another transmission of the challenge.`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - default: { - assertUnreachable(resp); - } - } - } catch (error) { - setNotif({ - message: i18n.str`Failed to send the verification code.`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), + const sendMessage = safeFunctionHandler((ch: Challenge) => + lib.instance.sendChallenge(ch.challenge_id), + ); + sendMessage.onSuccess = (success, ch) => { + if (success.earliest_retransmission) { + setRetransmission({ + ...retransmission, + [ch.tan_channel]: AbsoluteTime.fromProtocolTimestamp( + success.earliest_retransmission, + ), }); } - } + setSelected({ + ch, + expiration: !success.solve_expiration + ? AbsoluteTime.never() + : AbsoluteTime.fromProtocolTimestamp(success.solve_expiration), + }); + }; + + sendMessage.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Failed to send the verification code.`; + case HttpStatusCode.Forbidden: + return i18n.str`The request was valid, but the server is refusing action.`; + case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN: + return i18n.str`The backend is not aware of the specified MFA challenge.`; + case TalerErrorCode.MERCHANT_TAN_MFA_HELPER_EXEC_FAILED: + return i18n.str`Code transmission failed.`; + case TalerErrorCode.MERCHANT_TAN_CHALLENGE_SOLVED: + return i18n.str`Already solved.`; + case TalerErrorCode.MERCHANT_TAN_TOO_EARLY: + return i18n.str`It is too early to request another transmission of the challenge.`; + } + }; + const doComplete = onCompleted.withArgs(solved); + + const selectChallenge = safeFunctionHandler(async (ch: Challenge) => { + setSelected({ + ch, + expiration: AbsoluteTime.never(), + }); + return opEmptySuccess(); + }); return ( <Fragment> @@ -397,32 +327,44 @@ export function SolveMFAChallenges({ </i18n.Translate> )} </section> - {currentChallenge.challenges.map((d) => { - const time = retransmission[d.tan_channel]; + {currentChallenge.challenges.map((challenge) => { + const time = retransmission[challenge.tan_channel]; const alreadySent = !AbsoluteTime.isExpired(time); + const noNeedToComplete = + hasSolvedEnough || + solved.indexOf(challenge.challenge_id) !== -1; + + const doSelect = noNeedToComplete + ? selectChallenge + : selectChallenge.withArgs(challenge); + + const doSend = + alreadySent || noNeedToComplete + ? sendMessage + : sendMessage.withArgs(challenge); return ( <section class="modal-card-body" style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} > - {(function (): VNode { - switch (d.tan_channel) { + {(function (ch: TanChannel): VNode { + switch (ch) { case TanChannel.SMS: return ( <i18n.Translate> An SMS to the phone number starting with{" "} - {d.tan_info} + {challenge.tan_info} </i18n.Translate> ); case TanChannel.EMAIL: return ( <i18n.Translate> - An email to the address starting with {d.tan_info} + An email to the address starting with {challenge.tan_info} </i18n.Translate> ); } - })()} + })(challenge.tan_channel)} {alreadySent && time.t_ms !== "never" ? ( <p> @@ -439,27 +381,29 @@ export function SolveMFAChallenges({ display: "flex", }} > - <button - disabled={ - hasSolvedEnough || solved.indexOf(d.challenge_id) !== -1 - } + <ButtonBetterBulma + // disabled={ + // hasSolvedEnough || solved.indexOf(d.challenge_id) !== -1 + // } class="button" - onClick={() => { - setSelected({ - ch: d, - expiration: AbsoluteTime.never(), - }); - }} + // onClick={() => { + // setSelected({ + // ch: d, + // expiration: AbsoluteTime.never(), + // }); + // }} + onClick={doSelect} > <i18n.Translate>I have a code</i18n.Translate> - </button> + </ButtonBetterBulma> <ButtonBetterBulma - disabled={ - hasSolvedEnough || - solved.indexOf(d.challenge_id) !== -1 || - alreadySent - } - onClick={() => doSendCodeImpl(d)} + // disabled={ + // hasSolvedEnough || + // solved.indexOf(d.challenge_id) !== -1 || + // alreadySent + // } + onClick={doSend} + // onClick={() => doSendCodeImpl(d)} > <i18n.Translate>Send me a message</i18n.Translate> </ButtonBetterBulma> @@ -480,8 +424,7 @@ export function SolveMFAChallenges({ </button> <ButtonBetterBulma type="is-info" - disabled={!hasSolvedEnough} - onClick={async () => onCompleted(solved)} + onClick={doComplete} > <i18n.Translate>Complete</i18n.Translate> </ButtonBetterBulma> diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -22,6 +22,7 @@ import { Paytos, PaytoString, + PaytoType, PaytoUri, stringifyPaytoUri, TranslatedString, @@ -300,8 +301,8 @@ export function CompareAccountsModal({ <td> <i18n.Translate>IBAN</i18n.Translate> </td> - <td>{formPayto?.targetPath ?? "--"}</td> - <td>{testPayto.targetPath}</td> + <td>{formPayto?.displayName ?? "--"}</td> + <td>{testPayto.iban}</td> </tr> )} {testPayto.targetType === "bitcoin" && ( @@ -309,8 +310,8 @@ export function CompareAccountsModal({ <td> <i18n.Translate>Address</i18n.Translate> </td> - <td>{formPayto?.targetPath ?? "--"}</td> - <td>{testPayto.targetPath}</td> + <td>{formPayto?.displayName ?? "--"}</td> + <td>{testPayto.address}</td> </tr> )} {testPayto.targetType === "x-taler-bank" && ( @@ -319,17 +320,17 @@ export function CompareAccountsModal({ <td> <i18n.Translate>Host</i18n.Translate> </td> - <td>{getHostFromHostPath(formPayto?.targetPath) ?? "--"}</td> - <td>{getHostFromHostPath(testPayto.targetPath)}</td> + <td>{getHostFromHostPath(formPayto?.fullPath) ?? "--"}</td> + <td>{getHostFromHostPath(testPayto.fullPath)}</td> </tr> <tr> <td> <i18n.Translate>Account ID</i18n.Translate> </td> <td> - {getAccountIdFromHostPath(formPayto?.targetPath) ?? "--"} + {getAccountIdFromHostPath(formPayto?.fullPath) ?? "--"} </td> - <td>{getAccountIdFromHostPath(testPayto.targetPath)}</td> + <td>{getAccountIdFromHostPath(testPayto.fullPath)}</td> </tr> </Fragment> )} @@ -379,16 +380,12 @@ export function ValidBankAccount({ const payto = targets[0]; const subject = payto.params["message"]; - const accountPart = !payto.isKnown ? ( - <Fragment> - <Row name={i18n.str`Account`} value={payto.targetPath} /> - </Fragment> - ) : payto.targetType === "x-taler-bank" ? ( + const accountPart = payto.targetType === PaytoType.TalerBank ? ( <Fragment> <Row name={i18n.str`Bank host`} value={payto.host} /> <Row name={i18n.str`Bank account`} value={payto.account} /> </Fragment> - ) : payto.targetType === "iban" ? ( + ) : payto.targetType === PaytoType.IBAN ? ( <Fragment> {payto.bic !== undefined ? ( <Row name={i18n.str`BIC`} value={payto.bic} /> @@ -404,18 +401,16 @@ export function ValidBankAccount({ const receiverTown = payto.params["receiver-town"] || undefined; - const from = !origin.isKnown - ? origin.targetPath - : origin.targetType === "iban" + const from = origin.targetType === PaytoType.IBAN ? origin.iban - : origin.targetType === "taler-reserve" || - origin.targetType === "taler-reserve-http" + : origin.targetType === PaytoType.TalerReserve || + origin.targetType === PaytoType.TalerReserveHttp ? origin.reservePub - : origin.targetType === "bitcoin" + : origin.targetType === PaytoType.Bitcoin ? `${origin.address.substring(0, 8)}...` - : origin.targetType === "ethereum" + : origin.targetType === PaytoType.Ethereum ? `${origin.address.substring(0, 8)}...` - : origin.account; + : origin.displayName; return ( <ConfirmModal diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -24,10 +24,11 @@ import { AmountString, Amounts, Duration, + HttpStatusCode, TalerMerchantApi, TalerProtocolDuration, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format, isFuture } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -54,7 +55,7 @@ import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; export interface Props { - onCreated: (id:string) => void; + onCreated: (id: string) => void; onBack?: () => void; instanceConfig: InstanceConfig; instanceInventory: (TalerMerchantApi.ProductDetail & WithId)[]; @@ -143,12 +144,12 @@ function isInvalidUrl(url: string) { } export function CreatePage({ - onCreate, + onCreated, onBack, instanceConfig, instanceInventory, }: Props): VNode { - const { config } = useSessionContext(); + const { config, lib, state: session } = useSessionContext(); const instance_default = with_defaults(instanceConfig, config.currency); const [value, valueHandler] = useState(instance_default); const zero = Amounts.zeroOfCurrency(config.currency); @@ -177,25 +178,25 @@ export function CreatePage({ refund_deadline: !value.payments?.refund_deadline ? undefined : value.payments.pay_deadline && + Duration.cmp( + value.payments.refund_deadline, + value.payments.pay_deadline, + ) === -1 + ? i18n.str`The refund deadline cannot be earlier than the payment deadline.` + : value.payments.wire_transfer_deadline && Duration.cmp( + value.payments.wire_transfer_deadline, value.payments.refund_deadline, - value.payments.pay_deadline, ) === -1 - ? i18n.str`The refund deadline cannot be earlier than the payment deadline.` - : value.payments.wire_transfer_deadline && - Duration.cmp( - value.payments.wire_transfer_deadline, - value.payments.refund_deadline, - ) === -1 ? i18n.str`Wire transfer deadline can't be before refund deadline` : undefined, pay_deadline: !value.payments?.pay_deadline ? i18n.str`Required` : value.payments.wire_transfer_deadline && - Duration.cmp( - value.payments.wire_transfer_deadline, - value.payments.pay_deadline, - ) === -1 + Duration.cmp( + value.payments.wire_transfer_deadline, + value.payments.pay_deadline, + ) === -1 ? i18n.str`Wire transfer deadline can't be before pay deadline` : undefined, wire_transfer_deadline: !value.payments?.wire_transfer_deadline @@ -206,9 +207,9 @@ export function CreatePage({ : !value.payments?.refund_deadline ? i18n.str`Must have a refund deadline` : Duration.cmp( - value.payments.refund_deadline, - value.payments.auto_refund_deadline, - ) == -1 + value.payments.refund_deadline, + value.payments.auto_refund_deadline, + ) == -1 ? i18n.str`Auto refund can't be after refund deadline` : undefined, }), @@ -220,77 +221,84 @@ export function CreatePage({ : undefined, fulfillment_message: !!value.shipping?.fulfillment_message && - !!value.shipping?.fulfillment_url + !!value.shipping?.fulfillment_url ? i18n.str`Either fulfillment url or fulfillment message must be specified.` : undefined, fulfillment_url: !!value.shipping?.fulfillment_message && - !!value.shipping?.fulfillment_url + !!value.shipping?.fulfillment_url ? i18n.str`Either fulfillment url or fulfillment message must be specified.` : !!value.shipping?.fulfillment_url && - isInvalidUrl(value.shipping.fulfillment_url) + isInvalidUrl(value.shipping.fulfillment_url) ? i18n.str`is not a valid URL` : undefined, }), }); const hasErrors = errors !== undefined; - const submit = (): void => { - const order = value; - const price = order.pricing?.order_price as AmountString | undefined; - const summary = order.pricing?.summary; - if (!value.payments) return; - if (!value.shipping) return; - if (!price || !summary) return; - - const request: TalerMerchantApi.PostOrderRequest = { - order: { - amount: price, - summary: summary, - products: productList, - extra: undefinedIfEmpty(value.extra), - pay_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.pay_deadline!, - ), + const order = value; + const price = order.pricing?.order_price as AmountString | undefined; + const summary = order.pricing?.summary; + + const request: undefined | TalerMerchantApi.PostOrderRequest = !value.payments || !value.shipping || !price || !summary ? undefined : { + order: { + amount: price, + summary: summary, + products: productList, + extra: undefinedIfEmpty(value.extra), + pay_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.pay_deadline!, ), - wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.wire_transfer_deadline!, - ), + ), + wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.wire_transfer_deadline!, ), - refund_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.refund_deadline!, - ), + ), + refund_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.refund_deadline!, ), - auto_refund: value.payments.auto_refund_deadline - ? Duration.toTalerProtocolDuration( - value.payments.auto_refund_deadline, - ) - : undefined, - max_fee: value.payments.max_fee as AmountString, - delivery_date: value.shipping.delivery_date - ? { t_s: value.shipping.delivery_date.getTime() / 1000 } - : undefined, - delivery_location: value.shipping.delivery_location, - fulfillment_url: value.shipping.fulfillment_url, - fulfillment_message: value.shipping.fulfillment_message, - minimum_age: value.payments.minimum_age, - }, - inventory_products: inventoryList.map((p) => ({ - product_id: p.product.id, - quantity: p.quantity, - })), - create_token: value.payments.createToken, - }; - - onCreate(request); + ), + auto_refund: value.payments.auto_refund_deadline + ? Duration.toTalerProtocolDuration( + value.payments.auto_refund_deadline, + ) + : undefined, + max_fee: value.payments.max_fee as AmountString, + delivery_date: value.shipping.delivery_date + ? { t_s: value.shipping.delivery_date.getTime() / 1000 } + : undefined, + delivery_location: value.shipping.delivery_location, + fulfillment_url: value.shipping.fulfillment_url, + fulfillment_message: value.shipping.fulfillment_message, + minimum_age: value.payments.minimum_age, + }, + inventory_products: inventoryList.map((p) => ({ + product_id: p.product.id, + quantity: p.quantity, + })), + create_token: value.payments.createToken, }; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const create = safeFunctionHandler(lib.instance.createOrder, !session.token || !request ? undefined : [session.token, request]) + create.onSuccess = (resp) => { + onCreated(resp.order_id) + } + create.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` + case HttpStatusCode.NotFound: return i18n.str`Not found` + case HttpStatusCode.Conflict: return i18n.str`Conflict` + case HttpStatusCode.Gone: return i18n.str`Product with ID "${fail.body.product_id}" is out of stock.` + case HttpStatusCode.UnavailableForLegalReasons: return i18n.str`No exchange would accept a payment because of KYC requirements.` + } + } const addProductToTheInventoryList = ( product: TalerMerchantApi.ProductDetail & WithId, quantity: number, @@ -388,6 +396,7 @@ export function CreatePage({ return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="tabs is-toggle is-fullwidth is-small"> <ul> @@ -547,161 +556,161 @@ export function CreatePage({ {(pref.advanceOrderMode || requiresSomeTalerOptions || errors?.payments) && ( - <InputGroup - name="payments" - label={i18n.str`Taler payment options`} - tooltip={i18n.str`Override default Taler payment settings for this order`} - > - {(pref.advanceOrderMode || - noDefault_payDeadline || - errors?.payments?.pay_deadline !== undefined) && ( - <InputDuration - name="payments.pay_deadline" - label={i18n.str`Payment time`} - help={ - <DeadlineHelp duration={value.payments?.pay_deadline} /> - } - withForever - withoutClear - tooltip={i18n.str`Time for the customer to pay before the offer expires. Inventory products will be reserved until this deadline. The time starts after the order is created.`} - side={ - <span> - <button - class="button" - onClick={() => { - const c = { - ...value, - payments: { - ...(value.payments ?? {}), - pay_deadline: - instance_default.payments?.pay_deadline, - }, - }; - valueHandler(c); - }} - > - <i18n.Translate>Default</i18n.Translate> - </button> - </span> - } - /> - )} - {(pref.advanceOrderMode || - errors?.payments?.refund_deadline !== undefined) && ( - <InputDuration - name="payments.refund_deadline" - label={i18n.str`Refund time`} - help={ - <DeadlineHelp - duration={value.payments?.refund_deadline} + <InputGroup + name="payments" + label={i18n.str`Taler payment options`} + tooltip={i18n.str`Override default Taler payment settings for this order`} + > + {(pref.advanceOrderMode || + noDefault_payDeadline || + errors?.payments?.pay_deadline !== undefined) && ( + <InputDuration + name="payments.pay_deadline" + label={i18n.str`Payment time`} + help={ + <DeadlineHelp duration={value.payments?.pay_deadline} /> + } + withForever + withoutClear + tooltip={i18n.str`Time for the customer to pay before the offer expires. Inventory products will be reserved until this deadline. The time starts after the order is created.`} + side={ + <span> + <button + class="button" + onClick={() => { + const c = { + ...value, + payments: { + ...(value.payments ?? {}), + pay_deadline: + instance_default.payments?.pay_deadline, + }, + }; + valueHandler(c); + }} + > + <i18n.Translate>Default</i18n.Translate> + </button> + </span> + } /> - } - withForever - withoutClear - tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} - side={ - <span> - <button - class="button" - onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - refund_deadline: - instance_default.payments?.refund_deadline, - }, - }); - }} - > - <i18n.Translate>Default</i18n.Translate> - </button> - </span> - } - /> - )} - {(pref.advanceOrderMode || - noDefault_wireDeadline || - errors?.payments?.wire_transfer_deadline !== undefined) && ( - <InputDuration - name="payments.wire_transfer_deadline" - label={i18n.str`Wire transfer time`} - help={ - <DeadlineHelp - duration={value.payments?.wire_transfer_deadline} + )} + {(pref.advanceOrderMode || + errors?.payments?.refund_deadline !== undefined) && ( + <InputDuration + name="payments.refund_deadline" + label={i18n.str`Refund time`} + help={ + <DeadlineHelp + duration={value.payments?.refund_deadline} + /> + } + withForever + withoutClear + tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} + side={ + <span> + <button + class="button" + onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + refund_deadline: + instance_default.payments?.refund_deadline, + }, + }); + }} + > + <i18n.Translate>Default</i18n.Translate> + </button> + </span> + } /> - } - withoutClear - withForever - tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} - side={ - <span> - <button - class="button" - onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - wire_transfer_deadline: - instance_default.payments - ?.wire_transfer_deadline, - }, - }); - }} - > - <i18n.Translate>Default</i18n.Translate> - </button> - </span> - } - /> - )} - {(pref.advanceOrderMode || - errors?.payments?.auto_refund_deadline !== undefined) && ( - <InputDuration - name="payments.auto_refund_deadline" - label={i18n.str`Auto-refund time`} - help={ - <DeadlineHelp - duration={value.payments?.auto_refund_deadline} + )} + {(pref.advanceOrderMode || + noDefault_wireDeadline || + errors?.payments?.wire_transfer_deadline !== undefined) && ( + <InputDuration + name="payments.wire_transfer_deadline" + label={i18n.str`Wire transfer time`} + help={ + <DeadlineHelp + duration={value.payments?.wire_transfer_deadline} + /> + } + withoutClear + withForever + tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} + side={ + <span> + <button + class="button" + onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + wire_transfer_deadline: + instance_default.payments + ?.wire_transfer_deadline, + }, + }); + }} + > + <i18n.Translate>Default</i18n.Translate> + </button> + </span> + } /> - } - tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} - withForever - /> - )} - - {(pref.advanceOrderMode || - errors?.payments?.max_fee !== undefined) && ( - <InputCurrency - name="payments.max_fee" - label={i18n.str`Maximum fee`} - tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} - /> - )} - {(pref.advanceOrderMode || - errors?.payments?.createToken !== undefined) && ( - <InputToggle - name="payments.createToken" - label={i18n.str`Create token`} - tooltip={i18n.str`If the order ID is easy to guess, the token will prevent other users from claiming the order.`} - /> - )} - {(pref.advanceOrderMode || - errors?.payments?.minimum_age !== undefined) && ( - <InputNumber - name="payments.minimum_age" - label={i18n.str`Minimum age required`} - tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} - help={ - minAgeByProducts > 0 - ? i18n.str`Minimum age defined by the products is ${minAgeByProducts}` - : i18n.str`No product with age restriction in this order` - } - /> - )} - </InputGroup> - )} + )} + {(pref.advanceOrderMode || + errors?.payments?.auto_refund_deadline !== undefined) && ( + <InputDuration + name="payments.auto_refund_deadline" + label={i18n.str`Auto-refund time`} + help={ + <DeadlineHelp + duration={value.payments?.auto_refund_deadline} + /> + } + tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} + withForever + /> + )} + + {(pref.advanceOrderMode || + errors?.payments?.max_fee !== undefined) && ( + <InputCurrency + name="payments.max_fee" + label={i18n.str`Maximum fee`} + tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} + /> + )} + {(pref.advanceOrderMode || + errors?.payments?.createToken !== undefined) && ( + <InputToggle + name="payments.createToken" + label={i18n.str`Create token`} + tooltip={i18n.str`If the order ID is easy to guess, the token will prevent other users from claiming the order.`} + /> + )} + {(pref.advanceOrderMode || + errors?.payments?.minimum_age !== undefined) && ( + <InputNumber + name="payments.minimum_age" + label={i18n.str`Minimum age required`} + tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} + help={ + minAgeByProducts > 0 + ? i18n.str`Minimum age defined by the products is ${minAgeByProducts}` + : i18n.str`No product with age restriction in this order` + } + /> + )} + </InputGroup> + )} {(pref.advanceOrderMode || errors?.extra !== undefined) && ( <InputGroup @@ -789,13 +798,9 @@ export function CreatePage({ <i18n.Translate>Cancel</i18n.Translate> </button> )} - <button - class="button is-success" - onClick={submit} - disabled={hasErrors} - > + <ButtonBetterBulma class="button is-success" onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </button> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -25,16 +25,11 @@ import { TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { useSessionContext } from "../../../../context/session.js"; import { useInstanceDetails } from "../../../../hooks/instance.js"; import { useInstanceProducts } from "../../../../hooks/product.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CreatePage } from "./CreatePage.js"; @@ -48,12 +43,10 @@ interface Props { onConfirm: (id: string) => void; } export default function OrderCreate({ onConfirm, onBack }: Props): VNode { - const { state, lib } = useSessionContext(); const detailsResult = useInstanceDetails(); // FIXME: if the product list is big the will bring a lot of info const inventoryResult = useInstanceProducts(); - const { i18n } = useTranslationContext(); if (!detailsResult) return <Loading />; if (detailsResult instanceof TalerError) { @@ -96,62 +89,6 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { <CreatePage onBack={onBack} onCreated={onConfirm} - // onCreate={(request: TalerMerchantApi.PostOrderRequest) => { - // lib.instance - // .createOrder(state.token, request) - // .then((resp) => { - // if (resp.type === "ok") { - // return onConfirm(resp.body.order_id); - // } else { - // switch (resp.case) { - // case HttpStatusCode.UnavailableForLegalReasons: { - // setNotif({ - // message: i18n.str`Could not create order`, - // type: "ERROR", - // description: i18n.str`No exchange would accept a payment because of KYC requirements.`, - // }); - // return; - // } - // case HttpStatusCode.Unauthorized: - // case HttpStatusCode.Conflict: { - // setNotif({ - // message: i18n.str`Could not create order`, - // type: "ERROR", - // description: resp.detail?.hint, - // }); - // return; - // } - // case HttpStatusCode.Gone: { - // setNotif({ - // message: i18n.str`Could not create order`, - // type: "ERROR", - // description: i18n.str`Product with ID "${resp.body.product_id}" is out of stock.`, - // }); - // return; - // } - // case HttpStatusCode.NotFound: { - // setNotif({ - // message: i18n.str`Could not create order`, - // type: "ERROR", - // description: resp.detail?.hint, - // }); - // return; - // } - // default: { - // assertUnreachable(resp); - // } - // } - // } - // }) - // .catch((error) => { - // setNotif({ - // message: i18n.str`Could not create order`, - // type: "ERROR", - // description: - // error instanceof Error ? error.message : String(error), - // }); - // }); - // }} instanceConfig={detailsResult.body} instanceInventory={inventoryResult.body} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -775,7 +775,7 @@ export function DetailPage({ id, selected, onRefunded, onBack }: Props): VNode { order={selected} id={id} onCancel={() => setShowRefund(undefined)} - onConfirm={() => { + onConfirmed={() => { onRefunded(); setShowRefund(undefined); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -274,7 +274,7 @@ function EmptyTable(): VNode { interface RefundModalProps { onCancel: () => void; - onConfirm: () => void; + onConfirmed: () => void; order: TalerMerchantApi.MerchantOrderStatusResponse; id: string; } @@ -283,7 +283,7 @@ export function RefundModal({ order, id, onCancel, - onConfirm, + onConfirmed, }: RefundModalProps): VNode { type State = { mainReason?: string; description?: string; refund?: string }; const [form, setValue] = useState<State>({}); @@ -364,7 +364,7 @@ export function RefundModal({ lib.instance.addRefund(token, id, request), !session.token || !req ? undefined : [session.token, id, req], ); - refund.onSuccess = onConfirm; + refund.onSuccess = onConfirmed; refund.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -100,24 +100,33 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { const isWiredActive = filter.wired === true ? "is-active" : ""; const isAllActive = filter.paid === undefined && - filter.refunded === undefined && - filter.wired === undefined + filter.refunded === undefined && + filter.wired === undefined ? "is-active" : ""; - const create = safeFunctionHandler( - lib.instance.informWireTransfer, - !session.token || !!errors ? undefined : [session.token, data], + const data = {} as TalerMerchantApi.RefundRequest + const refund = safeFunctionHandler( + lib.instance.addRefund, + !session.token || !orderToBeRefunded ? undefined : [session.token, orderToBeRefunded.order_id, data], ); - create.onSuccess = onCreated; - create.onFail = (fail) => { + refund.onSuccess = () => { + setOrderToBeRefunded(undefined) + }; + refund.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; + case HttpStatusCode.Forbidden: + return i18n.str`Forbidden.`; case HttpStatusCode.NotFound: return i18n.str`Not found.`; case HttpStatusCode.Conflict: return i18n.str`Conflict.`; + case HttpStatusCode.Gone: + return i18n.str`Gone.`; + case HttpStatusCode.UnavailableForLegalReasons: + return i18n.str`UnavailableForLegalReasons.`; } }; return ( @@ -157,53 +166,7 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { <RefundModalForTable id={orderToBeRefunded.order_id} onCancel={() => setOrderToBeRefunded(undefined)} - onConfirm={(value) => { - lib.instance - .addRefund(state.token, orderToBeRefunded.order_id, value) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Refund created successfully`, - type: "SUCCESS", - }); - } else { - switch (resp.case) { - case HttpStatusCode.UnavailableForLegalReasons: { - setNotif({ - message: i18n.str`Could not create the refund`, - type: "ERROR", - description: i18n.str`There are pending KYC requirements.`, - }); - return; - } - case HttpStatusCode.Unauthorized: - case HttpStatusCode.Forbidden: - case HttpStatusCode.NotFound: - case HttpStatusCode.Conflict: - case HttpStatusCode.Gone: { - setNotif({ - message: i18n.str`Could not create the refund`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - default: { - assertUnreachable(resp); - } - } - } - }) - .catch((error) => - setNotif({ - message: i18n.str`Could not create the refund`, - type: "ERROR", - description: - error instanceof Error ? error.message : String(error), - }), - ) - .then(() => setOrderToBeRefunded(undefined)); - }} + onConfirmed={() => setOrderToBeRefunded(undefined)} /> )} </section> @@ -213,10 +176,10 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { interface RefundProps { id: string; onCancel: () => void; - onConfirm: () => void; + onConfirmed: () => void; } -function RefundModalForTable({ id, onConfirm, onCancel }: RefundProps): VNode { +function RefundModalForTable({ id, onConfirmed, onCancel }: RefundProps): VNode { const { i18n } = useTranslationContext(); const result = useOrderDetails(id); @@ -246,7 +209,7 @@ function RefundModalForTable({ id, onConfirm, onCancel }: RefundProps): VNode { id={id} order={result.body} onCancel={onCancel} - onConfirm={onConfirm} + onConfirmed={onConfirmed} /> ); }