summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-03-12 23:56:54 -0300
committerSebastian <sebasjm@gmail.com>2023-03-12 23:56:54 -0300
commitb874f9a0c50084803de58febb698864aa8dd061a (patch)
tree50f23d8faa674a94646a21c5821fd4f494c60f64
parentae1aee13581469a8398321b57e95cc85f210047b (diff)
downloadwallet-core-b874f9a0c50084803de58febb698864aa8dd061a.tar.gz
wallet-core-b874f9a0c50084803de58febb698864aa8dd061a.tar.bz2
wallet-core-b874f9a0c50084803de58febb698864aa8dd061a.zip
print and setup totp
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx7
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts9
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/templates.ts6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx43
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx65
-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.tsx42
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx19
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/utils/crypto.ts53
10 files changed, 227 insertions, 20 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
index 7a419ebb9..021977dfe 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
@@ -25,6 +25,7 @@ interface Props<T> extends InputProps<T> {
readonly?: boolean;
expand?: boolean;
values: string[];
+ convert?: (v: string) => any;
toStr?: (v?: any) => string;
fromStr?: (s: string) => any;
}
@@ -41,6 +42,7 @@ export function InputSelector<T>({
label,
help,
values,
+ convert,
toStr = defaultToString,
}: Props<keyof T>): VNode {
const { error, value, onChange } = useField<T>(name);
@@ -66,7 +68,10 @@ export function InputSelector<T>({
disabled={readonly}
readonly={readonly}
onChange={(e) => {
- onChange(e.currentTarget.value as any);
+ const v = convert
+ ? convert(e.currentTarget.value)
+ : e.currentTarget.value;
+ onChange(v);
}}
>
{placeholder && <option>{placeholder}</option>}
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index c9380760c..9fc4f0d77 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -1287,6 +1287,9 @@ export namespace MerchantBackend {
// This parameter is optional.
pos_key?: string;
+ // Algorithm for computing the POS confirmation, 0 for none.
+ pos_algorithm?: number;
+
// Additional information in a separate template.
template_contract: TemplateContractDetails;
}
@@ -1313,6 +1316,9 @@ export namespace MerchantBackend {
// This parameter is optional.
pos_key?: string;
+ // Algorithm for computing the POS confirmation, 0 for none.
+ pos_algorithm?: Integer;
+
// Additional information in a separate template.
template_contract: TemplateContractDetails;
}
@@ -1338,6 +1344,9 @@ export namespace MerchantBackend {
// This parameter is optional.
pos_key?: string;
+ // Algorithm for computing the POS confirmation, 0 for none.
+ pos_algorithm?: Integer;
+
// Additional information in a separate template.
template_contract: TemplateContractDetails;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts
index dd096e4f9..97fb165b9 100644
--- a/packages/merchant-backoffice-ui/src/hooks/templates.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts
@@ -244,7 +244,11 @@ export function useTemplateDetails(
});
if (isValidating) return { loading: true, data: data?.data };
- if (data) return data;
+ if (data) {
+ const d = structuredClone(data);
+ d.data.pos_algorithm = 1;
+ return d;
+ }
if (error) return error.info;
return { loading: true };
}
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 22f86002a..144e968c5 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
@@ -31,9 +31,11 @@ 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 { MerchantBackend } from "../../../../declaration.js";
+import { randomBase32Key } from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
type Entity = MerchantBackend.Template.TemplateAddDetails;
@@ -43,6 +45,13 @@ interface Props {
onBack?: () => void;
}
+const algorithms = ["0", "1", "2"];
+const algorithmsNames = [
+ "off",
+ "30s 8d TOTP-SHA1 without amount",
+ "30s 8d eTOTP-SHA1 with amount",
+];
+
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
@@ -104,7 +113,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>
-
<Input<Entity>
name="template_description"
label={i18n.str`Description`}
@@ -134,12 +142,35 @@ 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.`}
/>
- <Input<Entity>
- name="pos_key"
- label={i18n.str`Point-of-sale key`}
- help=""
- tooltip={i18n.str`Useful to validate the purchase`}
+ <InputSelector<Entity>
+ name="pos_algorithm"
+ label={i18n.str`Veritifaction algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ convert={(v) => Number(v)}
/>
+ {state.pos_algorithm && state.pos_algorithm > 0 ? (
+ <Input<Entity>
+ name="pos_key"
+ label={i18n.str`Point-of-sale key`}
+ help=""
+ tooltip={i18n.str`Useful to validate the purchase`}
+ side={
+ <span data-tooltip={i18n.str`generate random secret key`}>
+ <button
+ class="button is-info mr-3"
+ onClick={(e) => {
+ const pos_key = randomBase32Key();
+ setState((s) => ({ ...s, pos_key }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ </span>
+ }
+ />
+ ) : undefined}
</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 756909d15..66ac72ff5 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
@@ -31,8 +31,10 @@ import {
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
import { useBackendContext } from "../../../../context/backend.js";
import { useConfigContext } from "../../../../context/config.js";
+import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
@@ -46,7 +48,9 @@ interface Props {
export function QrPage({ template, 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,
@@ -82,8 +86,33 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`;
+ const issuer = encodeURIComponent(
+ `${new URL(backendUrl).hostname}/${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;
return (
<div>
+ {oauthUri && (
+ <ConfirmModal
+ description="Setup TOTP"
+ active={setupTOTP}
+ onConfirm={() => {
+ setSetupTOTP(false);
+ }}
+ >
+ <p>Scan this qr code with your TOTP device</p>
+ <QR text={oauthUri} />
+ <pre style={{ textAlign: "center" }}>
+ <a href={oauthUri}>{oauthUri}</a>
+ </pre>
+ </ConfirmModal>
+ )}
<section class="section is-main-section">
<div class="columns">
<div class="column" />
@@ -114,20 +143,48 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
<i18n.Translate>Cancel</i18n.Translate>
</button>
)}
- <button class="button is-info" onClick={onBack}>
+ <button
+ class="button is-info"
+ onClick={() => saveAsPDF(templateId)}
+ >
<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" />
</div>
</section>
- <section>
- <pre>
+ <section id="printThis">
+ <QR text={payTemplateUri} />
+ <pre style={{ textAlign: "center" }}>
<a href={payTemplateUri}>{payTemplateUri}</a>
</pre>
- <QR text={payTemplateUri} />
</section>
</div>
);
}
+
+function saveAsPDF(name: string): void {
+ const printWindow = window.open("", "", "height=400,width=800");
+ if (!printWindow) return;
+ const divContents = document.getElementById("printThis");
+ if (!divContents) return;
+ printWindow.document.write(
+ `<html><head><title>Order template for ${name}</title><style>`,
+ );
+ printWindow.document.write("</style></head><body>&nbsp;</body></html>");
+ printWindow.document.close();
+ printWindow.document.body.appendChild(divContents.cloneNode(true));
+ printWindow.addEventListener("load", () => {
+ printWindow.print();
+ printWindow.close();
+ });
+}
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 97d25b700..044cc7d79 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
@@ -51,10 +51,8 @@ export default function TemplateQrPage({
onNotFound,
onUnauthorized,
}: Props): VNode {
- const { createOrderFromTemplate } = useTemplateAPI();
const result = useTemplateDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { i18n } = useTranslationContext();
if (result.clientError && result.isUnauthorized) return onUnauthorized();
if (result.clientError && result.isNotfound) return onNotFound();
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 eba212517..e34e2c746 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
@@ -31,9 +31,11 @@ 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 { MerchantBackend, WithId } from "../../../../declaration.js";
+import { randomBase32Key } from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
@@ -44,6 +46,13 @@ interface Props {
template: Entity;
}
+const algorithms = ["0", "1", "2"];
+const algorithmsNames = [
+ "off",
+ "30s 8d TOTP-SHA1 without amount",
+ "30s 8d eTOTP-SHA1 with amount",
+];
+
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
@@ -143,12 +152,35 @@ 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.`}
/>
- <Input<Entity>
- name="pos_key"
- label={i18n.str`Point-of-sale key`}
- help=""
- tooltip={i18n.str`Useful to validate the purchase`}
+ <InputSelector<Entity>
+ name="pos_algorithm"
+ label={i18n.str`Veritifaction algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ convert={(v) => Number(v)}
/>
+ {state.pos_algorithm && state.pos_algorithm > 0 ? (
+ <Input<Entity>
+ name="pos_key"
+ label={i18n.str`Point-of-sale key`}
+ help=""
+ tooltip={i18n.str`Useful to validate the purchase`}
+ side={
+ <span data-tooltip={i18n.str`generate random secret key`}>
+ <button
+ class="button is-info mr-3"
+ onClick={(e) => {
+ const pos_key = randomBase32Key();
+ setState((s) => ({ ...s, pos_key }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ </span>
+ }
+ />
+ ) : undefined}
</FormProvider>
<div class="buttons is-right mt-5">
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
index a63469763..5abc6b153 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
@@ -34,12 +34,13 @@ import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
interface Props {
+ id: string;
template: MerchantBackend.Template.TemplateDetails;
onCreateOrder: (d: Entity) => Promise<void>;
onBack?: () => void;
}
-export function UsePage({ template, onCreateOrder, onBack }: Props): VNode {
+export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const [state, setState] = useState<Partial<Entity>>({
@@ -75,6 +76,22 @@ export function UsePage({ template, onCreateOrder, onBack }: Props): VNode {
return (
<div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>New order for template</i18n.Translate>:{" "}
+ <b>{id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
index d5fa6d39d..b6175bcfb 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -68,6 +68,7 @@ export default function TemplateUsePage({
<NotificationCard notification={notif} />
<UsePage
template={result.data}
+ id={tid}
onBack={onBack}
onCreateOrder={(
request: MerchantBackend.Template.UsingTemplateDetails,
diff --git a/packages/merchant-backoffice-ui/src/utils/crypto.ts b/packages/merchant-backoffice-ui/src/utils/crypto.ts
new file mode 100644
index 000000000..7bab8abf1
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/utils/crypto.ts
@@ -0,0 +1,53 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+const encTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+// base32 RFC 3548
+function encodeBase32(data: ArrayBuffer) {
+ const dataBytes = new Uint8Array(data);
+ let sb = "";
+ const size = data.byteLength;
+ let bitBuf = 0;
+ let numBits = 0;
+ let pos = 0;
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ const d = dataBytes[pos++];
+ bitBuf = (bitBuf << 8) | d;
+ numBits += 8;
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf << (5 - numBits);
+ numBits = 5;
+ }
+ const v = (bitBuf >>> (numBits - 5)) & 31;
+ sb += encTable[v];
+ numBits -= 5;
+ }
+ return sb;
+}
+
+export function randomBase32Key(): string {
+ var buf = new Uint8Array(20);
+ window.crypto.getRandomValues(buf);
+ return encodeBase32(buf);
+}