gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated (24491ab -> 61f845b)


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated (24491ab -> 61f845b)
Date: Thu, 24 Jun 2021 14:30:09 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a change to branch master
in repository merchant-backoffice.

    from 24491ab  going over many help texts of the SPA. Fixes #6894.
     new ba74497  add qr
     new a75d12b  from star to alert, using amounts api
     new 520e1d8  joint inventory product and custom products
     new 4323da4  split payment and shipping
     new 61f845b  duration picker

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/frontend/package.json                     |   3 +-
 .../src/components/form/DurationPicker.scss        |  71 +++++++
 .../form/DurationPicker.stories.tsx}               |  18 +-
 .../src/components/form/DurationPicker.tsx         | 154 ++++++++++++++
 .../frontend/src/components/form/FormProvider.tsx  |   2 +-
 packages/frontend/src/components/form/Input.tsx    |   4 +-
 .../frontend/src/components/form/InputDate.tsx     |   7 +-
 .../frontend/src/components/form/InputDuration.tsx | 104 ++++++++--
 .../frontend/src/components/form/InputGroup.tsx    |  10 +-
 .../src/components/form/InputWithAddon.tsx         |   5 +-
 packages/frontend/src/components/form/useField.tsx |  12 +-
 .../frontend/src/components/form/useGroupField.tsx |   7 +-
 .../instance/DefaultInstanceFormFields.tsx         |   1 +
 packages/frontend/src/components/modal/index.tsx   |  12 ++
 .../product/InventoryProductForm.stories.tsx}      |  40 ++--
 .../product}/InventoryProductForm.tsx              |  31 ++-
 .../product}/NonInventoryProductForm.tsx           |  30 ++-
 .../src/components/product/ProductForm.tsx         |   1 +
 .../src/components/product/ProductList.tsx         |   7 +-
 packages/frontend/src/declaration.d.ts             |   3 -
 .../frontend/src/paths/admin/create/CreatePage.tsx |   2 +-
 .../instance/orders/create/Create.stories.tsx      |   2 +-
 .../paths/instance/orders/create/CreatePage.tsx    | 227 ++++++++++++---------
 .../src/paths/instance/orders/create/index.tsx     |   2 +-
 .../src/paths/instance/orders/list/Table.tsx       |  21 +-
 .../paths/instance/products/create/CreatePage.tsx  |   2 +-
 .../paths/instance/products/update/UpdatePage.tsx  |   2 +-
 .../paths/instance/reserves/create/CreatePage.tsx  |   4 +-
 ...stories.tsx => CreatedSuccessfully.stories.tsx} |  15 +-
 .../reserves/create/CreatedSuccessfully.tsx        |  26 ++-
 .../paths/instance/transfers/create/CreatePage.tsx |   2 +-
 .../src/paths/instance/update/UpdatePage.tsx       |   9 +-
 packages/frontend/src/utils/amount.ts              |  15 +-
 pnpm-lock.yaml                                     |   6 +
 34 files changed, 621 insertions(+), 236 deletions(-)
 create mode 100644 packages/frontend/src/components/form/DurationPicker.scss
 copy packages/frontend/src/{paths/admin/create/Create.stories.tsx => 
components/form/DurationPicker.stories.tsx} (72%)
 create mode 100644 packages/frontend/src/components/form/DurationPicker.tsx
 copy packages/frontend/src/{paths/instance/products/list/List.stories.tsx => 
components/product/InventoryProductForm.stories.tsx} (65%)
 rename packages/frontend/src/{paths/instance/orders/create => 
components/product}/InventoryProductForm.tsx (76%)
 rename packages/frontend/src/{paths/instance/orders/create => 
components/product}/NonInventoryProductForm.tsx (85%)
 copy packages/frontend/src/paths/instance/reserves/create/{Create.stories.tsx 
=> CreatedSuccessfully.stories.tsx} (73%)

diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index b0bc05f..d800e9a 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -50,6 +50,7 @@
     "jed": "^1.1.1",
     "preact": "^10.5.13",
     "preact-router": "^3.2.1",
+    "qrcode-generator": "^1.4.4",
     "swr": "^0.5.5",
     "yup": "^0.32.9"
   },
@@ -109,7 +110,7 @@
     "preset": "jest-preset-preact",
     "transformIgnorePatterns": [
       "node_modules/.pnpm/(?!(@gnu-taler\\+taler-util))",
-      "\\.pnp\\.[^\\\/]+$"
+      "\\.pnp\\.[^\\/]+$"
     ],
     "setupFiles": [
       "<rootDir>/tests/__mocks__/browserMocks.ts",
diff --git a/packages/frontend/src/components/form/DurationPicker.scss 
b/packages/frontend/src/components/form/DurationPicker.scss
new file mode 100644
index 0000000..a355753
--- /dev/null
+++ b/packages/frontend/src/components/form/DurationPicker.scss
@@ -0,0 +1,71 @@
+
+.rdp-picker {
+  display: flex;
+  height: 175px;
+}
+
+@media (max-width: 400px) {
+  .rdp-picker {
+    width: 250px;
+  }
+}
+
+.rdp-masked-div {
+  overflow: hidden;
+  height: 175px;
+  position: relative;
+}
+
+.rdp-column-container {
+  flex-grow: 1;
+  display: inline-block;
+}
+
+.rdp-column {
+  position: absolute;
+  z-index: 0;
+  width: 100%;
+}
+
+.rdp-reticule {
+  border: 0;
+  border-top: 2px solid rgba(109, 202, 236, 1);
+  height: 2px;
+  position: absolute;
+  width: 80%;
+  margin: 0;
+  z-index: 100;
+  left: 50%;
+  -webkit-transform: translateX(-50%);
+  transform: translateX(-50%);
+}
+
+.rdp-text-overlay {
+  position: absolute;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 35px;
+  font-size: 20px;
+  left: 50%;
+  -webkit-transform: translateX(-50%);
+  transform: translateX(-50%);
+}
+
+.rdp-cell div {
+  font-size: 17px;
+  color: gray;
+  font-style: italic;
+}
+
+.rdp-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 35px;
+  font-size: 18px;
+}
+
+.rdp-center {
+  font-size: 25px;
+}
diff --git a/packages/frontend/src/paths/admin/create/Create.stories.tsx 
b/packages/frontend/src/components/form/DurationPicker.stories.tsx
similarity index 72%
copy from packages/frontend/src/paths/admin/create/Create.stories.tsx
copy to packages/frontend/src/components/form/DurationPicker.stories.tsx
index c128755..275c80f 100644
--- a/packages/frontend/src/paths/admin/create/Create.stories.tsx
+++ b/packages/frontend/src/components/form/DurationPicker.stories.tsx
@@ -19,12 +19,13 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 
-import { h, VNode, FunctionalComponent } from 'preact';
-import { CreatePage as TestedComponent } from './CreatePage';
+import { h, FunctionalComponent } from 'preact';
+import { useState } from 'preact/hooks';
+import { DurationPicker as TestedComponent } from './DurationPicker';
 
 
 export default {
-  title: 'Pages/Instance/Create',
+  title: 'Components/Picker/Duration',
   component: TestedComponent,
   argTypes: {
     onCreate: { action: 'onCreate' },
@@ -39,8 +40,11 @@ function createExample<Props>(Component: 
FunctionalComponent<Props>, props: Part
 }
 
 export const Example = createExample(TestedComponent, {
+  days: true, minutes: true, hours: true, seconds: true,
+  value: 10000000
 });
-// export const Example = (a: any): VNode => <CreatePage {...a} />;
-// Example.args = {
-//   isLoading: false
-// }
+
+export const WithState = () => {
+  const [v,s] = useState<number>(1000000)
+  return <TestedComponent value={v} onChange={s} days minutes hours seconds />
+}
diff --git a/packages/frontend/src/components/form/DurationPicker.tsx 
b/packages/frontend/src/components/form/DurationPicker.tsx
new file mode 100644
index 0000000..2d51f2d
--- /dev/null
+++ b/packages/frontend/src/components/form/DurationPicker.tsx
@@ -0,0 +1,154 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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)
+*/
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useTranslator } from "../../i18n";
+import './DurationPicker.scss'
+
+export interface Props {
+  hours?: boolean;
+  minutes?: boolean;
+  seconds?: boolean;
+  days?: boolean;
+  onChange: (value: number) => void;
+  value: number
+}
+
+// inspiration taken from https://github.com/flurmbo/react-duration-picker
+export function DurationPicker({ days, hours, minutes, seconds, onChange, 
value }: Props): VNode {
+  const s = 1000
+  const m = s * 60
+  const h = m * 60
+  const d = h * 24
+  const i18n = useTranslator()
+  
+  return <div class="rdp-picker">
+    {days && <DurationColumn unit={i18n`days`} max={99}
+      value={Math.floor(value / d)}
+      onDecrease={value >= d ? () => onChange(value - d) : undefined}
+      onIncrease={value < 99 * d ? () => onChange(value + d) : undefined}
+      onChange={diff => onChange(value + diff * d)}
+    />}
+    {hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
+      value={Math.floor(value / h) % 24}
+      onDecrease={value >= h ? () => onChange(value - h) : undefined}
+      onIncrease={value < 99 * d ? () => onChange(value + h) : undefined}
+      onChange={diff => onChange(value + diff * h)}
+    />}
+    {minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
+      value={Math.floor(value / m) % 60}
+      onDecrease={value >= m ? () => onChange(value - m) : undefined}
+      onIncrease={value < 99 * d ? () => onChange(value + m) : undefined}
+      onChange={diff => onChange(value + diff * m)}
+    />}
+    {seconds && <DurationColumn unit={i18n`seconds`} max={59}
+      value={Math.floor(value / s) % 60}
+      onDecrease={value >= s ? () => onChange(value - s) : undefined}
+      onIncrease={value < 99 * d ? () => onChange(value + s) : undefined}
+      onChange={diff => onChange(value + diff * s)}
+    />}
+  </div>
+}
+
+interface ColProps {
+  unit: string,
+  min?: number,
+  max: number,
+  value: number,
+  onIncrease?: () => void;
+  onDecrease?: () => void;
+  onChange?: (diff: number) => void;
+}
+
+function InputNumber({ initial, onChange }: { initial: number, onChange: (n: 
number) => void }) {
+  const [value, handler] = useState<{v:string}>({
+    v: toTwoDigitString(initial)
+  })
+
+  return <input
+    value={value.v}
+    onBlur={(e) => onChange(parseInt(value.v, 10))}
+    onInput={(e) => {
+      e.preventDefault()
+      const n = Number.parseInt(e.currentTarget.value, 10);
+      if (isNaN(n)) return handler({v:toTwoDigitString(initial)}) 
+      return handler({v:toTwoDigitString(n)})
+    }}
+    style={{ width: 50, border: 'none', fontSize: 'inherit', background: 
'inherit' }} />
+}
+
+function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, 
onChange }: ColProps): VNode {
+
+  const cellHeight = 35
+  return (
+    <div class="rdp-column-container">
+      <div class="rdp-masked-div">
+        <hr class="rdp-reticule" style={{ top: cellHeight * 2 - 1 }} />
+        <hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
+
+        <div class="rdp-column" style={{ top: 0 }}>
+
+          <div class="rdp-cell" key={value - 1}>
+            {onDecrease && <button style={{ width: '100%', textAlign: 
'center', margin: 5 }}
+              onClick={onDecrease}>
+              <span class="icon">
+                <i class="mdi mdi-chevron-up" />
+              </span>
+            </button>}
+          </div>
+          <div class="rdp-cell" key={value - 1}>
+            {value > min ? toTwoDigitString(value - 1) : ''}
+          </div>
+          <div class="rdp-cell rdp-center" key={value}>
+            {onChange ?
+              <InputNumber initial={value} onChange={(n) => onChange(n - 
value)} /> :
+              toTwoDigitString(value)
+            }
+            <div>{unit}</div>
+          </div>
+
+          <div class="rdp-cell" key={value + 1}>
+            {value < max ? toTwoDigitString(value + 1) : ''}
+          </div>
+
+          <div class="rdp-cell" key={value - 1}>
+            {onIncrease && <button style={{ width: '100%', textAlign: 
'center', margin: 5 }}
+              onClick={onIncrease}>
+              <span class="icon">
+                <i class="mdi mdi-chevron-down" />
+              </span>
+            </button>}
+          </div>
+
+        </div>
+      </div>
+    </div>
+  );
+}
+
+
+function toTwoDigitString(n: number) {
+  if (n < 10) {
+    return `0${n}`;
+  }
+  return `${n}`;
+}
\ No newline at end of file
diff --git a/packages/frontend/src/components/form/FormProvider.tsx 
b/packages/frontend/src/components/form/FormProvider.tsx
index d1fb77b..4855553 100644
--- a/packages/frontend/src/components/form/FormProvider.tsx
+++ b/packages/frontend/src/components/form/FormProvider.tsx
@@ -65,7 +65,7 @@ export function useFormContext<T>() {
 }
 
 export type FormErrors<T> = {
-  [P in keyof T]?: string
+  [P in keyof T]?: string | FormErrors<T[P]>
 }
 
 export type FormtoStr<T> = {
diff --git a/packages/frontend/src/components/form/Input.tsx 
b/packages/frontend/src/components/form/Input.tsx
index 4147f2c..f7e0b5c 100644
--- a/packages/frontend/src/components/form/Input.tsx
+++ b/packages/frontend/src/components/form/Input.tsx
@@ -59,8 +59,8 @@ export function Input<T>({ name, readonly, placeholder, 
tooltip, label, expand,
             onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => 
onChange(fromStr(e.currentTarget.value))} />
           {help}
           {children}
-          { required && <span class="icon is-danger is-right">
-            <i class="mdi mdi-star" />
+          { required && <span class="icon has-text-danger is-right">
+            <i class="mdi mdi-alert" />
           </span> }
         </p>
         {error && <p class="help is-danger">{error}</p>}
diff --git a/packages/frontend/src/components/form/InputDate.tsx 
b/packages/frontend/src/components/form/InputDate.tsx
index 654d608..614e44a 100644
--- a/packages/frontend/src/components/form/InputDate.tsx
+++ b/packages/frontend/src/components/form/InputDate.tsx
@@ -36,7 +36,7 @@ export function InputDate<T>({ name, readonly, label, 
placeholder, help, tooltip
   const [opened, setOpened] = useState(false)
   const i18n = useTranslator()
 
-  const { error, value, onChange } = useField<T>(name);
+  const { error, required, value, onChange } = useField<T>(name);
 
   let strValue = ''
   if (!value) {
@@ -61,12 +61,15 @@ export function InputDate<T>({ name, readonly, label, 
placeholder, help, tooltip
     <div class="field-body is-flex-grow-3">
       <div class="field">
         <div class="field has-addons">
-          <p class={expand ? "control is-expanded" : "control"}>
+          <p class={expand ? "control is-expanded has-icons-right" : "control 
has-icons-right"}>
             <input class="input" type="text"
               readonly value={strValue}
               placeholder={placeholder}
               onClick={() => { if (!readonly) setOpened(true) }}
             />
+          { required && <span class="icon has-text-danger is-right">
+              <i class="mdi mdi-alert" />
+            </span> }
             {help}
           </p>
           <div class="control" onClick={() => { if (!readonly) setOpened(true) 
}}>
diff --git a/packages/frontend/src/components/form/InputDuration.tsx 
b/packages/frontend/src/components/form/InputDuration.tsx
index 30afd65..76e9022 100644
--- a/packages/frontend/src/components/form/InputDuration.tsx
+++ b/packages/frontend/src/components/form/InputDuration.tsx
@@ -18,33 +18,99 @@
 *
 * @author Sebastian Javier Marchano (sebasjm)
 */
-import { formatDuration, intervalToDuration } from "date-fns";
+import { intervalToDuration, formatDuration } from "date-fns";
 import { h, VNode } from "preact";
-import { RelativeTime } from "../../declaration";
+import { useState } from "preact/hooks";
+import { Translate, useTranslator } from "../../i18n";
+import { SimpleModal } from "../modal";
+import { DurationPicker } from "./DurationPicker";
 import { InputProps, useField } from "./useField";
-import { InputWithAddon } from "./InputWithAddon";
 
 export interface Props<T> extends InputProps<T> {
   expand?: boolean;
   readonly?: boolean;
+  withForever?: boolean;
 }
 
-export function InputDuration<T>({ name, expand, placeholder, tooltip, label, 
help, readonly }: Props<keyof T>): VNode {
-  const { value } = useField<T>(name);
-  return <InputWithAddon<T> name={name} readonly={readonly} 
addonAfter={readableDuration(value as any)}
-    expand={expand}
-    label={label} placeholder={placeholder} help={help} tooltip={tooltip}
-    toStr={(v?: RelativeTime) => `${(v && v.d_ms !== "forever" && v.d_ms ? 
v.d_ms : '')}`}
-    fromStr={(v: string) => ({ d_ms: (parseInt(v, 10)) || undefined })}
-  />
-}
+export function InputDuration<T>({ name, expand, placeholder, tooltip, label, 
help, readonly, withForever }: Props<keyof T>): VNode {
+  const [opened, setOpened] = useState(false)
+  const i18n = useTranslator()
 
-function readableDuration(duration?: RelativeTime): string {
-  if (!duration) return ""
-  if (duration.d_ms === "forever") return "forever"
-  try {
-    return formatDuration(intervalToDuration({ start: 0, end: duration.d_ms }))
-  } catch (e) {
-    return ''
+  const { error, required, value, onChange } = useField<T>(name);
+  let strValue = ''
+  if (!value) {
+    strValue = ''
+  } else if (value.d_ms === 'forever') {
+    strValue = i18n`forever`
+  } else {
+    strValue = formatDuration(intervalToDuration({ start: 0, end: value.d_ms 
}), {
+      locale: {
+        formatDistance: (name, value) => {
+          switch(name) {
+            case 'xMonths': return i18n`${value}M`;
+            case 'xYears': return i18n`${value}Y`;
+            case 'xDays': return i18n`${value}d`;
+            case 'xHours': return i18n`${value}h`;
+            case 'xMinutes': return i18n`${value}min`;
+            case 'xSeconds': return i18n`${value}sec`;
+          }
+        },
+        localize: {
+          day: () => 's',
+          month: () => 'm',
+          ordinalNumber: () => 'th',
+          dayPeriod: () => 'p',
+          quarter: () => 'w',
+          era: () => 'e'
+        }
+      },
+    })
   }
+
+  return <div class="field is-horizontal">
+    <div class="field-label is-normal">
+      <label class="label">
+        {label}
+        {tooltip && <span class="icon" data-tooltip={tooltip}>
+          <i class="mdi mdi-information" />
+        </span>}
+      </label>
+
+    </div>
+    <div class="field-body is-flex-grow-3">
+      <div class="field">
+        <div class="field has-addons">
+          <p class={expand ? "control is-expanded " : "control "}>
+            <input class="input" type="text"
+              readonly value={strValue}
+              placeholder={placeholder}
+              onClick={() => { if (!readonly) setOpened(true) }}
+            />
+            {required && <span class="icon has-text-danger is-right">
+              <i class="mdi mdi-alert" />
+            </span>}
+            {help}
+          </p>
+          <div class="control" onClick={() => { if (!readonly) setOpened(true) 
}}>
+            <a class="button is-static" >
+              <span class="icon"><i class="mdi mdi-clock" /></span>
+            </a>
+          </div>
+        </div>
+        {error && <p class="help is-danger">{error}</p>}
+      </div>
+      {!readonly && <span data-tooltip={i18n`change value to empty`}>
+        <button class="button is-info mr-3" onClick={() => onChange(undefined 
as any)} ><Translate>clear</Translate></button>
+      </span>}
+      {withForever && <span data-tooltip={i18n`change value to never`}>
+        <button class="button is-info" onClick={() => onChange({ d_ms: 
'forever' } as any)}><Translate>forever</Translate></button>
+      </span>}
+    </div>
+    {opened && <SimpleModal onCancel={() => setOpened(false)}>
+      <DurationPicker days hours minutes
+        value={!value || value.d_ms === 'forever' ? 0 : value.d_ms}
+        onChange={(v) => { onChange({ d_ms: v } as any) }}
+      />
+    </SimpleModal>}
+  </div>
 }
diff --git a/packages/frontend/src/components/form/InputGroup.tsx 
b/packages/frontend/src/components/form/InputGroup.tsx
index 0720cfb..146b977 100644
--- a/packages/frontend/src/components/form/InputGroup.tsx
+++ b/packages/frontend/src/components/form/InputGroup.tsx
@@ -28,19 +28,23 @@ export interface Props<T> {
   label: ComponentChildren;
   tooltip?: ComponentChildren;
   alternative?: ComponentChildren;
+  initialActive?: boolean;
 }
 
-export function InputGroup<T>({ name, label, children, tooltip, alternative }: 
Props<keyof T>): VNode {
-  const [active, setActive] = useState(false);
+export function InputGroup<T>({ name, label, children, tooltip, alternative, 
initialActive }: Props<keyof T>): VNode {
+  const [active, setActive] = useState(initialActive);
   const group = useGroupField<T>(name);
 
   return <div class="card">
     <header class="card-header">
-      <p class={!group?.hasError ? "card-header-title" : "card-header-title 
has-text-danger"}>
+      <p class="card-header-title">
         {label}
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
+        {group?.hasError && <span class="icon has-text-danger" 
data-tooltip={tooltip}>
+          <i class="mdi mdi-alert" />
+        </span>}
       </p>
       <button class="card-header-icon" aria-label="more options" onClick={(): 
void => setActive(!active)}>
         <span class="icon">
diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx 
b/packages/frontend/src/components/form/InputWithAddon.tsx
index cb84c86..d2905df 100644
--- a/packages/frontend/src/components/form/InputWithAddon.tsx
+++ b/packages/frontend/src/components/form/InputWithAddon.tsx
@@ -19,7 +19,6 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 import { ComponentChildren, h, VNode } from "preact";
-import { useTranslator } from "../../i18n";
 import { InputProps, useField } from "./useField";
 
 export interface Props<T> extends InputProps<T> {
@@ -60,8 +59,8 @@ export function InputWithAddon<T>({ name, readonly, 
addonBefore, children, expan
               placeholder={placeholder} readonly={readonly}
               name={String(name)} value={toStr(value)}
               onChange={(e): void => onChange(fromStr(e.currentTarget.value))} 
/>
-            {required && <span class="icon is-danger is-right">
-              <i class="mdi mdi-star" />
+            {required && <span class="icon has-text-danger is-right">
+              <i class="mdi mdi-alert" />
             </span>}
             {help}
             {children}
diff --git a/packages/frontend/src/components/form/useField.tsx 
b/packages/frontend/src/components/form/useField.tsx
index c552711..8479d7a 100644
--- a/packages/frontend/src/components/form/useField.tsx
+++ b/packages/frontend/src/components/form/useField.tsx
@@ -29,7 +29,7 @@ interface Use<V> {
   initial: any;
   onChange: (v: V) => void;
   toStr: (f: V | undefined) => string;
-  fromStr: (v: string) => V 
+  fromStr: (v: string) => V
 }
 
 export function useField<T>(name: keyof T): Use<T[typeof name]> {
@@ -42,16 +42,16 @@ export function useField<T>(name: keyof T): Use<T[typeof 
name]> {
       return setValueDeeper(prev, String(field).split('.'), value)
     })
   }
-  
-  const defaultToString = ((f?: V):string => String(!f ? '': f))
-  const defaultFromString = ((v: string):V => v as any)
+
+  const defaultToString = ((f?: V): string => String(!f ? '' : f))
+  const defaultFromString = ((v: string): V => v as any)
   const value = readField(object, String(name))
   const initial = readField(initialObject, String(name))
   const isDirty = value !== initial
   const hasError = readField(errors, String(name))
   return {
     error: isDirty ? hasError : undefined,
-    required: !isDirty && hasError, 
+    required: !isDirty && hasError,
     value,
     initial,
     onChange: updateField(name) as any,
@@ -73,7 +73,7 @@ const readField = (object: any, name: string) => {
 const setValueDeeper = (object: any, names: string[], value: any): any => {
   if (names.length === 0) return value
   const [head, ...rest] = names
-  return {...object, [head]: setValueDeeper(object[head] || {}, rest, value) }
+  return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) }
 }
 
 export interface InputProps<T> {
diff --git a/packages/frontend/src/components/form/useGroupField.tsx 
b/packages/frontend/src/components/form/useGroupField.tsx
index 0a809e4..a73f464 100644
--- a/packages/frontend/src/components/form/useGroupField.tsx
+++ b/packages/frontend/src/components/form/useGroupField.tsx
@@ -30,8 +30,11 @@ export function useGroupField<T>(name: keyof T): Use {
   if (!f)
     return {};
 
-  const RE = new RegExp(`^${name}`);
   return {
-    hasError: Object.keys(f.errors).some(e => RE.test(e))
+    hasError: readField(f.errors, String(name))
   };
 }
+
+const readField = (object: any, name: string) => {
+  return name.split('.').reduce((prev, current) => prev && prev[current], 
object)
+}
diff --git 
a/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx 
b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
index 2bfbeda..2d7f93f 100644
--- a/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
@@ -79,6 +79,7 @@ export function DefaultInstanceFormFields({ readonlyId, 
showId }: { readonlyId?:
 
     <InputDuration<Entity> name="default_wire_transfer_delay"
       label={i18n`Default wire transfer delay`}
+      withForever
       tooltip={i18n`Maximum time an exchange is allowed to delay wiring funds 
to the merchant, enabling it to aggregate smaller payments into larger wire 
transfers and reducing wire fees.`} />
 
   </Fragment>;
diff --git a/packages/frontend/src/components/modal/index.tsx 
b/packages/frontend/src/components/modal/index.tsx
index 963dc05..a427215 100644
--- a/packages/frontend/src/components/modal/index.tsx
+++ b/packages/frontend/src/components/modal/index.tsx
@@ -82,6 +82,18 @@ export function ContinueModal({ active, description, 
onCancel, onConfirm, childr
   </div>
 }
 
+export function SimpleModal({onCancel, children}: any):VNode {
+  return <div class="modal is-active">
+  <div class="modal-background " onClick={onCancel} />
+  <div class="modal-card">
+    <section class="modal-card-body is-main-section">
+      {children}
+    </section>
+  </div>
+  <button class="modal-close is-large " aria-label="close" onClick={onCancel} 
/>
+</div>
+}
+
 export function ClearConfirmModal({ description, onCancel, onClear, onConfirm, 
children }: Props & { onClear?: () => void }): VNode {
   return <div class="modal is-active">
     <div class="modal-background " onClick={onCancel} />
diff --git 
a/packages/frontend/src/paths/instance/products/list/List.stories.tsx 
b/packages/frontend/src/components/product/InventoryProductForm.stories.tsx
similarity index 65%
copy from packages/frontend/src/paths/instance/products/list/List.stories.tsx
copy to 
packages/frontend/src/components/product/InventoryProductForm.stories.tsx
index beae83b..6504d85 100644
--- a/packages/frontend/src/paths/instance/products/list/List.stories.tsx
+++ b/packages/frontend/src/components/product/InventoryProductForm.stories.tsx
@@ -20,17 +20,14 @@
 */
 
 import { h, VNode, FunctionalComponent } from 'preact';
-import { CardTable as TestedComponent } from './Table';
+import { InventoryProductForm as TestedComponent } from 
'./InventoryProductForm';
 
 
 export default {
-  title: 'Pages/Product/List',
+  title: 'Components/Product/Add',
   component: TestedComponent,
   argTypes: {
-    onCreate: { action: 'onCreate' },
-    onSelect: { action: 'onSelect' },
-    onDelete: { action: 'onDelete' },
-    onUpdate: { action: 'onUpdate' },
+    onAddProduct: { action: 'onAddProduct' },
   },
 };
 
@@ -40,19 +37,22 @@ function createExample<Props>(Component: 
FunctionalComponent<Props>, props: Part
   return r
 }
 
+export const WithASimpleList = createExample(TestedComponent, {
+  inventory:[{
+    id: 'this id',
+    description: 'this is the description',
+  } as any]
+});
 
-export const Example = createExample(TestedComponent, {
-  instances: [{
-    id: 'orderid',
-    description: 'description1',
-    description_i18n: {} as any,
-    image: '',
-    price: 'TESTKUDOS:10',
-    taxes: [],
-    total_lost: 10,
-    total_sold: 5,
-    total_stock: 15,
-    unit: 'bar',
-    address: {}
-  }]
+export const WithAProductSelected = createExample(TestedComponent, {
+  inventory:[],
+  currentProducts: {
+    thisid: {
+      quantity: 1,
+      product: {
+        id: 'asd',
+        description: 'asdsadsad',
+      } as any
+    }
+  }
 });
diff --git 
a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx 
b/packages/frontend/src/components/product/InventoryProductForm.tsx
similarity index 76%
rename from 
packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
rename to packages/frontend/src/components/product/InventoryProductForm.tsx
index dde4505..6f05f26 100644
--- 
a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
+++ b/packages/frontend/src/components/product/InventoryProductForm.tsx
@@ -15,12 +15,12 @@
  */
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
-import { FormProvider, FormErrors } from 
"../../../../components/form/FormProvider";
-import { InputNumber } from "../../../../components/form/InputNumber";
-import { InputSearchProduct } from 
"../../../../components/form/InputSearchProduct";
-import { MerchantBackend, WithId } from "../../../../declaration";
-import { Translate, useTranslator } from "../../../../i18n";
-import { ProductMap } from "./CreatePage";
+import { FormProvider, FormErrors } from "../form/FormProvider";
+import { InputNumber } from "../form/InputNumber";
+import { InputSearchProduct } from "../form/InputSearchProduct";
+import { MerchantBackend, WithId } from "../../declaration";
+import { Translate, useTranslator } from "../../i18n";
+import { ProductMap } from "../../paths/instance/orders/create/CreatePage";
 
 type Form = {
   product: MerchantBackend.Products.ProductDetail & WithId,
@@ -77,10 +77,19 @@ export function InventoryProductForm({ currentProducts, 
onAddProduct, inventory
   }
 
   return <FormProvider<Form> errors={errors} object={state} 
valueHandler={setState}>
-    <InputSearchProduct selected={state.product} onChange={(p) => setState(v 
=> ({ ...v, product: p }))} products={inventory}  />
-    { state.product && !productWithInfiniteStock && <InputNumber<Form> 
name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be 
added`} /> }
-    <div class="buttons is-right mt-5">
-      <button class="button is-success" disabled={!state.product} 
onClick={submit}><Translate>Add</Translate></button>
-    </div>
+    <InputSearchProduct selected={state.product} onChange={(p) => setState(v 
=> ({ ...v, product: p }))} products={inventory} />
+    { state.product && <div class="columns mt-5">
+      <div class="column is-four-fifths">
+        {!productWithInfiniteStock &&
+          <InputNumber<Form> name="quantity" label={i18n`Quantity`} 
tooltip={i18n`how many products will be added`} />
+        }
+      </div>
+      <div class="column">
+        <div class="buttons is-right">
+          <button class="button is-success" 
onClick={submit}><Translate>Add</Translate></button>
+        </div>
+      </div>
+    </div> }
+
   </FormProvider>
 }
diff --git 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
 b/packages/frontend/src/components/product/NonInventoryProductForm.tsx
similarity index 85%
rename from 
packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
rename to packages/frontend/src/components/product/NonInventoryProductForm.tsx
index 34e5213..3ba4764 100644
--- 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
+++ b/packages/frontend/src/components/product/NonInventoryProductForm.tsx
@@ -15,21 +15,20 @@
  */
 import { Fragment, h, VNode } from "preact";
 import { useCallback, useEffect, useState } from "preact/hooks";
-import { FormProvider, FormErrors } from 
"../../../../components/form/FormProvider";
-import { Input } from "../../../../components/form/Input";
-import { InputCurrency } from "../../../../components/form/InputCurrency";
-import { InputImage } from "../../../../components/form/InputImage";
-import { InputNumber } from "../../../../components/form/InputNumber";
-import { InputTaxes } from "../../../../components/form/InputTaxes";
-import { ConfirmModal } from "../../../../components/modal";
-import { MerchantBackend } from "../../../../declaration";
-import { useListener } from "../../../../hooks/listener";
-
+import * as yup from 'yup';
+import { FormErrors, FormProvider } from "../form/FormProvider";
+import { Input } from "../form/Input";
+import { InputCurrency } from "../form/InputCurrency";
+import { InputImage } from "../form/InputImage";
+import { InputNumber } from "../form/InputNumber";
+import { InputTaxes } from "../form/InputTaxes";
+import { MerchantBackend } from "../../declaration";
+import { useListener } from "../../hooks/listener";
+import { Translate, useTranslator } from "../../i18n";
 import {
   NonInventoryProductSchema as schema
-} from '../../../../schemas';
-import * as yup from 'yup';
-import { Translate, useTranslator } from "../../../../i18n";
+} from '../../schemas';
+
 
 type Entity = MerchantBackend.Product
 
@@ -63,11 +62,9 @@ export function NonInventoryProductFrom({ productToEdit, 
onAddProduct }: Props):
 
   const i18n = useTranslator()
 
-  console.log('submit form', submitForm)
-
   return <Fragment>
     <div class="buttons">
-      <button class="button is-success" onClick={() => 
setShowCreateProduct(true)} ><Translate>add product</Translate></button>
+      <button class="button is-success" onClick={() => 
setShowCreateProduct(true)} ><Translate>add custom product</Translate></button>
     </div>
     {showCreateProduct && <div class="modal is-active">
       <div class="modal-background " onClick={() => 
setShowCreateProduct(false)} />
@@ -125,7 +122,6 @@ export function ProductForm({ onSubscribe, initial }: 
ProductProps) {
   const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 
undefined)
 
   useEffect(() => {
-    console.log('has errors', hasErrors)
     onSubscribe(hasErrors ? undefined : submit)
   }, [submit, hasErrors])
 
diff --git a/packages/frontend/src/components/product/ProductForm.tsx 
b/packages/frontend/src/components/product/ProductForm.tsx
index 09b5744..9e8ac97 100644
--- a/packages/frontend/src/components/product/ProductForm.tsx
+++ b/packages/frontend/src/components/product/ProductForm.tsx
@@ -51,6 +51,7 @@ export function ProductForm({ onSubscribe, initial, 
alreadyExist, }: Props) {
     taxes: [],
     next_restock: { t_ms: 'never' },
     ...initial,
+    price: ':0',
     stock: !initial || initial.total_stock === -1 ? undefined : {
       current: initial.total_stock || 0,
       lost: initial.total_lost || 0,
diff --git a/packages/frontend/src/components/product/ProductList.tsx 
b/packages/frontend/src/components/product/ProductList.tsx
index b1486d6..c343b75 100644
--- a/packages/frontend/src/components/product/ProductList.tsx
+++ b/packages/frontend/src/components/product/ProductList.tsx
@@ -15,9 +15,9 @@
  */
 import { h, VNode } from "preact"
 import { MerchantBackend } from "../../declaration"
-import { multiplyPrice } from "../../utils/amount"
+import { Amounts } from "@gnu-taler/taler-util";
 import emptyImage from "../../assets/empty.png";
-import { Translate, useTranslator } from "../../i18n";
+import { Translate } from "../../i18n";
 
 interface Props {
   list: MerchantBackend.Product[],
@@ -28,7 +28,6 @@ interface Props {
   }[]
 }
 export function ProductList({ list, actions = [] }: Props): VNode {
-  const i18n = useTranslator()
   return <div class="table-container">
     <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
       <thead>
@@ -52,7 +51,7 @@ export function ProductList({ list, actions = [] }: Props): 
VNode {
               {entry.quantity === 0 ? '--' : `${entry.quantity} ${entry.unit}`}
             </td>
             <td >{entry.price}</td>
-            <td >{multiplyPrice(entry.price, entry.quantity)}</td>
+            <td 
>{Amounts.stringify(Amounts.mult(Amounts.parseOrThrow(entry.price), 
entry.quantity).amount)}</td>
             <td class="is-actions-cell right-sticky">
               {actions.map((a, i) => {
                 return <div key={i} class="buttons is-right">
diff --git a/packages/frontend/src/declaration.d.ts 
b/packages/frontend/src/declaration.d.ts
index 82bc694..1722a3d 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -844,9 +844,6 @@ export namespace MerchantBackend {
             fulfillment_url?: string;
         }
 
-        // FIXME: Where is this being used?
-        // type ProductSpecification = (MinimalInventoryProduct | Product);
-
         interface MinimalInventoryProduct {
             // Which product is requested (here mandatory!)
             product_id: string;
diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx 
b/packages/frontend/src/paths/admin/create/CreatePage.tsx
index 196284e..c9276d7 100644
--- a/packages/frontend/src/paths/admin/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/admin/create/CreatePage.tsx
@@ -130,7 +130,7 @@ export function CreatePage({ onCreate, onBack, forceId }: 
Props): VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" 
onClick={onBack}><Translate>Cancel</Translate></button>}
             <AsyncButton onClick={submit} disabled={!isTokenSet || hasErrors} 
data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star and 
choose authorization method` : 'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields and choose 
authorization method` : 'confirm operation'
             }><Translate>Confirm</Translate></AsyncButton>
           </div>
 
diff --git 
a/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx 
b/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx
index daf48c0..a3a2987 100644
--- a/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx
@@ -43,7 +43,7 @@ export const Example = createExample(TestedComponent, {
     default_max_deposit_fee: '',
     default_max_wire_fee: '',
     default_pay_delay: {
-      d_ms: 'forever'
+      d_ms: 1000*60*60
     },
     default_wire_fee_amortization: 1
   },
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index ecc2f0a..22fa2f3 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -19,10 +19,10 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 
-import { add } from "date-fns";
+import { add, isAfter, isBefore, isFuture } from "date-fns";
+import { Amounts } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
-import * as yup from 'yup';
 import { FormProvider, FormErrors } from 
"../../../../components/form/FormProvider";
 import { Input } from "../../../../components/form/Input";
 import { InputCurrency } from "../../../../components/form/InputCurrency";
@@ -34,9 +34,9 @@ import { useConfigContext } from "../../../../context/config";
 import { Duration, MerchantBackend, WithId } from "../../../../declaration";
 import { Translate, useTranslator } from "../../../../i18n";
 import { OrderCreateSchema as schema } from '../../../../schemas/index';
-import { multiplyPrice, rate, sumPrices } from "../../../../utils/amount";
-import { InventoryProductForm } from "./InventoryProductForm";
-import { NonInventoryProductFrom } from "./NonInventoryProductForm";
+import { rate } from "../../../../utils/amount";
+import { InventoryProductForm } from 
"../../../../components/product/InventoryProductForm";
+import { NonInventoryProductFrom } from 
"../../../../components/product/NonInventoryProductForm";
 
 interface Props {
   onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@@ -59,14 +59,15 @@ function with_defaults(config: InstanceConfig): 
Partial<Entity> {
   return {
     inventoryProducts: {},
     products: [],
-    pricing: {} as any,
+    pricing: {
+    },
     payments: {
       max_wire_fee: config.default_max_wire_fee,
       max_fee: config.default_max_deposit_fee,
       wire_fee_amortization: config.default_wire_fee_amortization,
       pay_deadline: defaultPayDeadline,
       refund_deadline: defaultPayDeadline,
-    } as Partial<Payments>,
+    },
     extra: ''
   };
 }
@@ -84,44 +85,91 @@ interface Pricing {
   order_price: string;
   summary: string;
 }
+interface Shipping {
+  delivery_date?: Date;
+  delivery_location?: MerchantBackend.Location;
+  fullfilment_url?: string;
+}
 interface Payments {
   refund_deadline?: Date;
   pay_deadline?: Date;
   auto_refund_deadline?: Date;
-  delivery_date?: Date;
-  delivery_location?: MerchantBackend.Location;
   max_fee?: string;
   max_wire_fee?: string;
   wire_fee_amortization?: number;
-  fullfilment_url?: string;
 }
 interface Entity {
   inventoryProducts: ProductMap,
   products: MerchantBackend.Product[],
-  pricing: Pricing;
-  payments: Payments;
+  pricing: Partial<Pricing>;
+  payments: Partial<Payments>;
+  shipping: Partial<Shipping>;
   extra: string;
 }
 
-export function CreatePage({ onCreate, onBack, instanceConfig, 
instanceInventory }: Props): VNode {
-  const [value, valueHandler] = useState(with_defaults(instanceConfig))
-  // const [errors, setErrors] = useState<FormErrors<Entity>>({})
+const stringIsValidJSON = (value: string) => {
+  try {
+    JSON.parse(value.trim())
+    return true
+  } catch {
+    return false
+  }
+}
 
-  const inventoryList = Object.values(value.inventoryProducts || {})
-  const productList = Object.values(value.products || {})
+function undefinedIfEmpty<T>(obj: T): T | undefined {
+  return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj : 
undefined
+}
 
-  let errors: FormErrors<Entity> = {}
-  try {
-    schema.validateSync(value, { abortEarly: false })
-  } catch (err) {
-    const yupErrors = err.inner as yup.ValidationError[]
-    errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, 
[cur.path]: cur.message }), {})
+export function CreatePage({ onCreate, onBack, instanceConfig, 
instanceInventory }: Props): VNode {
+  const [value, valueHandler] = useState(with_defaults(instanceConfig))
+  const config = useConfigContext()
+  const zero = Amounts.getZero(config.currency)
+
+  const inventoryList = Object.values(value.inventoryProducts || {});
+  const productList = Object.values(value.products || {});
+
+  const i18n = useTranslator();
+  
+  const errors: FormErrors<Entity> = {
+    pricing: undefinedIfEmpty({
+      summary: !value.pricing?.summary ? i18n`required` : undefined,
+      order_price: !value.pricing?.order_price ? i18n`required` : (
+        (Amounts.parse(value.pricing.order_price)?.value || 0) <= 0 ?
+          i18n`must be greater than 0` : undefined
+      )
+    }),
+    extra: value.extra && !stringIsValidJSON(value.extra) ? i18n`not a valid 
json` : undefined,
+    payments: undefinedIfEmpty({
+      refund_deadline: !value.payments?.refund_deadline ? i18n`required` : (
+        !isFuture(value.payments.refund_deadline) ? i18n`should be in the 
future` : (
+          value.payments.pay_deadline && 
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) ?
+            i18n`pay deadline cannot be before refund deadline` : undefined
+        )
+      ),
+      pay_deadline: !value.payments?.pay_deadline ? i18n`required` : (
+        !isFuture(value.payments.pay_deadline) ? i18n`should be in the future` 
: undefined
+      ),
+      auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined 
: (
+        !isFuture(value.payments.auto_refund_deadline) ? i18n`should be in the 
future` : (
+          !value.payments?.refund_deadline ? i18n`should have a refund 
deadline` : (
+            !isAfter(value.payments.refund_deadline, 
value.payments.auto_refund_deadline) ?
+              i18n`auto refund cannot be after refund deadline` : undefined
+          )
+        )
+      ),
+    }),
+    shipping: undefinedIfEmpty({
+      delivery_date: !value.shipping?.delivery_date ? undefined : (
+        !isFuture(value.shipping.delivery_date) ? i18n`should be in the 
future` : undefined
+      ),
+    }),
   }
   const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 
undefined)
 
   const submit = (): void => {
     const order = schema.cast(value)
     if (!value.payments) return;
+    if (!value.shipping) return;
 
     const request: MerchantBackend.Orders.PostOrderRequest = {
       order: {
@@ -134,9 +182,10 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
         refund_deadline: value.payments.refund_deadline ? { t_ms: 
Math.floor(value.payments.refund_deadline.getTime() / 1000) * 1000 } : 
undefined,
         max_fee: value.payments.max_fee,
         max_wire_fee: value.payments.max_wire_fee,
-        delivery_date: value.payments.delivery_date ? { t_ms: 
value.payments.delivery_date.getTime() } : undefined,
-        delivery_location: value.payments.delivery_location,
-        fulfillment_url: value.payments.fullfilment_url,
+
+        delivery_date: value.shipping.delivery_date ? { t_ms: 
value.shipping.delivery_date.getTime() } : undefined,
+        delivery_location: value.shipping.delivery_location,
+        fulfillment_url: value.shipping.fullfilment_url,
       },
       inventory_products: inventoryList.map(p => ({
         product_id: p.product.id,
@@ -147,8 +196,6 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
     onCreate(request);
   }
 
-  const config = useConfigContext()
-
   const addProductToTheInventoryList = (product: 
MerchantBackend.Products.ProductDetail & WithId, quantity: number) => {
     valueHandler(v => {
       const inventoryProducts = { ...v.inventoryProducts }
@@ -182,38 +229,47 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
 
   const [editingProduct, setEditingProduct] = useState<MerchantBackend.Product 
| undefined>(undefined)
 
-  const totalPriceInventory = inventoryList.reduce((prev, cur) => 
sumPrices(prev, multiplyPrice(cur.product.price, cur.quantity)), 
`${config.currency}:0`)
-  const totalPriceProducts = productList.reduce((prev, cur) => sumPrices(prev, 
multiplyPrice(cur.price, cur.quantity)), `${config.currency}:0`)
+  const totalPriceInventory = inventoryList.reduce((prev, cur) => {
+    const p = Amounts.parseOrThrow(cur.product.price)
+    return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount
+  }, zero)
 
-  const hasProducts = inventoryList.length > 0 || productList.length > 0
-  const totalPrice = sumPrices(totalPriceInventory, totalPriceProducts)
-
-  useEffect(() => {
-    valueHandler(v => {
-      return ({
-        ...v, pricing: {
-          ...v.pricing!,
-          products_price: totalPrice,
-          order_price: totalPrice,
-        }
-      })
-    })
-  }, [hasProducts, totalPrice])
+  const totalPriceProducts = productList.reduce((prev, cur) => {
+    const p = Amounts.parseOrThrow(cur.price)
+    return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount
+  }, zero)
 
+  const hasProducts = inventoryList.length > 0 || productList.length > 0
+  const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts)
 
-  const discountOrRise = rate(value.pricing?.order_price || 
`${config.currency}:0`, totalPrice)
+  const totalAsString = Amounts.stringify(totalPrice.amount);
+  const allProducts = productList.concat(inventoryList.map(asProduct))
 
   useEffect(() => {
     valueHandler(v => {
       return ({
         ...v, pricing: {
-          ...v.pricing!
+          ...v.pricing,
+          products_price: hasProducts ? totalAsString : undefined,
+          order_price: hasProducts ? totalAsString : undefined,
+          // products_price: Amounts.stringify(totalPrice.amount),
+          // order_price: Amounts.stringify(totalPrice.amount),
         }
       })
     })
-  }, [value.pricing?.order_price])
+  }, [hasProducts, totalAsString])
+
+  const discountOrRise = rate(value.pricing?.order_price || 
`${config.currency}:0`, totalAsString)
 
-  const i18n = useTranslator()
+  // useEffect(() => {
+  //   valueHandler(v => {
+  //     return ({
+  //       ...v, pricing: {
+  //         ...v.pricing!
+  //       }
+  //     })
+  //   })
+  // }, [value.pricing?.order_price])
 
   return <div>
 
@@ -222,55 +278,40 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
         <div class="column" />
         <div class="column is-four-fifths">
 
-          <InputGroup name="inventory_products" label={i18n`Manage products 
from inventory in order`} alternative={
-            inventoryList.length > 0 &&
-            // FIXME: translating plural singular
-            <p>
-              {inventoryList.length} products
-              with a total price of {totalPriceInventory}.
+          {/* // FIXME: translating plural singular */}
+          <InputGroup name="inventory_products" label={i18n`Manage products in 
order`} alternative={
+            allProducts.length > 0 && <p>
+              {allProducts.length} products
+              with a total price of {totalAsString}.
             </p>
-          } tooltip={i18n`Manage list of products from managed inventory 
included in the order.`}>
+          } tooltip={i18n`Manage list of products in the order.`}>
+
             <InventoryProductForm
               currentProducts={value.inventoryProducts || {}}
               onAddProduct={addProductToTheInventoryList}
               inventory={instanceInventory}
             />
 
-            {inventoryList.length > 0 &&
-              <ProductList list={inventoryList.map(asProduct)}
-                actions={[{
-                  name: i18n`Remove`,
-                  tooltip: i18n`Remove this product from the order.`,
-                  handler: (e) => 
removeProductFromTheInventoryList(e.product_id!)
-                }]}
-              />
-            }
-          </InputGroup>
-
-          <InputGroup name="products" label={i18n`Manage products outside of 
inventory in order`} alternative={
-            productList.length > 0 && <p>
-            // FIXME: translating plural singular
-              {productList.length} products
-              with a total price of {totalPriceProducts}.
-            </p>
-          } tooltip={i18n`Manage list of products without inventory management 
included in the order.`}>
             <NonInventoryProductFrom productToEdit={editingProduct} 
onAddProduct={(p) => {
               setEditingProduct(undefined)
               return addNewProduct(p)
             }} />
 
-            {productList.length > 0 &&
-              <ProductList list={productList}
+            {allProducts.length > 0 &&
+              <ProductList list={allProducts}
                 actions={[{
                   name: i18n`Remove`,
                   tooltip: i18n`Remove this product from the order.`,
                   handler: (e, index) => {
-                    removeFromNewProduct(index);
-                    setEditingProduct(e);
+                    if (e.product_id) {
+                      removeProductFromTheInventoryList(e.product_id)
+                    } else {
+                      removeFromNewProduct(index);
+                      setEditingProduct(e);
+                    }
                   }
                 }]}
               />
-
             }
           </InputGroup>
 
@@ -280,7 +321,7 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
                 <InputCurrency name="pricing.products_price" label={i18n`Total 
price`} readonly tooltip={i18n`total product price added up`} />
                 <InputCurrency name="pricing.order_price"
                   label={i18n`Total price`}
-                  addonAfter={value.pricing?.order_price !== totalPrice && 
(discountOrRise < 1 ?
+                  addonAfter={discountOrRise > 0 && (discountOrRise < 1 ?
                     `discount of %${Math.round((1 - discountOrRise) * 100)}` :
                     `rise of %${Math.round((discountOrRise - 1) * 100)}`)
                   }
@@ -292,24 +333,28 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
 
             <Input name="pricing.summary" inputType="multiline" 
label={i18n`Summary`} tooltip={i18n`Title of the order to be shown to the 
customer`} />
 
+            <InputGroup name="shipping" label={i18n`Shipping and Fulfillment`} 
initialActive >
+              <InputDate name="shipping.delivery_date" label={i18n`Delivery 
date`} tooltip={i18n`Deadline for physical delivery assured by the merchant.`} 
/>
+              {value.shipping?.delivery_date &&
+                <InputGroup name="shipping.delivery_location" 
label={i18n`Location`} tooltip={i18n`address where the products will be 
delivered`} >
+                  <InputLocation name="shipping.delivery_location" />
+                </InputGroup>
+              }
+              <Input name="shipping.fullfilment_url" label={i18n`Fulfillment 
URL`} tooltip={i18n`URL to which the user will be redirected after successful 
payment.`} />
+            </InputGroup>
+
             <InputGroup name="payments" label={i18n`Taler payment options`} 
tooltip={i18n`Override default Taler payment settings for this order`}>
-              <InputDate name="payments.auto_refund_deadline" 
label={i18n`Auto-refund deadline`} tooltip={i18n`Time until which the wallet 
will automatically check for refunds without user interaction.`} />
-              <InputDate name="payments.refund_deadline" label={i18n`Refund 
deadline`} tooltip={i18n`Time until which the order can be refunded by the 
merchant.`} />
               <InputDate name="payments.pay_deadline" label={i18n`Payment 
deadline`} tooltip={i18n`Deadline for the customer to pay for the offer before 
it expires. Inventory products will be reserved until this deadline.`} />
+              <InputDate name="payments.refund_deadline" label={i18n`Refund 
deadline`} tooltip={i18n`Time until which the order can be refunded by the 
merchant.`} />
+              <InputDate name="payments.auto_refund_deadline" 
label={i18n`Auto-refund deadline`} tooltip={i18n`Time until which the wallet 
will automatically check for refunds without user interaction.`} />
 
-              <InputDate name="payments.delivery_date" label={i18n`Delivery 
date`} tooltip={i18n`Deadline for physical delivery assured by the merchant.`} 
/>
-              {value.payments?.delivery_date && <InputGroup 
name="payments.delivery_location" label={i18n`Location`} tooltip={i18n`address 
where the products will be delivered`} >
-                <InputLocation name="payments.delivery_location" />
-              </InputGroup>}
-
-              <InputCurrency name="payments.max_fee" label={i18n`Maximum 
deposit fee`}  tooltip={i18n`Maximum deposit fees the merchant is willing to 
cover for this order. Higher deposit fees must be covered in full by the 
consumer.`} />
-              <InputCurrency name="payments.max_wire_fee" label={i18n`Maximum 
wire fee`} tooltip={i18n`Maximum aggregate wire fees the merchant is willing to 
cover for this order. Wire fees exceeding this amount are to be covered by the 
customers.`}/>
-              <Input name="payments.wire_fee_amortization" label={i18n`Wire 
fee amortization`} tooltip={i18n`Factor by which wire fees exceeding the above 
threshold are divided to determine the share of excess wire fees to be paid 
explicitly by the consumer.`}/>
-              <Input name="payments.fullfilment_url" label={i18n`Fulfillment 
URL`} tooltip={i18n`URL to which the user will be redirected after successful 
payment.`} />
+              <InputCurrency name="payments.max_fee" label={i18n`Maximum 
deposit fee`} tooltip={i18n`Maximum deposit fees the merchant is willing to 
cover for this order. Higher deposit fees must be covered in full by the 
consumer.`} />
+              <InputCurrency name="payments.max_wire_fee" label={i18n`Maximum 
wire fee`} tooltip={i18n`Maximum aggregate wire fees the merchant is willing to 
cover for this order. Wire fees exceeding this amount are to be covered by the 
customers.`} />
+              <Input name="payments.wire_fee_amortization" label={i18n`Wire 
fee amortization`} tooltip={i18n`Factor by which wire fees exceeding the above 
threshold are divided to determine the share of excess wire fees to be paid 
explicitly by the consumer.`} />
             </InputGroup>
 
             <InputGroup name="extra" label={i18n`Additional information`} 
tooltip={i18n`Custom information to be included in the contract for this 
order.`}>
-              <Input name="extra" inputType="multiline" label={`Value`}  
tooltip={i18n`You must enter a value in JavaScript Object Notation (JSON).`} />
+              <Input name="extra" inputType="multiline" label={`Value`} 
tooltip={i18n`You must enter a value in JavaScript Object Notation (JSON).`} />
             </InputGroup>
           </FormProvider>
 
diff --git a/packages/frontend/src/paths/instance/orders/create/index.tsx 
b/packages/frontend/src/paths/instance/orders/create/index.tsx
index 71f5b7f..c447c4b 100644
--- a/packages/frontend/src/paths/instance/orders/create/index.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/index.tsx
@@ -79,4 +79,4 @@ export default function OrderCreate({ onConfirm, onBack, 
onLoadError, onNotFound
       instanceInventory={inventoryResult.data}
       />
   </Fragment>
-}
\ No newline at end of file
+}
diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx 
b/packages/frontend/src/paths/instance/orders/list/Table.tsx
index ed8ee3a..64a5d22 100644
--- a/packages/frontend/src/paths/instance/orders/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -30,9 +30,11 @@ import { InputSelector } from 
"../../../../components/form/InputSelector";
 import { ConfirmModal } from "../../../../components/modal";
 import { MerchantBackend, WithId } from "../../../../declaration";
 import { Translate, useTranslator } from "../../../../i18n";
-import { RefundSchema as RefundSchema } from "../../../../schemas";
-import { mergeRefunds, subtractPrices, sumPrices } from 
"../../../../utils/amount";
+import { RefundSchema } from "../../../../schemas";
+import { mergeRefunds } from "../../../../utils/amount";
 import { AMOUNT_ZERO_REGEX } from "../../../../utils/constants";
+import { Amounts } from "@gnu-taler/taler-util";
+import { useConfigContext } from "../../../../context/config";
 
 type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId
 interface Props {
@@ -174,13 +176,14 @@ export function RefundModal({ order, onCancel, onConfirm 
}: RefundModalProps): V
     }
   }
 
-  const refunds = (order.order_status === 'paid' ? order.refund_details : [])
-    .reduce(mergeRefunds, [])
-  const totalRefunded = refunds.map(r => r.amount).reduce((p, c) => 
sumPrices(c, p), ':0')
-  const orderPrice = (order.order_status === 'paid' ? 
order.contract_terms.amount : undefined)
-  const totalRefundable = !orderPrice ? undefined : (refunds.length ? 
subtractPrices(orderPrice, totalRefunded) : orderPrice)
+  const refunds = (order.order_status === 'paid' ? order.refund_details : 
[]).reduce(mergeRefunds, [])
 
-  const isRefundable = totalRefundable && 
!AMOUNT_ZERO_REGEX.test(totalRefundable)
+  const config = useConfigContext()
+  const totalRefunded = refunds.map(r => r.amount).reduce((p, c) => 
Amounts.add(p, Amounts.parseOrThrow(c)).amount, 
Amounts.getZero(config.currency) )
+  const orderPrice = (order.order_status === 'paid' ? 
Amounts.parseOrThrow(order.contract_terms.amount) : undefined)
+  const totalRefundable = !orderPrice ? 
Amounts.getZero(totalRefunded.currency) : (refunds.length ? 
Amounts.sub(orderPrice, totalRefunded).amount : orderPrice)
+
+  const isRefundable = Amounts.isNonZero(totalRefundable)
   //FIXME: parameters in the translation
   return <ConfirmModal description="refund" danger active onCancel={onCancel} 
onConfirm={validateAndConfirm}>
     {refunds.length > 0 && <div class="columns">
@@ -212,7 +215,7 @@ export function RefundModal({ order, onCancel, onConfirm }: 
RefundModalProps): V
 
     {isRefundable && <FormProvider<State> errors={errors} object={form} 
valueHandler={(d) => setValue(d as any)}>
       <InputCurrency<State> name="refund" label={i18n`Refund`} 
tooltip={i18n`amount to be refunded`}>
-        <Translate>Max refundable:</Translate> {totalRefundable}
+        <Translate>Max refundable:</Translate> 
{Amounts.stringify(totalRefundable)}
       </InputCurrency>
       <InputSelector name="mainReason" label={i18n`Reason`} 
values={[i18n`duplicated`, i18n`requested by the customer`, i18n`other`]} 
tooltip={i18n`why this order is being refunded`} />
       {form.mainReason && <Input<State> label={i18n`Description`} 
name="description" tooltip={i18n`more information to give context`} />}
diff --git 
a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
index 0f2411b..ed669f6 100644
--- a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
@@ -53,7 +53,7 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} 
><Translate>Cancel</Translate></button>}
             <AsyncButton onClick={submitForm} data-tooltip={
-              !submitForm ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              !submitForm ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } 
disabled={!submitForm}><Translate>Confirm</Translate></AsyncButton>
           </div>
 
diff --git 
a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx 
b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
index 32d67c0..d7eb3d1 100644
--- a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
@@ -66,7 +66,7 @@ export function UpdatePage({ product, onUpdate, onBack }: 
Props): VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} 
><Translate>Cancel</Translate></button>}
             <AsyncButton onClick={submitForm} data-tooltip={
-              !submitForm ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              !submitForm ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } 
disabled={!submitForm}><Translate>Confirm</Translate></AsyncButton>
           </div>
         </div>
diff --git 
a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
index 05f5e06..2e85cf9 100644
--- a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
@@ -92,7 +92,7 @@ function ViewStep({ step, setCurrentStep, reserve, onBack, 
submitForm, setReserv
               setExchangeQueryError(r.message)
             })
           }} data-tooltip={
-            hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+            hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
           } disabled={hasErrors} ><Translate>Next</Translate></AsyncButton>
         </div>
       </Fragment>
@@ -113,7 +113,7 @@ function ViewStep({ step, setCurrentStep, reserve, onBack, 
submitForm, setReserv
         <div class="buttons is-right mt-5">
           {onBack && <button class="button" onClick={() => 
setCurrentStep(Steps.EXCHANGE)} ><Translate>Back</Translate></button>}
           <AsyncButton onClick={submitForm} data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } disabled={hasErrors} 
><Translate>Confirm</Translate></AsyncButton>
         </div>
       </Fragment>
diff --git 
a/packages/frontend/src/paths/instance/reserves/create/Create.stories.tsx 
b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
similarity index 73%
copy from 
packages/frontend/src/paths/instance/reserves/create/Create.stories.tsx
copy to 
packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
index e138770..f013040 100644
--- a/packages/frontend/src/paths/instance/reserves/create/Create.stories.tsx
+++ 
b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
@@ -20,11 +20,11 @@
 */
 
 import { h, VNode, FunctionalComponent } from 'preact';
-import { CreatePage as TestedComponent } from './CreatePage';
+import { CreatedSuccessfully as TestedComponent } from './CreatedSuccessfully';
 
 
 export default {
-  title: 'Pages/Reserve/Create',
+  title: 'Pages/Reserve/CreatedSuccessfully',
   component: TestedComponent,
   argTypes: {
     onCreate: { action: 'onCreate' },
@@ -39,4 +39,15 @@ function createExample<Props>(Component: 
FunctionalComponent<Props>, props: Part
 }
 
 export const Example = createExample(TestedComponent, {
+  entity: {
+    request: {
+      exchange_url: 'http://exchange.taler/',
+      initial_balance: 'TESTKUDOS:1',
+      wire_method: 'x-taler-bank',
+    },
+    response: {
+      payto_uri: 'payto://x-taler-bank/bank.taler:8080/exchange_account',
+      reserve_pub: 'WEQWDASDQWEASDADASDQWEQWEASDAS'
+    }
+  }
 });
diff --git 
a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx 
b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
index d9b5e3a..3712bd7 100644
--- 
a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
+++ 
b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
@@ -14,9 +14,11 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 import { h, VNode } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
 import { CreatedSuccessfully as Template } from 
"../../../../components/notifications/CreatedSuccessfully";
 import { MerchantBackend } from "../../../../declaration";
 import { Translate } from "../../../../i18n";
+import qrcode from "qrcode-generator"
 
 type Entity = { request: MerchantBackend.Tips.ReserveCreateRequest, response: 
MerchantBackend.Tips.ReserveCreateConfirmation };
 
@@ -27,6 +29,7 @@ interface Props {
 }
 
 export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: 
Props): VNode {
+  const link = 
`${entity.response.payto_uri}?message=${entity.response.reserve_pub}&amount=${entity.request.initial_balance}`
 
   return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
     <div class="field is-horizontal">
@@ -66,11 +69,28 @@ export function CreatedSuccessfully({ entity, onConfirm, 
onCreateAnother }: Prop
       </div>
     </div>
     <p class="is-size-5"><Translate>To complete the setup of the reserve, you 
must now initiate a wire transfer using the given wire transfer subject and 
crediting the specified amount to the indicated account of the 
exchange.</Translate></p>
-
+    
     <p class="is-size-5"><Translate>If your system supports RFC 8905, you can 
do this by opening this URI:</Translate></p>
     <pre>
-      
{entity.response.payto_uri}?message={entity.response.reserve_pub}&amount={entity.request.initial_balance}
+      <a target="_blank" rel="noreferrer" href={link}>{link}</a>
     </pre>
-  </Template>; // FIXME: turn the payto_uri into a clickable link, and maybe 
also generate a QR code (maybe on the client-side --- we do this using qrious 
in the auditor.git already, easy, but not sure how to best include large JS 
dependency in the SPA for that ...)
+    <QR text={link} />
+  </Template>;
+}
 
+const QR = ({ text }: { text: string }) => {
+  const divRef = useRef<HTMLDivElement>(null);
+  useEffect(() => {
+    const qr = qrcode(0, 'L')
+    qr.addData(text)
+    qr.make()
+    divRef.current.innerHTML = qr.createSvgTag({
+      scalable: true,
+    })
+  })
+
+  return <div style={{ width: '100%', display: 'flex', flexDirection: 
'column', alignItems: 'center' }}>
+    <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
+  </div>
 }
+
diff --git 
a/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx
index 5c74326..566483e 100644
--- a/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx
@@ -94,7 +94,7 @@ export function CreatePage({ accounts, onCreate, onBack }: 
Props): VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} 
><Translate>Cancel</Translate></button>}
             <AsyncButton disabled={hasErrors} data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } onClick={submitForm} 
><Translate>Confirm</Translate></AsyncButton>
           </div>
 
diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx 
b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
index 7612d6f..c900192 100644
--- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
@@ -24,13 +24,6 @@ import { useState } from "preact/hooks";
 import * as yup from 'yup';
 import { AsyncButton } from "../../../components/exception/AsyncButton";
 import { FormProvider, FormErrors } from 
"../../../components/form/FormProvider";
-import { Input } from "../../../components/form/Input";
-import { InputCurrency } from "../../../components/form/InputCurrency";
-import { InputDuration } from "../../../components/form/InputDuration";
-import { InputGroup } from "../../../components/form/InputGroup";
-import { InputLocation } from "../../../components/form/InputLocation";
-import { InputPayto } from "../../../components/form/InputPayto";
-import { InputSecured } from "../../../components/form/InputSecured";
 import { UpdateTokenModal } from "../../../components/modal";
 import { useInstanceContext } from "../../../context/instance";
 import { MerchantBackend } from "../../../declaration";
@@ -157,7 +150,7 @@ export function UpdatePage({ onUpdate, onChangeAuth, 
selected, onBack }: Props):
             <button class="button" onClick={onBack} data-tooltip="cancel 
operation"><Translate>Cancel</Translate></button>
 
             <AsyncButton onClick={submit} data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } disabled={hasErrors} 
><Translate>Confirm</Translate></AsyncButton>
           </div>
         </div>
diff --git a/packages/frontend/src/utils/amount.ts 
b/packages/frontend/src/utils/amount.ts
index 823ad9d..062ddaf 100644
--- a/packages/frontend/src/utils/amount.ts
+++ b/packages/frontend/src/utils/amount.ts
@@ -22,7 +22,7 @@ import { MerchantBackend } from "../declaration";
  * @param two 
  * @returns 
  */
-export const sumPrices = (one: string, two: string) => {
+const sumPrices = (one: string, two: string) => {
   const [currency, valueOne] = one.split(':')
   const [, valueTwo] = two.split(':')
   return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
@@ -55,19 +55,6 @@ export function mergeRefunds(prev: 
MerchantBackend.Orders.RefundDetails[], cur:
   return prev
 }
 
-export const multiplyPrice = (price: string, q: number) => {
-  const a = Amounts.parseOrThrow(price)
-  const r = Amounts.mult(a, q)
-  return Amounts.stringify(r.amount)
-}
-
-export const subtractPrices = (one: string, two: string) => {
-  const a = Amounts.parseOrThrow(one)
-  const b = Amounts.parseOrThrow(two)
-  const r = Amounts.sub(a, b)
-  return Amounts.stringify(r.amount)
-}
-
 export const rate = (one: string, two: string) => {
   const a = Amounts.parseOrThrow(one)
   const b = Amounts.parseOrThrow(two)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 17be104..17ca1ec 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -57,6 +57,7 @@ importers:
       preact-render-to-json: ^3.6.6
       preact-render-to-string: ^5.1.19
       preact-router: ^3.2.1
+      qrcode-generator: ^1.4.4
       rimraf: ^3.0.2
       sass: ^1.32.13
       sass-loader: 10.1.1
@@ -74,6 +75,7 @@ importers:
       jed: 1.1.1
       preact: 10.5.13
       preact-router: 3.2.1_preact@10.5.13
+      qrcode-generator: 1.4.4
       swr: 0.5.5
       yup: 0.32.9
     devDependencies:
@@ -12487,6 +12489,10 @@ packages:
     engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
     dev: true
 
+  /qrcode-generator/1.4.4:
+    resolution: {integrity: 
sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
+    dev: false
+
   /qs/6.10.1:
     resolution: {integrity: 
sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==}
     engines: {node: '>=0.6'}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]