diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths')
23 files changed, 804 insertions, 582 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx index ec54dc150..39fdb6bdc 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx @@ -20,8 +20,8 @@ */ import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; import { CreatePage as TestedComponent } from "./CreatePage.js"; +import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser"; export default { title: "Pages/Instance/Create", @@ -37,19 +37,32 @@ function createExample<Props>( props: Partial<Props>, ) { const r = (args: any) => ( - <ConfigContextProvider + <MerchantApiProviderTesting value={{ - currency: "ARS", - version: "1", - currencies: { - currency: "TESTKUDOS" + cancelRequest: () => {}, + config: { + currency: "ARS", + version: "1", + currencies: { + "ASD": { + name: "testkudos", + alt_unit_names: {}, + num_fractional_input_digits: 1, + num_fractional_normal_digits: 1, + num_fractional_trailing_zero_digits: 1, + } + }, + exchanges: [], + name: "taler-merchant" }, - exchanges: [], - name: "taler-merchant" + hints: [], + lib: {} as any, + onActivity: (() => {}) as any, + url: new URL("asdasd"), }} > <Component {...args} /> - </ConfigContextProvider> + </MerchantApiProviderTesting> ); r.args = props; return r; diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx index cbda65bfe..440cd2b07 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx @@ -17,16 +17,18 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { + useMerchantApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../components/menu/index.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; -import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { useAdminAPI } from "../../../hooks/instance.js"; +import { useSessionContext } from "../../../context/session.js"; import { Notification } from "../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -import { useCredentialsChecker } from "../../../hooks/backend.js"; -import { useBackendContext } from "../../../context/backend.js"; interface Props { onBack?: () => void; @@ -39,8 +41,8 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { const { createInstance } = useAdminAPI(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const { requestNewLoginToken } = useCredentialsChecker() - const { url: backendURL, updateToken } = useBackendContext() + const { lib } = useMerchantApiContext(); + const { state, logIn } = useSessionContext(); return ( <Fragment> @@ -53,15 +55,29 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { d: MerchantBackend.Instances.InstanceConfigurationMessage, ) => { try { - await createInstance(d) + await createInstance(d); if (d.auth.token) { - const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) + const result = await lib.authenticate.createAccessToken( + d.auth.token, + { + scope: "write", + duration: { + d_us: "forever", + }, + refreshable: true, + }, + ); + if (result.type === "ok") { + const { access_token } = result.body; + logIn({ token: access_token }); } + // const resp = await requestNewLoginToken(backendURL.href, d.auth.token as AccessToken) + // if (resp.valid) { + // const { token, expiration } = resp + // updateToken({ token, expiration }); + // } else { + // updateToken(undefined) + // } } onConfirm(); } catch (ex) { @@ -72,7 +88,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { description: ex.message, }); } else { - console.error(ex) + console.error(ex); } } }} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx index 9a947c9d5..8166dc739 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx @@ -19,9 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; +import { FunctionalComponent, h } from "preact"; import { CreatePage as TestedComponent } from "./CreatePage.js"; +import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser"; export default { title: "Pages/Instance/Create", @@ -37,19 +37,32 @@ function createExample<Props>( props: Partial<Props>, ) { const component = (args: any) => ( - <ConfigContextProvider + <MerchantApiProviderTesting value={{ - currency: "TESTKUDOS", - version: "1", - currencies: { - currency: "TESTKUDOS" + cancelRequest: () => {}, + config: { + currency: "ARS", + version: "1", + currencies: { + "ASD": { + name: "testkudos", + alt_unit_names: {}, + num_fractional_input_digits: 1, + num_fractional_normal_digits: 1, + num_fractional_trailing_zero_digits: 1, + } + }, + exchanges: [], + name: "taler-merchant" }, - exchanges: [], - name: "taler-merchant" + hints: [], + lib: {} as any, + onActivity: (() => {}) as any, + url: new URL("asdasd"), }} > <Internal {...(props as any)} /> - </ConfigContextProvider> + </MerchantApiProviderTesting> ); return { component, props }; } diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx index 711a5a4f0..bc18bb352 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx @@ -23,7 +23,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { StateUpdater, useEffect, useState } from "preact/hooks"; import { MerchantBackend } from "../../../declaration.js"; -import { useSessionState } from "../../../hooks/session.js"; +import { useSessionContext } from "../../../context/session.js"; interface Props { instances: MerchantBackend.Instances.Instance[]; @@ -149,7 +149,7 @@ function Table({ onPurge, }: TableProps): VNode { const { i18n } = useTranslationContext(); - const { impersonate } = useSessionState() + const { impersonate } = useSessionContext() return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx index 13dd3a2f6..2a37ee588 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx @@ -18,11 +18,11 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Loading } from "../../../components/exception/loading.js"; import { DeleteModal } from "../../../components/modal/index.js"; -import { useInstanceContext } from "../../../context/instance.js"; import { MerchantBackend } from "../../../declaration.js"; import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; import { DetailPage } from "./DetailPage.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { useSessionContext } from "../../../context/session.js"; interface Props { onUnauthorized: () => VNode; @@ -39,7 +39,7 @@ export default function Detail({ onDelete, onNotFound, }: Props): VNode { - const { id } = useInstanceContext(); + const { state } = useSessionContext(); const result = useInstanceDetails(); const [deleting, setDeleting] = useState<boolean>(false); @@ -69,7 +69,7 @@ export default function Detail({ /> {deleting && ( <DeleteModal - element={{ name: result.data.name, id }} + element={{ name: result.data.name, id: state.instance }} onCancel={() => setDeleting(false)} onConfirm={async (): Promise<void> => { try { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx index aabe67e00..94e19bb6e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx @@ -19,8 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; +import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser"; +import { FunctionalComponent, h } from "preact"; import { DetailPage as TestedComponent } from "./DetailPage.js"; export default { @@ -37,19 +37,32 @@ function createExample<Props>( props: Partial<Props>, ) { const component = (args: any) => ( - <ConfigContextProvider + <MerchantApiProviderTesting value={{ - currency: "TESTKUDOS", - version: "1", - currencies: { - currency: "TESTKUDOS" + cancelRequest: () => {}, + config: { + currency: "ARS", + version: "1", + currencies: { + "ASD": { + name: "testkudos", + alt_unit_names: {}, + num_fractional_input_digits: 1, + num_fractional_normal_digits: 1, + num_fractional_trailing_zero_digits: 1, + } + }, + exchanges: [], + name: "taler-merchant" }, - exchanges: [], - name: "taler-merchant" + hints: [], + lib: {} as any, + onActivity: (() => {}) as any, + url: new URL("asdasd"), }} > <Internal {...(props as any)} /> - </ConfigContextProvider> + </MerchantApiProviderTesting> ); return { component, props }; } 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 index 5633d93ab..fca123773 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -19,10 +19,18 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AbsoluteTime, + Amounts, + Duration, + TalerProtocolDuration, +} from "@gnu-taler/taler-util"; +import { + useMerchantApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { format, isFuture } from "date-fns"; -import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { FormErrors, @@ -39,10 +47,8 @@ import { InputToggle } from "../../../../components/form/InputToggle.js"; import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js"; import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js"; import { ProductList } from "../../../../components/product/ProductList.js"; -import { useConfigContext } from "../../../../context/config.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; import { usePreference } from "../../../../hooks/preference.js"; -import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; @@ -58,9 +64,16 @@ interface InstanceConfig { default_wire_transfer_delay: TalerProtocolDuration; } -function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> { - const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay); - const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay); +function with_defaults( + config: InstanceConfig, + _currency: string, +): Partial<Entity> { + const defaultPayDeadline = Duration.fromTalerProtocolDuration( + config.default_pay_delay, + ); + const defaultWireDeadline = Duration.fromTalerProtocolDuration( + config.default_wire_transfer_delay, + ); return { inventoryProducts: {}, @@ -69,9 +82,9 @@ function with_defaults(config: InstanceConfig, currency: string): Partial<Entity payments: { max_fee: undefined, createToken: true, - pay_deadline: (defaultPayDeadline), - refund_deadline: (defaultPayDeadline), - wire_transfer_deadline: (defaultWireDeadline), + pay_deadline: defaultPayDeadline, + refund_deadline: defaultPayDeadline, + wire_transfer_deadline: defaultWireDeadline, }, shipping: {}, extra: {}, @@ -114,26 +127,17 @@ interface Entity { extra: Record<string, string>; } -const stringIsValidJSON = (value: string) => { - try { - JSON.parse(value.trim()); - return true; - } catch { - return false; - } -}; - export function CreatePage({ onCreate, onBack, instanceConfig, instanceInventory, }: Props): VNode { - const config = useConfigContext(); - const instance_default = with_defaults(instanceConfig, config.currency) + const { config } = useMerchantApiContext(); + const instance_default = with_defaults(instanceConfig, config.currency); const [value, valueHandler] = useState(instance_default); const zero = Amounts.zeroOfCurrency(config.currency); - const [settings, updateSettings] = usePreference() + const [settings, updateSettings] = usePreference(); const inventoryList = Object.values(value.inventoryProducts || {}); const productList = Object.values(value.products || {}); @@ -158,22 +162,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`refund deadline cannot be before pay deadline` - : value.payments.wire_transfer_deadline && Duration.cmp( - value.payments.wire_transfer_deadline, value.payments.refund_deadline, + value.payments.pay_deadline, ) === -1 + ? i18n.str`refund deadline cannot be before pay deadline` + : value.payments.wire_transfer_deadline && + Duration.cmp( + value.payments.wire_transfer_deadline, + value.payments.refund_deadline, + ) === -1 ? i18n.str`wire transfer deadline cannot 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 cannot be before pay deadline` : undefined, wire_transfer_deadline: !value.payments?.wire_transfer_deadline @@ -184,12 +191,11 @@ export function CreatePage({ : !value.payments?.refund_deadline ? i18n.str`should 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 cannot be after refund deadline` : undefined, - }), shipping: undefinedIfEmpty({ delivery_date: !value.shipping?.delivery_date @@ -214,18 +220,34 @@ export function CreatePage({ summary: order.pricing.summary, products: productList, extra: undefinedIfEmpty(value.extra), - pay_deadline: !value.payments.pay_deadline ? - i18n.str`required` : - AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline)) - ,// : undefined, + pay_deadline: !value.payments.pay_deadline + ? i18n.str`required` + : AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.pay_deadline, + ), + ), // : undefined, wire_transfer_deadline: value.payments.wire_transfer_deadline - ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline)) + ? AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.wire_transfer_deadline, + ), + ) : undefined, refund_deadline: value.payments.refund_deadline - ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline)) + ? AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.refund_deadline, + ), + ) : undefined, auto_refund: value.payments.auto_refund_deadline - ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline) + ? Duration.toTalerProtocolDuration( + value.payments.auto_refund_deadline, + ) : undefined, max_fee: value.payments.max_fee as string, @@ -301,7 +323,7 @@ export function CreatePage({ const totalAsString = Amounts.stringify(totalPrice.amount); const allProducts = productList.concat(inventoryList.map(asProduct)); - const [newField, setNewField] = useState("") + const [newField, setNewField] = useState(""); useEffect(() => { valueHandler((v) => { @@ -328,37 +350,43 @@ export function CreatePage({ ); // if there is no default pay deadline - const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline + const noDefault_payDeadline = + !instance_default.payments || !instance_default.payments.pay_deadline; // and there is no default wire deadline - const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline + const noDefault_wireDeadline = + !instance_default.payments || + !instance_default.payments.wire_transfer_deadline; // user required to set the taler options - const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline - + const requiresSomeTalerOptions = + noDefault_payDeadline || noDefault_wireDeadline; return ( <div> - <section class="section is-main-section"> <div class="tabs is-toggle is-fullwidth is-small"> <ul> - <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: false - }) - }}> - <a > - <span><i18n.Translate>Simple</i18n.Translate></span> + <li + class={!settings.advanceOrderMode ? "is-active" : ""} + onClick={() => { + updateSettings("advanceOrderMode", false); + }} + > + <a> + <span> + <i18n.Translate>Simple</i18n.Translate> + </span> </a> </li> - <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: true - }) - }}> - <a > - <span><i18n.Translate>Advanced</i18n.Translate></span> + <li + class={settings.advanceOrderMode ? "is-active" : ""} + onClick={() => { + updateSettings("advanceOrderMode", true); + }} + > + <a> + <span> + <i18n.Translate>Advanced</i18n.Translate> + </span> </a> </li> </ul> @@ -386,7 +414,7 @@ export function CreatePage({ inventory={instanceInventory} /> - {settings.advanceOrderMode && + {settings.advanceOrderMode && ( <NonInventoryProductFrom productToEdit={editingProduct} onAddProduct={(p) => { @@ -394,7 +422,7 @@ export function CreatePage({ return addNewProduct(p); }} /> - } + )} {allProducts.length > 0 && ( <ProductList @@ -437,8 +465,8 @@ export function CreatePage({ discountOrRise > 0 && (discountOrRise < 1 ? `discount of %${Math.round( - (1 - discountOrRise) * 100, - )}` + (1 - discountOrRise) * 100, + )}` : `rise of %${Math.round((discountOrRise - 1) * 100)}`) } tooltip={i18n.str`Amount to be paid by the customer`} @@ -459,7 +487,7 @@ export function CreatePage({ tooltip={i18n.str`Title of the order to be shown to the customer`} /> - {settings.advanceOrderMode && + {settings.advanceOrderMode && ( <InputGroup name="shipping" label={i18n.str`Shipping and Fulfillment`} @@ -485,146 +513,201 @@ export function CreatePage({ tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} /> </InputGroup> - } + )} - {(settings.advanceOrderMode || requiresSomeTalerOptions) && + {(settings.advanceOrderMode || requiresSomeTalerOptions) && ( <InputGroup name="payments" label={i18n.str`Taler payment options`} tooltip={i18n.str`Override default Taler payment settings for this order`} > - {(settings.advanceOrderMode || noDefault_payDeadline) && <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 for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run 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> - } - />} - {settings.advanceOrderMode && <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> - } - />} - {(settings.advanceOrderMode || noDefault_wireDeadline) && <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> - } - />} - {settings.advanceOrderMode && <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 - />} - - {settings.advanceOrderMode && <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.`} - />} - {settings.advanceOrderMode && <InputToggle - name="payments.createToken" - label={i18n.str`Create token`} - tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} - />} - {settings.advanceOrderMode && <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`Min age defined by the producs is ${minAgeByProducts}` - : i18n.str`No product with age restriction in this order` - } - />} + {(settings.advanceOrderMode || noDefault_payDeadline) && ( + <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 for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run 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> + } + /> + )} + {settings.advanceOrderMode && ( + <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> + } + /> + )} + {(settings.advanceOrderMode || noDefault_wireDeadline) && ( + <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> + } + /> + )} + {settings.advanceOrderMode && ( + <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 + /> + )} + + {settings.advanceOrderMode && ( + <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.`} + /> + )} + {settings.advanceOrderMode && ( + <InputToggle + name="payments.createToken" + label={i18n.str`Create token`} + tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} + /> + )} + {settings.advanceOrderMode && ( + <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`Min age defined by the producs is ${minAgeByProducts}` + : i18n.str`No product with age restriction in this order` + } + /> + )} </InputGroup> - } + )} - {settings.advanceOrderMode && + {settings.advanceOrderMode && ( <InputGroup name="extra" label={i18n.str`Additional information`} tooltip={i18n.str`Custom information to be included in the contract for this order.`} > - {Object.keys(value.extra ?? {}).map((key) => { - - return <Input - name={`extra.${key}`} - inputType="multiline" - label={key} - tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} - side={ - <button class="button" onClick={(e) => { - if (value.extra && value.extra[key] !== undefined) { - console.log(value.extra) - delete value.extra[key] - } - valueHandler({ - ...value, - }) - }}>remove</button> - } - /> + {Object.keys(value.extra ?? {}).map((key, idx) => { + return ( + <Input + name={`extra.${key}`} + key={String(idx)} + inputType="multiline" + label={key} + tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} + side={ + <button + class="button" + onClick={(e) => { + if ( + value.extra && + value.extra[key] !== undefined + ) { + console.log(value.extra); + delete value.extra[key]; + } + valueHandler({ + ...value, + }); + e.preventDefault(); + }} + > + remove + </button> + } + /> + ); })} <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> <i18n.Translate>Custom field name</i18n.Translate> - <span class="icon has-tooltip-right" data-tooltip={"new extra field"}> + <span + class="icon has-tooltip-right" + data-tooltip={"new extra field"} + > <i class="mdi mdi-information" /> </span> </label> @@ -632,23 +715,33 @@ export function CreatePage({ <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} /> + <input + class="input " + value={newField} + onChange={(e) => setNewField(e.currentTarget.value)} + /> </p> </div> </div> - <button class="button" onClick={(e) => { - setNewField("") - valueHandler({ - ...value, - extra: { - ...(value.extra ?? {}), - [newField]: "" - } - }) - }}>add</button> + <button + class="button" + onClick={(e) => { + setNewField(""); + valueHandler({ + ...value, + extra: { + ...(value.extra ?? {}), + [newField]: "", + }, + }); + e.preventDefault(); + }} + > + add + </button> </div> </InputGroup> - } + )} </FormProvider> <div class="buttons is-right mt-5"> @@ -686,20 +779,24 @@ function asProduct(p: ProductAndQuantity): MerchantBackend.Product { }; } - function DeadlineHelp({ duration }: { duration?: Duration }): VNode { const { i18n } = useTranslationContext(); - const [now, setNow] = useState(AbsoluteTime.now()) + const [now, setNow] = useState(AbsoluteTime.now()); useEffect(() => { const iid = setInterval(() => { - setNow(AbsoluteTime.now()) - }, 60 * 1000) + setNow(AbsoluteTime.now()); + }, 60 * 1000); return () => { - clearInterval(iid) - } - }) - if (!duration) return <i18n.Translate>Disabled</i18n.Translate> - const when = AbsoluteTime.addDuration(now, duration) - if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate> - return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate> + clearInterval(iid); + }; + }); + if (!duration) return <i18n.Translate>Disabled</i18n.Translate>; + const when = AbsoluteTime.addDuration(now, duration); + if (when.t_ms === "never") + return <i18n.Translate>No deadline</i18n.Translate>; + return ( + <i18n.Translate> + Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")} + </i18n.Translate> + ); } 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 index 1efaaf6e0..69e9df52e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -20,7 +20,7 @@ */ import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util"; -import { useMerchantApiContext, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format, formatDistance } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -33,6 +33,7 @@ import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { TextField } from "../../../../components/form/TextField.js"; import { ProductList } from "../../../../components/product/ProductList.js"; +import { useSessionContext } from "../../../../context/session.js"; import { MerchantBackend } from "../../../../declaration.js"; import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js"; import { mergeRefunds } from "../../../../utils/amount.js"; @@ -415,10 +416,12 @@ function PaidPage({ }) const [value, valueHandler] = useState<Partial<Paid>>(order); - const { url: backendURL } = useMerchantApiContext(); + const { + state: { backendUrl }, + } = useSessionContext(); const refundurl = stringifyRefundUri({ - merchantBaseUrl: backendURL.href, + merchantBaseUrl: backendUrl, orderId: order.contract_terms.order_id }) const refundable = @@ -764,7 +767,3 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { </Fragment> ); } - -async function copyToClipboard(text: string) { - return navigator.clipboard.writeText(text); -} 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 index 87e84945c..cebc4afe6 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -20,7 +20,10 @@ */ import { Amounts } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useMerchantApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; @@ -33,10 +36,12 @@ import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; -import { useConfigContext } from "../../../../context/config.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; import { mergeRefunds } from "../../../../utils/amount.js"; -import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js"; +import { + datetimeFormatForSettings, + usePreference, +} from "../../../../hooks/preference.js"; type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId; interface Props { @@ -141,10 +146,7 @@ function Table({ return ( <div class="table-container"> {hasMoreBefore && ( - <button - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > + <button class="button is-fullwidth" onClick={onLoadMoreBefore}> <i18n.Translate>load newer orders</i18n.Translate> </button> )} @@ -174,9 +176,9 @@ function Table({ {i.timestamp.t_s === "never" ? "never" : format( - new Date(i.timestamp.t_s * 1000), - datetimeFormatForSettings(settings), - )} + new Date(i.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} </td> <td onClick={(): void => onSelect(i)} @@ -218,10 +220,7 @@ function Table({ </tbody> </table> {hasMoreAfter && ( - <button - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > + <button class="button is-fullwidth" onClick={onLoadMoreAfter}> <i18n.Translate>load older orders</i18n.Translate> </button> )} @@ -268,7 +267,7 @@ export function RefundModal({ order.order_status === "paid" ? order.refund_details : [] ).reduce(mergeRefunds, []); - const config = useConfigContext(); + const { config } = useMerchantApiContext(); const totalRefunded = refunds .map((r) => r.amount) .reduce( @@ -362,9 +361,9 @@ export function RefundModal({ {r.timestamp.t_s === "never" ? "never" : format( - new Date(r.timestamp.t_s * 1000), - datetimeFormatForSettings(settings), - )} + new Date(r.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} </td> <td>{r.amount}</td> <td>{r.reason}</td> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx index 83345de3e..930a0d82c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx @@ -34,7 +34,6 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend } from "../../../../declaration.js"; type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; @@ -49,7 +48,6 @@ const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const backend = useBackendContext(); const [state, setState] = useState<Partial<Entity>>({}); @@ -145,6 +143,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { ...s, otp_key: randomRfc3548Base32Key(), })); + e.preventDefault(); }} > <i18n.Translate>random</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx index c6591cdbe..60abc3ca6 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx @@ -15,12 +15,11 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { VNode, h } from "preact"; import { QR } from "../../../../components/exception/QR.js"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { useInstanceContext } from "../../../../context/instance.js"; +import { useSessionContext } from "../../../../context/session.js"; import { MerchantBackend } from "../../../../declaration.js"; -import { useBackendContext } from "../../../../context/backend.js"; type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; @@ -34,11 +33,13 @@ export function CreatedSuccessfully({ onConfirm, }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backendURL).hostname; - const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; - const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; + const { + state: { backendUrl }, + } = useSessionContext(); + const { state } = useSessionContext(); + const issuer = backendUrl; + const qrText = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; + const qrTextSafe = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; return ( <Template onConfirm={onConfirm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx index d27f6a022..b07582252 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -23,11 +23,12 @@ import { AmountString, Amounts, Duration, - MerchantTemplateContractDetails, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -39,12 +40,11 @@ import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; +import { InputTab } from "../../../../components/form/InputTab.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; +import { useSessionContext } from "../../../../context/session.js"; import { MerchantBackend } from "../../../../declaration.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; enum Steps { BOTH_FIXED, @@ -55,14 +55,14 @@ enum Steps { // type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps }; type Entity = { - id?: string, - description?: string, - otpId?: string, - summary?: string, - amount?: AmountString, - minimum_age?: number, - pay_duration?: Duration, - type: Steps, + id?: string; + description?: string; + otpId?: string; + summary?: string; + amount?: AmountString; + minimum_age?: number; + pay_duration?: Duration; + type: Steps; }; interface Props { @@ -72,8 +72,10 @@ interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const devices = useInstanceOtpDevices() + const { + state: { backendUrl }, + } = useSessionContext(); + const devices = useInstanceOtpDevices(); const [state, setState] = useState<Partial<Entity>>({ minimum_age: 0, @@ -83,9 +85,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { type: Steps.NON_FIXED, }); - const parsedPrice = !state.amount - ? undefined - : Amounts.parse(state.amount); + const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); const errors: FormErrors<Entity> = { id: !state.id @@ -93,10 +93,10 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : !/[a-zA-Z0-9]*/.test(state.id) ? i18n.str`no valid. only characters and numbers` : undefined, - description: !state.description - ? i18n.str`should not be empty` - : undefined, - amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) + description: !state.description ? i18n.str`should not be empty` : undefined, + amount: !( + state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED + ) ? undefined : !state.amount ? i18n.str`required` @@ -105,7 +105,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : Amounts.isZero(parsedPrice) ? i18n.str`must be greater than 0` : undefined, - summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) + summary: !( + state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED + ) ? undefined : !state.summary ? i18n.str`required` @@ -130,55 +132,60 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const submitForm = () => { if (hasErrors || state.type === undefined) return Promise.reject(); switch (state.type) { - case Steps.FIXED_PRICE: return onCreate({ - template_id: state.id!, - template_description: state.description!, - template_contract: { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - amount: state.amount!, - // summary: state.summary, - }, - otp_id: state.otpId! - }) - case Steps.FIXED_SUMMARY: return onCreate({ - template_id: state.id!, - template_description: state.description!, - template_contract: { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - // amount: state.amount!, - summary: state.summary, - }, - otp_id: state.otpId!, - }) - case Steps.NON_FIXED: return onCreate({ - template_id: state.id!, - template_description: state.description!, - template_contract: { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - // amount: state.amount!, - // summary: state.summary, - }, - otp_id: state.otpId!, - }) - case Steps.BOTH_FIXED: return onCreate({ - template_id: state.id!, - template_description: state.description!, - template_contract: { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - amount: state.amount!, - summary: state.summary, - }, - otp_id: state.otpId!, - }) - default: assertUnreachable(state.type) + case Steps.FIXED_PRICE: + return onCreate({ + template_id: state.id!, + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: state.amount!, + // summary: state.summary, + }, + otp_id: state.otpId!, + }); + case Steps.FIXED_SUMMARY: + return onCreate({ + template_id: state.id!, + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + // amount: state.amount!, + summary: state.summary, + }, + otp_id: state.otpId!, + }); + case Steps.NON_FIXED: + return onCreate({ + template_id: state.id!, + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + // amount: state.amount!, + // summary: state.summary, + }, + otp_id: state.otpId!, + }); + case Steps.BOTH_FIXED: + return onCreate({ + template_id: state.id!, + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: state.amount!, + summary: state.summary, + }, + otp_id: state.otpId!, + }); + default: + assertUnreachable(state.type); // return onCreate(state); - }; - } - const deviceList = !devices.ok ? [] : devices.data.otp_devices + } + }; + const deviceList = !devices.ok ? [] : devices.data.otp_devices; return ( <div> @@ -193,7 +200,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { > <InputWithAddon<Entity> name="id" - help={`${backendURL}/templates/${state.id ?? ""}`} + help={new URL(`templates/${state.id ?? ""}`, backendUrl).href} label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} /> @@ -207,12 +214,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { name="type" label={i18n.str`Type`} help={(() => { - if (state.type === undefined) return "" + if (state.type === undefined) return ""; switch (state.type) { - case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` - case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` - case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` - case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` + case Steps.NON_FIXED: + return i18n.str`User will be able to input price and summary before payment.`; + case Steps.FIXED_PRICE: + return i18n.str`User will be able to add a summary before payment.`; + case Steps.FIXED_SUMMARY: + return i18n.str`User will be able to set the price before payment.`; + case Steps.BOTH_FIXED: + return i18n.str`User will not be able to change the price or the summary.`; } })()} tooltip={i18n.str`Define what the user be allowed to modify`} @@ -224,28 +235,34 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { ]} toStr={(v: Steps): string => { switch (v) { - case Steps.NON_FIXED: return i18n.str`Simple` - case Steps.FIXED_PRICE: return i18n.str`With price` - case Steps.FIXED_SUMMARY: return i18n.str`With summary` - case Steps.BOTH_FIXED: return i18n.str`With price and summary` + case Steps.NON_FIXED: + return i18n.str`Simple`; + case Steps.FIXED_PRICE: + return i18n.str`With price`; + case Steps.FIXED_SUMMARY: + return i18n.str`With summary`; + case Steps.BOTH_FIXED: + return i18n.str`With price and summary`; } }} /> - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? + {state.type === Steps.BOTH_FIXED || + state.type === Steps.FIXED_SUMMARY ? ( <Input<Entity> name="summary" inputType="multiline" label={i18n.str`Fixed summary`} tooltip={i18n.str`If specified, this template will create order with the same summary`} /> - : undefined} - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + ) : undefined} + {state.type === Steps.BOTH_FIXED || + state.type === Steps.FIXED_PRICE ? ( <InputCurrency<Entity> name="amount" label={i18n.str`Fixed price`} tooltip={i18n.str`If specified, this template will create order with the same price`} /> - : undefined} + ) : undefined} <InputNumber<Entity> name="minimum_age" label={i18n.str`Minimum age`} @@ -262,28 +279,29 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { name="otpId" label={i18n.str`OTP device`} readonly - side={<button - class="button is-danger" - data-tooltip={i18n.str`without otp device`} - onClick={(): void => { - setState((v) => ({ ...v, otpId: undefined })); - }} - > - <span> - <i18n.Translate>remove</i18n.Translate> - </span> - </button>} + side={ + <button + class="button is-danger" + data-tooltip={i18n.str`without otp device`} + onClick={(): void => { + setState((v) => ({ ...v, otpId: undefined })); + }} + > + <span> + <i18n.Translate>remove</i18n.Translate> + </span> + </button> + } tooltip={i18n.str`Use to verify transaction in offline mode.`} /> <InputSearchOnList label={i18n.str`Search device`} onChange={(p) => setState((v) => ({ ...v, otpId: p?.id }))} - list={deviceList.map(e => ({ + list={deviceList.map((e) => ({ description: e.device_description, - id: e.otp_device_id + id: e.otp_device_id, }))} /> - </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx index 809151565..1aa5bc317 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -20,7 +20,10 @@ */ import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useMerchantApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { QR } from "../../../../components/exception/QR.js"; @@ -30,9 +33,7 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { useInstanceContext } from "../../../../context/instance.js"; +import { useSessionContext } from "../../../../context/session.js"; import { MerchantBackend } from "../../../../declaration.js"; type Entity = MerchantBackend.Template.UsingTemplateDetails; @@ -45,9 +46,10 @@ interface Props { export function QrPage({ contract, id: templateId, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const { id: instanceId } = useInstanceContext(); - const config = useConfigContext(); + const { + state: { backendUrl }, + } = useSessionContext(); + const { config } = useMerchantApiContext(); const [state, setState] = useState<Partial<Entity>>({ amount: contract.amount, @@ -59,30 +61,26 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode { const fixedAmount = !!contract.amount; const fixedSummary = !!contract.summary; - const templateParams: Record<string, string> = {} + const templateParams: Record<string, string> = {}; if (!fixedAmount) { if (state.amount) { - templateParams.amount = state.amount + templateParams.amount = state.amount; } else { - templateParams.amount = config.currency + templateParams.amount = config.currency; } } if (!fixedSummary) { - templateParams.summary = state.summary ?? "" + templateParams.summary = state.summary ?? ""; } - const merchantBaseUrl = new URL(backendURL).href; + const merchantBaseUrl = backendUrl; const payTemplateUri = stringifyPayTemplateUri({ merchantBaseUrl, templateId, - templateParams - }) - - const issuer = encodeURIComponent( - `${new URL(backendURL).host}/${instanceId}`, - ); + templateParams, + }); return ( <div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx index cdf2ebab4..ae11ad991 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -26,7 +26,7 @@ import { assertUnreachable } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -37,11 +37,10 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; import { InputTab } from "../../../../components/form/InputTab.js"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; +import { useSessionContext } from "../../../../context/session.js"; import { MerchantBackend } from "../../../../declaration.js"; -import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; enum Steps { @@ -68,7 +67,10 @@ interface Props { export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() + const { + state: { backendUrl }, + } = useSessionContext(); + const intialStep = template.template_contract.amount === undefined && template.template_contract.summary === undefined @@ -187,7 +189,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - {backendURL}/templates/{template.otp_id} + {new URL(`templates/${template.otp_id}`,backendUrl).href} </span> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx index 1e9186624..f2b1db29b 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -25,19 +25,23 @@ import { useState } from "preact/hooks"; import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { AccessToken } from "../../../declaration.js"; import { NotificationCard } from "../../../components/menu/index.js"; +import { useSessionContext } from "../../../context/session.js"; +import { AccessToken } from "@gnu-taler/taler-util"; interface Props { - instanceId: string; hasToken: boolean | undefined; onClearToken: (c: AccessToken | undefined) => void; onNewToken: (c: AccessToken | undefined, s: AccessToken) => void; onBack?: () => void; } -export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode { +export function DetailPage({ + hasToken, + onBack, + onNewToken, + onClearToken, +}: Props): VNode { type State = { old_token: string; new_token: string; repeat_token: string }; const [form, setValue] = useState<Partial<State>>({ old_token: "", @@ -47,9 +51,10 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo const { i18n } = useTranslationContext(); const errors = { - old_token: hasToken && !form.old_token - ? i18n.str`you need your access token to perform the operation` - : undefined, + old_token: + hasToken && !form.old_token + ? i18n.str`you need your access token to perform the operation` + : undefined, new_token: !form.new_token ? i18n.str`cannot be empty` : form.new_token === form.old_token @@ -65,15 +70,17 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo (k) => (errors as any)[k] !== undefined, ); - const instance = useInstanceContext(); + const { state } = useSessionContext(); - const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`; + const text = i18n.str`You are updating the access token from instance with id "${state.instance}"`; async function submitForm() { if (hasErrors) return; - const oldToken = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined; + const oldToken = hasToken + ? (`secret-token:${form.old_token}` as AccessToken) + : undefined; const newToken = `secret-token:${form.new_token}` as AccessToken; - onNewToken(oldToken, newToken) + onNewToken(oldToken, newToken); } return ( @@ -84,9 +91,7 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo <div class="level"> <div class="level-left"> <div class="level-item"> - <span class="is-size-4"> - {text} - </span> + <span class="is-size-4">{text}</span> </div> </div> </div> @@ -94,7 +99,7 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo </section> <hr /> - {!hasToken && + {!hasToken && ( <NotificationCard notification={{ message: i18n.str`This instance doesn't have authentication token.`, @@ -102,7 +107,7 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo type: "WARN", }} /> - } + )} <div class="columns"> <div class="column" /> @@ -119,7 +124,8 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo /> <p> <i18n.Translate> - Clearing the access token will mean public access to the instance. + Clearing the access token will mean public access to the + instance. </i18n.Translate> </p> <div class="buttons is-right mt-5"> @@ -127,10 +133,11 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo class="button" onClick={() => { if (hasToken) { - const oldToken = `secret-token:${form.old_token}` as AccessToken; - onClearToken(oldToken) + const oldToken = + `secret-token:${form.old_token}` as AccessToken; + onClearToken(oldToken); } else { - onClearToken(undefined) + onClearToken(undefined); } }} > @@ -140,7 +147,6 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo </Fragment> )} - <Input<State> name="new_token" label={i18n.str`New access token`} @@ -176,7 +182,6 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo </div> <div class="column" /> </div> - </section> </div> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx index 13642ec22..d7bf7a6d5 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -16,15 +16,13 @@ import { HttpStatusCode } from "@gnu-taler/taler-util"; import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { Loading } from "../../../components/exception/loading.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; -import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; -import { DetailPage } from "./DetailPage.js"; -import { useInstanceContext } from "../../../context/instance.js"; import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading.js"; import { NotificationCard } from "../../../components/menu/index.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; import { Notification } from "../../../utils/types.js"; -import { useBackendContext } from "../../../context/backend.js"; +import { DetailPage } from "./DetailPage.js"; interface Props { onUnauthorized: () => VNode; @@ -45,7 +43,6 @@ export default function Token({ const [notif, setNotif] = useState<Notification | undefined>(undefined); const { clearAccessToken, setNewAccessToken } = useInstanceAPI(); - const { id } = useInstanceContext(); const result = useInstanceDetails() if (result.loading) return <Loading />; @@ -69,7 +66,6 @@ export default function Token({ <Fragment> <NotificationCard notification={notif} /> <DetailPage - instanceId={id} onBack={onCancel} hasToken={hasToken} onClearToken={async (currentToken): Promise<void> => { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx index eb25045a0..576c21cd2 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -20,7 +20,7 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -30,7 +30,6 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { useConfigContext } from "../../../../context/config.js"; import { MerchantBackend } from "../../../../declaration.js"; import { CROCKFORD_BASE32_REGEX, @@ -47,7 +46,6 @@ interface Props { export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { currency } = useConfigContext(); const [state, setState] = useState<Partial<Entity>>({ wtid: "", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx index ff0d55d2d..f0f0bfac9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { Duration } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -28,10 +29,9 @@ import { FormProvider, } from "../../../components/form/FormProvider.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { useInstanceContext } from "../../../context/instance.js"; import { MerchantBackend } from "../../../declaration.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -import { Duration } from "@gnu-taler/taler-util"; +import { useSessionContext } from "../../../context/session.js"; export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { default_pay_delay: Duration, @@ -64,7 +64,7 @@ export function UpdatePage({ selected, onBack, }: Props): VNode { - const { id } = useInstanceContext(); + const { state } = useSessionContext(); const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); @@ -125,7 +125,7 @@ export function UpdatePage({ <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - <i18n.Translate>Instance id</i18n.Translate>: <b>{id}</b> + <i18n.Translate>Instance id</i18n.Translate>: <b>{state.instance}</b> </span> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx index be3793ac3..de1371974 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -24,8 +24,7 @@ import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { Loading } from "../../../components/exception/loading.js"; import { NotificationCard } from "../../../components/menu/index.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; +import { MerchantBackend } from "../../../declaration.js"; import { useInstanceAPI, useInstanceDetails, @@ -65,7 +64,6 @@ function CommonUpdate( onConfirm, onLoadError, onNotFound, - onUpdateError, onUnauthorized, }: Props, result: HttpResponse< diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx index b89e5e6bf..83604711e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx @@ -28,12 +28,8 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { MerchantBackend } from "../../../../declaration.js"; type Entity = MerchantBackend.Webhooks.WebhookAddDetails; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx index 304ac90f3..be21629d5 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -28,7 +28,6 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId; diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx index d94b7e506..1c0b915bd 100644 --- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -19,7 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + AccessToken, + HttpStatusCode, + TalerAuthentication, +} from "@gnu-taler/taler-util"; import { useMerchantApiContext, useTranslationContext, @@ -27,40 +31,68 @@ import { import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../components/menu/index.js"; -import { AccessToken } from "../../declaration.js"; -import { DEFAULT_ADMIN_USERNAME, useSessionState } from "../../hooks/session.js"; +import { + DEFAULT_ADMIN_USERNAME, + useSessionContext, +} from "../../context/session.js"; import { Notification } from "../../utils/types.js"; -interface Props { -} +interface Props {} -function normalizeToken(r: string): AccessToken { - return `secret-token:${r}` as AccessToken; -} +const tokenRequest = { + scope: "write", + duration: { + d_us: "forever" as const, + }, + refreshable: true, +}; export function LoginPage(_p: Props): VNode { const [token, setToken] = useState(""); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { state, logIn } = useSessionState(); + const { state, logIn } = useSessionContext(); const { lib } = useMerchantApiContext(); const { i18n } = useTranslationContext(); + async function doImpersonateImpl(instanceId: string) { + const result = await lib + .impersonate(instanceId) + .createAccessTokenMerchant(token, tokenRequest); + if (result.type === "ok") { + const { token } = result.body; + logIn({ token }); + return; + } else { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + setNotif({ + message: "Your password is incorrect", + type: "ERROR", + }); + return; + } + case HttpStatusCode.NotFound: { + setNotif({ + message: "Your instance not found", + type: "ERROR", + }); + return; + } + } + } + } async function doLoginImpl() { - const secretToken = normalizeToken(token); - const result = await lib.authenticate.createAccessToken(secretToken, { - scope: "write", - duration: { - d_us: "forever" - }, - refreshable: true, - }); + const result = await lib.authenticate.createAccessTokenMerchant( + token, + tokenRequest, + ); if (result.type === "ok") { - const { access_token } = result.body; - logIn({ instance: state.instance, token: access_token }); + const { token } = result.body; + logIn({ token }); return; } else { - switch(result.case) { + switch (result.case) { case HttpStatusCode.Unauthorized: { setNotif({ message: "Your password is incorrect", @@ -79,8 +111,8 @@ export function LoginPage(_p: Props): VNode { } } - if (state.isAdmin && state.instance !== DEFAULT_ADMIN_USERNAME) { - //admin trying to access another instance + if (state.status === "loggedIn" && state.impersonate !== undefined) { + //the user is loggedin but trying to do an impersonation return ( <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> @@ -115,7 +147,9 @@ export function LoginPage(_p: Props): VNode { placeholder={"current access token"} name="token" onKeyPress={(e) => - e.keyCode === 13 ? doLoginImpl() : null + e.keyCode === 13 + ? doImpersonateImpl(state.instance) + : null } value={token} onInput={(e): void => setToken(e?.currentTarget.value)} @@ -133,7 +167,7 @@ export function LoginPage(_p: Props): VNode { borderTop: 0, }} > - <AsyncButton onClick={doLoginImpl}> + <AsyncButton onClick={() => doImpersonateImpl(state.instance)}> <i18n.Translate>Confirm</i18n.Translate> </AsyncButton> </footer> diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx index 4efda43be..6290f48e6 100644 --- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -1,10 +1,30 @@ +/* + This file is part of GNU Taler + (C) 2021-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 { useLang, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { FormErrors, FormProvider } from "../../components/form/FormProvider.js"; +import { + FormErrors, + FormProvider, +} from "../../components/form/FormProvider.js"; import { InputSelector } from "../../components/form/InputSelector.js"; import { InputToggle } from "../../components/form/InputToggle.js"; import { LangSelector } from "../../components/menu/LangSelector.js"; -import { Settings, usePreference } from "../../hooks/preference.js"; +import { Preferences, usePreference } from "../../hooks/preference.js"; +import { AbsoluteTime } from "@gnu-taler/taler-util"; function getBrowserLang(): string | undefined { if (typeof window === "undefined") return undefined; @@ -14,99 +34,107 @@ function getBrowserLang(): string | undefined { } export function Settings({ onClose }: { onClose?: () => void }): VNode { - const { i18n } = useTranslationContext() - const borwserLang = getBrowserLang() - const { update } = useLang(undefined, {}) + const { i18n } = useTranslationContext(); + const borwserLang = getBrowserLang(); + const { update } = useLang(undefined, {}); - const [value, updateValue] = usePreference() - const errors: FormErrors<Settings> = { - } + const [value, , updateValue] = usePreference(); + const errors: FormErrors<Preferences> = {}; - function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void { - const next = s(value) - const v: Settings = { + function valueHandler(s: (d: Partial<Preferences>) => Partial<Preferences>): void { + const next = s(value); + const v: Preferences = { advanceOrderMode: next.advanceOrderMode ?? false, - dateFormat: next.dateFormat ?? "ymd" - } - updateValue(v) + hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(), + dateFormat: next.dateFormat ?? "ymd", + }; + updateValue(v); } - return <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <div> - - <FormProvider<Settings> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Language</i18n.Translate> - <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}> - <i class="mdi mdi-information" /> - </span> - </label> - </div> - <div class="field field-body has-addons is-flex-grow-3"> - <LangSelector /> - - {borwserLang !== undefined && <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-2" - onClick={(e) => { - update(borwserLang.substring(0, 2)) - }} - > - <i18n.Translate>Set default</i18n.Translate> - </button>} + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div> + <FormProvider<Preferences> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Language</i18n.Translate> + <span + class="icon has-tooltip-right" + data-tooltip={ + "Force language setting instance of taking the browser" + } + > + <i class="mdi mdi-information" /> + </span> + </label> + </div> + <div class="field field-body has-addons is-flex-grow-3"> + <LangSelector /> + + {borwserLang !== undefined && ( + <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-2" + onClick={(e) => { + update(borwserLang.substring(0, 2)); + e.preventDefault() + }} + > + <i18n.Translate>Set default</i18n.Translate> + </button> + )} + </div> </div> - </div> - <InputToggle<Settings> - label={i18n.str`Advance order creation`} - tooltip={i18n.str`Shows more options in the order creation form`} - name="advanceOrderMode" - /> - <InputSelector<Settings> - name="dateFormat" - label={i18n.str`Date format`} - expand={true} - help={ - value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : "" - } - toStr={(e) => { - if (e === "ymd") return "year month day" - if (e === "mdy") return "month day year" - if (e === "dmy") return "day month year" - return "choose one" - }} - values={[ - "ymd", - "mdy", - "dmy", - ]} - tooltip={i18n.str`how the date is going to be displayed`} - /> - </FormProvider> + <InputToggle<Preferences> + label={i18n.str`Advance order creation`} + tooltip={i18n.str`Shows more options in the order creation form`} + name="advanceOrderMode" + /> + <InputSelector<Preferences> + name="dateFormat" + label={i18n.str`Date format`} + expand={true} + help={ + value.dateFormat === "dmy" + ? "31/12/2001" + : value.dateFormat === "mdy" + ? "12/31/2001" + : value.dateFormat === "ymd" + ? "2001/12/31" + : "" + } + toStr={(e) => { + if (e === "ymd") return "year month day"; + if (e === "mdy") return "month day year"; + if (e === "dmy") return "day month year"; + return "choose one"; + }} + values={["ymd", "mdy", "dmy"]} + tooltip={i18n.str`how the date is going to be displayed`} + /> + </FormProvider> + </div> </div> + <div class="column" /> </div> - <div class="column" /> - </div> - </section > - {onClose && - <section class="section is-main-section"> - <button - class="button" - onClick={onClose} - > - <i18n.Translate>Close</i18n.Translate> - </button> </section> - } - </div > -}
\ No newline at end of file + {onClose && ( + <section class="section is-main-section"> + <button class="button" onClick={onClose}> + <i18n.Translate>Close</i18n.Translate> + </button> + </section> + )} + </div> + ); +} |