aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/paths/instance/templates
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-09-04 14:17:55 -0300
committerSebastian <sebasjm@gmail.com>2023-09-04 14:17:55 -0300
commite1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7 (patch)
treed4ed5506ab3550a7e9b1a082d7ffeddf9f3c4954 /packages/merchant-backoffice-ui/src/paths/instance/templates
parentff20c3e25e076c24f7cb93eabe58b6f934f51f35 (diff)
downloadwallet-core-e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7.tar.xz
backoffcie new version, lot of changes
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/templates')
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx127
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx51
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx121
5 files changed, 91 insertions, 272 deletions
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 e20b9bc27..8629d8dee 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
@@ -24,7 +24,7 @@ import {
MerchantTemplateContractDetails,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -35,17 +35,16 @@ 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 { InputSelector } from "../../../../components/form/InputSelector.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
import { useBackendContext } from "../../../../context/backend.js";
+import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
import {
- isBase32RFC3548Charset,
- randomBase32Key,
+ isBase32RFC3548Charset
} from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
-import { QR } from "../../../../components/exception/QR.js";
-import { useInstanceContext } from "../../../../context/instance.js";
+import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
type Entity = MerchantBackend.Template.TemplateAddDetails;
@@ -54,16 +53,11 @@ interface Props {
onBack?: () => void;
}
-const algorithms = [0, 1, 2];
-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 { id: instanceId } = useInstanceContext();
- const issuer = new URL(backend.url).hostname;
+ const devices = useInstanceOtpDevices()
- const [showKey, setShowKey] = useState(false);
const [state, setState] = useState<Partial<Entity>>({
template_contract: {
minimum_age: 0,
@@ -78,7 +72,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: Amounts.parse(state.template_contract?.amount);
const errors: FormErrors<Entity> = {
- template_id: !state.template_id ? i18n.str`should not be empty` : undefined,
+ template_id: !state.template_id
+ ? i18n.str`should not be empty`
+ : !/[a-zA-Z0-9]*/.test(state.template_id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
template_description: !state.template_description
? i18n.str`should not be empty`
: undefined,
@@ -104,15 +102,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
? i18n.str`to short`
: undefined,
} as Partial<MerchantTemplateContractDetails>),
- pos_key: !state.pos_key
- ? !state.pos_algorithm
- ? undefined
- : i18n.str`required`
- : !isBase32RFC3548Charset(state.pos_key)
- ? i18n.str`just letters and numbers from 2 to 7`
- : state.pos_key.length !== 32
- ? i18n.str`size of the key should be 32`
- : undefined,
};
const hasErrors = Object.keys(errors).some(
@@ -124,7 +113,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
return onCreate(state as any);
};
- const qrText = `otpauth://totp/${instanceId}/${state.template_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`;
+ const deviceList = !devices.ok ? [] : devices.data.otp_devices
return (
<div>
@@ -139,7 +128,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputWithAddon<Entity>
name="template_id"
- help={`${backend.url}/instances/templates/${state.template_id ?? ""}`}
+ help={`${backend.url}/templates/${state.template_id ?? ""}`}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>
@@ -172,83 +161,21 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
help=""
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
/>
- <InputSelector<Entity>
- name="pos_algorithm"
- label={i18n.str`Verification algorithm`}
- tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
- values={algorithms}
- toStr={(v) => algorithmsNames[v]}
- fromStr={(v) => Number(v)}
+ <Input<Entity>
+ name="otp_id"
+ label={i18n.str`OTP device`}
+ readonly
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ <InputSearchOnList
+ label={i18n.str`Search device`}
+ onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))}
+ list={deviceList.map(e => ({
+ description: e.device_description,
+ id: e.otp_device_id
+ }))}
/>
- {state.pos_algorithm && state.pos_algorithm > 0 ? (
- <Fragment>
- <InputWithAddon<Entity>
- name="pos_key"
- label={i18n.str`Point-of-sale key`}
- inputType={showKey ? "text" : "password"}
- help="Be sure to be very hard to guess or use the random generator"
- tooltip={i18n.str`Useful to validate the purchase`}
- fromStr={(v) => v.toUpperCase()}
- addonAfter={
- <span class="icon">
- {showKey ? (
- <i class="mdi mdi-eye" />
- ) : (
- <i class="mdi mdi-eye-off" />
- )}
- </span>
- }
- side={
- <span style={{ display: "flex" }}>
- <button
- data-tooltip={i18n.str`generate random secret key`}
- class="button is-info mr-3"
- onClick={(e) => {
- const pos_key = randomBase32Key();
- setState((s) => ({ ...s, pos_key }));
- }}
- >
- <i18n.Translate>random</i18n.Translate>
- </button>
- <button
- data-tooltip={
- showKey
- ? i18n.str`show secret key`
- : i18n.str`hide secret key`
- }
- class="button is-info mr-3"
- onClick={(e) => {
- setShowKey(!showKey);
- }}
- >
- {showKey ? (
- <i18n.Translate>hide</i18n.Translate>
- ) : (
- <i18n.Translate>show</i18n.Translate>
- )}
- </button>
- </span>
- }
- />
- {showKey && (
- <Fragment>
- <QR text={qrText} />
- <div
- style={{
- color: "grey",
- fontSize: "small",
- width: 200,
- textAlign: "center",
- margin: "auto",
- wordBreak: "break-all",
- }}
- >
- {qrText}
- </div>
- </Fragment>
- )}
- </Fragment>
- ) : undefined}
+
</FormProvider>
<div class="buttons is-right mt-5">
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
index 2f91298bf..3c9bb231c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -36,6 +36,7 @@ import {
import { Notification } from "../../../../utils/types.js";
import { ListPage } from "./ListPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
interface Props {
onUnauthorized: () => VNode;
@@ -61,6 +62,8 @@ export default function ListTemplates({
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { deleteTemplate } = useTemplateAPI();
const result = useInstanceTemplates({ position }, (id) => setPosition(id));
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Template.TemplateEntry | null>(null);
if (result.loading) return <Loading />;
if (!result.ok) {
@@ -97,23 +100,45 @@ export default function ListTemplates({
onQR={(e) => {
onQR(e.template_id);
}}
- onDelete={(e: MerchantBackend.Template.TemplateEntry) =>
- deleteTemplate(e.template_id)
- .then(() =>
+ onDelete={(e: MerchantBackend.Template.TemplateEntry) => {
+ setDeleting(e)
+ }
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete template`}
+ description={`Delete the template "${deleting.template_description}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteTemplate(deleting.template_id);
setNotif({
- message: i18n.str`template delete successfully`,
+ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
type: "SUCCESS",
- }),
- )
- .catch((error) =>
+ });
+ } catch (error) {
setNotif({
- message: i18n.str`could not delete the template`,
+ message: i18n.str`Failed to delete template`,
type: "ERROR",
- description: error.message,
- }),
- )
- }
- />
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the template <b>&quot;{deleting.template_description}&quot;</b> (ID:{" "}
+ <b>{deleting.template_id}</b>) you may loose information
+ </p>
+ <p class="warning">
+ Deleting an template <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
</Fragment>
);
}
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 0f30efafd..c65cf6a19 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
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { QR } from "../../../../components/exception/QR.js";
@@ -35,35 +35,32 @@ import { useConfigContext } from "../../../../context/config.js";
import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
+import { useOtpDeviceDetails } from "../../../../hooks/otp.js";
+import { Loading } from "../../../../components/exception/loading.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
interface Props {
- template: MerchantBackend.Template.TemplateDetails;
+ contract: MerchantBackend.Template.TemplateContractDetails;
id: string;
onBack?: () => void;
}
-export function QrPage({ template, id: templateId, onBack }: Props): VNode {
+export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const { url: backendUrl } = useBackendContext();
const { id: instanceId } = useInstanceContext();
const config = useConfigContext();
- const [setupTOTP, setSetupTOTP] = useState(false);
const [state, setState] = useState<Partial<Entity>>({
- amount: template.template_contract.amount,
- summary: template.template_contract.summary,
+ amount: contract.amount,
+ summary: contract.summary,
});
const errors: FormErrors<Entity> = {};
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
-
- const fixedAmount = !!template.template_contract.amount;
- const fixedSummary = !!template.template_contract.summary;
+ const fixedAmount = !!contract.amount;
+ const fixedSummary = !!contract.summary;
const templateParams: Record<string, string> = {}
if (!fixedAmount) {
@@ -89,40 +86,9 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
const issuer = encodeURIComponent(
`${new URL(backendUrl).host}/${instanceId}`,
);
- const oauthUri = !template.pos_algorithm
- ? undefined
- : template.pos_algorithm === 1
- ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : template.pos_algorithm === 2
- ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : undefined;
-
- const keySlice = template.pos_key?.substring(0, 4);
-
- const oauthUriWithoutSecret = !template.pos_algorithm
- ? undefined
- : template.pos_algorithm === 1
- ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : template.pos_algorithm === 2
- ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : undefined;
+
return (
<div>
- {oauthUri && (
- <ConfirmModal
- description="Setup TOTP"
- active={setupTOTP}
- onCancel={() => {
- setSetupTOTP(false);
- }}
- >
- <p>Scan this qr code with your TOTP device</p>
- <QR text={oauthUri} />
- <pre style={{ textAlign: "center" }}>
- <a href={oauthUri}>{oauthUriWithoutSecret}</a>
- </pre>
- </ConfirmModal>
- )}
<section class="section is-main-section">
<div class="columns">
<div class="column" />
@@ -176,14 +142,6 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
>
<i18n.Translate>Print</i18n.Translate>
</button>
- {oauthUri && (
- <button
- class="button is-info"
- onClick={() => setSetupTOTP(true)}
- >
- <i18n.Translate>Setup TOTP</i18n.Translate>
- </button>
- )}
</div>
</div>
<div class="column" />
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
index 1f74afc2b..7db7478f7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
@@ -74,7 +74,7 @@ export default function TemplateQrPage({
return (
<>
<NotificationCard notification={notif} />
- <QrPage template={result.data} id={tid} onBack={onBack} />
+ <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
</>
);
}
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 30e5502bb..30d47385c 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
@@ -61,10 +61,7 @@ const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
- const { id: instanceId } = useInstanceContext();
- const issuer = new URL(backend.url).hostname;
- const [showKey, setShowKey] = useState(false);
const [state, setState] = useState<Partial<Entity>>(template);
const parsedPrice = !state.template_contract?.amount
@@ -78,34 +75,25 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
template_contract: !state.template_contract
? undefined
: undefinedIfEmpty({
- amount: !state.template_contract?.amount
- ? undefined
- : !parsedPrice
+ amount: !state.template_contract?.amount
+ ? undefined
+ : !parsedPrice
? i18n.str`not valid`
: Amounts.isZero(parsedPrice)
- ? i18n.str`must be greater than 0`
- : undefined,
- minimum_age:
- state.template_contract.minimum_age < 0
- ? i18n.str`should be greater that 0`
+ ? i18n.str`must be greater than 0`
: undefined,
- pay_duration: !state.template_contract.pay_duration
- ? i18n.str`can't be empty`
- : state.template_contract.pay_duration.d_us === "forever"
+ minimum_age:
+ state.template_contract.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.template_contract.pay_duration
+ ? i18n.str`can't be empty`
+ : state.template_contract.pay_duration.d_us === "forever"
? undefined
: state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
- ? i18n.str`to short`
- : undefined,
- } as Partial<MerchantTemplateContractDetails>),
- pos_key: !state.pos_key
- ? !state.pos_algorithm
- ? undefined
- : i18n.str`required`
- : !isBase32RFC3548Charset(state.pos_key)
- ? i18n.str`just letters and numbers from 2 to 7`
- : state.pos_key.length !== 32
- ? i18n.str`size of the key should be 32`
- : undefined,
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
};
const hasErrors = Object.keys(errors).some(
@@ -117,7 +105,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
return onUpdate(state as any);
};
- const qrText = `otpauth://totp/${instanceId}/${state.id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`;
return (
<div>
@@ -128,7 +115,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- {backend.url}/instances/template/{template.id}
+ {backend.url}/templates/{template.id}
</span>
</div>
</div>
@@ -182,84 +169,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
help=""
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
/>
- <InputSelector<Entity>
- name="pos_algorithm"
- label={i18n.str`Verification algorithm`}
- tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
- values={algorithms}
- toStr={(v) => algorithmsNames[v]}
- fromStr={(v) => Number(v)}
- />
- {state.pos_algorithm && state.pos_algorithm > 0 ? (
- <Fragment>
- <InputWithAddon<Entity>
- name="pos_key"
- label={i18n.str`Point-of-sale key`}
- inputType={showKey ? "text" : "password"}
- help="Be sure to be very hard to guess or use the random generator"
- expand
- tooltip={i18n.str`Useful to validate the purchase`}
- fromStr={(v) => v.toUpperCase()}
- addonAfter={
- <span class="icon">
- {showKey ? (
- <i class="mdi mdi-eye" />
- ) : (
- <i class="mdi mdi-eye-off" />
- )}
- </span>
- }
- side={
- <span style={{ display: "flex" }}>
- <button
- data-tooltip={i18n.str`generate random secret key`}
- class="button is-info mr-3"
- onClick={(e) => {
- const pos_key = randomBase32Key();
- setState((s) => ({ ...s, pos_key }));
- }}
- >
- <i18n.Translate>random</i18n.Translate>
- </button>
- <button
- data-tooltip={
- showKey
- ? i18n.str`show secret key`
- : i18n.str`hide secret key`
- }
- class="button is-info mr-3"
- onClick={(e) => {
- setShowKey(!showKey);
- }}
- >
- {showKey ? (
- <i18n.Translate>hide</i18n.Translate>
- ) : (
- <i18n.Translate>show</i18n.Translate>
- )}
- </button>
- </span>
- }
- />
- {showKey && (
- <Fragment>
- <QR text={qrText} />
- <div
- style={{
- color: "grey",
- fontSize: "small",
- width: 200,
- textAlign: "center",
- margin: "auto",
- wordBreak: "break-all",
- }}
- >
- {qrText}
- </div>
- </Fragment>
- )}
- </Fragment>
- ) : undefined}
</FormProvider>
<div class="buttons is-right mt-5">