commit b69c478e8e7b5d209a563629cb53cc3ccb23fd97
parent 1d633f06390c0de2b9cdb7d7a44fa0ae4f42e5fd
Author: Sebastian <sebasjm@gmail.com>
Date: Fri, 31 Oct 2025 15:58:39 -0300
backoffice completed
Diffstat:
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}
/>
);
}