aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-06-23 10:36:24 -0300
committerSebastian <sebasjm@gmail.com>2023-06-23 10:36:24 -0300
commit4f30506dcacc587586381b1a8fa20c5442784e41 (patch)
tree5f7dafbaf96727c589ad82516efaa7830610f429 /packages/merchant-backoffice-ui/src
parent9dbf0bd7d2dc9353aa210a0c33faf0a5c5086f73 (diff)
downloadwallet-core-4f30506dcacc587586381b1a8fa20c5442784e41.tar.xz
show qr to import TOTP into other app
Diffstat (limited to 'packages/merchant-backoffice-ui/src')
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx117
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx119
2 files changed, 143 insertions, 93 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 137d50b3e..4dde202c4 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 { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -44,6 +44,8 @@ import {
randomBase32Key,
} from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { QR } from "../../../../components/exception/QR.js";
+import { useInstanceContext } from "../../../../context/instance.js";
type Entity = MerchantBackend.Template.TemplateAddDetails;
@@ -58,6 +60,8 @@ 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 [showKey, setShowKey] = useState(false);
const [state, setState] = useState<Partial<Entity>>({
@@ -120,6 +124,8 @@ 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}`;
+
return (
<div>
<section class="section is-main-section">
@@ -175,54 +181,73 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
fromStr={(v) => Number(v)}
/>
{state.pos_algorithm && state.pos_algorithm > 0 ? (
- <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);
- }}
- >
+ <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 ? (
- <i18n.Translate>hide</i18n.Translate>
+ <i class="mdi mdi-eye" />
) : (
- <i18n.Translate>show</i18n.Translate>
+ <i class="mdi mdi-eye-off" />
)}
- </button>
- </span>
- }
- />
+ </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>
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 20e40cf9b..30e5502bb 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
@@ -24,7 +24,7 @@ import {
MerchantTemplateContractDetails,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -44,6 +44,8 @@ import {
randomBase32Key,
} from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { QR } from "../../../../components/exception/QR.js";
+import { useInstanceContext } from "../../../../context/instance.js";
type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
@@ -59,6 +61,8 @@ 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);
@@ -113,6 +117,8 @@ 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>
<section class="section">
@@ -185,55 +191,74 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
fromStr={(v) => Number(v)}
/>
{state.pos_algorithm && state.pos_algorithm > 0 ? (
- <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);
- }}
- >
+ <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 ? (
- <i18n.Translate>hide</i18n.Translate>
+ <i class="mdi mdi-eye" />
) : (
- <i18n.Translate>show</i18n.Translate>
+ <i class="mdi mdi-eye-off" />
)}
- </button>
- </span>
- }
- />
+ </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>